Use when using the LimboAI addon — behavior trees and hierarchical state machines (C++ GDExtension) with a visual editor, BTTask subclassing, and a blackboard
Scanned 9/6/2026
Install to Claude Code
npx -y skills add jame581/GodotPrompter --skill limboai --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Limboai?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jame581-limboai)More formats (shields.io, HTML) on the badges page.
---
name: limboai
description: Use when using the LimboAI addon — behavior trees and hierarchical state machines (C++ GDExtension) with a visual editor, BTTask subclassing, and a blackboard
---
# LimboAI
> **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**.
---
## 1. When to use LimboAI
| 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.
---
## 2. Install & setup
### GDExtension (recommended — no custom engine)
1. **Godot AssetLib** → search "LimboAI" → Download → Reload project.
Or download from GitHub Releases and place `addons/limboai/` in `res://addons/limboai/`.
2. Enable the plugin: **Project → Project Settings → Plugins** → tick LimboAI.
3. The `.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.
### Module version (C# or full editor integration)
Download pre-compiled editor + export templates from [GitHub Releases](https://github.com/limbonaut/limboai/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
```
---
## 3. Behavior trees
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
```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
```
### C#
```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).
---
## 4. Custom tasks
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
```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
```
### C#
```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.
---
## 5. Blackboard
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
```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
```
### C#
```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.
---
## 6. Hierarchical state machine (LimboHSM)
`LimboHSM` is a `LimboState` node that manages child `LimboState` nodes. Transitions fire when a state calls `dispatch(event)`. See [references/hsm.md](references/hsm.md) for advanced patterns (any-state transitions, `BTState`, nested HSMs, guards).
### GDScript — scene-tree setup
```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)
```
### C# — scene-tree setup
```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 — state script
```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)
```
### C# — state script
```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);
}
}
```
---
## Implementation checklist
- [ ] `BTPlayer` added as a child of the agent; `behavior_tree` resource assigned
- [ ] Custom tasks annotated with `@tool` (GDScript) or `[Tool]` (C#) for editor display
- [ ] Every `_tick` returns `SUCCESS`, `FAILURE`, or `RUNNING` — never `void`/`null`
- [ ] Blackboard keys documented as exported `StringName` properties (suffix `_var`)
- [ ] C# Blackboard reads cast the `Variant` explicitly (`(float)Blackboard.GetVar(...)`)
- [ ] C# uses module build (not GDExtension) for C# support
- [ ] `BTPlayer.updated` signal used (not deprecated `behavior_tree_finished`)
- [ ] HSM: all `LimboState` nodes wired with `add_transition` before `initialize()`
- [ ] HSM: `set_active(true)` called after `initialize()`
- [ ] HSM transitions are exhaustive — every reachable state has an exit path
- [ ] `BBParam` inspector binding: use module build; GDExtension lacks the param editor UI
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!