beehave icon indicating copy to clipboard operation
beehave copied to clipboard

Cooldown should reset automatically when moving to another branch

Open Nodragem opened this issue 1 year ago • 3 comments

Is your feature request related to a problem? Please describe. When implementing an attack cooldown, we want the enemy to attack immediately the first time, then wait for the cooldown. However, at the moment, if the selector moves to another behavior the cooldown is not resetted. Thus, when we come back to the Attack sequence, we need to wait for the cooldown to be over for the attack to happen.

See video, the first time the IA goes in GoToAttack, the cooldown works as intended. Then I move away, the IA goes to GoToReach. It reaches me and come back to GoToAttack. Then, as the cooldown was never resetted, the IA has to wait the cooldown to attack.

https://github.com/bitbrain/beehave/assets/10520249/a4c3c551-b9e4-4829-8fd5-493ee8c13c79

Note: for the purpose of the video I disable the action TryStopAttackCooldown.

Describe the solution you'd like Somehow, when the cooldown is interrupted, it should be resetted.

Describe alternatives you've considered I played with the interrupted and run_after, but as the cooldown decorator returns FAILURE, they don't really work for resetting the timer. At the end, I set up a TryStopAttackCooldown in the another branch that is likely to be selected when the AttackSequence failed.

Additional context Tree with the alternative solution implemented, see TryStopAttackCooldown. image

Nodragem avatar Jun 02 '24 10:06 Nodragem

I don't think what you requested is possible because the cooldown is implemented as a decorator node so, as you correctly identified, it doesn't get the before_run and interrupt callbacks in the same way that action nodes do. You could implement a timer action node to act as your cooldown to achieve the resetting behaviour. Here's a sample implementation that should do the trick:

extends ActionLeaf
class_name WaitAction

@export var wait_time: float = 0.0

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

func before_run(actor: Node, blackboard: Blackboard) -> void:
	blackboard.set_value(cache_key, wait_time, str(actor.get_instance_id()))


func tick(actor: Node, blackboard: Blackboard) -> int:
	var remaining_time = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
	if remaining_time > 0.0:
		remaining_time -= get_physics_process_delta_time()
		blackboard.set_value(cache_key, remaining_time, str(actor.get_instance_id()))
		return RUNNING

	return SUCCESS


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

You will need to place this node after your attack node and remove the original cooldown decorator.

Also, note that your issue is partially related to #319. While I understand that you're requesting the reset behaviour in particular, the issue you're experiencing is exacerbated by the fact that the cooldown is not ticking down while the cooldown node is not active. This makes the cooldown longer in your case because your cooldown node is not on the behaviour tree's "hot path".

rxlecky avatar Jun 05 '24 00:06 rxlecky

yes, I could not find a way to implement it with how Beehave is working at the moment. I thought I would flag the issue anyway as it seems reasonable that a Cooldown should reset itself when we leave its branch to go to another branch.

As I understand it, the callback before_run, after_run and interrupt are all designed to work with RUNNING, while the cooldown decorator cannot be RUNNING. Hence, a solution might be to add a callback on_leaving_branch or on_exit which would work for decorators and actions?

Nodragem avatar Jun 06 '24 09:06 Nodragem

Same issue with the DelayDecorator. Imagine the following tree where an enemy either attacks the player or patrols an area. When the player enters enemy range, the condition leaf returns SUCCESS and switches to attacking the player, but when the player exits the range, the tree goes back to the DelayDecorator.

BehaveTree
└─ SelectorReactiveComposite
   ├─ SequenceComposite (attacking)
   │  ├─ ConditionLeaf (is in range of player)
   │  └─ SequenceComposite
   │     └─ ... attack the player
   └─ DelayDecorator (patrolling)
      └─ SequenceComposite
         └─ ... patrol an area

However, the DelayDecorator is never reset to 0 so it restarts from its previously selected child, which seems unintended.

I've just started digging into this addon so take my suggestion with a grain of salt, but maybe a new lifecycle method could reset the DelayDecorator when it becomes live?

deammer avatar Jul 11 '24 21:07 deammer

Raising up this subject, because I'm concerned in my project :)

oxeron avatar Apr 19 '25 11:04 oxeron

I will take a look at this.

bitbrain avatar Apr 19 '25 21:04 bitbrain

The core issue here seems to be that Beehave currently only calls interrupt() on nodes that are actively RUNNING. While this makes sense performance-wise, it causes problems for decorator nodes like cooldowns or delays that may not enter RUNNING themselves: especially when their children return FAILURE or SUCCESS early.

This means that if the active branch of the tree shifts (e.g., a selector picks a different path), decorators like cooldown may not be ticked or interrupted, but they still hold stale state and behave incorrectly when the branch is revisited later.

Unreal Engine solves this with an observer system that tracks relevant changes and invalidates branches when necessary. While very robust, it feels overkill for a lightweight addon like Beehave.

A potential middle-ground might be to implement a one-time passive interrupt strategy in composite nodes. For example: when a branch switch is detected (i.e., a previously RUNNING child is no longer selected), we could call interrupt() once on that child (and its decorators) even if they are not in RUNNING state, avoiding full-frame brute-force traversal while keeping nodes like cooldowns clean and predictable.

bitbrain avatar Apr 20 '25 14:04 bitbrain