BehaviourToolkit icon indicating copy to clipboard operation
BehaviourToolkit copied to clipboard

Add await support to BT

Open dfego opened this issue 1 year ago • 3 comments

BTRoot has been modified with an additional exported variable named allow_await. When true, the BTRoot modifies its flow such that it awaits for tick(), and suspends processing until that await returns.

This can be useful when wanting to truly have an actor wait for something to happen without having to fudge around it with nested FSMs or other hacks.

This relates to #78 , and implements it as far as I can tell with a few potential blind spots. Specifically:

  • I haven't made any changes to bt_simple_parallel.gd because I haven't used those and wasn't sure what appropriate behavior might be when doing an await in a parallel context.
  • I haven't made any changes to bt_integrated_fsm.gd, though that might already just be fine since I modified every place (except the one above) that calls tick().

Very open to feedback, and I know there are multiple PRs pending, but I wanted to put this out there since I had suggested it!

I added a fairly contrived but working example scene that simply waits on a timer to rate-limit a print leaf. That's not how I'd actually use it, but it was a very easy way to see if things were working the way I intended.

dfego avatar Feb 29 '24 03:02 dfego

I have looked at your example and as far I understand, this code

await get_tree().create_timer(1.0).timeout

basically pauses the whole BehaviourTree until it is resolved, right? I am quite unsure if this goes against the design principle of BTs, as these are mostly built upon non-blocking designs.

Have a look at The Behavior Tree Starter Kit by Alex J. Champandard and Philip Dunstan for further research, which mentions a SUSPENDED return state of leaves as part of a more event-driven approach.

Why not stop the BT and just wait?

I think the await keyword is great for quickly connecting signals and controlling the flow of async functions. However in this case, await can cause a few troubles:

  • The player commits to doing an action that requires a timer to timeout. They are interrupted while doing so. How can I cancel the leaf awaiting the timeout?
  • Having several awaits in one tree can lead to spaghetti code, as signals are connected without any safeguards and/or ways to manipulate them from the outside.
  • A glance in the future: When someday some kind of BehaviourManager (#3) is implemented to help with performance for huge groups of agents, this will cause problems.

How could we implement a similar feature to await?

This really depends on your usecase, but here are some suggestions:

  • A new AwaitSignal-Leaf that returns FAILURE until the a signal was emitted.
  • New Timer-Leaves that let you create and manage timers.

I am totally open to discussing your approach, totally new approaches or more tho. This is just my opinion and I could be wrong!

Conclusion

Very open to feedback, and I know there are multiple PRs pending, but I wanted to put this out there since I had suggested it!

Yeah sorry! I have been very busy for the last few months. :( Always appreciate your feedback and PRs! Let me know what you think and how you are generally planning to use this feature!

Further Reading and Research

ThePat02 avatar Mar 01 '24 11:03 ThePat02

Thanks so much for the detailed response! I think my gut pushing me towards "use await" is because syntactically and in my brain it's the simplest, most intended way in Godot to wait for something else to happen. Interruptibility creates another hurdle, but one that can also be managed with signals behind the scenes.

As we were talking about in #78, another solution is a nested state machine, but I'm hesitant to mix my paradigms. I really like that that's included in this library, and at the same time when I use both together I find myself second-guessing where to draw the lines between the BT and FSM.

Lastly, I'm a software engineer by trade, and theoretically I know better than to avoid premature optimization. However, in this case, it felt appropriate to at least consider the performance impact of a BT in a bunch of agents checking every single tick during times when they are doing a no-op. That's what signals are for. I haven't been doing game dev long, but they do seem to be useful and valuable. That said, I'm not totally sure how they specifically may mesh (or conflict with) the design goals of behavior trees.

With that in mind, I'll do some of the suggested reading and step back and think about how best to implement things and report back once I have the time to develop a more informed opinion!

dfego avatar Mar 01 '24 15:03 dfego

As we were talking about in https://github.com/ThePat02/BehaviourToolkit/discussions/78, another solution is a nested state machine, but I'm hesitant to mix my paradigms. I really like that that's included in this library, and at the same time when I use both together I find myself second-guessing where to draw the lines between the BT and FSM.

This whole plugin is about the idea of mixing those two patterns! What really convinced me was this article on GameAIPro. You really don't have to draw a line anywhere and just nest along.

Lastly, I'm a software engineer by trade, and theoretically I know better than to avoid premature optimization. However, in this case, it felt appropriate to at least consider the performance impact of a BT in a bunch of agents checking every single tick during times when they are doing a no-op. That's what signals are for. I haven't been doing game dev long, but they do seem to be useful and valuable. That said, I'm not totally sure how they specifically may mesh (or conflict with) the design goals of behavior trees.

Well the general solution for this is a manager that ticks different BTs or Patterns in groups so that not every thing is ticked at once. However you also could use an event based approach as mentioned in the articles I sent before. This is however very very complex and it really depends on your game and architectures. If you have so many agents that the CPU struggles with the workload of BTs, the only solution would be a rewrite of the critical code in C++ or the removal of these patterns. (Does every single agent of lets say 1000 need a fully functioning BT? Do only the nearest agents to the player need it etc.) I think there is a GDC talk from Ubisoft about their crowd behaviour about this.

ThePat02 avatar Mar 01 '24 18:03 ThePat02