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 3 与 Godot 4 的区别

在 Godot 4 中,AnimationTree.animation_player 被替换为了 AnimationTree.anim_player。playback 参数路径也发生了变化。如果你正在从 Godot 3 迁移,请查看官方迁移指南

3. 基本设置

设置 AnimationTree 需要四个步骤:

  1. 添加一个 AnimationTree 节点 — 将它与 AnimationPlayer 并列,作为角色(例如 CharacterBody2DCharacterBody3D)的子节点添加。
  2. 设置 anim_player — 在检查器中,将 "Anim Player" 属性指向你的 AnimationPlayer 节点。
  3. 设置 tree_root — 点击检查器中的 "Tree Root" 属性,创建一个新的 AnimationNodeStateMachine
  4. 设置 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
编辑器 vs 代码

在实际开发中,你几乎总是在编辑器里配置 AnimationTree。上面的代码是为了完整性而展示的,但通常你在脚本里只需要 anim_tree.active = true(而且这一项也可以在编辑器中设置)。

4. 状态机基础

AnimationTree 中的状态机基于一个简单的概念运作:状态(state)代表动画,而过渡(transition)定义了在它们之间切换的条件。

添加状态

一旦你的 AnimationTree 的 tree_root 变成了 AnimationNodeStateMachine,就在检查器中双击它打开状态机编辑器:

  1. 在图表区域右键点击并选择 Add Animation
  2. 从你的 AnimationPlayer 中选择一个动画(Idle、Walk、Run、Jump 等)
  3. 对每一个你需要的动画状态重复上述操作
特殊状态

Start 是入口点 — 第一个过渡总是从这里开始。End 是可选的,用于标示状态机已经结束(在嵌套状态机中很有用)。

添加过渡

要在两个状态之间创建过渡:

  1. 点击源状态节点
  2. 拖动到目标状态以创建一个过渡箭头
  3. 点击过渡箭头,在检查器中配置它

过渡属性

属性 说明
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()(手动过渡)。你可以将两种方式混合使用。

GDScript
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.

从代码触发

GDScript
extends 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.gd
extends 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
        )
本示例的 AnimationTree 结构

Root = BlendTree。内部:一个 StateMachine 节点(带有 Idle/Walk/Run/Jump/Fall 状态)连接到一个 OneShot 节点("AttackOneShot"),后者再连接到 Output。攻击动画连接到 OneShot 的 "shot" 输入。

11. 故障排查

"动画没有播放"

检查 AnimationTree 上的 active = true,以及 anim_player 属性是否指向一个有效的 AnimationPlayer。同时确认 AnimationPlayer 里确实存在你所引用的那些名称的动画。

"travel() 没有任何反应"

确保当前状态与目标状态之间存在有效的过渡路径。如果不存在路径,travel() 会静默失败。用 state_machine.get_current_node() 来调试你实际处于哪个状态。

"混合不起作用"

检查你的 blend_position 值是否落在混合空间各点的取值范围之内。如果你的点位于 0.0 和 1.0,那么 5.0 这个值就不会按预期工作。请使用 clamp()

"Warning: AnimationTree is not active"

在编辑器中(检查器复选框)或在你的 _ready() 函数中设置 active = true。AnimationTree 在激活之前不会做任何事情。

"基于条件的过渡没有触发"

请再三确认:(1) 该过渡的 advance_mode 被设置为 Auto,(2) advance_condition 名称与你在代码中设置的完全匹配(区分大小写),以及 (3) 你在 _physics_process() 中每一帧都设置了该参数。

"动画播放了但角色不动"

AnimationTree 只负责动画播放。移动逻辑(velocitymove_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
获取 Godot MCP Pro — $15