Godot动画状态机实战:从AnimationTree到角色动画控制
1. 项目概述
在游戏开发中,动画系统是赋予角色灵魂的关键。一个流畅、响应迅速且逻辑清晰的动画表现,直接决定了玩家的操作手感和游戏体验。如果你还在用一堆零散的AnimationPlayer节点,通过play()和queue()手动拼接动画,那么恭喜你,你即将打开新世界的大门。今天,我们就来深入探讨 Godot 引擎中一个强大但常被新手开发者低估的组件——AnimationTree,特别是其核心功能AnimationNodeStateMachine(动画状态机)。
简单来说,这个项目就是教你如何告别“动画脚本大杂烩”,构建一个专业、可维护、逻辑清晰的动画控制系统。我们将从一个基础的AnimationTree配置开始,逐步深入到状态机的设计哲学、状态切换的逻辑实现,最终打造一个能够应对复杂游戏角色(如跑、跳、攻击、受伤)的动画管理系统。无论你是正在制作 2D 平台跳跃游戏,还是 3D 动作 RPG,这套方法论都能让你的动画层代码变得井井有条。接下来,我会结合我多年踩坑的经验,手把手带你从零搭建,并分享那些官方文档里不会写的“实战心得”。
2. AnimationTree 核心概念与基础配置
在深入状态机之前,我们必须先理解AnimationTree是什么,以及它和AnimationPlayer的关系。很多开发者一开始会混淆这两者,导致配置错误,动画无法播放。
2.1 AnimationTree 与 AnimationPlayer 的角色分工
你可以把AnimationPlayer想象成一个动画仓库管理员。它的职责非常明确:存储和管理所有动画片段(Clip)。你可以在里面创建idle(待机)、run(奔跑)、jump(跳跃)等动画,设置关键帧,调整曲线。AnimationPlayer擅长“播放某一个具体的动画”。
而AnimationTree则是一个动画调度中心或导演。它本身不存储动画,而是引用一个或多个AnimationPlayer中的动画,并根据一套你定义的规则(状态机、混合树等)来决定:在某一时刻,应该播放哪个动画?如何从一个动画平滑过渡到另一个?多个动画如何混合(比如上半身攻击、下半身奔跑)?AnimationTree擅长“管理和决策动画的播放逻辑”。
为什么需要 AnimationTree?想象一个简单的场景:角色从“奔跑”切换到“跳跃”。如果只用AnimationPlayer,你需要在代码里精确计算run动画的结束点,然后调用play(“jump”)。这会导致两个问题:1. 切换生硬,没有过渡。2. 如果run是循环动画,你很难找到一个“完美”的切换点。AnimationTree的StateMachine和混合节点可以优雅地解决这些问题,提供交叉淡入淡出(Cross-fade)和基于条件(如速度、是否在地面)的自动切换。
2.2 基础配置步骤详解
让我们一步步搭建一个最基础的AnimationTree工作环境。假设我们有一个名为Player的 2D 或 3D 场景,里面已经有一个配置好idle和run动画的AnimationPlayer节点。
第一步:创建并关联节点
- 在
Player场景根节点下,添加一个AnimationTree节点。 - 在
AnimationTree节点的检视器(Inspector)中,找到Tree Root属性。点击下拉菜单,选择New AnimationNodeStateMachine。这会创建一个空的状态机作为动画树的根。 - 将Anim Player属性指向你场景中已有的
AnimationPlayer节点。
至此,AnimationTree已经和你的动画数据源连接上了。但此时运行游戏,角色不会有任何动画,因为状态机里是空的。
第二步:在状态机中添加第一个状态
- 选中
AnimationTree节点,编辑器底部会多出一个AnimationTree面板。如果没看到,请确保AnimationTree节点的Active复选框已被勾选。 - 在AnimationTree面板中,你应该能看到一个空的图,只有Start和End两个节点。右键点击空白处,选择“添加节点” -> “动画” -> “Animation”。你也可以点击图上的“+”号按钮。
- 在弹出的窗口中,为这个状态命名,例如
Idle。然后在右侧的Animation属性下拉菜单中,选择AnimationPlayer里对应的idle动画。 - 用同样的方法再添加一个
Run状态,并关联run动画。
现在你的状态机里有了两个状态节点,但它们还是孤立的,没有连接起来。
第三步:连接状态并设置过渡
- 点击图上方工具栏的“连接节点”按钮(图标像一条带箭头的线),或者按
A键。 - 先点击
Idle状态,再拖动到Run状态上。这会创建一条从Idle指向Run的过渡线(Transition)。 - 同样地,从
Run状态拖动一条线连回Idle状态。现在你有了双向连接。
第四步:配置过渡参数点击Idle->Run的过渡线,右侧检视器会显示其属性:
- Xfade Time: 交叉淡入淡出时间。设为
0.2秒,意味着从Idle切换到Run会有 0.2 秒的混合时间,动画过渡更平滑。 - Switch Mode: 切换模式。常用的是Immediate(立即切换)和At End(等当前动画播完再切换)。对于
Idle和Run这种可随时中断的动画,用Immediate。 - Advance Mode: 前进模式。这是控制状态切换逻辑的核心!我们稍后会详细讲。先保持为Disabled。
对Run->Idle的过渡线也进行类似设置。
第五步:从代码启动状态机并测试状态机配置好了,但它还不知道从哪里开始。我们需要告诉它初始状态,并通过代码驱动状态切换。
在你的角色控制脚本(例如player.gd)中:
extends CharacterBody2D # 或 CharacterBody3D @onready var animation_tree = $AnimationTree @onready var state_machine = animation_tree.get("parameters/playback") func _ready(): # 确保AnimationTree已激活 animation_tree.active = true # 让状态机从Idle状态开始 state_machine.start(“Idle”) func _physics_process(delta): var velocity = Vector2.ZERO # ... 你的移动逻辑,计算 velocity ... # 根据速度决定动画状态 if velocity.length() > 0.1: state_machine.travel(“Run”) else: state_machine.travel(“Idle”)关键点:
@onready var state_machine = animation_tree.get(“parameters/playback”):这行代码获取了状态机的播放控制器,它是AnimationNodeStateMachinePlayback类型,专门用于控制状态跳转。state_machine.start(“Idle”):在_ready中初始化,指定起始状态。state_machine.travel(“目标状态名”):这是切换状态的核心方法。travel()方法会智能地根据你绘制的过渡路径(如果存在)进行状态切换,而不是瞬间“传送”。
现在运行游戏,控制角色移动,你应该能看到Idle和Run动画之间有了平滑的过渡。
实操心得:很多新手会忘记在
_ready中调用start()或者忘记设置animation_tree.active = true,导致状态机不工作。务必把这两步作为固定流程。另外,travel()是状态切换的推荐方法,它尊重你定义的过渡逻辑。直接设置animation_tree[“parameters/StateMachine/current_state”]虽然也能切换,但会忽略过渡效果。
3. 状态机(StateMachine)的深度解析与高级配置
基础跑通后,我们来深入状态机的核心机制。一个健壮的状态机不仅仅是状态的集合,更是清晰逻辑的体现。
3.1 状态类型与嵌套结构
状态机中的每个节点不一定直接播放动画,它可以是:
- Animation 状态:最常用的类型,直接关联
AnimationPlayer中的一个动画。 - BlendTree 状态:关联一个混合树。例如,你可以创建一个
Move混合树状态,内部根据速度混合Walk、Run、Sprint动画,对外则作为一个整体状态。 - 另一个 StateMachine:这就是嵌套状态机。例如,一个
Combat主状态,内部嵌套一个包含Attack1、Attack2、AttackCombo的子状态机。这极大地提高了复杂动画逻辑的组织性。
如何创建嵌套状态机:
- 在AnimationTree面板,右键 -> “添加节点” -> “状态机”。
- 命名,比如
Combat。 - 双击
Combat状态节点,或者点击其右上角的铅笔图标,即可进入这个子状态机进行编辑。编辑完成后,点击面板左上角的“向上”箭头返回父状态机。
3.2 过渡(Transition)的三种模式与选择策略
点击任意过渡线,查看其Switch Mode属性,这是决定切换行为的核心:
Immediate(立即):
- 行为:立刻开始播放目标状态动画,并与当前状态的剩余部分进行混合。
- 适用场景:绝大多数即时反应的动作切换。如
Idle->Run,Run->Jump,Any State->Hit(受击)。它保证了响应的及时性。 - 注意事项:如果当前动画播放到一半,切换时会从目标动画的开头播放。对于连贯性要求高的动作(如攻击连招),可能需要配合
At End或Sync模式。
Sync(同步):
- 行为:立即切换到下一个状态,但新状态会从旧状态当前的时间点开始播放。
- 适用场景:多个动画长度不同,但你想保持动作“相位”同步。例如,角色有
Walk(1秒循环)和Run(0.8秒循环)两个动画。当从Walk的 0.3 秒切换到Run时,Run会从它自身时间轴的 0.3 秒开始播放。这能避免步伐突然错乱,但要求动画师对动画节奏有较好规划。 - 实战技巧:在混合空间(BlendSpace)中混合不同时长的循环动画时,
Sync模式非常有用。
At End(结束时):
- 行为:等待当前状态动画播放到最后一帧,然后切换到下一个状态的开头。
- 适用场景:需要完整播放不可中断的动画。例如,
Attack->Idle,你必须让攻击动作完全打完才能回到待机。或者Jump->Fall,跳跃的上升段必须播完才能进入下落段。 - 注意事项:滥用此模式会导致操作反馈延迟。通常只为关键的、有完整表演意义的动画设置
At End过渡。
我的经验法则:默认使用Immediate。只有当动画有明显的“收招”或“预备”帧,且中断会显得很突兀时,才考虑At End。Sync则专门用于解决循环动画混合时的节奏对齐问题。
3.3 前进条件(Advance Condition)与表达式(Advance Expression)
这是实现自动状态切换的灵魂。它让状态机从“被动响应代码指令”变为“主动感知游戏世界”。
Advance Condition(基础版): 在过渡线的属性中,有一个Advance Condition输入框。你可以在这里填入一个布尔变量的名字,例如
is_grounded。当此过渡的Advance Mode设置为Auto时,Godot 会每帧检查AnimationTree的parameters中是否存在一个名为is_grounded的布尔参数,且其值是否为true。如果是,则自动执行此过渡。- 局限:它只能检测“真”(truthy)。你不能写
!is_grounded来表示“不在地面时切换”。要实现反向逻辑,你需要创建另一个变量is_in_air并设置为!is_grounded,然后为is_in_air创建另一条过渡线。这很繁琐。
- 局限:它只能检测“真”(truthy)。你不能写
Advance Expression(表达式版 - Godot 4 强大功能): 这是更推荐的方式。在Advance Expression输入框中,你可以写入任何返回布尔值的 GDScript 表达式。
velocity.length() > 5:速度大于5时切换。is_on_floor() and Input.is_action_just_pressed(“jump”):在地面且按下跳跃键时切换。health <= 0:生命值归零时切换到死亡状态。current_state == “Attack” and animation_tree.get(“parameters/TimeScale/scale”) > 0.95:当前是攻击状态且动画播放进度超过95%时(可用于连招判定)。
配置 Advance Expression 的关键步骤:
- 在
AnimationTree节点的检视器中,找到Advance Expression Base Node属性。 - 将这个属性拖拽指向你的角色主节点(例如
CharacterBody2D)。这个节点就是表达式中变量的查找上下文。如果你的velocity、is_on_floor()等方法定义在角色脚本里,就必须指向它。 - 在过渡线上,设置Advance Mode为Auto,然后在Advance Expression中写入你的条件表达式。
避坑指南:
Advance Expression Base Node没设置是表达式失效的最常见原因。表达式是区分大小写的,且访问的是该基础节点上的属性或方法。例如,如果你的脚本里变量叫isAttacking(驼峰式),表达式里也必须写isAttacking。
3.4 状态机的 travel() 路径查找与优先级
我们之前用了state_machine.travel(“目标状态”)。这个方法很智能,它会尝试在状态图中找到一条从当前状态到目标状态的路径。路径由你绘制的过渡线定义。
路径查找规则:
- 使用 A* 算法寻找最短路径(以过渡次数为成本)。
- 如果存在路径,则按顺序触发路径上的所有过渡(遵守各自的
Xfade Time和Switch Mode)。 - 如果不存在路径(比如两个状态间没有连通的过渡线),则直接“传送”到目标状态。
过渡优先级(Priority): 当一个状态有多个出方向(过渡线)时,travel()如何选择?这时Priority属性就起作用了。优先级数字越小,优先级越高。 例如,从Idle状态出发,有两条自动过渡线:一条到Run(条件:velocity.length() > 0,优先级 0),另一条到Hurt(条件:health_changed == true,优先级 1)。当角色同时满足移动和受伤条件时,由于Run的优先级更高(0 < 1),状态机会优先切换到Run。你可以利用这个机制来定义状态切换的“权重”。
4. 混合树(BlendTree)与状态机的协同作战
状态机负责宏观逻辑(做什么动作),而混合树负责微观融合(动作如何混合)。两者结合,威力无穷。
4.1 在状态机内使用混合树节点
最常见的模式是:一个状态(如Locomotion移动)本身不是一个单一动画,而是一个BlendSpace2D或AnimationNodeBlendTree。
实战:创建基于速度的 2D 混合空间
- 在状态机中,添加一个“混合空间2D”节点,命名为
Locomotion。 - 双击进入
Locomotion混合空间。 - 在混合空间面板中,定义X轴为
Speed(范围 0 到 400),Y轴为Direction(范围 -1 到 1,表示向左或向右,对于八方向移动有用)。 - 添加点:(0, 0) 关联
idle动画;(200, 0) 关联walk动画;(400, 0) 关联run动画。你还可以在 (200, 1) 和 (200, -1) 添加对应的转身行走动画。 - 回到父状态机,将
Idle、Run等独立状态删除,只保留一个Locomotion状态作为移动状态。 - 在你的角色脚本中,不再使用
travel()在Idle和Run间切换,而是更新AnimationTree的参数来驱动混合空间:func _physics_process(delta): var input_vector = Input.get_vector(“move_left”, “move_right”, “move_up”, “move_down”) var speed = velocity.length() var direction = sign(velocity.x) # 简单示例,用x方向决定面向 # 更新混合空间参数 animation_tree.set(“parameters/Locomotion/blend_position”, Vector2(speed, direction)) # 或者使用更简洁的写法 animation_tree[“parameters/Locomotion/blend_position”] = Vector2(speed, direction) - 状态机现在可能只剩下
Locomotion、Jump、Attack、Hurt等状态。从Locomotion到Jump的过渡,使用Advance Expression如!is_on_floor()。
这样做的好处:移动动画(静止、走、跑)之间的过渡由混合空间平滑处理,无需定义多条过渡线,代码也更简洁。状态机只处理离散的状态跳转(如地面移动 -> 跳跃)。
4.2 分层混合:上半身与下半身独立控制
这是高级技巧,用于实现“边跑边射击”、“边走边挥手”等效果。你需要使用AnimationNodeBlendTree作为根节点,而不是StateMachine。
- 在
AnimationTree中,将Tree Root改为New AnimationNodeBlendTree。 - 在混合树图中,添加一个
Blend2节点,命名为UpperBody。再添加一个Blend2节点,命名为LowerBody。 - 将
UpperBody和LowerBody的输出都连接到一个新的Blend2节点(作为最终输出),但这个节点的混合模式通常设为“1.0”表示只取其中一个输入?不对,这里需要纠正。对于上下半身独立,我们需要用AnimationNodeBlendTree的Add2节点!Add2节点会将两个输入的动画叠加在一起。 - 正确做法:创建
Add2节点作为输出。将LowerBody(下半身,如移动)连接到Add2的in端口,将UpperBody(上半身,如持枪、挥手)连接到Add2的add端口。 - 在
LowerBody的Blend2节点里,混合idle和run动画,用速度参数控制。 - 在
UpperBody的Blend2节点里,混合upper_idle和upper_aim或upper_attack动画,用是否瞄准等参数控制。 - 在脚本中,你需要分别设置两个参数:
animation_tree[“parameters/LowerBody/blend_amount”] = run_blend_value animation_tree[“parameters/UpperBody/blend_amount”] = aim_blend_value
这样,下半身的移动动画和上半身的瞄准动画就完全独立了,可以任意组合。
注意事项:分层混合要求动画师在制作动画时,将骨骼或 Sprite 明确分为上下半身组,并且在
AnimationPlayer中编辑动画时,只对相应的骨骼轨道进行关键帧设置。否则上下半身动画会相互干扰。
5. 通过代码精细控制与参数传递
AnimationTree的所有可调参数都暴露在parameters这个字典属性下。通过代码动态修改这些参数,是实现游戏逻辑与动画表现联动的关键。
5.1 获取与设置参数
如前所述,可以通过animation_tree.set(“参数路径”, 值)或animation_tree[“参数路径”] = 值来设置。获取值亦然。
参数路径的规律:
- 对于状态机中的状态:
parameters/StateMachine/当前状态名 - 对于混合树中的混合节点:
parameters/混合节点名/blend_amount - 对于混合空间:
parameters/混合空间名/blend_position - 对于过渡条件变量:
parameters/变量名(需要在AnimationTree的Parameters列表中添加,或通过Advance Expression隐式使用)
在AnimationTree中添加自定义参数:
- 选中
AnimationTree节点。 - 在检视器中找到Parameters折叠栏,点击“添加参数”。
- 选择类型(Bool, Float, Vector2等),命名,如
is_aiming。 - 现在你就可以在代码中访问
animation_tree[“parameters/is_aiming”],也可以在任意过渡的Advance Expression中使用它。
5.2 响应动画事件:使用 AnimationTree 配合 AnimationPlayer 的信号
有时,你需要在动画的特定时刻触发游戏逻辑,比如在脚触地时播放声音,在武器挥到最高点时产生伤害判定框。
AnimationPlayer有animation_finished信号,但粒度太粗。更好的方法是在AnimationPlayer的动画轨道中插入方法调用轨道(Method Call Track)。
步骤:
- 在
AnimationPlayer中编辑你的attack动画。 - 在轨道列表上点击“添加轨道” -> “调用方法轨道”。
- 选择你想要调用方法的节点(通常是你的角色脚本所在的节点)。
- 在时间轴上,在你需要触发事件的帧处插入关键帧。
- 点击关键帧,在检视器中设置要调用的方法名,例如
on_attack_hit。 - 在你的角色脚本中定义这个方法:
func on_attack_hit(): # 生成伤害判定区域 spawn_hitbox() # 播放音效 $SwordSwingSound.play()
重要:即使这个动画是通过AnimationTree播放和混合的,AnimationPlayer中插入的方法调用关键帧依然会生效。这是将动画事件与游戏逻辑解耦的完美方式。
5.3 根运动(Root Motion)实战应用
对于 3D 角色动画,根运动是让动画驱动位移(而非代码直接修改位置)的高级技术。它使角色的移动步幅与动画完全匹配,看起来更自然。
配置根运动:
- 在 3D 模型中,动画师需要指定一根骨骼作为“根骨骼”(Root Bone),通常是人体的盆骨或髋部。
- 在
AnimationPlayer中,为这根根骨骼的变换(位置、旋转)创建动画轨道。 - 在
AnimationTree节点的检视器中,勾选Root Motion下的启用。 - 在根运动轨迹中,创建一个新的
AnimationNode(通常是一个AnimationNodeBlendTree),并将其路径指定给AnimationTree。 - 在这个专门的根运动混合树中,你混合的动画所产生的根骨骼位移,会被提取出来。
在代码中应用根运动:
func _physics_process(delta): # 先获取由动画产生的位移增量 var root_motion_translation = animation_tree.get_root_motion_position() var root_motion_rotation = animation_tree.get_root_motion_rotation() # 将位移应用到角色的移动逻辑中 # 注意:get_root_motion_position() 返回的是上一步动画更新以来的位移增量 # 通常需要结合角色的朝向进行变换 var motion = root_motion_translation.rotated(Vector3.UP, rotation.y) velocity = motion / delta # 转换为速度,这是一个简化示例,实际需结合物理 move_and_slide()get_root_motion_position()返回的是一个Vector3,表示自上一帧以来,根骨骼在模型局部空间中的位移。你需要根据角色当前的全局旋转,将其转换到世界空间,再应用到速度或直接移动角色上。
深度解析:根运动的本质是“视觉归位,数据提取”。动画播放时,根骨骼的变换在视觉上被抵消(角色模型看起来在原地运动),但其变换数据被提取出来供你代码使用。这确保了动画的脚部与地面接触点(IK 解算后)能精确对应,特别适合攀爬、复杂地形行走等场景。
6. 常见问题排查与性能优化
即使理解了原理,实战中还是会遇到各种妖魔鬼怪。下面是我总结的常见问题清单和解决方案。
6.1 动画不播放或状态不切换
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 角色完全静止 | 1.AnimationTree.active未设置为true。2. 状态机未调用 start(“初始状态”)。3. AnimationPlayer中没有动画,或动画未正确赋值给状态。 | 1. 在_ready()中设置active = true。2. 确保调用了 start()。3. 双击状态节点,检查Animation属性是否选中了有效动画。 |
| 状态切换没反应 | 1.travel()的目标状态名拼写错误。2. 当前状态到目标状态没有可用的过渡路径(线)。 3. 控制切换的代码逻辑条件永远不满足(如速度阈值设得过高)。 | 1. 检查状态名大小写,必须完全匹配。 2. 在AnimationTree面板检查过渡线是否连接。对于自动过渡,检查 Advance Mode和表达式。3. 使用 print()输出你的判断变量(如速度值),调试逻辑。 |
| 自动过渡不触发 | 1.Advance Expression Base Node未设置或设置错误。2. 表达式语法错误或访问了不存在的属性。 3. 该过渡的 Advance Mode不是Auto。 | 1. 确认Base Node指向了包含表达式所用变量/方法的节点。2. 写简单的表达式如 true测试。打开“输出”面板查看是否有表达式错误。3. 将 Advance Mode改为Auto。 |
6.2 动画混合效果异常
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 混合时角色扭曲或拉伸 | 1. 参与混合的动画初始姿势不一致。 2. 骨骼的放松姿势(Rest Pose)不标准。 | 1. 确保所有动画都基于同一个初始姿势制作。可以在AnimationPlayer中创建一个单帧的RESET动画来定义标准初始姿势。2. 在 3D 建模软件中,确保角色模型以 T-Pose 或 A-Pose 导出,这是 Godot 推荐的放松姿势。 |
| 混合过渡不平滑,有跳帧 | 过渡的Xfade Time太短。 | 适当增加交叉淡入淡出时间,如从0.1调到0.3。对于幅度大的动作切换,可能需要更长时间。 |
使用Sync模式后动画节奏奇怪 | 参与混合的动画长度差异过大,强行同步导致时间缩放比例失调。 | 1. 让动画师调整动画长度,使其循环周期接近。 2. 考虑使用 BlendSpace的Discrete或Carry模式代替Sync。3. 放弃 Sync,使用Immediate并接受节奏变化,或精心设计切换点。 |
6.3 性能考量与最佳实践
- 状态数量与复杂度:一个状态机不是状态越多越好。过于复杂的状态机难以维护和调试。对于复杂角色,考虑使用分层状态机或子状态机来组织逻辑。
- 混合节点开销:
Blend2/Blend3等节点混合多个动画是有计算成本的。避免在同一帧进行过多层级的深度混合。对于不重要的 NPC,可以简化其动画树。 - 参数更新频率:每帧在
_physics_process或_process中更新大量AnimationTree参数(如混合位置)是安全的,但应避免在每一帧设置完全相同的值,无意义的赋值也会产生微小开销。 - 动画资源优化:
AnimationTree最终驱动的是AnimationPlayer中的资源。确保动画资源本身是优化的:移除不必要的轨道,压缩关键帧,对于 3D 动画可以使用压缩格式。 - 使用 LOD(细节层次):对于远处的角色,可以使用更简单的动画树甚至完全禁用
AnimationTree,播放一个简化的循环动画,以节省性能。
6.4 调试技巧
- 可视化当前状态:在游戏的调试 UI 中,显示
state_machine.get_current_node()的返回值,实时了解角色处于哪个动画状态。 - 参数监视:同样,在调试 UI 中显示关键的混合参数值,如速度、混合权重等。
- 使用 AnimationTree 编辑器预览:在编辑器中,你可以手动修改
AnimationTree的Parameters面板中的值,并观察角色模型的实时反应,这对于调试混合逻辑非常有用。 - 检查警告:Godot 编辑器底部“输出”面板经常会给出关于动画树的警告,例如“未找到路径”、“表达式求值错误”等,务必关注。
构建一个健壮的AnimationTree状态机系统,前期需要一些设计和配置时间,但它带来的回报是巨大的:清晰解耦的动画逻辑、平滑的视觉体验、以及极高的可扩展性。当你的游戏角色动作越来越丰富时,你会庆幸自己当初选择了这条“正道”。希望这篇从基础到实战的指南,能帮助你驾驭 Godot 的动画系统,创造出真正活灵活现的游戏角色。
