11. 2.5D动画与骨骼系统
11. 2.5D动画与骨骼系统
动画是什么?
你小时候可能玩过"翻页动画"——在一本书的每一页角落画一个小人,每页的小人姿势稍微不同,快速翻页时小人就"动"起来了。
游戏动画的原理完全一样:快速切换一系列静止的画面(或姿势),让眼睛产生"在动"的错觉。每秒播放的画面越多(帧率越高),动画就越流畅。
在 Godot 中,动画系统负责记录和播放这些"姿势变化",让角色能走路、跑步、跳跃、攻击。
2.5D 游戏的三种动画方式
方式一:3D 骨骼动画(最常用)
## 定义: 给 3D 模型添加一套"骨架",通过控制骨骼的旋转和位移来驱动模型变形。就像木偶戏——木偶有关节,拉动关节的线就能让木偶做出各种动作。
优点:
- 动画流畅自然
- 可以混合多个动画(比如边走路边挥手)
- 支持从 Blender 等软件导入专业动画
适合: 大多数 2.5D 游戏的主角和敌人
方式二:Sprite3D 逐帧动画
## 定义: 在 3D 场景中放置一张始终面向摄像机的 2D 图片,通过快速切换图片来实现动画。就像把 2D 游戏的精灵图放进了 3D 世界。
优点:
- 美术风格独特(像素风、手绘风)
- 制作成本低
- 性能好
适合: 像素风 2.5D 游戏、复古风格游戏
方式三:补间动画(Tween)
## 定义: 用代码控制物体从一个状态平滑过渡到另一个状态。比如让一个箱子从位置 A 移动到位置 B,或者让一个按钮从小变大。
优点:
- 完全用代码控制,灵活
- 适合 UI 动画和简单的物体运动
- 不需要美术资源
适合: UI 动画、道具弹出效果、简单的场景动画
AnimationPlayer:动画播放器
AnimationPlayer 是 Godot 中最基础的动画节点,就像一个"录像机"——它记录了物体在不同时间点的状态,播放时按照记录重现这些状态。
基本使用
- 在场景中添加
AnimationPlayer节点(作为角色的子节点) - 在底部的 Animation 面板中点击 "Animation" → "New",创建新动画
- 选中要动画的节点,点击属性旁边的钥匙图标,添加关键帧
- 移动时间轴,修改属性,再添加关键帧
- 点击播放按钮预览动画
用代码控制 AnimationPlayer
using Godot;
public partial class AnimatedCharacter : CharacterBody3D
{
[Export] private AnimationPlayer _animPlayer;
// 当前动画状态
private string _currentAnim = "";
public override void _Ready()
{
// 播放默认动画
PlayAnimation("idle");
}
public override void _PhysicsProcess(double delta)
{
// 根据速度切换动画
float speed = Velocity.Length();
if (!IsOnFloor())
{
// 在空中
if (Velocity.Y > 0)
PlayAnimation("jump");
else
PlayAnimation("fall");
}
else if (speed > 0.1f)
{
PlayAnimation("walk");
}
else
{
PlayAnimation("idle");
}
}
// 播放动画(避免重复播放同一动画)
private void PlayAnimation(string animName)
{
if (_currentAnim == animName) return;
_currentAnim = animName;
_animPlayer.Play(animName);
}
// 播放一次性动画(如攻击),结束后回到idle
public void PlayAttack()
{
_animPlayer.Play("attack");
// 监听动画结束信号
_animPlayer.AnimationFinished += OnAttackFinished;
}
private void OnAttackFinished(StringName animName)
{
if (animName == "attack")
{
_animPlayer.AnimationFinished -= OnAttackFinished;
PlayAnimation("idle");
}
}
}extends CharacterBody3D
@export var anim_player: AnimationPlayer
# 当前动画状态
var _current_anim: String = ""
func _ready() -> void:
# 播放默认动画
play_animation("idle")
func _physics_process(delta: float) -> void:
# 根据速度切换动画
var speed = velocity.length()
if not is_on_floor():
# 在空中
if velocity.y > 0:
play_animation("jump")
else:
play_animation("fall")
elif speed > 0.1:
play_animation("walk")
else:
play_animation("idle")
# 播放动画(避免重复播放同一动画)
func play_animation(anim_name: String) -> void:
if _current_anim == anim_name:
return
_current_anim = anim_name
anim_player.play(anim_name)
# 播放一次性动画(如攻击),结束后回到idle
func play_attack() -> void:
anim_player.play("attack")
# 监听动画结束信号
anim_player.animation_finished.connect(_on_attack_finished, CONNECT_ONE_SHOT)
func _on_attack_finished(anim_name: StringName) -> void:
if anim_name == "attack":
play_animation("idle")AnimationTree:动画状态机
当游戏角色有很多动画(走、跑、跳、攻击、死亡……),用代码手动切换会变得很复杂。AnimationTree 提供了一个可视化的"动画状态机",让动画切换更有条理。
比喻: 想象一个交通信号灯控制系统——不同状态(红灯/绿灯/黄灯)之间有明确的切换规则。AnimationTree 就是角色动画的"信号灯控制系统"。
设置 AnimationTree
- 在场景中添加
AnimationTree节点 - 在 Inspector 中设置
Anim Player指向你的 AnimationPlayer - 将
Tree Root设为AnimNodeStateMachine(状态机) - 在编辑器中添加动画状态和切换条件
混合空间(BlendSpace)
混合空间可以根据参数值在多个动画之间平滑过渡。比如:
- 速度为 0 时播放 idle
- 速度为 3 时播放 walk
- 速度为 6 时播放 run
- 中间值自动混合
using Godot;
public partial class AnimTreeCharacter : CharacterBody3D
{
[Export] private AnimationTree _animTree;
// AnimationTree的状态机播放器
private AnimationNodeStateMachinePlayback _stateMachine;
public override void _Ready()
{
_animTree.Active = true;
// 获取状态机播放器
_stateMachine = (AnimationNodeStateMachinePlayback)_animTree.Get("parameters/playback");
}
public override void _PhysicsProcess(double delta)
{
float speed = new Vector2(Velocity.X, Velocity.Z).Length();
// 设置混合空间参数(控制walk/run混合)
_animTree.Set("parameters/blend_space/blend_position", speed);
// 设置状态机参数
_animTree.Set("parameters/conditions/is_on_floor", IsOnFloor());
_animTree.Set("parameters/conditions/is_jumping", !IsOnFloor() && Velocity.Y > 0);
// 手动切换状态
if (Input.IsActionJustPressed("attack"))
{
_stateMachine.Travel("attack");
}
}
}extends CharacterBody3D
@export var anim_tree: AnimationTree
# AnimationTree的状态机播放器
var _state_machine: AnimationNodeStateMachinePlayback
func _ready() -> void:
anim_tree.active = true
# 获取状态机播放器
_state_machine = anim_tree.get("parameters/playback")
func _physics_process(delta: float) -> void:
var speed = Vector2(velocity.x, velocity.z).length()
# 设置混合空间参数(控制walk/run混合)
anim_tree.set("parameters/blend_space/blend_position", speed)
# 设置状态机参数
anim_tree.set("parameters/conditions/is_on_floor", is_on_floor())
anim_tree.set("parameters/conditions/is_jumping", not is_on_floor() and velocity.y > 0)
# 手动切换状态
if Input.is_action_just_pressed("attack"):
_state_machine.travel("attack")从 Blender 导入动画
大多数 2.5D 游戏的角色动画都是在 Blender 中制作的,然后导入 Godot。
导出设置(在 Blender 中)
- 选择 File → Export → glTF 2.0 (.glb/.gltf)
- 勾选 "Include Animations"
- 将 "Sampling Rate" 设为 30(与游戏帧率匹配)
- 导出为
.glb格式(二进制,文件更小)
导入设置(在 Godot 中)
- 将
.glb文件拖入 Godot 的 FileSystem 面板 - 点击文件,在 Import 面板中:
- 将 "Animation" → "Import" 设为 True
- 设置 "Loop Mode" 为 Loop(循环动画)或 None(一次性动画)
- 点击 "Reimport"
动画命名规范
建议在 Blender 中按照以下规范命名动画,方便在 Godot 中管理:
| 动画名称 | 说明 |
|---|---|
| idle | 待机 |
| walk | 走路 |
| run | 跑步 |
| jump | 跳跃起始 |
| fall | 下落 |
| land | 落地 |
| attack_01 | 攻击1 |
| attack_02 | 攻击2 |
| hurt | 受伤 |
| die | 死亡 |
补间动画(Tween)
Tween 是用代码创建的平滑过渡动画,不需要 AnimationPlayer,非常适合 UI 动画和简单的物体运动。
using Godot;
public partial class TweenExamples : Node3D
{
// 示例1:物体从当前位置移动到目标位置
public void MoveToPosition(Vector3 targetPos, float duration = 1.0f)
{
Tween tween = CreateTween();
tween.TweenProperty(this, "global_position", targetPos, duration)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.InOut);
}
// 示例2:物体弹出效果(从小变大)
public void PopIn()
{
Scale = Vector3.Zero;
Tween tween = CreateTween();
tween.TweenProperty(this, "scale", Vector3.One, 0.3f)
.SetTrans(Tween.TransitionType.Back)
.SetEase(Tween.EaseType.Out);
}
// 示例3:闪烁效果(受伤时)
public void FlashEffect(int times = 3)
{
Tween tween = CreateTween();
tween.SetLoops(times);
tween.TweenProperty(this, "visible", false, 0.1f);
tween.TweenProperty(this, "visible", true, 0.1f);
}
// 示例4:连续动画(先移动,再旋转,再缩放)
public void SequenceAnimation()
{
Tween tween = CreateTween();
// 先移动
tween.TweenProperty(this, "global_position:y", GlobalPosition.Y + 2.0f, 0.5f);
// 然后旋转
tween.TweenProperty(this, "rotation:y", Mathf.Pi * 2, 0.5f);
// 最后缩小消失
tween.TweenProperty(this, "scale", Vector3.Zero, 0.3f);
// 动画结束后删除节点
tween.TweenCallback(Callable.From(QueueFree));
}
}extends Node3D
# 示例1:物体从当前位置移动到目标位置
func move_to_position(target_pos: Vector3, duration: float = 1.0) -> void:
var tween = create_tween()
tween.tween_property(self, "global_position", target_pos, duration)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN_OUT)
# 示例2:物体弹出效果(从小变大)
func pop_in() -> void:
scale = Vector3.ZERO
var tween = create_tween()
tween.tween_property(self, "scale", Vector3.ONE, 0.3)\
.set_trans(Tween.TRANS_BACK)\
.set_ease(Tween.EASE_OUT)
# 示例3:闪烁效果(受伤时)
func flash_effect(times: int = 3) -> void:
var tween = create_tween()
tween.set_loops(times)
tween.tween_property(self, "visible", false, 0.1)
tween.tween_property(self, "visible", true, 0.1)
# 示例4:连续动画(先移动,再旋转,再缩放)
func sequence_animation() -> void:
var tween = create_tween()
# 先移动
tween.tween_property(self, "global_position:y", global_position.y + 2.0, 0.5)
# 然后旋转
tween.tween_property(self, "rotation:y", PI * 2, 0.5)
# 最后缩小消失
tween.tween_property(self, "scale", Vector3.ZERO, 0.3)
# 动画结束后删除节点
tween.tween_callback(queue_free)小结
| 动画方式 | 适用场景 | 学习难度 |
|---|---|---|
| AnimationPlayer | 角色动画、场景动画 | 中等 |
| AnimationTree | 复杂角色动画状态管理 | 较高 |
| Sprite3D 逐帧 | 像素风、2D精灵在3D场景 | 简单 |
| Tween | UI动画、简单物体运动 | 简单 |
对于大多数 2.5D 游戏,推荐的动画工作流是:
- 在 Blender 中制作骨骼动画
- 导出为
.glb格式 - 在 Godot 中用 AnimationTree 管理动画状态
- 用 Tween 处理 UI 和特效动画
