Skip to content

Commit

Permalink
Add TimeLimiter node (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ohan17 committed May 21, 2023
1 parent 1a61542 commit f99d88e
Show file tree
Hide file tree
Showing 20 changed files with 312 additions and 36 deletions.
2 changes: 1 addition & 1 deletion addons/beehave/nodes/beehave_tree.gd
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func _physics_process(delta: float) -> void:

func tick() -> int:
var child := self.get_child(0)
if status == -1:
if status != RUNNING:
child.before_run(actor, blackboard)

status = child.tick(actor, blackboard)
Expand Down
4 changes: 4 additions & 0 deletions addons/beehave/nodes/composites/composite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func interrupt(actor: Node, blackboard: Blackboard) -> void:
running_child = null


func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null


func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Composite")
Expand Down
1 change: 1 addition & 0 deletions addons/beehave/nodes/composites/selector.gd
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func tick(actor: Node, blackboard: Blackboard) -> int:

func after_run(actor: Node, blackboard: Blackboard) -> void:
last_execution_index = 0
super(actor, blackboard)


func interrupt(actor: Node, blackboard: Blackboard) -> void:
Expand Down
1 change: 1 addition & 0 deletions addons/beehave/nodes/composites/selector_random.gd
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func tick(actor: Node, blackboard: Blackboard) -> int:

func after_run(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)


func interrupt(actor: Node, blackboard: Blackboard) -> void:
Expand Down
1 change: 1 addition & 0 deletions addons/beehave/nodes/composites/sequence_random.gd
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
func after_run(actor: Node, blackboard: Blackboard) -> void:
if not resume_on_failure:
_reset()
super(actor, blackboard)


func interrupt(actor: Node, blackboard: Blackboard) -> void:
Expand Down
4 changes: 4 additions & 0 deletions addons/beehave/nodes/decorators/decorator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func interrupt(actor: Node, blackboard: Blackboard) -> void:
running_child = null


func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null


func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Decorator")
Expand Down
46 changes: 46 additions & 0 deletions addons/beehave/nodes/decorators/time_limiter.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## The Time Limit Decorator will give its child a set amount of time to finish
## before interrupting it and return a `FAILURE` status code. The timer is reset
## every time before the node runs.
@tool
@icon("../../icons/limiter.svg")
class_name TimeLimiterDecorator extends Decorator

@export var wait_time: = 0.0

var time_left: = 0.0

@onready var child: BeehaveNode = get_child(0)


func tick(actor: Node, blackboard: Blackboard) -> int:
if time_left < wait_time:
time_left += get_physics_process_delta_time()
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)

if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))

if response == RUNNING:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))

return response
else:
child.after_run(actor, blackboard)
interrupt(actor, blackboard)
return FAILURE


func before_run(actor: Node, blackboard: Blackboard) -> void:
time_left = 0.0
child.before_run(actor, blackboard)


func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"TimeLimiterDecorator")
return classes
5 changes: 5 additions & 0 deletions docs/manual/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ An `Inverter` node reverses the outcome of its child node. It returns `FAILURE`
The `Limiter` node executes its child a specified number of times (x). When the maximum number of ticks is reached, it returns a `FAILURE` status code. This can be beneficial when you want to limit the number of times an action or condition is executed, such as limiting the number of attempts an NPC makes to perform a task.

**Example:** An NPC tries to unlock a door with lockpicks but will give up after three attempts if unsuccessful.

## TimeLimiter
The `TimeLimiter` node only gives its child a set amount of time to finish. When the time is up, it interrupts its child and returns a `FAILURE` status code. This is useful when you want to limit the execution time of a long running action.

**Example:** A mob aggros and tries to chase you, the chase action will last a maximum of 10 seconds before being aborted if not complete.
45 changes: 33 additions & 12 deletions test/nodes/composites/selector_random_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,72 @@ extends GdUnitTestSuite
const __source = "res://addons/beehave/nodes/composites/selector_random.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"
const RANDOM_SEED = 123

var selector:SelectorRandomComposite
var action1:ActionLeaf
var action2:ActionLeaf
var actor:Node
var blackboard:Blackboard
var tree: BeehaveTree
var selector: SelectorRandomComposite
var action1: ActionLeaf
var action2: ActionLeaf
var actor: Node
var blackboard: Blackboard


func before_test() -> void:
tree = auto_free(load(__tree).new())
selector = auto_free(load(__source).new())
action1 = auto_free(load(__count_up_action).new())
selector.add_child(action1)
action2 = auto_free(load(__count_up_action).new())
selector.add_child(action2)
actor = auto_free(Node2D.new())
blackboard = auto_free(load(__blackboard).new())

tree.add_child(selector)
selector.add_child(action1)
selector.add_child(action2)

tree.actor = actor
tree.blackboard = blackboard


func test_always_executing_first_successful_node() -> void:
selector.random_seed = RANDOM_SEED
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(1)



func test_execute_second_when_first_is_failing() -> void:
selector.random_seed = RANDOM_SEED
action1.status = BeehaveNode.FAILURE
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(2)



func test_random_even_execution() -> void:
selector.random_seed = RANDOM_SEED
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action1.count).is_equal(1)
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action2.count).is_equal(1)



func test_return_failure_of_none_is_succeeding() -> void:
selector.random_seed = RANDOM_SEED
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.FAILURE
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(1)




func test_clear_running_child_after_run() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
tree.tick()
assert_that(selector.running_child).is_equal(action2)
action2.status = BeehaveNode.FAILURE
tree.tick()
assert_that(selector.running_child).is_equal(null)
16 changes: 14 additions & 2 deletions test/nodes/composites/selector_reactive_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const __blackboard = "res://addons/beehave/blackboard.gd"
const __tree = "res://addons/beehave/nodes/beehave_tree.gd"

