不用 AnimationPlayer:代码驱动的游戏动画
godot-game-one 里没有一个 .tres 动画资源文件。所有动画——角色行走摆动、屏幕震动、受击闪光、受击特效——全部在 _process / _physics_process 或 Tween 回调里用数学函数驱动。
graph TD
A["程序化动画"] --> B["玩家行走"]
A --> C["屏幕震动"]
A --> D["受击闪光"]
A --> E["引擎呼吸"]
A --> F["受击特效"]
B --> B1["sin 波驱动 rotation"]
B --> B2["abs(sin) 驱动 position.y"]
C --> C1["线性衰减随机偏移"]
D --> D1["modulate 过曝 + scale"]
E --> E1["透明度 + 宽度脉动"]
F --> F1["Tween 缩放 + 淡出"]
行走摆动:一个 sin 函数搞定
玩家飞船的”行走”动画是 feedback_controller.gd 里的一个 _walk_cycle 计数器:
1 | var _walk_cycle: float = 0.0 |
两个三角函数,一个控制旋转,一个控制位移,速度阈值 > 10 决定播放还是归零。没有关键帧,没有曲线编辑器,改手感直接调 * 8.0(频率)和 * 0.02(幅度)的系数。
引擎呼吸光效
独立于行走动画的另一个脉动循环:
1 | var _breath_timer: float = 0.0 |
频率 2.5Hz,幅度 ±0.15,让引擎火焰看起来像在”呼吸”。和行走摆动用不同的计时器,频率不同步,视觉上更有机。
屏幕震动:衰减随机偏移
受伤或爆炸时触发屏幕震动:
1 | # screen_shake.gd — 独立 Node2D,作为一次性子节点添加到场景 |
核心原理:一个独立的 screen_shake.tscn 节点,由 powerup.gd 实例化后添加到场景。用倒计时 _timer 按线性衰减控制震幅,queue_free() 自动销毁。不是挂在 feedback_controller 里的,而是一次性的场景节点——用完即弃。
受击闪光:Tween 驱动 modulate
受击时全身闪白,不用 Shader,直接用 Tween 改 Sprite2D 的 modulate 和 scale:
1 | # feedback_controller.gd |
核心:40ms 内把 modulate 拉到 Color(3,3,3) 产生过曝闪白 + 微放大,然后 120ms 恢复。不改材质、不切 Shader,纯颜色插值。没有关键帧编辑器,改闪白时长直接调 0.04 和 0.12。
受击特效:独立场景节点
受击闪光的同时,还会从对象池取一个 hit_effect.tscn 节点放在敌人位置,用 Tween 做缩放 + 淡出:
1 | # hit_effect.gd |
150ms 内从原始大小膨胀到 2 倍,同时透明度归零,然后回收到对象池。和受击闪光配合——闪光在精灵身上,爆裂效果在旁边。
为什么不用 AnimationPlayer?
| AnimationPlayer | 程序化 | |
|---|---|---|
| 适用场景 | 复杂关键帧动画(角色骨骼、过场动画) | 简单循环效果(震动、脉动、摆动) |
| 迭代速度 | 打开编辑器调曲线 | 改一个数字 |
| 资源管理 | .tres 文件需要管理 | 纯代码,零外部依赖 |
| 运行时灵活性 | 播放/暂停/混合 | 可以和游戏状态实时联动 |
| 适用规模 | 大量动画需要复用 | 少量简单效果 |
这个项目的所有动画都是简单循环效果,没有骨骼、没有帧动画、没有过场。用 sin + lerp + randf 三个函数覆盖了全部需求。
程序化动画不是”穷人版动画系统”,是为特定问题量身定做的最简方案。