I just published v0.1.0 of go-bt and would love some feedback from the Go veterans here.
Thanks in advance!
As an aside:
I don’t particularly like behavior trees. Not sure why, but they feel brute-force-y to me, and I find them much harder to reason about than state machines. Once you express state machines as data, they can become just as powerful and feel less fiddly.
A different thought I have that I couldn’t get around exploring is to implement behavior trees with channels (no go routines). But that’s just a vague notion.
There was a article from Russ Cox „Storing Data in Control Flow“. Maybe there‘s something there?
I ultimately landed on a flag-driven, hierarchical state machine. Instead of implementing a full tree traversal, which requires complex flow control nodes, I use bit flags to define my system's entire set of rulesets or invariants.
A state doesn't need to ask "what do I do next?" but rather "are my activation conditions met?" These conditions are defined as bitwise flags evaluated through standard bit-masking operations (AND, OR, XOR, etc.). For example, a state might only become active if flags IN_RANGE & GOAL_IS_NPC & PATH_BLOCKED. The flags allow me to mathematically encode complex prerequisite combinations as simple integer comparisons.
I found this approach makes the transition logic nice and clean. It shifts the burden from managing the flow (which is complex) to managing the data state (which is simple and deterministic). The system still feels like a full BT - it has hierarchy and sequential logic - but the decision process is purely data-driven, which makes it really easy to reason about even when there's a many of layers of complexity for each state/substate.
I don't think "channels without go routines" are possible. One thread can't send and simultaneously receive on a channel. I remember libraries that used go channels for control flow because the code looked better this way. They had to start a second thread to make it work which is very inefficient.
Better approaches are "functions" with internal state (generators) or the relatively new stdlib iter package.
Edit: the iter package internally uses compiler quirks to implement coroutines to be able to send and receive values on the same thread.
Quick technical question: When a node returns 0 (Running), does the Supervisor strictly adhere to the fixed tick interval (like the 100ms in the example), or is there any built-in exponential backoff? I'm wondering how it handles CPU load if you have hundreds of trees just waiting on I/O. Really clean architecture, great work!