Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when using the LimboAI addon — behavior trees and hierarchical state machines (C++ GDExtension) with a visual editor, BTTask subclassing, and a blackboard
.claude/skills/jame581-limboai/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 215% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 311% | 0% |
> Related skills: ai-navigation for movement the tasks drive, state-machine for core-engine FSM (when you don't need an addon), godot-brainstorming for choosing an AI approach.
> Addon: LimboAI · version v1.8.0 · Godot 4.6+ (GDExtension) · MIT · source: https://github.com/limbonaut/limboai · written in C++ (GDExtension; engine-module build also available). GDExtension exposes GDScript; C# requires the module build (not GDExtension in v1.8.0), and the v1.8.0 module build targets Godot 4.7.
| Approach | Best for | |---|---| | Core-engine FSM (state-machine skill) | Simple agents, < 5 states, no addon | | Beehave (GDScript addon) | Lightweight BT, GDScript-only projects | | LimboAI | BT and HSM together, visual editor, C++ performance, C# support (module build) |
Choose LimboAI when you need a behavior tree with a polished visual debugger, want to combine it with a hierarchical state machine (BTState bridges them), or need C++ task execution speed. Note: LimboAI requires Godot 4.6+ and is not usable on 4.3–4.5. For a simpler GDScript-only behavior tree, Beehave is a lighter alternative. For plain state machines without a BT, use the built-in state-machine skill instead.
Or download from GitHub Releases and place addons/limboai/ in res://addons/limboai/.
.gdextension manifest ships at res://addons/limboai/bin/:ini[configuration] entry_symbol = "limboai_init" compatibility_minimum = "4.2" [libraries] windows.debug.x86_64 = "res://addons/limboai/bin/liblimboai.windows.editor.x86_64.dll" windows.release.x86_64 = "res://addons/limboai/bin/liblimboai.windows.template_release.x86_64.dll" linux.debug.x86_64 = "res://addons/limboai/bin/liblimboai.linux.editor.x86_64.so" linux.release.x86_64 = "res://addons/limboai/bin/liblimboai.linux.template_release.x86_64.so" macos.debug = "res://addons/limboai/bin/liblimboai.macos.editor.framework" macos.release = "res://addons/limboai/bin/liblimboai.macos.template_release.framework" # ... (additional platform entries for linux arm64/rv64, android, iOS, web)
GDExtension limitations: no in-editor documentation tooltips; BBParam property editor not available in the inspector.
Download pre-compiled editor + export templates from GitHub Releases. Requires the custom engine for export. v1.8.0 module builds are based on Godot 4.7 (GDExtension supports 4.6+). The module build ships a NuGet package for C#:
ini# Add local NuGet source to your project: # dotnet nuget add source path/to/nupkgs --name LimboNugetSource
A BehaviorTree resource holds the task tree. BTPlayer runs it each physics frame (or idle/manual). Add BTPlayer as a child of the agent node and assign a BehaviorTree resource.
gdscript# EnemyAI.gd — assign behavior_tree in the Inspector or here extends CharacterBody2D @onready var bt_player: BTPlayer = $BTPlayer func _ready() -> void: # BTPlayer starts executing automatically (active = true by default). # Connect to updated(status) to react when the tree finishes. bt_player.updated.connect(_on_bt_updated) func _on_bt_updated(status: int) -> void: if status == BT.SUCCESS: bt_player.restart() # loop the tree
csharp// EnemyAI.cs using Godot; public partial class EnemyAI : CharacterBody2D { [Export] private BTPlayer _btPlayer; public override void _Ready() { _btPlayer.Updated += OnBtUpdated; } private void OnBtUpdated(int status) { if (status == (int)BT.Status.Success) _btPlayer.Restart(); } }
BTPlayer.UpdateMode controls when the tree ticks: IDLE (every _process), PHYSICS (every _physics_process, default), or MANUAL (call bt_player.update(delta) yourself).
Subclass BTAction (multi-tick work) or BTCondition (immediate check). Annotate with @tool so _generate_name() and _get_configuration_warnings() work in the editor. Place scripts under res://ai/tasks/; subfolders become task categories.
gdscript@tool extends BTAction ## Moves the agent toward a blackboard position each tick. @export var target_pos_var: StringName = &"target_pos" @export var speed: float = 200.0 func _generate_name() -> String: return "MoveToward %s" % LimboUtility.decorate_var(target_pos_var) func _setup() -> void: pass # one-time init; agent and blackboard are available here func _enter() -> void: pass # called when task transitions from non-RUNNING → RUNNING func _tick(delta: float) -> Status: var target: Vector2 = blackboard.get_var(target_pos_var, Vector2.ZERO) if agent.global_position.distance_to(target) < 5.0: return SUCCESS agent.velocity = agent.global_position.direction_to(target) * speed agent.move_and_slide() return RUNNING func _exit() -> void: pass # cleanup after SUCCESS or FAILURE
gdscript@tool extends BTCondition ## Returns SUCCESS if the agent is within range of a target node. @export var target_var: StringName = &"target" @export var distance_max: float = 150.0 var _max_sq: float func _setup() -> void: _max_sq = distance_max * distance_max func _tick(_delta: float) -> Status: var target: Node2D = blackboard.get_var(target_var, null) if not is_instance_valid(target): return FAILURE var in_range := agent.global_position.distance_squared_to( target.global_position) <= _max_sq return SUCCESS if in_range else FAILURE
csharp// MoveTowardTask.cs — place in res://ai/tasks/ using Godot; [Tool] public partial class MoveTowardTask : BTAction { [Export] public StringName TargetPosVar { get; set; } = "target_pos"; [Export] public float Speed { get; set; } = 200f; public override string _GenerateName() => $"MoveToward {LimboUtility.DecorateVar(TargetPosVar)}"; public override void _Setup() { } public override void _Enter() { } public override Status _Tick(double delta) { var target = (Vector2)Blackboard.GetVar(TargetPosVar, Vector2.Zero); var body = (CharacterBody2D)Agent; if (body.GlobalPosition.DistanceTo(target) < 5f) return Status.Success; body.Velocity = body.GlobalPosition.DirectionTo(target) * Speed; body.MoveAndSlide(); return Status.Running; } public override void _Exit() { } }
csharp// InRangeCondition.cs using Godot; [Tool] public partial class InRangeCondition : BTCondition { [Export] public StringName TargetVar { get; set; } = "target"; [Export] public float DistanceMax { get; set; } = 150f; private float _maxSq; public override void _Setup() => _maxSq = DistanceMax * DistanceMax; public override Status _Tick(double delta) { var target = Blackboard.GetVar(TargetVar, default(Variant)).As<Node2D>(); if (!GodotObject.IsInstanceValid(target)) return Status.Failure; var agent2D = (Node2D)Agent; bool inRange = agent2D.GlobalPosition.DistanceSquaredTo( target.GlobalPosition) <= _maxSq; return inRange ? Status.Success : Status.Failure; } }
Task lifecycle: _setup() once before first tick → _enter() when status transitions from non-RUNNING → _tick(delta) every execution → _exit() after SUCCESS or FAILURE.
The Blackboard is a RefCounted key/value store shared by all tasks in a tree. Use StringName keys (&"key") and export them as task properties so the inspector shows a picker.
gdscript@tool extends BTAction @export var speed_var: StringName = &"speed" @export var target_var: StringName = &"target" func _tick(delta: float) -> Status: # Read with a default; use no type annotation for object vars # to avoid errors if the stored instance was freed. var speed: float = blackboard.get_var(speed_var, 100.0) var obj = blackboard.get_var(target_var, null) if not is_instance_valid(obj): return FAILURE # Write back blackboard.set_var(speed_var, speed * 1.1) return RUNNING
csharp// C# has no generic GetVar<T> — cast the returned Variant. using Godot; [Tool] public partial class SampleTask : BTAction { [Export] public StringName SpeedVar { get; set; } = "speed"; [Export] public StringName TargetVar { get; set; } = "target"; public override Status _Tick(double delta) { float speed = (float)Blackboard.GetVar(SpeedVar, 100f); var obj = Blackboard.GetVar(TargetVar, default(Variant)).As<GodotObject>(); if (!GodotObject.IsInstanceValid(obj)) return Status.Failure; Blackboard.SetVar(SpeedVar, speed * 1.1f); return Status.Running; } }
Useful Blackboard methods: has_var(name), erase_var(name), list_vars(), get_vars_as_dict(), bind_var_to_property(name, obj, prop), link_var(name, target_bb, target_name), print_state() (debug).
BlackboardPlan defines the variable schema (types, defaults, hints) and is edited in the Inspector on BTPlayer or LimboHSM. Call blackboard_plan.create_blackboard(scene_root) to create a scoped Blackboard at runtime.
LimboHSM is a LimboState node that manages child LimboState nodes. Transitions fire when a state calls dispatch(event). See references/hsm.md for advanced patterns (any-state transitions, BTState, nested HSMs, guards).
gdscript# Character.gd — scene tree: Character → LimboHSM → IdleState, MoveState extends CharacterBody2D @onready var hsm: LimboHSM = $LimboHSM @onready var idle: LimboState = $LimboHSM/IdleState @onready var move: LimboState = $LimboHSM/MoveState func _ready() -> void: hsm.add_transition(idle, move, idle.EVENT_FINISHED) hsm.add_transition(move, idle, move.EVENT_FINISHED) hsm.initialize(self) hsm.set_active(true)
csharp// Character.cs using Godot; public partial class Character : CharacterBody2D { [Export] private LimboHSM _hsm; [Export] private LimboState _idle; [Export] private LimboState _move; public override void _Ready() { _hsm.AddTransition(_idle, _move, _idle.EventFinished); _hsm.AddTransition(_move, _idle, _move.EventFinished); _hsm.Initialize(this); _hsm.SetActive(true); } }
gdscript# IdleState.gd extends LimboState func _setup() -> void: pass # agent and blackboard available; runs once during hsm.initialize() func _enter() -> void: agent.get_node("AnimationPlayer").play("idle") func _exit() -> void: pass func _update(delta: float) -> void: if Input.get_vector(&"ui_left", &"ui_right", &"ui_up", &"ui_down").length() > 0.1: dispatch(EVENT_FINISHED)
csharp// IdleState.cs using Godot; public partial class IdleState : LimboState { public override void _Setup() { } public override void _Enter() { Agent.GetNode<AnimationPlayer>("AnimationPlayer").Play("idle"); } public override void _Exit() { } public override void _Update(double delta) { if (Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down").Length() > 0.1f) Dispatch(EventFinished); } }
BTPlayer added as a child of the agent; behavior_tree resource assigned@tool (GDScript) or [Tool] (C#) for editor display_tick returns SUCCESS, FAILURE, or RUNNING — never void/nullStringName properties (suffix _var)Variant explicitly ((float)Blackboard.GetVar(...))BTPlayer.updated signal used (not deprecated behavior_tree_finished)LimboState nodes wired with add_transition before initialize()set_active(true) called after initialize()BBParam inspector binding: use module build; GDExtension lacks the param editor UI| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 17,621 | 10,496 | -40% | 1 | 1 | 0% | 3,368 | 5,963 | +77% | 0 | 0 | — |
case-02 | fail→pass | 16,429 | 10,244 | -38% | 1 | 1 | 0% | 3,072 | 5,804 | +89% | 0 | 0 | — |
case-03 | pass→pass | 14,259 | 11,517 | -19% | 1 | 1 | 0% | 2,699 | 6,008 | +123% | 0 | 0 | — |
case-04 | pass→pass | 10,473 | 4,072 | -61% | 1 | 1 | 0% | 1,700 | 4,469 | +163% | 0 | 0 | — |
case-05 | pass→pass | 15,767 | 11,048 | -30% | 1 | 1 | 0% | 2,809 | 5,729 | +104% | 0 | 0 | — |
case-06 | fail→pass | 15,729 | 5,396 | -66% | 1 | 1 | 0% | 2,345 | 4,797 | +105% | 0 | 0 | — |
case-07 | fail→pass | 9,049 | 5,273 | -42% | 1 | 1 | 0% | 1,514 | 4,771 | +215% | 0 | 0 | — |
case-08 | pass→pass | 6,861 | 3,750 | -45% | 1 | 1 | 0% | 1,136 | 4,528 | +299% | 0 | 0 | — |
case-09 | pass→pass | 11,556 | 7,900 | -32% | 1 | 1 | 0% | 1,962 | 5,153 | +163% | 0 | 0 | — |
case-10 | fail→pass | 7,970 | 5,884 | -26% | 1 | 1 | 0% | 1,191 | 4,892 | +311% | 0 | 0 | — |
case-11 | pass→pass | 6,866 | 4,974 | -28% | 1 | 1 | 0% | 1,223 | 4,694 | +284% | 0 | 0 | — |
case-12 | pass→pass | 11,962 | 5,317 | -56% | 1 | 1 | 0% | 2,182 | 4,707 | +116% | 0 | 0 | — |
case-13 | pass→pass | 6,463 | 3,842 | -41% | 1 | 1 | 0% | 1,024 | 4,516 | +341% | 0 | 0 | — |
case-14 | fail→pass | 8,595 | 6,206 | -28% | 1 | 1 | 0% | 1,384 | 4,983 | +260% | 0 | 0 | — |
case-15 | fail→pass | 6,252 | 4,126 | -34% | 1 | 1 | 0% | 931 | 4,548 | +389% | 0 | 0 | — |
case-16 | pass→pass | 16,496 | 6,667 | -60% | 1 | 1 | 0% | 2,547 | 4,889 | +92% | 0 | 0 | — |
case-17 | fail→pass | 10,416 | 5,843 | -44% | 1 | 1 | 0% | 1,772 | 4,937 | +179% | 0 | 0 | — |
case-18 | pass→pass | 12,796 | 8,636 | -33% | 1 | 1 | 0% | 1,952 | 5,343 | +174% | 0 | 0 | — |
case-19 | pass→pass | 8,910 | 6,110 | -31% | 1 | 1 | 0% | 1,522 | 4,847 | +218% | 0 | 0 | — |
case-20 | fail→pass | 10,103 | 5,318 | -47% | 1 | 1 | 0% | 1,815 | 4,859 | +168% | 0 | 0 | — |
case-21 | pass→pass | 11,925 | 4,638 | -61% | 1 | 1 | 0% | 1,956 | 4,635 | +137% | 0 | 0 | — |
case-22 | pass→pass | 10,700 | 5,635 | -47% | 1 | 1 | 0% | 1,755 | 4,730 | +170% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.