okay so I've been building out enemy AI for a wave-based action game in Godot 4 and went through the whole "should I implement a proper behavior tree" phase. watched the videos, started writing node base classes and composite nodes, and after a week realized I was writing a framework instead of a game.
ended up pivoting to a simple utility scorer and honestly it's been way more enjoyable to tune. each action has an evaluate() function returning a 0–1 weight based on game state, and the AI just picks the highest scorer each tick:
class_name UtilityAI extends Node
var actions: Array[AIAction] = []
func tick(context: AIContext) -> void:
var best_action: AIAction = null
var best_score := 0.0
for action in actions:
if not action.can_execute(context):
continue
var score := action.evaluate(context)
if score > best_score:
best_score = score
best_action = action
if best_action and best_score > 0.0:
best_action.execute(context)
each AIAction is a Resource subclass. the scoring functions are where enemy personality comes from. an aggressive enemy weights attack actions higher when in range, a skittish one cranks retreat when health drops below a threshold. tuning feels like adjusting sliders instead of debugging composite node traversal logic.
where it breaks down: sequenced behavior. "patrol → investigate sound → engage" has ordering constraints that utility scoring doesn't model naturally. I ended up adding cooldown tracking per-action to prevent thrashing, and at that point I started wondering if I was just reinventing BTs from the bottom up anyway.
I know Limbo AI is the go-to Godot BT plugin and people seem to love it, but for simpler enemy archetypes does anyone else find utility AI just... gets there faster? or is there a complexity threshold where BTs become clearly the right call and I'm just not there yet?