limboai
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLimboAI
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· 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.v1.8.0
相关 skill: ai-navigation 用于任务驱动的移动,state-machine 用于核心引擎 FSM(不需要插件时),godot-brainstorming 用于选择 AI 方案。插件: LimboAI · 版本· Godot 4.6+ (GDExtension) · MIT · 来源:https://github.com/limbonaut/limboai · 使用 C++ 编写(GDExtension;也提供引擎模块构建版本)。GDExtension 向 GDScript 暴露接口;C# 需要模块构建版本(v1.8.0 不支持 GDExtension),且 v1.8.0 模块构建版本面向 Godot 4.7。v1.8.0
1. When to use LimboAI
1. 何时使用 LimboAI
| Approach | Best for |
|---|---|
Core-engine FSM ( | 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 ( 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 skill instead.
BTStatestate-machine| 方案 | 最适合场景 |
|---|---|
核心引擎 FSM( | 简单 Agent、少于 5 个状态、无需插件 |
| Beehave(GDScript 插件) | 轻量 BT、纯 GDScript 项目 |
| LimboAI | 同时使用 BT 和 HSM、可视化编辑器、C++ 性能、C# 支持(模块构建版本) |
当需要带有完善可视化调试器的行为树、希望将行为树与分层状态机结合使用( 可桥接两者),或需要 C++ 任务执行速度时,请选择 LimboAI。请注意:LimboAI 要求 Godot 4.6+,无法在 4.3–4.5 上使用。对于更简单的纯 GDScript 行为树,Beehave 是更轻量的替代方案。对于不含 BT 的普通状态机,请使用内置的 skill。
BTStatestate-machine2. Install & setup
2. 安装与设置
GDExtension (recommended — no custom engine)
GDExtension(推荐——无需自定义引擎)
- Godot AssetLib → search "LimboAI" → Download → Reload project.
Or download from GitHub Releases and placeinaddons/limboai/.res://addons/limboai/ - Enable the plugin: Project → Project Settings → Plugins → tick LimboAI.
- The manifest ships at
.gdextension: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"- Godot AssetLib → 搜索 "LimboAI" → 下载 → 重新加载项目。
或从 GitHub Releases 下载,并将放入addons/limboai/。res://addons/limboai/ - 启用插件:项目 → 项目设置 → 插件 → 勾选 LimboAI。
- 清单文件位于
.gdextension: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)
... (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.
**GDExtension 限制:** 无编辑器内文档提示;Inspector 中不提供 `BBParam` 属性编辑器。Module version (C# or full editor integration)
模块版本(C# 或完整编辑器集成)
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
undefined从 GitHub Releases 下载预编译的编辑器版本和导出模板。导出时需要使用自定义引擎。v1.8.0 模块构建版本基于 Godot 4.7(GDExtension 支持 4.6+)。模块构建版本为 C# 提供 NuGet 包:
ini
undefinedAdd local NuGet source to your project:
Add local NuGet source to your project:
dotnet nuget add source path/to/nupkgs --name LimboNugetSource
dotnet nuget add source path/to/nupkgs --name LimboNugetSource
---
---3. Behavior trees
3. 行为树
A resource holds the task tree. runs it each physics frame (or idle/manual). Add as a child of the agent node and assign a resource.
BehaviorTreeBTPlayerBTPlayerBehaviorTreeBehaviorTreeBTPlayerBTPlayerBehaviorTreeGDScript
GDScript
gdscript
undefinedgdscript
undefinedEnemyAI.gd — assign behavior_tree in the Inspector or here
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
undefinedextends 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
undefinedC#
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.UpdateModeIDLE_processPHYSICS_physics_processMANUALbt_player.update(delta)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.UpdateModeIDLE_processPHYSICS_physics_processMANUALbt_player.update(delta)4. Custom tasks
4. 自定义任务
Subclass (multi-tick work) or (immediate check). Annotate with so and work in the editor. Place scripts under ; subfolders become task categories.
BTActionBTCondition@tool_generate_name()_get_configuration_warnings()res://ai/tasks/继承 (多次 tick 执行的工作)或 (即时检查)。添加 注解,使 和 能在编辑器中工作。将脚本放在 下;子文件夹会成为任务分类。
BTActionBTCondition@tool_generate_name()_get_configuration_warnings()res://ai/tasks/GDScript
GDScript
gdscript
@tool
extends BTActiongdscript
@tool
extends BTActionMoves the agent toward a blackboard position each tick.
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@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 BTConditionReturns SUCCESS if the agent is within range of a target node.
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
undefined@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
undefinedC#
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: once before first tick → when status transitions from non-RUNNING → every execution → after SUCCESS or FAILURE.
_setup()_enter()_tick(delta)_exit()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;
}
}任务生命周期: 在首次 tick 前执行一次 → 当状态从非 RUNNING 转为 RUNNING 时执行 → 每次执行时调用 → SUCCESS 或 FAILURE 后执行 。
_setup()_enter()_tick(delta)_exit()5. Blackboard
5. Blackboard
The is a key/value store shared by all tasks in a tree. Use keys () and export them as task properties so the inspector shows a picker.
BlackboardRefCountedStringName&"key"BlackboardRefCountedStringName&"key"GDScript
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 RUNNINGgdscript
@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 RUNNINGC#
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 methods: , , , , , , (debug).
Blackboardhas_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()BlackboardPlanBTPlayerLimboHSMblackboard_plan.create_blackboard(scene_root)Blackboardcsharp
// 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;
}
}常用的 方法:、、、、、、(调试)。
Blackboardhas_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()BlackboardPlanBTPlayerLimboHSMblackboard_plan.create_blackboard(scene_root)Blackboard6. Hierarchical state machine (LimboHSM)
6. 分层状态机(LimboHSM)
LimboHSMLimboStateLimboStatedispatch(event)BTStateLimboHSMLimboStateLimboStatedispatch(event)BTStateGDScript — scene-tree setup
GDScript——场景树设置
gdscript
undefinedgdscript
undefinedCharacter.gd — scene tree: Character → LimboHSM → IdleState, MoveState
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)
undefinedextends 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)
undefinedC# — scene-tree setup
C#——场景树设置
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);
}
}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——状态脚本
gdscript
undefinedgdscript
undefinedIdleState.gd
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)
undefinedextends 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)
undefinedC# — state script
C#——状态脚本
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);
}
}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
实现清单
- added as a child of the agent;
BTPlayerresource assignedbehavior_tree - Custom tasks annotated with (GDScript) or
@tool(C#) for editor display[Tool] - Every returns
_tick,SUCCESS, orFAILURE— neverRUNNING/voidnull - Blackboard keys documented as exported properties (suffix
StringName)_var - C# Blackboard reads cast the explicitly (
Variant)(float)Blackboard.GetVar(...) - C# uses module build (not GDExtension) for C# support
- signal used (not deprecated
BTPlayer.updated)behavior_tree_finished - HSM: all nodes wired with
LimboStatebeforeadd_transitioninitialize() - HSM: called after
set_active(true)initialize() - HSM transitions are exhaustive — every reachable state has an exit path
- inspector binding: use module build; GDExtension lacks the param editor UI
BBParam
- 已将 添加为 Agent 的子节点;已指定
BTPlayer资源behavior_tree - 自定义任务已添加 (GDScript)或
@tool(C#)注解,以便在编辑器中显示[Tool] - 每个 都返回
_tick、SUCCESS或FAILURE——绝不返回RUNNING/voidnull - Blackboard 键已记录为导出的 属性(后缀
StringName)_var - C# Blackboard 读取时显式转换 (
Variant)(float)Blackboard.GetVar(...) - C# 使用模块构建版本(而非 GDExtension)以获得支持
- 使用 信号(而非已弃用的
BTPlayer.updated)behavior_tree_finished - HSM:在 之前,已使用
initialize()连接所有add_transition节点LimboState - HSM:在 之后调用
initialize()set_active(true) - HSM 转换是穷尽的——每个可达状态都有退出路径
- Inspector 绑定:使用模块构建版本;GDExtension 缺少参数编辑器 UI
BBParam