bitbrain / beehave

🐝 behavior tree AI for Godot Engine
https://bitbra.in/beehave
MIT License
1.91k stars 117 forks source link

Cooldown should reset automatically when moving to another branch #340

Open Nodragem opened 4 months ago

Nodragem commented 4 months ago

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

rxlecky commented 4 months ago

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".

Nodragem commented 4 months ago

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?

deammer commented 2 months ago

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?