diff --git a/addons/beehave/icons/simple_parallel.svg b/addons/beehave/icons/simple_parallel.svg new file mode 100644 index 00000000..e9c8b008 --- /dev/null +++ b/addons/beehave/icons/simple_parallel.svg @@ -0,0 +1,13 @@ + + simple_parallel + + Layer 1 + + + + + + + + + \ No newline at end of file diff --git a/addons/beehave/nodes/composites/simple_parallel.gd b/addons/beehave/nodes/composites/simple_parallel.gd new file mode 100644 index 00000000..62974812 --- /dev/null +++ b/addons/beehave/nodes/composites/simple_parallel.gd @@ -0,0 +1,114 @@ +@tool +@icon("../../icons/simple_parallel.svg") +class_name SimpleParallelComposite extends Composite + +## Simple Parallel nodes will attampt to execute all chidren at same time and +## can only have exactly two children. First child as primary node, second +## child as secondary node. +## This node will always report primary node's state, and continue tick while +## primary node return 'RUNNING'. The state of secondary node will be ignored +## and executed like a subtree. +## If primary node return 'SUCCESS' or 'FAILURE', this node will interrupt +## secondary node and return primary node's result. +## If this node is running under delay mode, it will wait seconday node +## finish its action after primary node terminates. + +#how many times should secondary node repeat, zero means loop forever +@export var secondary_node_repeat_count:int = 0 + +#wether to wait secondary node finish its current action after primary node finished +@export var delay_mode:bool = false + +var delayed_result := SUCCESS +var main_task_finished:bool = false +var secondary_node_running:bool = false +var secondary_node_repeat_left:int = 0 + +func _get_configuration_warnings() -> PackedStringArray: + var warnings: PackedStringArray = super._get_configuration_warnings() + + if get_child_count() != 2: + warnings.append("SimpleParallel should have exactly two child nodes.") + + if not get_child(0) is ActionLeaf: + warnings.append("SimpleParallel should have an action leaf node as first child node.") + + return warnings + +func tick(actor, blackboard: Blackboard): + for c in get_children(): + var node_index = c.get_index() + if node_index == 0 and not main_task_finished: + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + delayed_result = response + match response: + SUCCESS,FAILURE: + _cleanup_running_task(c, actor, blackboard) + c.after_run(actor, blackboard) + main_task_finished = true + if not delay_mode: + if secondary_node_running: + get_child(1).interrupt(actor, blackboard) + _reset() + return delayed_result + RUNNING: + running_child = c + if c is ActionLeaf: + blackboard.set_value("running_action", c, str(actor.get_instance_id())) + + elif node_index == 1: + if secondary_node_repeat_count == 0 or secondary_node_repeat_left > 0: + if not secondary_node_running: + c.before_run(actor, blackboard) + var subtree_response = c.tick(actor, blackboard) + if subtree_response != RUNNING: + secondary_node_running = false + c.after_run(actor, blackboard) + if delay_mode and main_task_finished: + _reset() + return delayed_result + elif secondary_node_repeat_left > 0: + secondary_node_repeat_left -= 1 + else: + secondary_node_running = true + + return RUNNING + +func before_run(actor: Node, blackboard:Blackboard) -> void: + secondary_node_repeat_left = secondary_node_repeat_count + super(actor, blackboard) + +func interrupt(actor: Node, blackboard: Blackboard) -> void: + if not main_task_finished: + get_child(0).interrupt(actor, blackboard) + if secondary_node_running: + get_child(1).interrupt(actor, blackboard) + _reset() + super(actor, blackboard) + +func after_run(actor: Node, blackboard: Blackboard) -> void: + _reset() + super(actor, blackboard) + +func _reset() -> void: + main_task_finished = false + secondary_node_running = false + +## Changes `running_action` and `running_child` after the node finishes executing. +func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard): + var blackboard_name = str(actor.get_instance_id()) + if finished_action == running_child: + running_child = null + if finished_action == blackboard.get_value("running_action", null, blackboard_name): + blackboard.set_value("running_action", null, blackboard_name) + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SimpleParallelComposite") + return classes diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7346b538..1c2f7022 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -7,6 +7,7 @@ * [ Composites](/manual/composites.md) * [ Selector](/manual/selector.md) * [ Sequence](/manual/sequence.md) + * [ Sequence](/manual/simple_parallel.md) * [ Decorators](/manual/decorators.md) * [Debugging](/manual/debugging.md) * [Performance](/manual/performance.md) diff --git a/docs/assets/icons/simple_parallel.svg b/docs/assets/icons/simple_parallel.svg new file mode 100644 index 00000000..e9c8b008 --- /dev/null +++ b/docs/assets/icons/simple_parallel.svg @@ -0,0 +1,13 @@ + + simple_parallel + + Layer 1 + + + + + + + + + \ No newline at end of file diff --git a/docs/manual/simple_parallel.md b/docs/manual/simple_parallel.md new file mode 100644 index 00000000..b1a789dc --- /dev/null +++ b/docs/manual/simple_parallel.md @@ -0,0 +1,24 @@ +# Simple Parallel Node +The Simple Parallel node is a fundamental building block in Behavior Trees, used to execute two children at same time. It helps you run multiple actions simultaneously.Think of the Simple Parallel node as "While doing A, do B as well." + +## How does it work? +Simple Parallel nodes will attampt to execute all chidren at same time and can only have exactly two children. First child as primary node, second child as secondary node. +This node will always report primary node's state, and continue tick while primary node return `RUNNING`. The state of secondary node will be ignored and executed like a subtree. +If primary node return `SUCCESS` or `FAILURE`, this node will interrupt secondary node and return primary node's result. +If this node is running under delay mode, it will wait seconday node finish its action after primary node terminates. + + +## Example Scenarios +Here are some example scenarios to help you understand the Sequence node better: + +### Example: While attacking the enemy, move toward the enemy +Imagine you want a ranged enemy character trying to shoot you whenever he can while to move towards you. You can use a Simple Parallel node with the following child nodes architecture: + +1. Move to point A near player +2. Sequence Node + 1. Check if enemy can shoot + 2. Shoot + +The enemy will move to a location near player and try to shoot at same time, and if move action is successful or failure, the Simple Parallel node will termitate the child sequence node for shooting attempt, then return `SUCCESS` or `FAILURE` according to move action result. + +Simple Parallel can be nested to create complex behaviors while it's not suggested, because too much nesting would make it hard to maintain your behavior tree. diff --git a/test/nodes/composites/simple_parallel_test.gd b/test/nodes/composites/simple_parallel_test.gd new file mode 100644 index 00000000..c47980c1 --- /dev/null +++ b/test/nodes/composites/simple_parallel_test.gd @@ -0,0 +1,176 @@ +# GdUnit generated TestSuite +class_name SimpleParallelTest +extends GdUnitTestSuite +@warning_ignore("unused_parameter") +@warning_ignore("return_value_discarded") + +# TestSuite generated from +const __source = "res://addons/beehave/nodes/composites/simple_parallel.gd" +const __count_up_action = "res://test/actions/count_up_action.gd" +const __blackboard = "res://addons/beehave/blackboard.gd" +const __tree = "res://addons/beehave/nodes/beehave_tree.gd" + +var tree: BeehaveTree +var simple_parallel: SimpleParallelComposite +var action1: ActionLeaf +var action2: ActionLeaf +var actor: Node +var blackboard: Blackboard + + +func before_test() -> void: + tree = auto_free(load(__tree).new()) + simple_parallel = auto_free(load(__source).new()) + action1 = auto_free(load(__count_up_action).new()) + action2 = auto_free(load(__count_up_action).new()) + actor = auto_free(Node2D.new()) + blackboard = auto_free(load(__blackboard).new()) + + tree.add_child(simple_parallel) + simple_parallel.add_child(action1) + simple_parallel.add_child(action2) + + tree.actor = actor + tree.blackboard = blackboard + + +func test_always_return_first_node_result() -> void: + action2.status = BeehaveNode.FAILURE + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(0) + + action1.status = BeehaveNode.FAILURE + action2.status = BeehaveNode.SUCCESS + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(0) + +func test_interrupt_second_when_first_is_succeeding() -> void: + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.RUNNING + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + + action1.status = BeehaveNode.SUCCESS + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(0) + + +func test_interrupt_second_when_first_is_failing() -> void: + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.RUNNING + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + + action1.status = BeehaveNode.FAILURE + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(0) + + +func test_continue_tick_when_child_returns_failing() -> void: + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.FAILURE + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(2) + + +func test_child_continue_tick_in_delay_mode() -> void: + simple_parallel.delay_mode = true + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.RUNNING + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + + action1.status = BeehaveNode.SUCCESS + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(2) + + action2.status = BeehaveNode.FAILURE + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(3) + +func test_child_tick_count() -> void: + simple_parallel.secondary_node_repeat_count = 2 + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.FAILURE + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + assert_that(simple_parallel.secondary_node_repeat_left).is_equal(1) + + action2.status = BeehaveNode.RUNNING + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(2) + assert_that(simple_parallel.secondary_node_repeat_left).is_equal(1) + + action2.status = BeehaveNode.SUCCESS + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(3) + assert_that(action2.count).is_equal(3) + assert_that(simple_parallel.secondary_node_repeat_left).is_equal(0) + + assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(4) + assert_that(action2.count).is_equal(3) + +func test_nested_simple_parallel() -> void: + var simple_parallel2 = auto_free(load(__source).new()) + var action3 = auto_free(load(__count_up_action).new()) + simple_parallel.remove_child(action2) + simple_parallel.add_child(simple_parallel2) + simple_parallel2.add_child(action2) + simple_parallel2.add_child(action3) + + action1.status = BeehaveNode.RUNNING + action2.status = BeehaveNode.RUNNING + action3.status = BeehaveNode.RUNNING + + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + + assert_that(action1.count).is_equal(1) + assert_that(action2.count).is_equal(1) + assert_that(action3.count).is_equal(1) + + action2.status = BeehaveNode.SUCCESS + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(2) + assert_that(action2.count).is_equal(2) + assert_that(action3.count).is_equal(0) + + action3.status = BeehaveNode.RUNNING + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(3) + assert_that(action2.count).is_equal(3) + assert_that(action3.count).is_equal(0) + + action2.status = BeehaveNode.RUNNING + action3.status = BeehaveNode.RUNNING + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING) + assert_that(action1.count).is_equal(4) + assert_that(action2.count).is_equal(4) + assert_that(action3.count).is_equal(1) + + action1.status = BeehaveNode.SUCCESS + assert_that(simple_parallel.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS) + assert_that(action1.count).is_equal(5) + assert_that(action2.count).is_equal(0) + assert_that(action3.count).is_equal(0) + + simple_parallel2.remove_child(action2) + simple_parallel2.remove_child(action3) + simple_parallel.remove_child(simple_parallel2) + simple_parallel.add_child(action2)