godot-game-one 里没有一个 .tres 动画资源文件。所有动画——角色行走摆动、屏幕震动、受击闪光、受击特效——全部在 _process / _physics_process 或 Tween 回调里用数学函数驱动。

行走摆动:一个 sin 函数搞定

玩家飞船的”行走”动画是 feedback_controller.gd 里的一个 _walk_cycle 计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
var _walk_cycle: float = 0.0

func _update_walk_animation(delta: float) -> void:
if _player.velocity.length() > 10.0:
_walk_cycle += delta * 8.0
var swing: float = sin(_walk_cycle) * 0.02 # 左右微摆 ±1.15°
var bounce: float = abs(sin(_walk_cycle)) * 0.01 # 引擎上下弹跳
if _sprite:
_sprite.rotation = swing
if _engine_glow:
_engine_glow.position.y = 32.0 + bounce * 20.0
else:
reset_pose()

两个三角函数,一个控制旋转,一个控制位移,速度阈值 > 10 决定播放还是归零。没有关键帧,没有曲线编辑器,改手感直接调 * 8.0(频率)和 * 0.02(幅度)的系数。

引擎呼吸光效

独立于行走动画的另一个脉动循环:

1
2
3
4
5
6
var _breath_timer: float = 0.0
func _update_engine_breathing(delta: float) -> void:
_breath_timer += delta * 2.5
var breath: float = 0.85 + sin(_breath_timer) * 0.15
_engine_glow.modulate.a = breath # 透明度 0.7~1.0
_engine_glow.scale.x = breath # 宽度同步呼吸

频率 2.5Hz,幅度 ±0.15,让引擎火焰看起来像在”呼吸”。和行走摆动用不同的计时器,频率不同步,视觉上更有机。

屏幕震动:衰减随机偏移

受伤或爆炸时触发屏幕震动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# screen_shake.gd — 独立 Node2D,作为一次性子节点添加到场景
var _duration: float = 0.3
var _intensity: float = 8.0
var _timer: float = 0.0
var _camera: Camera2D = null

func _ready() -> void:
_camera = get_viewport().get_camera_2d()
_timer = _duration

func _process(delta: float) -> void:
_timer -= delta
if _timer <= 0:
queue_free()
return
var progress: float = _timer / _duration
var shake_amount: float = _intensity * progress
_camera.offset = Vector2(
randf_range(-shake_amount, shake_amount),
randf_range(-shake_amount, shake_amount)
)

核心原理:一个独立的 screen_shake.tscn 节点,由 powerup.gd 实例化后添加到场景。用倒计时 _timer 按线性衰减控制震幅,queue_free() 自动销毁。不是挂在 feedback_controller 里的,而是一次性的场景节点——用完即弃。

受击闪光:Tween 驱动 modulate

受击时全身闪白,不用 Shader,直接用 Tween 改 Sprite2D 的 modulate 和 scale:

1
2
3
4
5
6
7
8
9
10
11
12
13
# feedback_controller.gd
func _play_hit_animation() -> void:
if not _sprite:
return
var base_color: Color = _sprite.modulate
var tween := create_tween().set_parallel(true)
tween.tween_property(_sprite, "modulate", Color(3.0, 3.0, 3.0, 1.0), 0.04)
tween.tween_property(_sprite, "scale", Vector2(1.2, 1.2), 0.04)
tween.tween_callback(func():
var recover := create_tween().set_parallel(true)
recover.tween_property(_sprite, "modulate", base_color, 0.12)
recover.tween_property(_sprite, "scale", Vector2(1.0, 1.0), 0.12)
)

核心:40ms 内把 modulate 拉到 Color(3,3,3) 产生过曝闪白 + 微放大,然后 120ms 恢复。不改材质、不切 Shader,纯颜色插值。没有关键帧编辑器,改闪白时长直接调 0.04 和 0.12。

受击特效:独立场景节点

受击闪光的同时,还会从对象池取一个 hit_effect.tscn 节点放在敌人位置,用 Tween 做缩放 + 淡出:

1
2
3
4
5
6
7
8
9
# hit_effect.gd
func start() -> void:
_sprite.modulate = Color(1, 1, 1, 1)
_sprite.scale = Vector2(1, 1)
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(_sprite, "scale", Vector2(2, 2), 0.15)
tween.tween_property(_sprite, "modulate:a", 0.0, 0.15)
tween.tween_callback(func(): Pool.release(self))

150ms 内从原始大小膨胀到 2 倍,同时透明度归零,然后回收到对象池。和受击闪光配合——闪光在精灵身上,爆裂效果在旁边。

为什么不用 AnimationPlayer?

AnimationPlayer 程序化
适用场景 复杂关键帧动画(角色骨骼、过场动画) 简单循环效果(震动、脉动、摆动)
迭代速度 打开编辑器调曲线 改一个数字
资源管理 .tres 文件需要管理 纯代码,零外部依赖
运行时灵活性 播放/暂停/混合 可以和游戏状态实时联动
适用规模 大量动画需要复用 少量简单效果

这个项目的所有动画都是简单循环效果,没有骨骼、没有帧动画、没有过场。用 sin + lerp + randf 三个函数覆盖了全部需求。

程序化动画不是”穷人版动画系统”,是为特定问题量身定做的最简方案。