Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 fix limiter node behaviour #250

Merged
merged 8 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions addons/beehave/nodes/beehave_tree.gd
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ func _ready() -> void:
if Engine.is_editor_hint():
return

if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode:
push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()])
disable()
return

if not blackboard:
_internal_blackboard = Blackboard.new()
add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK)
Expand Down Expand Up @@ -124,6 +119,8 @@ func _physics_process(delta: float) -> void:


func tick() -> int:
if actor == null or get_child_count() == 0:
return FAILURE
var child := self.get_child(0)
if status != RUNNING:
child.before_run(actor, blackboard)
Expand All @@ -143,6 +140,9 @@ func tick() -> int:

func _get_configuration_warnings() -> PackedStringArray:
var warnings:PackedStringArray = []

if actor == null:
warnings.append("Configure target node on tree")

if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
Expand Down
8 changes: 0 additions & 8 deletions addons/beehave/nodes/composites/composite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@ class_name Composite extends BeehaveNode
var running_child: BeehaveNode = null


func _ready():
if Engine.is_editor_hint():
return

if self.get_child_count() < 1:
push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()])
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_configuration_warnings should be sufficient. No need to spam the console



func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()

Expand Down
8 changes: 0 additions & 8 deletions addons/beehave/nodes/decorators/decorator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ class_name Decorator extends BeehaveNode
var running_child: BeehaveNode = null


func _ready():
if Engine.is_editor_hint():
return

if self.get_child_count() != 1:
push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()])


func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()

Expand Down
29 changes: 23 additions & 6 deletions addons/beehave/nodes/decorators/limiter.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
@icon("../../icons/limiter.svg")
class_name LimiterDecorator extends Decorator

## The limiter will execute its child `x` amount of times. When the number of
## The limiter will execute its `RUNNING` child `x` amount of times. When the number of
## maximum ticks is reached, it will return a `FAILURE` status code.
## The count resets the next time that a child is not `RUNNING`

@onready var cache_key = 'limiter_%s' % self.get_instance_id()

@export var max_count : float = 0

func tick(actor: Node, blackboard: Blackboard) -> int:
var child = self.get_child(0)
var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id()))
if not get_child_count() == 1:
return FAILURE

if current_count == 0:
child.before_run(actor, blackboard)
var child = get_child(0)
var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id()))

if current_count < max_count:
blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id()))
Expand All @@ -29,14 +30,30 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
if child is ActionLeaf and response == RUNNING:
running_child = child
blackboard.set_value("running_action", child, str(actor.get_instance_id()))


if response != RUNNING:
child.after_run(actor, blackboard)

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


func before_run(actor: Node, blackboard: Blackboard) -> void:
blackboard.set_value(cache_key, 0, str(actor.get_instance_id()))
if get_child_count() > 0:
get_child(0).before_run(actor, blackboard)


func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"LimiterDecorator")
return classes


func _get_configuration_warnings() -> PackedStringArray:
if not get_child_count() == 1:
return ["Requires exactly one child node"]
return []
33 changes: 23 additions & 10 deletions addons/beehave/nodes/decorators/time_limiter.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
@icon("../../icons/limiter.svg")
class_name TimeLimiterDecorator extends Decorator

## 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.
## The Time Limit Decorator will give its `RUNNING` child a set amount of time to finish
## before interrupting it and return a `FAILURE` status code.
## The timer resets the next time that a child is not `RUNNING`

@export var wait_time: = 0.0

var time_left: = 0.0

@onready var child: BeehaveNode = get_child(0)
@onready var cache_key = 'time_limiter_%s' % self.get_instance_id()


func tick(actor: Node, blackboard: Blackboard) -> int:
if not get_child_count() == 1:
return FAILURE

var child = self.get_child(0)
var time_left = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))

if time_left < wait_time:
time_left += get_physics_process_delta_time()
blackboard.set_value(cache_key, time_left, str(actor.get_instance_id()))
var response = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response)
Expand All @@ -28,20 +33,28 @@ func tick(actor: Node, blackboard: Blackboard) -> int:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))

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


func before_run(actor: Node, blackboard: Blackboard) -> void:
time_left = 0.0
child.before_run(actor, blackboard)
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
if get_child_count() > 0:
get_child(0).before_run(actor, blackboard)


func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"TimeLimiterDecorator")
return classes


