type-safe behavior trees in godot
I want to make a mecha game about shooting bugs, and that means the bugs have to, like... do things, you know? On their own. I can't, like, hover behind every player in every game controlling every single bug that attacks them. I don't have that much power.
In games, I know this is generally done with behavior trees, so I looked for a behavior tree library for Godot and found Beehave. It's good -- you can tell that it's ideal for designers to use. Nice GUI controls, visual debugging tools, and -- really, the most important thing -- someone else wrote it and I don't have to. It's a great choice for implementing AI in your game.
It seems like it would be the perfect thing for me to use in my girl-kissing-disaster mecha game DOSIMETER. I'm a lazy developer. I generally prefer making my games to making tools to make my games. I've never made a behavior tree either, so obviously it should be better to use something someone more experienced than me has made. Beehave does, by all means, meet the requirements you might have for AI in a new game project. But I. Don't. Like it!
I mean, look at this! Look at what they expect me to deal with in the year of our lord, 2026:
- We need to use inheritance?
- The functions are basically untyped?
problems
inheritance
First, lets talk about inheritance. Inheritance is part of a paradigm, a way of doing things, known as object oriented programming. In my eyes, object oriented programming is almost always a bad choice and the software industry has largely recognized its flaws. In spite of this, the video game industry at large still embraces it -- especially Godot, the engine I'm using to make this game.
But this video is not really about the flaws of object oriented programming. It's just- because Beehave adopts it, there is one very annoying problem: there's too many files.
Whenever I want my AI to perform a single action,
I have to create a new class which inherits the
ActionLeaf class and each of these
classes have to be in a separate file.
It makes sense that Beehave does this; normally, you would want a developer to implement the behaviors that a gameplay designer can then later weave into an interesting AI. But, I am the gameplay designer, and writing short snippets of code keeps me closer to my flow state than having to manage so much boilerplate every single time I add a new thing I want the AI to do.
type information
Then, there's the lack of type information. When
you inherit ActionLeaf, we need to
implement a function whose type signature looks like
this:
func tick(actor: Node, blackboard: Blackboard) -> int:You might rightly point out that
actor does have a type, but we only
know that actor is a Node
-- one of the most abstract base types available in
Godot. We don't really know anything about what type
actor is; it's close to being useless
type information.
You might also say "well, we know that this
action will never be called with anything other than
the Bug type". But in such a situation,
it's important to recognize that the compiler will
never be able to help us. Since we didn't give the
compiler enough information to make sure that this
is always true automatically, it's now up
to us to make sure this is always true. And I'm
lazy, and I would much rather prefer it if the
compiler slapped me in the face and told me to do it
right instead of running the game and finding out
three hours into a play session that something is
wrong.
neuron
Enter neuron, a stateless behavior tree library for Godot which I wrote to delay the production of girl-kissing cinematics by at least three months. Let me show you how it works.
Say I've got a little guy here who can move to the right, like this:
class_name LittleGuy
extends RigidBody2D
@export var move_right: bool
func _physics_process(delta: float) -> void:
if move_right:
angular_velocity = max(PI, angular_velocity)
func go_right() -> Neuron.Status:
move_right = true
return Neuron.Status.SuccessThis little guy doesn't do anything on his own yet, because they're not thinking about anything. They've got head empty syndrome. There's nothing in there. Like me, some days. Or my cat, most days. So, let's give them a brain and a single brain cell:
class_name LittleGuy
@export var move_right: bool
+var neuron := NeuronAction.new(go_right)
+var brain := NeuronBrain.new(neuron)
+
func _physics_process(delta: float) -> void:
if move_right:
angular_velocity = max(PI, angular_velocity)
func go_right() -> Neuron.Status:
move_right = true
return Neuron.Status.SuccessThis is a very simple behavior tree, but you can
see that we've avoided several type safety issues.
go_right is always going to succeed
because it has a direct reference to this instance
of LittleGuy and if we ever rename
go_right the code will stop working at
compile time until we update all references to it.
All of the code is also in one file, which makes it
easier to find everything.
Oh! Uh... right, and then we need to tell them to actually think:
class_name LittleGuy
@export var move_right: bool
var neuron := NeuronAction.new(go_right)
var brain := NeuronBrain.new(neuron)
func _physics_process(delta: float) -> void:
+ brain.run(delta)
if move_right:
angular_velocity = max(PI, angular_velocity)
func go_right() -> Neuron.Status:
move_right = true
return Neuron.Status.Success...and there you go! Now our little guy goes to the right! Red Luigi would be proud.
Isn't this great? For comparison, in Beehave, we would have to wrap our function call so it can be added to the scene tree, and just be okay with the type cast:
@tool
class_name MoveRight extends ActionLeaf
func tick(actor: Node, _blackboard: Blackboard) -> int:
if actor is LittleGuy:
var little_guy: LittleGuy = actor
return little_guy.move_right()
return FAILUREIn neuron, we can call the function directly:
NeuronAction.new(go_right)
so now we can make a game
So yeah! That's what's driving the design behind neuron and a small peek at how it works. I wanted to use something easier to maintain and that guarantees more things upfront at compile time rather than fucking around and finding out at runtime.
I think it's really cool, and I can't wait to use it in DOSIMETER. Now, the only problem is... I've never designed an AI before-
<Jojo "To be continued..." theme>
thanks
If you liked this video, please consider subscribing to my brand-new newsletter at buttondown.com/exodrifter! I link to all the cool things I'm up to that the algorithm won't tell you about, like what I thought about GDC 2026 and the cute yandere Miku music I'm listening to.
Finally, thanks to my Patreon, Ko-fi, and Discord patrons who are currently supporting me:
- Skull
- Daagr
- Gabby
- Ikethepro18
- Jesse Luna
- Lain Bailey :3
- ZeikJT
- Bone
- Aaron Angert
- BigLube
- Cesar Longoria
- ChiliAllGone
- Cypher Eleven
- Danita Rambo
- laaster (new!)
- Nicolas Morales
- PGComai
I love y'all. Hope to tell you more about DOSIMETER soon!