9. 音效与视觉特效
2026/4/14大约 5 分钟
音效与视觉特效
音效和视觉特效虽然不是游戏的核心逻辑,但它们是"手感"的关键组成部分。CS 的枪声、脚步声和爆炸声不只是装饰——它们是玩家获取信息的核心渠道。这一章我们实现 3D 空间音效和基本的视觉特效。
3D 空间音效
CS 的音效是"3D"的——你能听到枪声从哪个方向传来,能根据脚步声判断敌人的位置。Godot 的 AudioStreamPlayer3D 天然支持这种空间音效。
音效类型
| 音效类型 | 数量 | 说明 |
|---|---|---|
| 枪声 | 8-12 | 每种武器独特的枪声 |
| 脚步声 | 4-6 | 不同地面的脚步声 |
| 炸弹 | 3 | 安放声、滴滴声、爆炸声 |
| 环境音 | 2-3 | 背景氛围 |
| UI 音效 | 3-5 | 买枪、回合开始、击杀提示 |
| 弹壳声 | 1-2 | 弹壳落地 |
| 受击声 | 2 | 被打中、爆头 |
音效管理器
C#
// Scripts/Systems/AudioManager.cs
using Godot;
/// <summary>
/// 全局音效管理器(Autoload 单例)。
///
/// 两个核心功能:
/// 1. 播放 2D 音效(UI 音效、背景音乐)
/// 2. 在指定 3D 位置播放空间音效(枪声、脚步声、爆炸声)
///
/// 空间音效的"空间感"体现在:
/// - 距离越远,声音越小
/// - 左边传来的声音,左耳机大右耳机小
/// - 被墙挡住时声音变闷(需要设置声音遮挡)
/// </summary>
public partial class AudioManager : Node
{
[Export] public AudioStream BuySound { get; set; }
[Export] public AudioStream RoundStartSound { get; set; }
[Export] public AudioStream RoundEndSound { get; set; }
[Export] public AudioStream HeadshotSound { get; set; }
[Export] public AudioStream HitSound { get; set; }
private AudioStreamPlayer _uiPlayer;
private AudioStreamPlayer _musicPlayer;
public override void _Ready()
{
_uiPlayer = new AudioStreamPlayer();
_musicPlayer = new AudioStreamPlayer();
AddChild(_uiPlayer);
AddChild(_musicPlayer);
// 监听游戏事件
var gameEvents = GetNode<GameEvents>("/root/GameEvents");
gameEvents.PlayerShot += OnPlayerShot;
gameEvents.PlayerHit += OnPlayerHit;
gameEvents.RoundStarted += OnRoundStarted;
gameEvents.BombExploded += OnBombExploded;
}
/// <summary>
/// 播放 2D UI 音效(不随位置变化,比如菜单点击声)
/// </summary>
public void PlayUI(AudioStream stream)
{
if (stream == null) return;
_uiPlayer.Stream = stream;
_uiPlayer.Play();
}
/// <summary>
/// 在指定 3D 位置播放一次性音效。
///
/// 原理:创建一个临时的 AudioStreamPlayer3D 节点,
/// 播放完后自动销毁。
/// </summary>
public void Play3DAt(AudioStream stream, Vector3 position,
float volumeDb = 0f, float maxDistance = 40f)
{
if (stream == null) return;
var player = new AudioStreamPlayer3D();
player.Stream = stream;
player.VolumeDb = volumeDb;
player.MaxDistance = maxDistance;
player.UnitSize = 10f; // 声音衰减参数
player.Position = position;
AddChild(player);
player.Play();
// 播放结束后自动清理
player.Finished += () => player.QueueFree();
}
// 事件处理
private void OnPlayerShot(Vector3 position, Vector3 direction)
{
// 枪声在射击位置播放
// 不同武器的枪声由武器脚本自己播放
// 这里只处理通用音效(比如远处的枪声回音)
}
private void OnPlayerHit(Node3D target, float damage, string hitZone)
{
if (hitZone == "head")
{
PlayUI(HeadshotSound);
}
else
{
PlayUI(HitSound);
}
}
private void OnRoundStarted(int roundNumber)
{
PlayUI(RoundStartSound);
}
private void OnBombExploded()
{
// 爆炸声非常大,全图都能听到
// 由 BombSystem 自己播放近距离爆炸声
// 这里播放远处的回音
}
}GDScript
# Scripts/Systems/AudioManager.gd
extends Node
## 全局音效管理器(Autoload 单例)。
@export var buy_sound: AudioStream
@export var round_start_sound: AudioStream
@export var round_end_sound: AudioStream
@export var headshot_sound: AudioStream
@export var hit_sound: AudioStream
var _ui_player: AudioStreamPlayer
var _music_player: AudioStreamPlayer
func _ready():
_ui_player = AudioStreamPlayer.new()
_music_player = AudioStreamPlayer.new()
add_child(_ui_player)
add_child(_music_player)
# 监听游戏事件
var game_events = get_node("/root/GameEvents") as GameEvents
game_events.player_shot.connect(_on_player_shot)
game_events.player_hit.connect(_on_player_hit)
game_events.round_started.connect(_on_round_started)
game_events.bomb_exploded.connect(_on_bomb_exploded)
## 播放 2D UI 音效
func play_ui(stream: AudioStream):
if not stream:
return
_ui_player.stream = stream
_ui_player.play()
## 在指定 3D 位置播放一次性音效。
func play_3d_at(stream: AudioStream, position: Vector3,
volume_db: float = 0.0, max_distance: float = 40.0):
if not stream:
return
var player = AudioStreamPlayer3D.new()
player.stream = stream
player.volume_db = volume_db
player.max_distance = max_distance
player.unit_size = 10.0
player.position = position
add_child(player)
player.play()
# 播放结束后自动清理
player.finished.connect(player.queue_free)
func _on_player_shot(_position: Vector3, _direction: Vector3):
pass
func _on_player_hit(_target: Node3D, _damage: float, hit_zone: String):
if hit_zone == "head":
play_ui(headshot_sound)
else:
play_ui(hit_sound)
func _on_round_started(_round_number: int):
play_ui(round_start_sound)
func _on_bomb_exploded():
pass视觉特效
枪口闪光
每次射击时枪口处短暂出现一个亮光,给玩家即时的视觉反馈。
C#
// 在 HitscanWeapon.cs 中添加枪口闪光
private void ShowMuzzleFlash()
{
var flash = new MeshInstance3D();
var quad = new QuadMesh();
quad.Size = new Vector2(0.3f, 0.3f);
flash.Mesh = quad;
var material = new StandardMaterial3D();
material.AlbedoColor = Colors.Yellow;
material.EmissionEnabled = true;
material.Emission = Colors.White;
material.EmissionEnergyMultiplier = 5f;
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
flash.MaterialOverride = material;
// 放在枪口位置
flash.Position = new Vector3(0, 0, -0.5f);
flash.LookAt(flash.GlobalPosition + Vector3.Forward, Vector3.Up);
AddChild(flash);
// 0.05 秒后移除
GetTree().CreateTimer(0.05).Timeout += () => flash.QueueFree();
}GDScript
# 枪口闪光
func _show_muzzle_flash():
var flash = MeshInstance3D.new()
var quad = QuadMesh.new()
quad.size = Vector2(0.3, 0.3)
flash.mesh = quad
var material = StandardMaterial3D.new()
material.albedo_color = Color.YELLOW
material.emission_enabled = true
material.emission = Color.WHITE
material.emission_energy_multiplier = 5.0
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
flash.material_override = material
flash.position = Vector3(0, 0, -0.5)
flash.look_at(flash.global_position + Vector3.FORWARD, Vector3.UP)
add_child(flash)
# 0.05 秒后移除
get_tree().create_timer(0.05).timeout.connect(flash.queue_free)弹孔效果
子弹命中墙壁或地面时留下的弹孔标记。
C#
// 弹孔创建
private void CreateBulletHole(Vector3 position, Vector3 normal)
{
var hole = new MeshInstance3D();
var quad = new QuadMesh();
quad.Size = new Vector2(0.05f, 0.05f);
hole.Mesh = quad;
var material = new StandardMaterial3D();
material.AlbedoColor = new Color(0.1f, 0.1f, 0.1f);
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
hole.MaterialOverride = material;
hole.GlobalPosition = position + normal * 0.01f; // 略微偏移防止 z-fighting
hole.LookAt(position + normal, Vector3.Up);
// 将弹孔添加到地图场景中(不随武器移动)
GetTree().CurrentScene.AddChild(hole);
// 10 秒后移除(避免弹孔无限堆积)
GetTree().CreateTimer(10.0).Timeout += () => hole.QueueFree();
}GDScript
# 弹孔创建
func _create_bullet_hole(position: Vector3, normal: Vector3):
var hole = MeshInstance3D.new()
var quad = QuadMesh.new()
quad.size = Vector2(0.05, 0.05)
hole.mesh = quad
var material = StandardMaterial3D.new()
material.albedo_color = Color(0.1, 0.1, 0.1)
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
hole.material_override = material
hole.global_position = position + normal * 0.01 # 略微偏移
hole.look_at(position + normal, Vector3.UP)
get_tree().current_scene.add_child(hole)
get_tree().create_timer(10.0).timeout.connect(hole.queue_free)受击效果
玩家被打中时屏幕边缘闪红,给予被攻击的视觉反馈。
C#
// 添加到 HUD 中的受击效果
private ColorRect _damageOverlay;
public void ShowDamageIndicator(Vector3 damageSource)
{
// 屏幕边缘闪红
_damageOverlay.Color = new Color(1, 0, 0, 0.3f);
_damageOverlay.Visible = true;
// 0.3 秒后淡出
var tween = CreateTween();
tween.TweenProperty(_damageOverlay, "color:a", 0f, 0.3f);
tween.TweenCallback(Callable.From(() => _damageOverlay.Visible = false));
}GDScript
# 受击效果
var _damage_overlay: ColorRect
func show_damage_indicator(_damage_source: Vector3):
_damage_overlay.color = Color(1, 0, 0, 0.3)
_damage_overlay.visible = true
var tween = create_tween()
tween.tween_property(_damage_overlay, "color:a", 0.0, 0.3)
tween.tween_callback(func(): _damage_overlay.visible = false)小结
这一章实现了音效和视觉特效:
- 3D 空间音效:AudioStreamPlayer3D 实现方向感和距离衰减
- 音效管理器:全局单例,统一管理 UI 音效和 3D 音效
- 视觉特效:枪口闪光、弹孔效果、受击屏幕闪红
- 自动清理:临时特效播放完毕后自动销毁,防止内存泄漏
下一章我们做最后的打磨和发布准备。