func _get_configuration_warnings() -> PackedStringArray:
if not get_child_count() == 1:
return ["Requires exactly one child node"]
return []
8 changes: 6 additions & 2 deletions docs/manual/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ An `Inverter` node reverses the outcome of its child node. It returns `FAILURE`
**Example:** An NPC is patrolling an area and should change its path if it *doesn't* detect an enemy.

## Limiter
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.
The `Limiter` node executes its `RUNNING` child a specified number of times (x). When the maximum number of ticks is reached, it returns a `FAILURE` status code. The limiter resets its counter after its child returns either `SUCCESS` or `FAILURE`.

This node 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. Once a limiter reaches its maximum number of ticks, it will start interrupting its child on every tick.

**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.
The `TimeLimiter` node only gives its `RUNNING` child a set amount of time to finish. When the time is up, it interrupts its child and returns a `FAILURE` status code. The time limiter resets its time after its child returns either `SUCCESS` or `FAILURE`.

This note is useful when you want to limit the execution time of a long running action. Once a time limiter reaches its time limit, it will start interrupting its child on every tick.

**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.
19 changes: 17 additions & 2 deletions test/nodes/decorators/limiter_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,25 @@ func before_test() -> void:

func test_max_count(count: int, test_parameters: Array = [[2], [0]]) -> void:
limiter.max_count = count

action.status = BeehaveNode.RUNNING
for i in range(count):
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)

assert_that(action.count).is_equal(count)
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)
# ensure it resets its child after it reached max count
assert_that(action.count).is_equal(0)


func test_interrupt_after_run() -> void:
action.status = BeehaveNode.RUNNING
limiter.max_count = 1
tree.tick()
assert_that(limiter.running_child).is_equal(action)
action.status = BeehaveNode.FAILURE
tree.tick()
assert_that(action.count).is_equal(0)
assert_that(limiter.running_child).is_equal(null)


func test_clear_running_child_after_run() -> void:
Expand All @@ -46,4 +60,5 @@ func test_clear_running_child_after_run() -> void:
assert_that(limiter.running_child).is_equal(action)
action.status = BeehaveNode.SUCCESS
tree.tick()
assert_that(action.count).is_equal(2)
assert_that(limiter.running_child).is_equal(null)
36 changes: 20 additions & 16 deletions test/nodes/decorators/time_limiter_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,51 @@ const __blackboard = "res://addons/beehave/blackboard.gd"
var tree: BeehaveTree
var action: ActionLeaf
var time_limiter: TimeLimiterDecorator
var actor: Node2D
var blackboard: Blackboard
var runner:GdUnitSceneRunner


func before_test() -> void:
tree = auto_free(load(__tree).new())
actor = auto_free(Node2D.new())
blackboard = auto_free(load(__blackboard).new())

tree.actor = actor
tree.blackboard = blackboard
action = auto_free(load(__action).new())
time_limiter = auto_free(load(__source).new())

var actor = auto_free(Node2D.new())
var blackboard = auto_free(load(__blackboard).new())

time_limiter.add_child(action)
tree.add_child(time_limiter)
time_limiter.child = action

tree.actor = actor
tree.blackboard = blackboard
runner = scene_runner(tree)


func test_return_failure_when_child_exceeds_time_limiter() -> void:
time_limiter.wait_time = 1.0
time_limiter.wait_time = 0.1
action.status = BeehaveNode.RUNNING
await runner.simulate_frames(1, 10)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 0.5
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 1.0
await runner.simulate_frames(5, 100)
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)


func test_reset_when_child_finishes() -> void:
time_limiter.wait_time = 1.0
time_limiter.wait_time = 0.5
action.status = BeehaveNode.RUNNING
await runner.simulate_frames(1, 10)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 0.5
await runner.simulate_frames(5, 100)
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)


func test_clear_running_child_after_run() -> void:
time_limiter.wait_time = 1.0
time_limiter.wait_time = 0.5
action.status = BeehaveNode.RUNNING
tree.tick()
await runner.simulate_frames(1, 50)
assert_that(time_limiter.running_child).is_equal(action)
action.status = BeehaveNode.SUCCESS
tree.tick()
assert_that(time_limiter.running_child).is_equal(null)
await runner.simulate_frames(1, 50)
assert_that(time_limiter.running_child).is_null()