AnimationTree 状态机
Godot 4 完全指南
状态机、过渡、混合树、OneShot、travel() 与条件的对比的完整指南 — 附带真实的 GDScript 代码示例。
目录
1. 简介
AnimationTree 是 Godot 4 用于复杂动画混合与状态过渡的系统。如果你曾经尝试用散落在代码各处的 AnimationPlayer.play() 调用来管理多个动画,你就会知道这有多快变得难以维护。AnimationTree 通过一张由状态和过渡组成的可视化图表解决了这个问题。
尽管 AnimationTree 极其强大,但它的官方文档却相当稀少,常常让开发者只能靠猜测。本指南从基本设置一路讲到高级混合树,并提供可以直接复制到你项目中的真实 GDScript 代码。
状态机、过渡、混合空间(1D 与 2D)、用于攻击的 OneShot、travel() 与条件的对比,以及一套完整的角色控制器模式。
2. 前提条件
在设置 AnimationTree 之前,你需要具备:
- 一个已经创建了至少 Idle、Walk、Run 和 Jump 动画的
AnimationPlayer节点 - AnimationPlayer 必须是你添加 AnimationTree 的那个节点的兄弟节点或子节点(通常两者都是角色根节点的子节点)
- Godot 4.x(本指南使用 Godot 4 API — AnimationTree 的 API 相比 Godot 3 有重大变化)
在 Godot 4 中,AnimationTree.animation_player 被替换为了 AnimationTree.anim_player。playback 参数路径也发生了变化。如果你正在从 Godot 3 迁移,请查看官方迁移指南。
3. 基本设置
设置 AnimationTree 需要四个步骤:
-
添加一个 AnimationTree 节点
— 将它与 AnimationPlayer 并列,作为角色(例如
CharacterBody2D或CharacterBody3D)的子节点添加。 -
设置
anim_player— 在检查器中,将 "Anim Player" 属性指向你的 AnimationPlayer 节点。 -
设置
tree_root— 点击检查器中的 "Tree Root" 属性,创建一个新的AnimationNodeStateMachine。 -
设置
active = true— 在检查器中勾选 "Active" 复选框,或在代码中设置。
你的场景树应该看起来像这样:
CharacterBody2D (or CharacterBody3D)
+-- Sprite2D (or Sprite3D)
+-- CollisionShape2D
+-- AnimationPlayer <-- has Idle, Walk, Run, Jump animations
+-- AnimationTree <-- points to AnimationPlayer above
GDScript
# Minimal code setup (usually done via the editor instead):
@onready var anim_tree: AnimationTree = $AnimationTree
func _ready() -> void:
# If you set these in the Inspector, you don't need this code
anim_tree.anim_player = ^"../AnimationPlayer"
anim_tree.tree_root = AnimationNodeStateMachine.new()
anim_tree.active = true
在实际开发中,你几乎总是在编辑器里配置 AnimationTree。上面的代码是为了完整性而展示的,但通常你在脚本里只需要 anim_tree.active = true(而且这一项也可以在编辑器中设置)。
4. 状态机基础
AnimationTree 中的状态机基于一个简单的概念运作:状态(state)代表动画,而过渡(transition)定义了在它们之间切换的条件。
添加状态
一旦你的 AnimationTree 的 tree_root 变成了 AnimationNodeStateMachine,就在检查器中双击它打开状态机编辑器:
- 在图表区域右键点击并选择 Add Animation
- 从你的 AnimationPlayer 中选择一个动画(Idle、Walk、Run、Jump 等)
- 对每一个你需要的动画状态重复上述操作
Start 是入口点 — 第一个过渡总是从这里开始。End 是可选的,用于标示状态机已经结束(在嵌套状态机中很有用)。
添加过渡
要在两个状态之间创建过渡:
- 点击源状态节点
- 拖动到目标状态以创建一个过渡箭头
- 点击过渡箭头,在检查器中配置它
过渡属性
| 属性 | 说明 |
|---|---|
advance_mode |
Auto — 当其条件为 true 时触发。Enabled — 始终可供 travel() 使用。Disabled — 被阻止。 |
advance_condition |
一个布尔参数的名称(例如 is_moving)。为 true 时,过渡会自动触发。 |
xfade_time |
交叉淡入淡出的时长(秒)。让动画之间平滑混合。典型值:0.1 – 0.3 秒。 |
switch_mode |
Immediate — 立即切换。Sync — 匹配播放位置。AtEnd — 等待当前动画播放完毕。 |
一个平台游戏角色的典型设置:
Start --> Idle
Idle --> Walk (condition: is_moving)
Walk --> Idle (condition: is_idle)
Idle --> Jump (condition: is_jumping)
Walk --> Jump (condition: is_jumping)
Jump --> Fall (condition: is_falling)
Fall --> Idle (condition: is_on_floor, switch_mode: Immediate)
5. 从代码控制状态机
驱动状态机主要有两种方式:设置条件参数(自动过渡)和调用 travel()(手动过渡)。你可以将两种方式混合使用。
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")
func _physics_process(delta: float) -> void:
# ... movement logic here ...
move_and_slide()
_update_animation_parameters()
func _update_animation_parameters() -> void:
# Approach 1: Set condition parameters — transitions fire automatically
anim_tree.set("parameters/conditions/is_moving", velocity.length() > 10.0)
anim_tree.set("parameters/conditions/is_idle", velocity.length() <= 10.0)
anim_tree.set("parameters/conditions/is_on_floor", is_on_floor())
anim_tree.set("parameters/conditions/is_jumping", velocity.y < 0 and not is_on_floor())
anim_tree.set("parameters/conditions/is_falling", velocity.y > 0 and not is_on_floor())
# Approach 2: Use travel() for direct control
# if velocity.length() > 10.0:
# state_machine.travel("Walk")
# else:
# state_machine.travel("Idle")
对于设置在过渡上的条件,参数遵循 parameters/conditions/<condition_name> 这一模式。条件名称必须与你在编辑器中为过渡设置的 advance_condition 完全一致。
6. travel() 与条件的对比
基于条件(推荐用于移动)
每一帧设置布尔参数,让过渡自动触发。这种方式更具声明式风格,能让你的代码保持整洁。状态机会为你处理过渡逻辑、交叉淡入淡出以及边界情况。
# Declarative: just describe the current state of the world
anim_tree.set("parameters/conditions/is_moving", velocity.length() > 10.0)
anim_tree.set("parameters/conditions/is_on_floor", is_on_floor())
travel()(推荐用于一次性动作)
travel() 请求进行状态过渡。它会遵循过渡规则 — 如果从当前状态到目标状态不存在有效路径,该调用就会被忽略。这使得反复调用它是安全的。可以用它来处理一次性触发,比如攻击、表情动作或过场动画。
# travel() — requests a transition (respects transition rules)
state_machine.travel("Jump")
# Get current state name
var current: StringName = state_machine.get_current_node()
print(current) # "Idle", "Walk", etc.
# Check if travel is possible
var is_playing: bool = state_machine.is_playing()
print(is_playing)
条件用于连续状态(待机、行走、奔跑、下落)。travel()用于事件触发的状态(攻击、闪避、交互)。许多项目会两者并用:移动用条件,战斗动作用 travel()。
7. 混合树
混合树让你可以基于一个连续值在多个动画之间平滑插值,而不是在离散状态之间硬切换。这非常适合行走/奔跑速度的混合以及方向性移动。
BlendSpace1D
沿单一轴在两个或多个动画之间进行 1D 混合。常见用途:根据移动速度混合 Walk 和 Run。
在编辑器中,在你的状态机内部(或作为独立的树根节点)创建一个 BlendSpace1D 节点。添加动画点:
# BlendSpace1D setup (in editor):
# Point 0.0 = Walk animation
# Point 1.0 = Run animation
# Control from code:
var speed_factor: float = clamp(velocity.length() / max_speed, 0.0, 1.0)
anim_tree.set("parameters/WalkRun/blend_position", speed_factor)
BlendSpace2D
使用两个轴进行 2D 混合。非常适合 8 方向移动,或角色可朝任意方向移动的俯视角游戏。
# BlendSpace2D setup (in editor):
# Place animations at positions:
# Idle at (0, 0)
# WalkRight at (1, 0), WalkLeft at (-1, 0)
# WalkUp at (0, -1), WalkDown at (0, 1)
# Diagonals at corners
# Control from code:
var input_dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
anim_tree.set("parameters/Movement/blend_position", input_dir)
BlendSpace2D 支持多种混合模式:默认的三角剖分在大多数情况下都能很好地工作。如果你想要不带插值的像素风动画,也可以选择离散模式(吸附到最近的点)。
8. 常见节点类型
AnimationTree 支持多种可以组合起来创建复杂动画行为的节点类型:
| 节点类型 | 用途 |
|---|---|
AnimationNodeStateMachine |
带过渡的状态机。最常见的根节点。状态可以是动画,也可以是嵌套的状态机。 |
AnimationNodeBlendSpace1D |
沿一个轴的 1D 混合。行走/奔跑速度、瞄准角度等。 |
AnimationNodeBlendSpace2D |
使用两个轴的 2D 混合。方向性移动、侧移混合。 |
AnimationNodeBlendTree |
由混合操作组成的图。用自定义逻辑组合多个混合节点。 |
AnimationNodeAdd2 |
加法混合。在一个动画之上叠加另一个动画(例如在行走之上叠加瞄准偏移)。 |
AnimationNodeTimeScale |
速度控制。在运行时让动画播放得更快或更慢。 |
AnimationNodeOneShot |
一次性动画叠加。非常适合攻击、表情动作、受击反应。 |
AnimationNodeTransition |
带交叉淡入淡出地在多个输入之间切换。在更简单的设置中可作为状态机的替代方案。 |
9. OneShot 模式(攻击、表情动作)
OneShot 节点是 AnimationTree 中最实用的模式之一。它会在你的基础动画之上播放一次性动画(就像在行走时播放一次挥砍攻击),然后自动返回基础动画。
在 BlendTree 中设置
要使用 OneShot,你的 AnimationTree 根节点(或其中的某个状态)需要是一个 BlendTree:
# BlendTree graph setup:
#
# [StateMachine] ---> [OneShot "AttackOneShot"] ---> [Output]
# (base locomotion) ^
# |
# [Animation "Attack"]
# (shot input)
#
# The StateMachine provides the base (idle/walk/run).
# The Attack animation is connected to the OneShot's "shot" input.
从代码触发
GDScriptextends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
func _input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
_play_attack()
func _play_attack() -> void:
# Fire the one-shot animation
anim_tree.set(
"parameters/AttackOneShot/request",
AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
)
func _process(delta: float) -> void:
# Check if the one-shot is currently active
var is_attacking: bool = anim_tree.get("parameters/AttackOneShot/active")
if is_attacking:
# Optionally disable movement during attack
pass
OneShot 请求常量
| 常量 | 效果 |
|---|---|
ONE_SHOT_REQUEST_FIRE |
开始播放一次性动画 |
ONE_SHOT_REQUEST_ABORT |
取消一次性动画并立即返回基础动画 |
ONE_SHOT_REQUEST_FADE_OUT |
淡出一次性动画(使用 fadeout_time 属性) |
对于连击系统,可以依次使用多个 OneShot 节点,或在 OneShot 的 shot 输入内使用一个带有 Attack1 → Attack2 → Attack3 状态的嵌套状态机。
10. 实战示例:完整的角色控制器
这是一个完整的 2D 平台游戏角色脚本,它将状态机移动与 OneShot 攻击结合在一起。这是一个可以直接用于生产、并能适配到你自己项目中的模式。
GDScript — player.gdextends CharacterBody2D
const SPEED := 200.0
const JUMP_VELOCITY := -350.0
const SPRINT_MULTIPLIER := 1.6
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var playback: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")
@onready var sprite: Sprite2D = $Sprite2D
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
func _ready() -> void:
anim_tree.active = true
func _physics_process(delta: float) -> void:
_apply_gravity(delta)
_handle_jump()
_handle_movement()
move_and_slide()
_update_animation()
func _apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
func _handle_jump() -> void:
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
func _handle_movement() -> void:
var direction := Input.get_axis("move_left", "move_right")
var is_sprinting := Input.is_action_pressed("sprint")
var current_speed := SPEED * (SPRINT_MULTIPLIER if is_sprinting else 1.0)
if direction != 0.0:
velocity.x = direction * current_speed
sprite.flip_h = direction < 0.0
else:
velocity.x = move_toward(velocity.x, 0.0, SPEED)
func _update_animation() -> void:
# Skip animation updates during attack
var is_attacking: bool = anim_tree.get("parameters/AttackOneShot/active")
if is_attacking:
return
if not is_on_floor():
if velocity.y < 0:
playback.travel("Jump")
else:
playback.travel("Fall")
elif abs(velocity.x) > 10.0:
if Input.is_action_pressed("sprint"):
playback.travel("Run")
else:
playback.travel("Walk")
else:
playback.travel("Idle")
func _input(event: InputEvent) -> void:
if event.is_action_pressed("attack") and is_on_floor():
anim_tree.set(
"parameters/AttackOneShot/request",
AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
)
Root = BlendTree。内部:一个 StateMachine 节点(带有 Idle/Walk/Run/Jump/Fall 状态)连接到一个 OneShot 节点("AttackOneShot"),后者再连接到 Output。攻击动画连接到 OneShot 的 "shot" 输入。
11. 故障排查
检查 AnimationTree 上的 active = true,以及 anim_player 属性是否指向一个有效的 AnimationPlayer。同时确认 AnimationPlayer 里确实存在你所引用的那些名称的动画。
确保当前状态与目标状态之间存在有效的过渡路径。如果不存在路径,travel() 会静默失败。用 state_machine.get_current_node() 来调试你实际处于哪个状态。
检查你的 blend_position 值是否落在混合空间各点的取值范围之内。如果你的点位于 0.0 和 1.0,那么 5.0 这个值就不会按预期工作。请使用 clamp()。
在编辑器中(检查器复选框)或在你的 _ready() 函数中设置 active = true。AnimationTree 在激活之前不会做任何事情。
请再三确认:(1) 该过渡的 advance_mode 被设置为 Auto,(2) advance_condition 名称与你在代码中设置的完全匹配(区分大小写),以及 (3) 你在 _physics_process() 中每一帧都设置了该参数。
AnimationTree 只负责动画播放。移动逻辑(velocity、move_and_slide())是独立的,必须在你脚本的 _physics_process() 中实现。
想让 AI 帮你构建 AnimationTree?
Godot MCP Pro 可以创建状态机、添加状态和过渡、配置混合树、设置参数 — 全部只需一条提示。告诉你的 AI 助手你想要什么样的动画行为,它就会为你构建整个 AnimationTree。
- create_animation_tree
- add_state_machine_state
- add_state_machine_transition
- set_blend_tree_node
- set_tree_parameter
- get_animation_tree_structure