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 2 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
5 changes: 5 additions & 0 deletions addons/beehave/nodes/beehave_tree.gd
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func _physics_process(delta: float) -> void:


func tick() -> int:
if actor == null:
Copy link
Owner Author

Choose a reason for hiding this comment

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

there was a scenario where you can set a null actor on a behaviour tree. In this case, the tree should not break the game but show a configuration warning.

return FAILURE
var child := self.get_child(0)
if status != RUNNING:
child.before_run(actor, blackboard)
Expand All @@ -143,6 +145,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
17 changes: 15 additions & 2 deletions addons/beehave/nodes/decorators/limiter.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ class_name LimiterDecorator extends Decorator
@export var max_count : float = 0

func tick(actor: Node, blackboard: Blackboard) -> int:
var child = self.get_child(0)
if not get_child_count():
Copy link
Owner Author

Choose a reason for hiding this comment

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

if for whatever reason there is no child node added to this node, we should skip this behaviour (and show a configuration warning)

return FAILURE

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

if current_count == 0:
Expand All @@ -29,9 +32,13 @@ 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

Expand All @@ -40,3 +47,9 @@ 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():
return ["Requires at least one child node"]
return []
30 changes: 20 additions & 10 deletions addons/beehave/nodes/decorators/time_limiter.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ class_name TimeLimiterDecorator extends Decorator

@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():
Copy link
Owner Author

Choose a reason for hiding this comment

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

if for whatever reason there is no child node added to this node, we should skip this behaviour (and show a configuration warning)

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 == 0.0 and get_child_count() > 0:
get_child(0).before_run(actor, blackboard)

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 +36,22 @@ 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:
bitbrain marked this conversation as resolved.
Show resolved Hide resolved
time_left = 0.0
child.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():
return ["Requires at least one child node"]
return []
4 changes: 2 additions & 2 deletions docs/manual/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ 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 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. 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 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. 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.
13 changes: 13 additions & 0 deletions test/nodes/decorators/limiter_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,19 @@ func test_max_count(count: int, test_parameters: Array = [[2], [0]]) -> void:
for i in range(count):
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

assert_that(action.count).is_equal(count)
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)


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 +58,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)
14 changes: 8 additions & 6 deletions test/nodes/decorators/time_limiter_test.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ const __blackboard = "res://addons/beehave/blackboard.gd"
var tree: BeehaveTree
var action: ActionLeaf
var time_limiter: TimeLimiterDecorator
var actor: Node2D
var blackboard: Blackboard


func before_test() -> void:
tree = auto_free(load(__tree).new())
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())
actor = auto_free(Node2D.new())
blackboard = auto_free(load(__blackboard).new())

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

tree.actor = actor
tree.blackboard = blackboard
Expand All @@ -34,17 +36,17 @@ func test_return_failure_when_child_exceeds_time_limiter() -> void:
time_limiter.wait_time = 1.0
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 0.5
blackboard.set_value(time_limiter.cache_key, 0.5, str(actor.get_instance_id()))
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 1.0
blackboard.set_value(time_limiter.cache_key, 1.0, str(actor.get_instance_id()))
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)


func test_reset_when_child_finishes() -> void:
time_limiter.wait_time = 1.0
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
time_limiter.time_left = 0.5
blackboard.set_value(time_limiter.cache_key, 0.5, str(actor.get_instance_id()))
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

Expand Down
Loading