9. 音效与特效
2026/4/14大约 6 分钟
休闲益智——音效与特效
你有没有注意到:玩消消乐的时候,如果关掉声音,游戏会变得无聊很多?那些"叮叮叮"的消除声、连击时的音调升高、过关时的欢快旋律——它们虽然看不见,却极大地影响了游戏的"手感"。
音效和特效是三消游戏的"调味料"——没有也能玩,但有了会让体验提升好几个档次。
本章你将学到
- 三消游戏的音效分类和设计要点
- 创建音效管理器(AudioManager)
- 消除音效(连击时音调递增)
- 特殊方块触发的音效
- 背景音乐管理
- 粒子特效(消除爆炸、过关庆祝)
音效分类
| 类型 | 说明 | 何时播放 |
|---|---|---|
| 操作音效 | 玩家操作时的反馈 | 点击选中、交换方块 |
| 消除音效 | 方块被消除时的声音 | 三消、四消、五消各不同 |
| 特殊音效 | 特殊方块触发时 | 条纹激活、炸弹爆炸、彩虹全消 |
| UI 音效 | 界面交互 | 按钮点击、过关、失败 |
| 背景音乐 | 持续播放的旋律 | 游戏全程循环播放 |
消除音效是灵魂
三消游戏的消除音效特别重要——它要有"清脆感"和"满足感"。很多玩家玩消消乐就是冲着那个"叮"的声音去的。连击时音调逐级升高,会让玩家心跳加速。
音效管理器
把所有音效集中到一个管理器里,方便统一控制音量、切换音效。
C
using Godot;
/// <summary>
/// 音效管理器 —— 统一管理游戏中的所有声音。
/// 就像一台音响的主控台:控制音量、切换曲目、播放音效。
/// </summary>
public partial class AudioManager : Node
{
public static AudioManager Instance { get; private set; }
/// <summary>音效播放器(短音效用这个)</summary>
private AudioStreamPlayer _sfxPlayer;
/// <summary>背景音乐播放器(长音频用这个)</summary>
private AudioStreamPlayer _bgmPlayer;
/// <summary>音效音量(0.0 ~ 1.0)</summary>
private float _sfxVolume = 1.0f;
/// <summary>背景音乐音量(0.0 ~ 1.0)</summary>
private float _bgmVolume = 0.7f;
// 音效资源引用
[Export] public AudioStream SelectSound { get; set; }
[Export] public AudioStream SwapSound { get; set; }
[Export] public AudioStream MatchSound { get; set; }
[Export] public AudioStream InvalidSwapSound { get; set; }
[Export] public AudioStream SpecialSound { get; set; }
[Export] public AudioStream LevelCompleteSound { get; set; }
[Export] public AudioStream FailSound { get; set; }
[Export] public AudioStream BgmMusic { get; set; }
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
// 创建两个独立的播放器
_sfxPlayer = new AudioStreamPlayer();
_sfxPlayer.Bus = "SFX";
AddChild(_sfxPlayer);
_bgmPlayer = new AudioStreamPlayer();
_bgmPlayer.Bus = "BGM";
AddChild(_bgmPlayer);
}
/// <summary>播放音效</summary>
public void PlaySFX(AudioStream sound, float pitchScale = 1.0f)
{
if (sound == null) return;
_sfxPlayer.Stream = sound;
_sfxPlayer.PitchScale = pitchScale;
_sfxPlayer.VolumeDb = Mathf.LinearToDb(_sfxVolume);
_sfxPlayer.Play();
}
/// <summary>
/// 播放消除音效(连击时音调逐级升高)
/// 连击越高音调越高,最高升高一个八度(2倍)
/// </summary>
public void PlayMatchSound(int combo)
{
float pitch = Mathf.Min(1.0f + (combo - 1) * 0.15f, 2.0f);
PlaySFX(MatchSound, pitch);
}
/// <summary>播放背景音乐</summary>
public void PlayBGM()
{
if (BgmMusic == null) return;
_bgmPlayer.Stream = BgmMusic;
_bgmPlayer.VolumeDb = Mathf.LinearToDb(_bgmVolume);
_bgmPlayer.Play();
}
/// <summary>停止背景音乐</summary>
public void StopBGM()
{
_bgmPlayer.Stop();
}
/// <summary>设置音效音量</summary>
public void SetSFXVolume(float volume)
{
_sfxVolume = Mathf.Clamp(volume, 0f, 1f);
}
/// <summary>设置背景音乐音量</summary>
public void SetBGMVolume(float volume)
{
_bgmVolume = Mathf.Clamp(volume, 0f, 1f);
_bgmPlayer.VolumeDb = Mathf.LinearToDb(_bgmVolume);
}
}GDScript
extends Node
## 音效管理器 —— 统一管理游戏中的所有声音
@onready var instance: Node = self
## 音效播放器
var _sfx_player: AudioStreamPlayer
## 背景音乐播放器
var _bgm_player: AudioStreamPlayer
## 音量
var _sfx_volume: float = 1.0
var _bgm_volume: float = 0.7
# 音效资源
@export var select_sound: AudioStream
@export var swap_sound: AudioStream
@export var match_sound: AudioStream
@export var invalid_swap_sound: AudioStream
@export var special_sound: AudioStream
@export var level_complete_sound: AudioStream
@export var fail_sound: AudioStream
@export var bgm_music: AudioStream
func _ready():
if instance and instance != self:
queue_free()
return
instance = self
_sfx_player = AudioStreamPlayer.new()
_sfx_player.bus = "SFX"
add_child(_sfx_player)
_bgm_player = AudioStreamPlayer.new()
_bgm_player.bus = "BGM"
add_child(_bgm_player)
## 播放音效
func play_sfx(sound: AudioStream, pitch_scale: float = 1.0) -> void:
if sound == null:
return
_sfx_player.stream = sound
_sfx_player.pitch_scale = pitch_scale
_sfx_player.volume_db = linear_to_db(_sfx_volume)
_sfx_player.play()
## 播放消除音效(连击时音调升高)
func play_match_sound(combo: int) -> void:
var pitch: float = mini(1.0 + (combo - 1) * 0.15, 2.0)
play_sfx(match_sound, pitch)
## 播放背景音乐
func play_bgm() -> void:
if bgm_music == null:
return
_bgm_player.stream = bgm_music
_bgm_player.volume_db = linear_to_db(_bgm_volume)
_bgm_player.play()
## 停止背景音乐
func stop_bgm() -> void:
_bgm_player.stop()
## 设置音效音量
func set_sfx_volume(volume: float) -> void:
_sfx_volume = clampf(volume, 0.0, 1.0)
## 设置背景音乐音量
func set_bgm_volume(volume: float) -> void:
_bgm_volume = clampf(volume, 0.0, 1.0)
_bgm_player.volume_db = linear_to_db(_bgm_volume)音效总线设置
在 Godot 中需要先创建音效总线(Bus),让音效和背景音乐的音量可以独立控制:
- 打开
项目 → 项目设置 → 音频总线 - 添加两个总线:
- SFX(音效):用于短音效(消除、点击等)
- BGM(背景音乐):用于持续播放的背景旋律
为什么分两个总线?
这样玩家可以单独关闭背景音乐但保留音效,或者反过来。大多数游戏都提供这种独立音量控制。
粒子特效
除了声音,视觉特效也是"打击感"的重要组成部分。Godot 内置了强大的粒子系统(GPUParticles2D),可以用它来做消除时的爆炸效果。
消除爆炸粒子
为每种颜色创建一组粒子效果:消除红色方块时喷出红色粒子,消除蓝色方块时喷出蓝色粒子。
C
/// <summary>
/// 在指定位置播放消除粒子特效。
/// 创建一组 GPUParticles2D,播放完毕后自动销毁。
/// </summary>
private void SpawnMatchParticles(Vector2 position, Color color)
{
var particles = new GPUParticles2D();
particles.Position = position;
particles.Amount = 12;
particles.Lifetime = 0.5f;
particles.Explosiveness = 0.8f;
particles.OneShot = true;
// 设置粒子材质
var material = new ParticleProcessMaterial();
material.EmissionShape = ParticleProcessMaterial.EmissionShapeEnum.Box;
material.EmissionBoxExtents = new Vector3(20, 20, 0);
// 粒子颜色
material.ColorRamp = new GradientTexture1D
{
Gradient = new Gradient
{
Colors = new[] { color, new Color(color, 0f) },
Offsets = new[] { 0.0f, 1.0f }
}
};
// 粒子运动方向:向外扩散
material.Direction = new Vector3(0, -1, 0);
material.Spread = 180.0f;
material.InitialVelocityMin = 50.0f;
material.InitialVelocityMax = 150.0f;
material.Gravity = new Vector3(0, 200, 0);
particles.ProcessMaterial = material;
AddChild(particles);
// 播放完毕后自动销毁
var timer = GetTree().CreateTimer(1.0f);
timer.Timeout += () => particles.QueueFree();
}GDScript
## 在指定位置播放消除粒子特效
func spawn_match_particles(pos: Vector2, color: Color) -> void:
var particles = GPUParticles2D.new()
particles.position = pos
particles.amount = 12
particles.lifetime = 0.5
particles.explosiveness = 0.8
particles.one_shot = true
var material = ParticleProcessMaterial.new()
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
material.emission_box_extents = Vector3(20, 20, 0)
# 粒子颜色
var gradient = Gradient.new()
gradient.colors = PackedColorArray([color, Color(color, 0.0)])
gradient.offsets = PackedFloat32Array([0.0, 1.0])
var color_ramp = GradientTexture1D.new()
color_ramp.gradient = gradient
material.color_ramp = color_ramp
# 粒子运动
material.direction = Vector3(0, -1, 0)
material.spread = 180.0
material.initial_velocity_min = 50.0
material.initial_velocity_max = 150.0
material.gravity = Vector3(0, 200, 0)
particles.process_material = material
add_child(particles)
# 自动销毁
var timer = get_tree().create_timer(1.0)
timer.timeout.connect(particles.queue_free)屏幕震动
屏幕震动是增强"打击感"的经典技巧。当触发特殊方块或高连击时,让摄像机微微抖动。
/// <summary>
/// 屏幕震动 —— 让摄像机在短时间内随机偏移,模拟"震动"。
/// 就像手机震动一样,轻微的抖动能传达"力量感"。
/// </summary>
public void ScreenShake(float intensity = 3f, float duration = 0.2f)
{
var camera = GetParent().GetNode<Camera2D>("Camera2D");
if (camera == null) return;
var originalOffset = camera.Offset;
var tween = CreateTween();
// 震动次数
int shakeCount = Mathf.Max(1, (int)(duration / 0.02f));
for (int i = 0; i < shakeCount; i++)
{
tween.TweenCallback(Callable.From(() =>
{
camera.Offset = originalOffset + new Vector2(
(float)GD.RandRange(-intensity, intensity),
(float)GD.RandRange(-intensity, intensity)
);
}));
}
// 震动结束,恢复原位
tween.TweenCallback(Callable.From(() => camera.Offset = originalOffset));
}音效资源获取
如果你没有音效素材,可以从以下免费资源网站获取:
| 网站 | 说明 | 许可证 |
|---|---|---|
| Kenney.nl | 游戏音效包,完全免费 | CC0(公共领域) |
| freesound.org | 大量免费音效 | 需注册,注意许可证 |
| opengameart.org | 开源游戏资源 | 各不相同 |
音效设计建议
- 消除音效用短促的"叮"声,清脆明亮
- 连击音效每次音调升高一点,让玩家感到"越来越爽"
- 过关音效用欢快的短旋律,失败音效用低沉柔和的声音
- 背景音乐要轻快但不抢戏,音量控制在音效的 70% 左右
下一章预告
音效和特效让游戏"有声有色"了。最后一章,我们将进行最终的打磨和发布准备——从"能玩"到"好玩"的最后一步。
