Skip to content

Commit

Permalink
Delay and Cooldown Decorators (#276)
Browse files Browse the repository at this point in the history
* Create delay decorator

* rename "delay.gd" to "delayer.gd"

* Unit test for delayer node

* Delayer now saves running child

* Fix first delayer unit test + second test

* Delay decorator icon and documentation

* Delay decorator tracks time via blackboard

Consistent with time limit decorator

* Undo changes to unit test config

* Delayer polish

variable rename + more accurate documentation

* Assure repeatability in unit tests

Make sure that the delayer resets properly the next time it is ticked after its child has ran.

* Update delayer_test.gd

* Cooldown decorator

* Fixed cooldown decorator resetting early

Now only resets after it actually runs its child

* Update cooldown decorator docs

* Update GdUnitRunner.cfg

* Write better delay decorator unit test

* Add icon for cooldown decorator

* Add docs for the new nodes

---------

Co-authored-by: miguel <[email protected]>
  • Loading branch information
RedstoneParadox and bitbrain authored Jan 6, 2024
1 parent 902d480 commit 32d456a
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 0 deletions.
38 changes: 38 additions & 0 deletions addons/beehave/icons/cooldown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions addons/beehave/icons/delayer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions addons/beehave/nodes/decorators/cooldown.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@tool
@icon("../../icons/cooldown.svg")
extends Decorator
class_name CooldownDecorator

## The Cooldown Decorator will return 'FAILURE' for a set amount of time
## after executing its child.
## The timer resets the next time its child is executed and it is not `RUNNING`

## The wait time in seconds
@export var wait_time: = 0.0

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


func tick(actor: Node, blackboard: Blackboard) -> int:
var c = get_child(0)
var remaining_time = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
var response

if c != running_child:
c.before_run(actor, blackboard)

if remaining_time > 0:
response = FAILURE

remaining_time -= get_physics_process_delta_time()
blackboard.set_value(cache_key, remaining_time, str(actor.get_instance_id()))

if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response)
else:
response = c.tick(actor, blackboard)

if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)

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

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

if response != RUNNING:
blackboard.set_value(cache_key, wait_time, str(actor.get_instance_id()))

return response


49 changes: 49 additions & 0 deletions addons/beehave/nodes/decorators/delayer.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@tool
@icon("../../icons/delayer.svg")
extends Decorator
class_name DelayDecorator

## The Delay Decorator will return 'RUNNING' for a set amount of time
## before executing its child.
## The timer resets when both it and its child are not `RUNNING`

## The wait time in seconds
@export var wait_time: = 0.0

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

func tick(actor: Node, blackboard: Blackboard) -> int:
var c = get_child(0)
var total_time = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
var response

if c != running_child:
c.before_run(actor, blackboard)

if total_time < wait_time:
response = RUNNING

total_time += get_physics_process_delta_time()
blackboard.set_value(cache_key, total_time, str(actor.get_instance_id()))

if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response)
else:
response = c.tick(actor, blackboard)

if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response)

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

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

if response != RUNNING:
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))

return response

10 changes: 10 additions & 0 deletions docs/manual/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ The `TimeLimiter` node only gives its `RUNNING` child a set amount of time to fi
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.

## Delayer
When first executing the `Delayer` node, it will start an internal timer and return `RUNNING` until the timer is complete, after which it will execute its child node. The delayer resets its time after its child returns either `SUCCESS` or `FAILURE`.

**Example:** You stun a boss mob and it waits a certain amount of time before resuming its attack patterns.

## Cooldown
The `Cooldown` node executes its child until it either returns `SUCCESS` or `FAILURE`, after which it will start an internal timer and return `FAILURE` until the timer is complete. The cooldown is then able to execute its child again.

**Example:** A mob attacks you and has to wait before it can attack you again.
42 changes: 42 additions & 0 deletions test/nodes/decorators/cooldown_test.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# GdUnit generated TestSuite
class_name CooldownDecoratorTest
extends GdUnitTestSuite
@warning_ignore('unused_parameter')
@warning_ignore('return_value_discarded')

# TestSuite generated from
const __source = 'res://addons/beehave/nodes/decorators/cooldown.gd'
const __action = "res://test/actions/count_up_action.gd"
const __tree = "res://addons/beehave/nodes/beehave_tree.gd"
const __blackboard = "res://addons/beehave/blackboard.gd"

var tree: BeehaveTree
var action: ActionLeaf
var cooldown: CooldownDecorator
var runner:GdUnitSceneRunner

func before_test() -> void:
tree = auto_free(load(__tree).new())
action = auto_free(load(__action).new())
cooldown = auto_free(load(__source).new())

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

tree.add_child(cooldown)
cooldown.add_child(action)

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

func test_running_then_fail() -> void:
cooldown.wait_time = 1.0
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.FAILURE)
await runner.simulate_frames(1, 2000)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
56 changes: 56 additions & 0 deletions test/nodes/decorators/delayer_test.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# GdUnit generated TestSuite
class_name DelayDecoratorTest
extends GdUnitTestSuite
@warning_ignore('unused_parameter')
@warning_ignore('return_value_discarded')

# TestSuite generated from
const __source = 'res://addons/beehave/nodes/decorators/delayer.gd'
const __action = "res://test/actions/count_up_action.gd"
const __tree = "res://addons/beehave/nodes/beehave_tree.gd"
const __blackboard = "res://addons/beehave/blackboard.gd"

var tree: BeehaveTree
var action: ActionLeaf
var delayer: DelayDecorator
var runner:GdUnitSceneRunner

func before_test() -> void:
tree = auto_free(load(__tree).new())
action = auto_free(load(__action).new())
delayer = auto_free(load(__source).new())

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

tree.add_child(delayer)
delayer.add_child(action)

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

func test_return_success_after_delay() -> void:
delayer.wait_time = get_physics_process_delta_time()
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)
# Assure that the delayer properly resets
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

func test_return_running_after_delay() -> void:
delayer.wait_time = 1.0
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
await runner.simulate_frames(1, 1000)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)
# Assure that the delayer properly resets
action.status = BeehaveNode.RUNNING
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
await runner.simulate_frames(1, 1000)
assert_that(tree.tick()).is_equal(BeehaveNode.RUNNING)
action.status = BeehaveNode.SUCCESS
assert_that(tree.tick()).is_equal(BeehaveNode.SUCCESS)

0 comments on commit 32d456a

Please sign in to comment.