Skip to content

Commit

Permalink
Added a new composite node: simple_parallel (#332)
Browse files Browse the repository at this point in the history
* Added a new composite node simple_parallel

* add unit test

* update unit test

* icon and doc

* remove temp file
  • Loading branch information
DarkAngelZT authored Apr 23, 2024
1 parent 25a9e75 commit 2e51a71
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 0 deletions.
13 changes: 13 additions & 0 deletions addons/beehave/icons/simple_parallel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 114 additions & 0 deletions addons/beehave/nodes/composites/simple_parallel.gd
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [<img src="/beehave/assets/icons/category_composite.svg"> Composites](/manual/composites.md)
* [<img src="/beehave/assets/icons/selector.svg"> Selector](/manual/selector.md)
* [<img src="/beehave/assets/icons/sequence.svg"> Sequence](/manual/sequence.md)
* [<img src="/beehave/assets/icons/simple_parallel.svg"> Sequence](/manual/simple_parallel.md)
* [<img src="/beehave/assets/icons/category_decorator.svg"> Decorators](/manual/decorators.md)
* [Debugging](/manual/debugging.md)
* [Performance](/manual/performance.md)
Expand Down
13 changes: 13 additions & 0 deletions docs/assets/icons/simple_parallel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/manual/simple_parallel.md
Original file line number Diff line number Diff line change
@@ -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.
176 changes: 176 additions & 0 deletions test/nodes/composites/simple_parallel_test.gd
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 2e51a71

Please sign in to comment.