9. 音效与特效
2026/4/14大约 4 分钟
9. 雷霆战机——音效与特效
9.1 音效设计
射击游戏的音效非常关键——密集的射击声和爆炸声营造了紧张刺激的战斗氛围。
| 事件 | 音效 | 特点 |
|---|---|---|
| 玩家射击 | 短促的"嘟"声 | 高频、极短(0.05s) |
| 敌人爆炸 | "轰"声 | 低频、带混响 |
| 小敌人被消灭 | "啪"声 | 轻快 |
| Boss受伤 | 金属撞击声 | 中频 |
| Boss爆炸 | 巨大的爆炸声 | 低频+震动 |
| 道具拾取 | "叮"声 | 清脆悦耳 |
| 炸弹使用 | "嗡——"声 | 持续1秒 |
| 武器升级 | 上升音阶 | "叮叮叮" |
| 被击中 | 警报声 | 刺耳 |
| 警告(Boss出现) | 低沉警报 | 循环播放 |
9.2 音效管理器
C
using Godot;
/// <summary>
/// 雷霆战机音效管理器
/// </summary>
public partial class RaidenAudioManager : Node
{
private AudioStreamPlayer[] _sfxPlayers;
private const int SfxPoolSize = 10;
private int _sfxIndex = 0;
private AudioStreamPlayer _musicPlayer;
private AudioStreamPlayer _bossMusicPlayer;
private Dictionary<string, AudioStream> _sfx = new();
public override void _Ready()
{
// 创建音效池
_sfxPlayers = new AudioStreamPlayer[SfxPoolSize];
for (int i = 0; i < SfxPoolSize; i++)
{
var player = new AudioStreamPlayer();
player.Bus = "SFX";
AddChild(player);
_sfxPlayers[i] = player;
}
// 音乐播放器
_musicPlayer = new AudioStreamPlayer();
_musicPlayer.Bus = "Music";
AddChild(_musicPlayer);
_bossMusicPlayer = new AudioStreamPlayer();
_bossMusicPlayer.Bus = "Music";
AddChild(_bossMusicPlayer);
LoadAudio();
}
private void LoadAudio()
{
_sfx["player_shoot"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/player_shoot.wav");
_sfx["enemy_hit"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/enemy_hit.wav");
_sfx["enemy_explode"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/enemy_explode.wav");
_sfx["boss_hit"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/boss_hit.wav");
_sfx["boss_explode"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/boss_explode.wav");
_sfx["powerup"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/powerup.wav");
_sfx["bomb"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/bomb.wav");
_sfx["player_hit"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/player_hit.wav");
_sfx["weapon_upgrade"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/weapon_upgrade.wav");
_sfx["warning"] = GD.Load<AudioStream>(
"res://assets/audio/sfx/warning.wav");
}
/// <summary>
/// 播放音效(从池中取播放器)
/// </summary>
public void PlaySFX(string name, float volumeDb = 0f)
{
if (!_sfx.ContainsKey(name)) return;
var player = _sfxPlayers[_sfxIndex];
_sfxIndex = (_sfxIndex + 1) % SfxPoolSize;
player.Stream = _sfx[name];
player.VolumeDb = volumeDb;
player.Play();
}
/// <summary>
/// 播放背景音乐
/// </summary>
public void PlayStageMusic()
{
var music = GD.Load<AudioStream>(
"res://assets/audio/music/stage.ogg");
_musicPlayer.Stream = music;
_musicPlayer.Play();
}
/// <summary>
/// 切换到Boss音乐
/// </summary>
public void PlayBossMusic()
{
_musicPlayer.Stop();
var music = GD.Load<AudioStream>(
"res://assets/audio/music/boss.ogg");
_bossMusicPlayer.Stream = music;
_bossMusicPlayer.Play();
}
/// <summary>
/// 停止Boss音乐,恢复普通音乐
/// </summary>
public void StopBossMusic()
{
_bossMusicPlayer.Stop();
_musicPlayer.Play();
}
}GDScript
extends Node
## 雷霆战机音效管理器
var _sfx_players: Array = []
const SFX_POOL_SIZE: int = 10
var _sfx_index: int = 0
var _music_player: AudioStreamPlayer
var _boss_music_player: AudioStreamPlayer
var _sfx: Dictionary = {}
func _ready() -> void:
for i in range(SFX_POOL_SIZE):
var player = AudioStreamPlayer.new()
player.bus = "SFX"
add_child(player)
_sfx_players.append(player)
_music_player = AudioStreamPlayer.new()
_music_player.bus = "Music"
add_child(_music_player)
_boss_music_player = AudioStreamPlayer.new()
_boss_music_player.bus = "Music"
add_child(_boss_music_player)
_load_audio()
func _load_audio() -> void:
_sfx["player_shoot"] = load("res://assets/audio/sfx/player_shoot.wav")
_sfx["enemy_hit"] = load("res://assets/audio/sfx/enemy_hit.wav")
_sfx["enemy_explode"] = load("res://assets/audio/sfx/enemy_explode.wav")
_sfx["boss_hit"] = load("res://assets/audio/sfx/boss_hit.wav")
_sfx["boss_explode"] = load("res://assets/audio/sfx/boss_explode.wav")
_sfx["powerup"] = load("res://assets/audio/sfx/powerup.wav")
_sfx["bomb"] = load("res://assets/audio/sfx/bomb.wav")
_sfx["player_hit"] = load("res://assets/audio/sfx/player_hit.wav")
_sfx["weapon_upgrade"] = load("res://assets/audio/sfx/weapon_upgrade.wav")
_sfx["warning"] = load("res://assets/audio/sfx/warning.wav")
## 播放音效
func play_sfx(name: String, volume_db: float = 0.0) -> void:
if not _sfx.has(name):
return
var player = _sfx_players[_sfx_index]
_sfx_index = (_sfx_index + 1) % SFX_POOL_SIZE
player.stream = _sfx[name]
player.volume_db = volume_db
player.play()
## 播放背景音乐
func play_stage_music() -> void:
var music = load("res://assets/audio/music/stage.ogg")
_music_player.stream = music
_music_player.play()
## 切换到Boss音乐
func play_boss_music() -> void:
_music_player.stop()
var music = load("res://assets/audio/music/boss.ogg")
_boss_music_player.stream = music
_boss_music_player.play()
## 停止Boss音乐
func stop_boss_music() -> void:
_boss_music_player.stop()
_music_player.play()9.3 爆炸粒子特效
爆炸是射击游戏中最常见的特效。我们使用GPUParticles2D创建不同大小的爆炸效果。
C
using Godot;
/// <summary>
/// 爆炸特效
/// </summary>
public partial class Explosion : GpuParticles2D
{
public override void _Ready()
{
// 自动销毁
Emitting = true;
var timer = GetTree().CreateTimer(Lifetime);
timer.Timeout += QueueFree;
}
/// <summary>
/// 创建小型爆炸(敌人被消灭)
/// </summary>
public static Explosion CreateSmall(Vector2 position, Node parent)
{
var scene = GD.Load<PackedScene>(
"res://scenes/effects/explosion_small.tscn");
var fx = scene.Instantiate<Explosion>();
fx.GlobalPosition = position;
fx.Amount = 15;
fx.Scale = new Vector2(0.5f, 0.5f);
parent.AddChild(fx);
return fx;
}
/// <summary>
/// 创建中型爆炸
/// </summary>
public static Explosion CreateMedium(Vector2 position, Node parent)
{
var scene = GD.Load<PackedScene>(
"res://scenes/effects/explosion_medium.tscn");
var fx = scene.Instantiate<Explosion>();
fx.GlobalPosition = position;
fx.Amount = 30;
fx.Scale = Vector2.One;
parent.AddChild(fx);
return fx;
}
/// <summary>
/// 创建大型爆炸(Boss死亡)
/// </summary>
public static Explosion CreateLarge(Vector2 position, Node parent)
{
var scene = GD.Load<PackedScene>(
"res://scenes/effects/explosion_large.tscn");
var fx = scene.Instantiate<Explosion>();
fx.GlobalPosition = position;
fx.Amount = 60;
fx.Scale = new Vector2(2f, 2f);
parent.AddChild(fx);
return fx;
}
}GDScript
extends GPUParticles2D
## 爆炸特效
func _ready() -> void:
emitting = true
var timer = get_tree().create_timer(lifetime)
timer.timeout.connect(queue_free)
## 创建小型爆炸
static func create_small(pos: Vector2, parent: Node) -> Explosion:
var scene = load("res://scenes/effects/explosion_small.tscn")
var fx = scene.instantiate()
fx.global_position = pos
fx.amount = 15
fx.scale = Vector2(0.5, 0.5)
parent.add_child(fx)
return fx
## 创建中型爆炸
static func create_medium(pos: Vector2, parent: Node) -> Explosion:
var scene = load("res://scenes/effects/explosion_medium.tscn")
var fx = scene.instantiate()
fx.global_position = pos
fx.amount = 30
fx.scale = Vector2.ONE
parent.add_child(fx)
return fx
## 创建大型爆炸
static func create_large(pos: Vector2, parent: Node) -> Explosion:
var scene = load("res://scenes/effects/explosion_large.tscn")
var fx = scene.instantiate()
fx.global_position = pos
fx.amount = 60
fx.scale = Vector2(2, 2)
parent.add_child(fx)
return fx9.4 屏幕震动
屏幕震动在射击游戏中非常重要——爆炸时的震动让打击感更强。
| 事件 | 震动强度 | 持续时间 |
|---|---|---|
| 小敌人爆炸 | 2px | 0.1s |
| 中型敌人爆炸 | 4px | 0.15s |
| Boss受伤 | 3px | 0.2s |
| 玩家使用炸弹 | 8px | 0.5s |
| Boss死亡 | 12px | 1.0s |
屏幕震动的实现方式和俄罗斯方块中完全相同,可以复用同一个 ScreenShake 脚本。
9.5 视觉反馈总结
| 反馈类型 | 触发事件 | 实现方式 |
|---|---|---|
| 粒子爆炸 | 敌人被消灭 | GPUParticles2D |
| 受伤闪烁 | 敌人被击中 | Modulate闪烁 |
| 屏幕震动 | 爆炸/炸弹 | Camera2D偏移 |
| 分数弹出 | 敌人消灭 | 飘动的Label |
| 白色闪光 | 炸弹使用 | ColorRect淡出 |
| 入场动画 | Boss出现 | 缓动移动 |
9.6 本章小结
| 组件 | 说明 |
|---|---|
| 音效管理器 | 对象池播放,区分音效/音乐 |
| 爆炸特效 | 小/中/大三种尺寸 |
| 屏幕震动 | 按事件分级 |
| 背景音乐 | 普通音乐/Boss音乐切换 |
| 视觉反馈 | 闪烁、震动、粒子、飘字 |
关键设计点:
- 射击音效需要极短(0.05s),否则会重叠成一团噪音
- Boss音乐和普通音乐切换要有过渡
- 爆炸特效自动销毁,不造成内存泄漏
- 屏幕震动强度要适度,太强会让玩家不舒服
下一章我们将进行最后的打磨和发布准备。