var tree: BeehaveTree
var selector: SelectorReactiveComposite
var action1: ActionLeaf
var action2: ActionLeaf

Expand All @@ -20,7 +21,7 @@ func before_test() -> void:
tree = auto_free(load(__tree).new())
action1 = auto_free(load(__count_up_action).new())
action2 = auto_free(load(__count_up_action).new())
var selector = auto_free(load(__source).new())
selector = auto_free(load(__source).new())
var actor = auto_free(Node2D.new())
var blackboard = auto_free(load(__blackboard).new())

Expand Down Expand Up @@ -105,7 +106,8 @@ func test_keeps_restarting_child_until_failure() -> void:
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)
assert_that(action1.count).is_equal(4)
assert_that(action2.count).is_equal(4)



func test_interrupt_second_when_first_is_running() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
Expand All @@ -117,3 +119,13 @@ func test_interrupt_second_when_first_is_running() -> void:
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
assert_that(action1.count).is_equal(2)
assert_that(action2.count).is_equal(0)


func test_clear_running_child_after_run() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
tree.tick()
assert_that(selector.running_child).is_equal(action2)
action2.status = BeehaveNode.FAILURE
tree.tick()
assert_that(selector.running_child).is_equal(null)
46 changes: 35 additions & 11 deletions test/nodes/composites/selector_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,55 @@ extends GdUnitTestSuite
const __source = "res://addons/beehave/nodes/composites/selector.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 selector: SelectorComposite
var action1: ActionLeaf
var action2: ActionLeaf
var actor: Node
var blackboard: Blackboard

var selector:SelectorComposite
var action1:ActionLeaf
var action2:ActionLeaf
var actor:Node
var blackboard:Blackboard

func before_test() -> void:
tree = auto_free(load(__tree).new())
selector = auto_free(load(__source).new())
action1 = auto_free(load(__count_up_action).new())
selector.add_child(action1)
action2 = auto_free(load(__count_up_action).new())
selector.add_child(action2)
actor = auto_free(Node2D.new())
blackboard = auto_free(load(__blackboard).new())

tree.add_child(selector)
selector.add_child(action1)
selector.add_child(action2)

tree.actor = actor
tree.blackboard = blackboard


func test_always_executing_first_successful_node() -> void:
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action1.count).is_equal(2)
assert_that(action2.count).is_equal(0)



func test_execute_second_when_first_is_failing() -> void:
action1.status = BeehaveNode.FAILURE
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.SUCCESS)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(2)



func test_return_failure_of_none_is_succeeding() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.FAILURE
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.FAILURE)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(1)



func test_not_interrupt_second_when_first_is_succeeding() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
Expand All @@ -56,6 +69,7 @@ func test_not_interrupt_second_when_first_is_succeeding() -> void:
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(2)


func test_not_interrupt_second_when_first_is_running() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
Expand All @@ -67,7 +81,7 @@ func test_not_interrupt_second_when_first_is_running() -> void:
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(2)


func test_tick_again_when_child_returns_running() -> void:
action1.status = BeehaveNode.FAILURE
Expand All @@ -76,3 +90,13 @@ func test_tick_again_when_child_returns_running() -> void:
assert_that(selector.tick(actor, blackboard)).is_equal(BeehaveNode.RUNNING)
assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(2)


func test_clear_running_child_after_run() -> void:
action1.status = BeehaveNode.FAILURE
action2.status = BeehaveNode.RUNNING
tree.tick()
assert_that(selector.running_child).is_equal(action2)
action2.status = BeehaveNode.FAILURE
tree.tick()
assert_that(selector.running_child).is_equal(null)
13 changes: 12 additions & 1 deletion test/nodes/composites/sequence_random_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const __tree = "res://addons/beehave/nodes/beehave_tree.gd"
const RANDOM_SEED = 123

var tree: BeehaveTree
var sequence: SequenceRandomComposite
var action1: ActionLeaf
var action2: ActionLeaf

Expand All @@ -21,7 +22,7 @@ func before_test() -> void:
tree = auto_free(load(__tree).new())
action1 = auto_free(load(__count_up_action).new())
action2 = auto_free(load(__count_up_action).new())
var sequence = auto_free(load(__source).new())
sequence = auto_free(load(__source).new())
sequence.random_seed = RANDOM_SEED
var actor = auto_free(Node2D.new())
var blackboard = auto_free(load(__blackboard).new())
Expand Down Expand Up @@ -70,3 +71,13 @@ func test_return_failure_of_none_is_succeeding() -> void:

assert_that(action1.count).is_equal(1)
assert_that(action2.count).is_equal(0)


func test_clear_running_child_after_run() -> void:
action1.status = BeehaveNode.SUCCESS
action2.status = BeehaveNode.RUNNING
tree.tick()
assert_that(sequence.running_child).is_equal(action2)
action2.status = BeehaveNode.SUCCESS
tree.tick()
assert_that(sequence.running_child).is_equal(null)
10 changes: 10 additions & 0 deletions test/nodes/composites/sequence_reactive_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,13 @@ func test_interrupt_second_when_first_is_running() -> void:
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
assert_that(action1.count).is_equal(2)
assert_that(action2.count).is_equal(0)


func test_clear_running_child_after_run() -> void:
action1.status = BeehaveNode.SUCCESS
action2.status = BeehaveNode.RUNNING
tree.tick()
assert_that(sequence.running_child).is_equal(action2)
action2.status = BeehaveNode.SUCCESS
tree.tick()
assert_that(sequence.running_child).is_equal(null)
Loading

0 comments on commit f99d88e

Please sign in to comment.