toc

content

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

This 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.Success

This 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 FAILURE

In 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!

newsletter

Sign up for my newsletter!

meta

tags: behavior-tree, dosimeter, godot, gdscript

created: published: modified:

crossposts: @ youtube @ bsky @ vt.social

backlinks: exodrifter

commit: ff5b2822