5. 吃子战斗动画
2026/4/13大约 12 分钟
5. 吃子战斗动画:让吃子"炸"起来
5.1 战魂版的核心体验——吃子动画
传统象棋里,吃子就是把对方的棋子拿掉。但在"战魂版"中,吃子是一场视觉盛宴:
- 攻击方棋子冲向目标
- 两颗棋子碰撞
- 被吃的棋子碎裂/飞走
- 粒子爆炸特效
- 屏幕震动
- 攻击方棋子停在目标位置
动画时序设计
整个吃子动画大约持续 1.5 秒:
| 阶段 | 时间 | 内容 |
|---|---|---|
| 冲刺 | 0.0-0.3s | 攻击方飞向目标 |
| 碰撞 | 0.3-0.5s | 屏幕震动 + 碰撞音效 |
| 爆炸 | 0.3-0.8s | 粒子特效 + 被吃棋子飞走 |
| 消散 | 0.8-1.5s | 粒子逐渐消散 |
| 归位 | 1.5s | 攻击方停在目标位置 |
5.2 Tween 补间动画——棋子平滑移动
Tween(补间动画)就像"自动补全":你告诉它"从这里到那里",它自动计算中间的每一帧位置。
普通走子动画(不吃子)
C#
using Godot;
public partial class MoveAnimator : Node
{
[Export] public float MoveDuration { get; set; } = 0.4f; // 移动时间
[Export] public float CaptureDuration { get; set; } = 0.3f; // 冲刺时间
/// <summary>
/// 播放普通走子动画(平滑移动到目标位置)
/// </summary>
public void PlayMoveAnimation(Piece3D piece, Vector3 targetPos)
{
// 创建一个 Tween
var tween = CreateTween();
// 让棋子平滑移动到目标位置
tween.TweenProperty(piece, "position", targetPos, MoveDuration)
.SetTrans(Tween.TransitionType.Sine) // 使用正弦缓动(先快后慢)
.SetEase(Tween.EaseType.Out);
// 走子音效
tween.Parallel().TweenCallback(Callable.From(() =>
PlaySound("res://audio/move.wav")));
// 动画结束后通知游戏管理器
tween.TweenCallback(Callable.From(() =>
GD.Print("走子动画结束")));
}
/// <summary>
/// 播放吃子战斗动画(冲刺 + 碰撞 + 爆炸)
/// </summary>
public void PlayCaptureAnimation(Piece3D attacker, Piece3D target,
Vector3 targetPos)
{
var tween = CreateTween();
// ===== 第一阶段:冲刺 =====
// 棋子跳起来冲向目标(Y轴升高再落下)
Vector3 midPos = new Vector3(
(attacker.Position.X + targetPos.X) / 2,
1.5f, // 跳起高度
(attacker.Position.Z + targetPos.Z) / 2
);
// 先跳起来
tween.TweenProperty(attacker, "position:y", 1.5f, CaptureDuration * 0.3f)
.SetTrans(Tween.TransitionType.Quad)
.SetEase(Tween.EaseType.Out);
// 同时水平移向目标
tween.Parallel().TweenProperty(attacker, "position:x", targetPos.X, CaptureDuration)
.SetTrans(Tween.TransitionType.Back)
.SetEase(Tween.EaseType.In);
tween.Parallel().TweenProperty(attacker, "position:z", targetPos.Z, CaptureDuration)
.SetTrans(Tween.TransitionType.Back)
.SetEase(Tween.EaseType.In);
// ===== 第二阶段:碰撞 =====
// 落下砸向目标
tween.TweenProperty(attacker, "position:y", 0.2f, 0.1f)
.SetTrans(Tween.TransitionType.Bounce)
.SetEase(Tween.EaseType.In);
// 碰撞瞬间:播放效果
tween.TweenCallback(Callable.From(() =>
{
OnCaptureImpact(attacker, target);
}));
// ===== 第三阶段:被吃棋子飞走 =====
tween.TweenCallback(Callable.From(() =>
{
AnimateDefeatedPiece(target);
}));
// ===== 第四阶段:攻击方归位 =====
tween.TweenInterval(1.0f);
tween.TweenCallback(Callable.From(() =>
{
attacker.Position = targetPos;
GD.Print("吃子动画结束");
}));
}
/// <summary>
/// 碰撞瞬间效果
/// </summary>
private void OnCaptureImpact(Piece3D attacker, Piece3D target)
{
// 播放碰撞音效
PlaySound("res://audio/capture.wav");
// 屏幕震动
ShakeCamera(0.3f, 0.15f);
// 粒子爆炸
SpawnExplosion(target.Position);
// 闪光效果
SpawnFlash(target.Position);
}
/// <summary>
/// 被吃棋子飞走的动画
/// </summary>
private void AnimateDefeatedPiece(Piece3D piece)
{
var tween = CreateTween();
// 随机飞出方向
float randomAngle = (float)GD.RandRange(0, Mathf.Tau);
float flyDistance = 3.0f;
Vector3 flyTarget = piece.Position + new Vector3(
Mathf.Cos(randomAngle) * flyDistance,
3.0f,
Mathf.Sin(randomAngle) * flyDistance
);
// 飞出
tween.TweenProperty(piece, "position", flyTarget, 0.8f)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.Out);
// 同时旋转
tween.Parallel().TweenProperty(piece, "rotation_degrees:y", 720, 0.8f)
.SetTrans(Tween.TransitionType.Linear);
// 同时缩小
tween.Parallel().TweenProperty(piece, "scale", Vector3.Zero, 0.8f)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.In);
// 消失后移除
tween.TweenCallback(Callable.From(() =>
{
piece.QueueFree();
}));
}
/// <summary>
/// 播放音效
/// </summary>
private void PlaySound(string path)
{
var stream = GD.Load<AudioStream>(path);
var player = new AudioStreamPlayer3D();
player.Stream = stream;
player.VolumeDb = -5.0f;
AddChild(player);
player.Play();
// 播完后自动清理
player.Finished += () => player.QueueFree();
}
/// <summary>
/// 摄像机震动
/// </summary>
private void ShakeCamera(float duration, float intensity)
{
var camera = GetViewport().GetCamera3D();
if (camera == null) return;
var originalPos = camera.Position;
var tween = CreateTween();
for (int i = 0; i < 6; i++)
{
var offset = new Vector3(
(float)GD.RandRange(-intensity, intensity),
(float)GD.RandRange(-intensity, intensity),
0
);
tween.TweenProperty(camera, "position", originalPos + offset, duration / 6);
}
tween.TweenProperty(camera, "position", originalPos, duration / 6);
}
// 占位方法,在粒子特效部分实现
private void SpawnExplosion(Vector3 pos) { }
private void SpawnFlash(Vector3 pos) { }
}GDScript
extends Node
@export var move_duration: float = 0.4 ## 移动时间
@export var capture_duration: float = 0.3 ## 冲刺时间
## 播放普通走子动画(平滑移动到目标位置)
func play_move_animation(piece: Piece3D, target_pos: Vector3):
var tween := create_tween()
# 让棋子平滑移动到目标位置
tween.tween_property(piece, "position", target_pos, move_duration) \
.set_trans(Tween.TRANS_SINE) \
.set_ease(Tween.EASE_OUT)
# 走子音效
tween.parallel().tween_callback(func(): play_sound("res://audio/move.wav"))
# 动画结束后通知
tween.tween_callback(func(): print("走子动画结束"))
## 播放吃子战斗动画(冲刺 + 碰撞 + 爆炸)
func play_capture_animation(attacker: Piece3D, target: Piece3D, target_pos: Vector3):
var tween := create_tween()
# ===== 第一阶段:冲刺 =====
# 先跳起来
tween.tween_property(attacker, "position:y", 1.5, capture_duration * 0.3) \
.set_trans(Tween.TRANS_QUAD) \
.set_ease(Tween.EASE_OUT)
# 同时水平移向目标
tween.parallel().tween_property(attacker, "position:x", target_pos.x, capture_duration) \
.set_trans(Tween.TRANS_BACK) \
.set_ease(Tween.EASE_IN)
tween.parallel().tween_property(attacker, "position:z", target_pos.z, capture_duration) \
.set_trans(Tween.TRANS_BACK) \
.set_ease(Tween.EASE_IN)
# ===== 第二阶段:碰撞 =====
tween.tween_property(attacker, "position:y", 0.2, 0.1) \
.set_trans(Tween.TRANS_BOUNCE) \
.set_ease(Tween.EASE_IN)
# 碰撞瞬间效果
tween.tween_callback(func(): on_capture_impact(attacker, target))
# ===== 第三阶段:被吃棋子飞走 =====
tween.tween_callback(func(): animate_defeated_piece(target))
# ===== 第四阶段:攻击方归位 =====
tween.tween_interval(1.0)
tween.tween_callback(func():
attacker.position = target_pos
print("吃子动画结束")
)
## 碰撞瞬间效果
func on_capture_impact(attacker: Piece3D, target: Piece3D):
play_sound("res://audio/capture.wav")
shake_camera(0.3, 0.15)
spawn_explosion(target.position)
spawn_flash(target.position)
## 被吃棋子飞走的动画
func animate_defeated_piece(piece: Piece3D):
var tween := create_tween()
# 随机飞出方向
var random_angle: float = randf_range(0, TAU)
var fly_distance: float = 3.0
var fly_target := piece.position + Vector3(
cos(random_angle) * fly_distance,
3.0,
sin(random_angle) * fly_distance
)
# 飞出
tween.tween_property(piece, "position", fly_target, 0.8) \
.set_trans(Tween.TRANS_SINE) \
.set_ease(Tween.EASE_OUT)
# 同时旋转
tween.parallel().tween_property(piece, "rotation_degrees:y", 720, 0.8) \
.set_trans(Tween.TRANS_LINEAR)
# 同时缩小
tween.parallel().tween_property(piece, "scale", Vector3.ZERO, 0.8) \
.set_trans(Tween.TRANS_SINE) \
.set_ease(Tween.EASE_IN)
# 消失后移除
tween.tween_callback(func(): piece.queue_free())
## 播放音效
func play_sound(path: String):
var stream := load(path) as AudioStream
var player := AudioStreamPlayer3D.new()
player.stream = stream
player.volume_db = -5.0
add_child(player)
player.play()
player.finished.connect(func(): player.queue_free())
## 摄像机震动
func shake_camera(duration: float, intensity: float):
var camera := get_viewport().get_camera_3d()
if camera == null:
return
var original_pos := camera.position
var tween := create_tween()
for i in range(6):
var offset := Vector3(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity),
0
)
tween.tween_property(camera, "position", original_pos + offset, duration / 6.0)
tween.tween_property(camera, "position", original_pos, duration / 6.0)
# 占位方法
func spawn_explosion(pos: Vector3): pass
func spawn_flash(pos: Vector3): pass5.3 粒子爆炸特效
粒子特效就像烟花——从一个点喷出很多小粒子,然后逐渐消散。Godot 使用 GPUParticles3D 节点来实现。
粒子特效原理
GPUParticles3D 的工作流程:
1. 在一个点上"发射"大量小粒子
2. 每个粒子有自己的速度、颜色、大小、生命周期
3. 粒子随时间移动、变色、缩小
4. 生命周期结束后粒子消失C#
using Godot;
public partial class BattleEffects : Node3D
{
/// <summary>
/// 在指定位置生成爆炸粒子特效
/// </summary>
public void SpawnExplosion(Vector3 position)
{
var particles = new GPUParticles3D();
particles.Name = "Explosion";
particles.Position = position;
// ===== 粒子材质(控制粒子的外观和行为)=====
var processMaterial = new ParticleProcessMaterial();
processMaterial.EmissionShape = ParticleProcessMaterial.EmissionShapeEnum.Point;
processMaterial.ParticleFlagDisableZ = false;
// 粒子发射方向:球形扩散
processMaterial.Direction = new Vector3(0, 1, 0);
processMaterial.Spread = 180.0f; // 360度扩散
// 粒子初始速度
processMaterial.InitialVelocityMin = 2.0f;
processMaterial.InitialVelocityMax = 5.0f;
// 重力(让粒子往下掉)
processMaterial.Gravity = new Vector3(0, -5.0f, 0);
// 粒子大小随时间变化(先大后小)
var scaleCurve = new Curve();
scaleCurve.AddPoint(new Vector2(0, 1.0f)); // 开始时满大小
scaleCurve.AddPoint(new Vector2(0.5f, 0.8f)); // 中间缩小一点
scaleCurve.AddPoint(new Vector2(1.0f, 0.0f)); // 结束时消失
processMaterial.ScaleCurve = scaleCurve;
// 颜色随时间变化(亮黄 → 橙红 → 灰色)
var colorRamp = new Gradient();
colorRamp.Colors = new Color[]
{
new Color(1.0f, 1.0f, 0.5f, 1.0f), // 亮黄色
new Color(1.0f, 0.5f, 0.0f, 0.8f), // 橙色
new Color(0.5f, 0.2f, 0.1f, 0.0f) // 暗红色(透明)
};
colorRamp.Offsets = new float[] { 0.0f, 0.5f, 1.0f };
processMaterial.ColorRamp = colorRamp;
particles.ProcessMaterial = processMaterial;
// ===== 粒子数量和生命周期 =====
particles.Amount = 50; // 50个粒子
particles.Lifetime = 1.0f; // 每个粒子存活1秒
particles.OneShot = true; // 只发射一次
particles.Explosiveness = 0.9f; // 几乎同时发射所有粒子
particles.Randomness = 0.5f; // 50%随机性
// 粒子网格(每个粒子的形状)
var quadMesh = new QuadMesh();
quadMesh.Size = new Vector2(0.2f, 0.2f);
particles.DrawPass1 = quadMesh;
AddChild(particles);
// 自动清理:粒子播放完后移除节点
particles.Emitting = true;
GetTree().CreateTimer(particles.Lifetime + 0.5).Timeout +=
() => particles.QueueFree();
}
/// <summary>
/// 在指定位置生成闪光效果
/// </summary>
public void SpawnFlash(Vector3 position)
{
// 使用 OmniLight3D 模拟闪光
var light = new OmniLight3D();
light.Position = position;
light.LightColor = new Color(1.0f, 0.9f, 0.5f); // 暖黄色
light.LightEnergy = 10.0f; // 非常亮
light.OmniRange = 5.0f;
light.ShadowEnabled = false;
AddChild(light);
// 闪光渐灭
var tween = CreateTween();
tween.TweenProperty(light, "light_energy", 0.0f, 0.3f)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.Out);
tween.TweenCallback(Callable.From(() => light.QueueFree()));
}
/// <summary>
/// 在指定位置生成火花粒子(小规模特效)
/// </summary>
public void SpawnSparks(Vector3 position)
{
var particles = new GPUParticles3D();
particles.Position = position;
var processMaterial = new ParticleProcessMaterial();
processMaterial.EmissionShape = ParticleProcessMaterial.EmissionShapeEnum.Point;
processMaterial.Direction = new Vector3(0, 1, 0);
processMaterial.Spread = 30.0f; // 较窄的角度
processMaterial.InitialVelocityMin = 3.0f;
processMaterial.InitialVelocityMax = 8.0f;
processMaterial.Gravity = new Vector3(0, -9.8f, 0);
// 火花颜色(白 → 黄 → 红)
var colorRamp = new Gradient();
colorRamp.Colors = new Color[]
{
new Color(1.0f, 1.0f, 1.0f, 1.0f),
new Color(1.0f, 0.8f, 0.0f, 0.6f),
new Color(1.0f, 0.2f, 0.0f, 0.0f)
};
colorRamp.Offsets = new float[] { 0.0f, 0.3f, 1.0f };
processMaterial.ColorRamp = colorRamp;
particles.ProcessMaterial = processMaterial;
particles.Amount = 20;
particles.Lifetime = 0.5f;
particles.OneShot = true;
particles.Explosiveness = 1.0f;
AddChild(particles);
particles.Emitting = true;
GetTree().CreateTimer(1.0).Timeout +=
() => particles.QueueFree();
}
}GDScript
extends Node3D
## 在指定位置生成爆炸粒子特效
func spawn_explosion(pos: Vector3):
var particles := GPUParticles3D.new()
particles.name = "Explosion"
particles.position = pos
# ===== 粒子材质 =====
var process_material := ParticleProcessMaterial.new()
process_material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_POINT
process_material.particle_flag_disable_z = false
# 粒子发射方向:球形扩散
process_material.direction = Vector3(0, 1, 0)
process_material.spread = 180.0
# 粒子初始速度
process_material.initial_velocity_min = 2.0
process_material.initial_velocity_max = 5.0
# 重力
process_material.gravity = Vector3(0, -5.0, 0)
# 大小变化曲线
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0, 1.0))
scale_curve.add_point(Vector2(0.5, 0.8))
scale_curve.add_point(Vector2(1.0, 0.0))
process_material.scale_curve = scale_curve
# 颜色渐变
var color_ramp := Gradient.new()
color_ramp.colors = PackedColorArray([
Color(1.0, 1.0, 0.5, 1.0), # 亮黄色
Color(1.0, 0.5, 0.0, 0.8), # 橙色
Color(0.5, 0.2, 0.1, 0.0) # 暗红色(透明)
])
color_ramp.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
process_material.color_ramp = color_ramp
particles.process_material = process_material
# 粒子参数
particles.amount = 50
particles.lifetime = 1.0
particles.one_shot = true
particles.explosiveness = 0.9
particles.randomness = 0.5
# 粒子网格
var quad_mesh := QuadMesh.new()
quad_mesh.size = Vector2(0.2, 0.2)
particles.draw_pass_1 = quad_mesh
add_child(particles)
particles.emitting = true
# 自动清理
get_tree().create_timer(particles.lifetime + 0.5).timeout.connect(
func(): particles.queue_free()
)
## 在指定位置生成闪光效果
func spawn_flash(pos: Vector3):
var light := OmniLight3D.new()
light.position = pos
light.light_color = Color(1.0, 0.9, 0.5)
light.light_energy = 10.0
light.omni_range = 5.0
light.shadow_enabled = false
add_child(light)
var tween := create_tween()
tween.tween_property(light, "light_energy", 0.0, 0.3) \
.set_trans(Tween.TRANS_SINE) \
.set_ease(Tween.EASE_OUT)
tween.tween_callback(func(): light.queue_free())
## 在指定位置生成火花粒子
func spawn_sparks(pos: Vector3):
var particles := GPUParticles3D.new()
particles.position = pos
var process_material := ParticleProcessMaterial.new()
process_material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_POINT
process_material.direction = Vector3(0, 1, 0)
process_material.spread = 30.0
process_material.initial_velocity_min = 3.0
process_material.initial_velocity_max = 8.0
process_material.gravity = Vector3(0, -9.8, 0)
var color_ramp := Gradient.new()
color_ramp.colors = PackedColorArray([
Color(1, 1, 1, 1),
Color(1, 0.8, 0, 0.6),
Color(1, 0.2, 0, 0)
])
color_ramp.offsets = PackedFloat32Array([0.0, 0.3, 1.0])
process_material.color_ramp = color_ramp
particles.process_material = process_material
particles.amount = 20
particles.lifetime = 0.5
particles.one_shot = true
particles.explosiveness = 1.0
add_child(particles)
particles.emitting = true
get_tree().create_timer(1.0).timeout.connect(
func(): particles.queue_free()
)5.4 粒子参数详解
GPUParticles3D 关键参数
| 参数 | 含义 | 推荐值 |
|---|---|---|
| Amount | 同时存在的粒子数量 | 20-100 |
| Lifetime | 每个粒子存活的时间(秒) | 0.5-2.0 |
| OneShot | 是否只发射一次 | true(爆炸用) |
| Explosiveness | 发射间隔的随机性(0=均匀, 1=同时) | 0.8-1.0(爆炸用) |
| Randomness | 各种参数的随机程度 | 0.3-0.5 |
ParticleProcessMaterial 关键参数
| 参数 | 含义 | 爆炸效果推荐值 |
|---|---|---|
| EmissionShape | 粒子从哪里出来 | Point(从一个点) |
| Direction | 粒子初始方向 | (0,1,0) |
| Spread | 扩散角度 | 180(360度) |
| InitialVelocityMin/Max | 初始速度范围 | 2-8 |
| Gravity | 重力 | (0, -5, 0) |
| ScaleCurve | 大小随时间变化 | 先大后小 |
| ColorRamp | 颜色随时间变化 | 亮 → 暗 → 透明 |
5.5 AnimationPlayer 制作复杂动画
对于更复杂的动画序列(比如棋子变形、光环扩散等),可以使用 AnimationPlayer。
在编辑器中制作动画
- 选中棋子节点
- 添加 AnimationPlayer 子节点
- 打开动画面板(底部)
- 点击"创建新动画"
- 添加轨道(属性动画)
C#
using Godot;
public partial class PieceAnimationPlayer : Node3D
{
private AnimationPlayer _animPlayer;
public override void _Ready()
{
_animPlayer = new AnimationPlayer();
_animPlayer.Name = "AnimationPlayer";
AddChild(_animPlayer);
CreateCaptureAnimation();
}
/// <summary>
/// 代码创建吃子动画(等同于在编辑器里手动制作的动画)
/// </summary>
private void CreateCaptureAnimation()
{
var anim = new Animation();
anim.Length = 1.5f;
int trackIdx;
// 轨道1:棋子上跳
trackIdx = anim.AddTrack(Animation.TrackType.Value);
anim.TrackSetPath(trackIdx, "PieceMesh:position:y");
anim.TrackInsertKey(trackIdx, 0.0f, 0.0f);
anim.TrackInsertKey(trackIdx, 0.15f, 0.8f); // 跳起
anim.TrackInsertKey(trackIdx, 0.3f, 0.0f); // 落下
// 轨道2:棋子缩放(碰撞时膨胀再缩小)
trackIdx = anim.AddTrack(Animation.TrackType.Value);
anim.TrackSetPath(trackIdx, "PieceMesh:scale");
anim.TrackInsertKey(trackIdx, 0.0f, new Vector3(1, 1, 1));
anim.TrackInsertKey(trackIdx, 0.25f, new Vector3(1.2f, 1.2f, 1.2f)); // 膨胀
anim.TrackInsertKey(trackIdx, 0.35f, new Vector3(0.9f, 0.9f, 0.9f)); // 压扁
anim.TrackInsertKey(trackIdx, 0.5f, new Vector3(1.0f, 1.0f, 1.0f)); // 恢复
// 轨道3:旋转
trackIdx = anim.AddTrack(Animation.TrackType.Value);
anim.TrackSetPath(trackIdx, "PieceMesh:rotation_degrees:y");
anim.TrackInsertKey(trackIdx, 0.0f, 0.0f);
anim.TrackInsertKey(trackIdx, 0.3f, 180.0f); // 翻转半圈
anim.TrackInsertKey(trackIdx, 0.5f, 360.0f); // 翻转一圈
// 添加到 AnimationPlayer
var lib = new AnimationLibrary();
lib.AddAnimation("capture", anim);
_animPlayer.AddLibrary("default", lib);
}
/// <summary>
/// 播放吃子动画
/// </summary>
public void PlayCapture()
{
_animPlayer.Play("capture");
}
}GDScript
extends Node3D
var _anim_player: AnimationPlayer
func _ready():
_anim_player = AnimationPlayer.new()
_anim_player.name = "AnimationPlayer"
add_child(_anim_player)
create_capture_animation()
## 代码创建吃子动画
func create_capture_animation():
var anim := Animation.new()
anim.length = 1.5
var track_idx: int
# 轨道1:棋子上跳
track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, "PieceMesh:position:y")
anim.track_insert_key(track_idx, 0.0, 0.0)
anim.track_insert_key(track_idx, 0.15, 0.8)
anim.track_insert_key(track_idx, 0.3, 0.0)
# 轨道2:缩放
track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, "PieceMesh:scale")
anim.track_insert_key(track_idx, 0.0, Vector3(1, 1, 1))
anim.track_insert_key(track_idx, 0.25, Vector3(1.2, 1.2, 1.2))
anim.track_insert_key(track_idx, 0.35, Vector3(0.9, 0.9, 0.9))
anim.track_insert_key(track_idx, 0.5, Vector3(1.0, 1.0, 1.0))
# 轨道3:旋转
track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, "PieceMesh:rotation_degrees:y")
anim.track_insert_key(track_idx, 0.0, 0.0)
anim.track_insert_key(track_idx, 0.3, 180.0)
anim.track_insert_key(track_idx, 0.5, 360.0)
var lib := AnimationLibrary.new()
lib.add_animation("capture", anim)
_anim_player.add_library("default", lib)
## 播放吃子动画
func play_capture():
_anim_player.play("capture")5.6 音效系统
音效让游戏"有感觉"。走子的"哒"声、吃子的"砰"声、将军的警告声,都是游戏体验的重要组成部分。
音效文件准备
| 音效 | 来源 | 格式 | 时长 |
|---|---|---|---|
| move.wav | 棋子放下的"啪"声 | WAV | 0.2s |
| capture.wav | 碰撞爆炸声 | WAV | 0.5s |
| check.wav | 将军警告声 | WAV | 0.3s |
| victory.wav | 胜利音乐 | OGG | 3.0s |
| select.wav | 选中棋子的轻响 | WAV | 0.1s |
音效获取途径
- 免费音效网站:freesound.org、opengameart.org
- 自己录制:用手机录下棋子碰撞的声音
- 合成音效:用 Bfxr、sfxr 等工具生成
音效管理器
C#
using Godot;
using System.Collections.Generic;
public partial class AudioManager : Node
{
public static AudioManager Instance { get; private set; }
private Dictionary<string, AudioStream> _sounds = new();
private AudioStreamPlayer _musicPlayer;
public override void _Ready()
{
Instance = this;
LoadSounds();
}
private void LoadSounds()
{
_sounds["move"] = GD.Load<AudioStream>("res://audio/move.wav");
_sounds["capture"] = GD.Load<AudioStream>("res://audio/capture.wav");
_sounds["check"] = GD.Load<AudioStream>("res://audio/check.wav");
_sounds["select"] = GD.Load<AudioStream>("res://audio/select.wav");
_sounds["victory"] = GD.Load<AudioStream>("res://audio/victory.ogg");
}
/// <summary>
/// 播放音效
/// </summary>
public void PlaySound(string name, float volumeDb = 0.0f)
{
if (!_sounds.ContainsKey(name))
{
GD.PrintErr($"音效不存在: {name}");
return;
}
var player = new AudioStreamPlayer();
player.Stream = _sounds[name];
player.VolumeDb = volumeDb;
AddChild(player);
player.Play();
player.Finished += () => player.QueueFree();
}
/// <summary>
/// 播放 3D 空间音效
/// </summary>
public void PlaySound3D(string name, Vector3 position, float volumeDb = 0.0f)
{
if (!_sounds.ContainsKey(name)) return;
var player = new AudioStreamPlayer3D();
player.Stream = _sounds[name];
player.Position = position;
player.VolumeDb = volumeDb;
AddChild(player);
player.Play();
player.Finished += () => player.QueueFree();
}
}GDScript
extends Node
static var instance: AudioManager
var _sounds: Dictionary = {}
var _music_player: AudioStreamPlayer
func _ready():
instance = self
load_sounds()
func load_sounds():
_sounds["move"] = load("res://audio/move.wav")
_sounds["capture"] = load("res://audio/capture.wav")
_sounds["check"] = load("res://audio/check.wav")
_sounds["select"] = load("res://audio/select.wav")
_sounds["victory"] = load("res://audio/victory.ogg")
## 播放音效
func play_sound(name: String, volume_db: float = 0.0):
if not _sounds.has(name):
push_error("音效不存在: %s" % name)
return
var player := AudioStreamPlayer.new()
player.stream = _sounds[name]
player.volume_db = volume_db
add_child(player)
player.play()
player.finished.connect(func(): player.queue_free())
## 播放 3D 空间音效
func play_sound_3d(name: String, pos: Vector3, volume_db: float = 0.0):
if not _sounds.has(name):
return
var player := AudioStreamPlayer3D.new()
player.stream = _sounds[name]
player.position = pos
player.volume_db = volume_db
add_child(player)
player.play()
player.finished.connect(func(): player.queue_free())5.7 特效等级对比
根据设备性能,你可以调整特效的"豪华程度":
| 等级 | 粒子数量 | 屏幕震动 | 闪光 | 火花 | 适用场景 |
|---|---|---|---|---|---|
| 低 | 10个 | 无 | 无 | 无 | 低端手机 |
| 中 | 30个 | 轻微 | 有 | 无 | 中端设备 |
| 高 | 50个 | 明显 | 有 | 有 | 高端设备 |
| 极高 | 100个 | 强烈 | 有 | 有 | 展示/截屏 |
本章小结
| 要点 | 说明 |
|---|---|
| Tween 动画 | 用于走子和吃子的平滑移动动画 |
| 吃子流程 | 冲刺 → 碰撞 → 爆炸 → 被吃棋子飞走 → 归位 |
| 粒子特效 | GPUParticles3D 实现爆炸、火花效果 |
| 闪光效果 | OmniLight3D 模拟碰撞闪光 |
| 音效系统 | AudioStreamPlayer 管理走子/吃子/将军音效 |
| AnimationPlayer | 用于制作更复杂的动画序列 |
