9. 音效与特效
2026/4/14大约 5 分钟
9. 超级玛丽奥——音效与特效
简介
音效和特效让超级玛丽奥变得"有感觉"——跳跃时有清脆的"boing"声,踩敌人时有满足的"噗"声,吃蘑菇时有欢快的升级音,无敌时玛丽奥闪烁彩虹色。
这些看似不起眼的细节,实际上决定了游戏的"手感好坏"。一个没有音效的游戏,就像一部没有配音的电影,总让人觉得少了什么。
音效列表
超级玛丽奥需要以下音效:
| 音效名称 | 触发时机 | 说明 |
|---|---|---|
| jump | 玩家跳跃 | 轻快的弹跳声 |
| stomp | 踩敌人 | 短促的"噗"声 |
| coin | 收集金币 | "叮"声 |
| powerup | 吃蘑菇/火焰花 | 上升音调的升级声 |
| powerdown | 被敌人碰到缩小 | 下降音调的受伤声 |
| bump | 顶砖块 | "砰"声 |
| brick_break | 砖块碎裂 | 碎裂声 |
| fireball | 发射火球 | "嗖"声 |
| flagpole | 碰旗杆 | 下滑音效 |
| game_over | 游戏结束 | 低沉的音乐 |
| 1up | 获得1UP | 特殊音效 |
| star_powerup | 获得星星无敌 | 欢快的音乐 |
| kick | 踢龟壳 | 脚踢声 |
音效管理器
// AudioManager.cs - 音效管理器
using Godot;
using System.Collections.Generic;
public partial class AudioManager : Node
{
public static AudioManager Instance { get; private set; }
[Export] public float MasterVolume { get; set; } = 1.0f;
[Export] public float SfxVolume { get; set; } = 0.8f;
[Export] public float MusicVolume { get; set; } = 0.5f;
private Dictionary<string, AudioStream> _sfxCache = new();
private AudioStreamPlayer _musicPlayer;
private const int MAX_SFX_PLAYERS = 8;
private List<AudioStreamPlayer> _sfxPlayers = new();
public override void _EnterTree()
{
if (Instance != null) { QueueFree(); return; }
Instance = this;
}
public override void _Ready()
{
// 创建音乐播放器
_musicPlayer = new AudioStreamPlayer();
AddChild(_musicPlayer);
// 创建音效播放器池
for (int i = 0; i < MAX_SFX_PLAYERS; i++)
{
var player = new AudioStreamPlayer();
AddChild(player);
_sfxPlayers.Add(player);
}
// 预加载常用音效
PreloadSfx();
}
private void PreloadSfx()
{
var sfxList = new[]
{
"jump", "stomp", "coin", "powerup", "powerdown",
"bump", "brick_break", "fireball", "flagpole",
"game_over", "1up", "star_powerup", "kick"
};
foreach (var name in sfxList)
{
var path = $"res://assets/audio/sfx/{name}.wav";
if (ResourceLoader.Exists(path))
{
_sfxCache[name] = GD.Load<AudioStream>(path);
}
}
}
// 播放音效
public void PlaySfx(string name)
{
if (!_sfxCache.TryGetValue(name, out var stream)) return;
AudioStreamPlayer player = null;
foreach (var p in _sfxPlayers)
{
if (!p.Playing) { player = p; break; }
}
if (player == null) player = _sfxPlayers[0];
player.Stream = stream;
player.VolumeDb = Mathf.LinearToDb(SfxVolume * MasterVolume);
player.Play();
}
// 播放音乐
public void PlayMusic(string name)
{
var path = $"res://assets/audio/music/{name}.ogg";
if (!ResourceLoader.Exists(path)) return;
_musicPlayer.Stream = GD.Load<AudioStream>(path);
_musicPlayer.VolumeDb = Mathf.LinearToDb(MusicVolume * MasterVolume);
_musicPlayer.Play();
}
public void StopMusic() => _musicPlayer.Stop();
public void SetMusicVolume(float vol)
{
MusicVolume = Mathf.Clamp(vol, 0, 1);
_musicPlayer.VolumeDb = Mathf.LinearToDb(MusicVolume * MasterVolume);
}
public void SetSfxVolume(float vol) => SfxVolume = Mathf.Clamp(vol, 0, 1);
}# audio_manager.gd - 音效管理器
extends Node
var instance: Node = null
@export var master_volume: float = 1.0
@export var sfx_volume: float = 0.8
@export var music_volume: float = 0.5
var _sfx_cache: Dictionary = {}
var _music_player: AudioStreamPlayer
const MAX_SFX_PLAYERS: int = 8
var _sfx_players: Array[AudioStreamPlayer] = []
func _enter_tree() -> void:
if instance != null:
queue_free()
return
instance = self
func _ready() -> void:
_music_player = AudioStreamPlayer.new()
add_child(_music_player)
for i in range(MAX_SFX_PLAYERS):
var player = AudioStreamPlayer.new()
add_child(player)
_sfx_players.append(player)
preload_sfx()
func preload_sfx() -> void:
var sfx_list = [
"jump", "stomp", "coin", "powerup", "powerdown",
"bump", "brick_break", "fireball", "flagpole",
"game_over", "1up", "star_powerup", "kick"
]
for name in sfx_list:
var path = "res://assets/audio/sfx/%s.wav" % name
if ResourceLoader.exists(path):
_sfx_cache[name] = load(path)
## 播放音效
func play_sfx(name: String) -> void:
if not name in _sfx_cache:
return
var player: AudioStreamPlayer = null
for p in _sfx_players:
if not p.playing:
player = p
break
if player == null:
player = _sfx_players[0]
player.stream = _sfx_cache[name]
player.volume_db = linear_to_db(sfx_volume * master_volume)
player.play()
## 播放音乐
func play_music(name: String) -> void:
var path = "res://assets/audio/music/%s.ogg" % name
if not ResourceLoader.exists(path):
return
_music_player.stream = load(path)
_music_player.volume_db = linear_to_db(music_volume * master_volume)
_music_player.play()
func stop_music() -> void:
_music_player.stop()
func set_music_volume(vol: float) -> void:
music_volume = clampf(vol, 0.0, 1.0)
_music_player.volume_db = linear_to_db(music_volume * master_volume)
func set_sfx_volume(vol: float) -> void:
sfx_volume = clampf(vol, 0.0, 1.0)变大闪烁特效
当玛丽奥从小变大时,会有一个闪烁效果——角色在变大和变小之间快速切换几帧。
// 在 Player.cs 中添加变大动画
private void PlayGrowAnimation()
{
// 禁用输入
SetPhysicsProcess(false);
var smallFrames = GD.Load<SpriteFrames>("res://assets/sprites/player/small.tres");
var bigFrames = GD.Load<SpriteFrames>("res://assets/sprites/player/big.tres");
var tween = CreateTween();
float duration = 0.6f;
int switches = 6;
for (int i = 0; i < switches; i++)
{
float t = (float)i / switches * duration;
// 切换到小
tween.TweenCallback(Callable.From(() => _animatedSprite.SpriteFrames = smallFrames));
tween.TweenInterval(duration / switches / 2);
// 切换到大
tween.TweenCallback(Callable.From(() => _animatedSprite.SpriteFrames = bigFrames));
tween.TweenInterval(duration / switches / 2);
}
// 动画结束后恢复正常
tween.TweenCallback(Callable.From(() =>
{
_animatedSprite.SpriteFrames = bigFrames;
SetPhysicsProcess(true);
}));
}
// 缩小动画
private void PlayShrinkAnimation()
{
SetPhysicsProcess(false);
var smallFrames = GD.Load<SpriteFrames>("res://assets/sprites/player/small.tres");
var bigFrames = GD.Load<SpriteFrames>("res://assets/sprites/player/big.tres");
var tween = CreateTween();
tween.TweenCallback(Callable.From(() => _animatedSprite.SpriteFrames = bigFrames));
tween.TweenInterval(0.1f);
tween.TweenCallback(Callable.From(() => _animatedSprite.SpriteFrames = smallFrames));
tween.TweenInterval(0.1f);
tween.TweenCallback(Callable.From(() => _animatedSprite.SpriteFrames = bigFrames));
tween.TweenInterval(0.1f);
tween.TweenCallback(Callable.From(() =>
{
_animatedSprite.SpriteFrames = smallFrames;
UpdateCollisionSize();
SetPhysicsProcess(true);
}));
}# 在 player.gd 中添加变大动画
func play_grow_animation() -> void:
set_physics_process(false)
var small_frames = load("res://assets/sprites/player/small.tres")
var big_frames = load("res://assets/sprites/player/big.tres")
var tween = create_tween()
var duration: float = 0.6
var switches: int = 6
for i in range(switches):
var t = float(i) / switches * duration
# 切换到小
tween.tween_callback(func(): animated_sprite.sprite_frames = small_frames)
tween.tween_interval(duration / switches / 2)
# 切换到大
tween.tween_callback(func(): animated_sprite.sprite_frames = big_frames)
tween.tween_interval(duration / switches / 2)
# 动画结束恢复正常
tween.tween_callback(func():
animated_sprite.sprite_frames = big_frames
set_physics_process(true)
)
## 缩小动画
func play_shrink_animation() -> void:
set_physics_process(false)
var small_frames = load("res://assets/sprites/player/small.tres")
var big_frames = load("res://assets/sprites/player/big.tres")
var tween = create_tween()
tween.tween_callback(func(): animated_sprite.sprite_frames = big_frames)
tween.tween_interval(0.1)
tween.tween_callback(func(): animated_sprite.sprite_frames = small_frames)
tween.tween_interval(0.1)
tween.tween_callback(func(): animated_sprite.sprite_frames = big_frames)
tween.tween_interval(0.1)
tween.tween_callback(func():
animated_sprite.sprite_frames = small_frames
update_collision_size()
set_physics_process(true)
)无敌闪烁特效
当玛丽奥吃了星星后,会闪烁彩虹色。这个效果通过在 _Process 中不断改变角色的颜色来实现。
// 在 Player.cs 中添加无敌闪烁
private float _starTimer = 0.0f;
public override void _Process(double delta)
{
if (GameManager.Instance.CurrentState != GameManager.GameState.Playing) return;
// 星星无敌闪烁
if (_isStarPower)
{
_starTimer += (float)delta * 10;
float hue = Mathf.PosMod(_starTimer, 1.0f);
_animatedSprite.Modulate = Color.FromHsv(hue, 0.5f, 1.0f);
}
// 普通受伤无敌闪烁
if (_isInvincible && !_isStarPower)
{
// 这个在 StartInvincibility 中用 Tween 处理
}
}# 在 player.gd 中添加无敌闪烁
var _star_timer: float = 0.0
func _process(delta: float) -> void:
if GameManager.current_state != GameManager.GameState.PLAYING:
return
# 星星无敌闪烁
if is_star_power:
_star_timer += delta * 10
var hue: float = fmod(_star_timer, 1.0)
animated_sprite.modulate = Color.from_hsv(hue, 0.5, 1.0)分数弹出特效
当玩家踩敌人、吃道具、顶金币时,在对应位置弹出分数文字:
// ScorePopup.cs - 分数弹出特效
using Godot;
public partial class ScorePopup : Node2D
{
private Label _label;
private float _lifetime = 1.0f;
public void Show(int score, Vector2 position)
{
Position = position;
_label = new Label
{
Text = score.ToString(),
HorizontalAlignment = HorizontalAlignment.Center,
ThemeOverrideFontSizes = { { "font_size", 16 } },
ThemeOverrideColors = { { "font_color", new Color(1, 1, 1) } }
};
AddChild(_label);
// 向上飘动并淡出
var tween = CreateTween();
tween.TweenProperty(this, "position:y", Position.Y - 32, _lifetime);
tween.Parallel().TweenProperty(_label, "modulate:a", 0.0f, _lifetime);
tween.TweenCallback(Callable.From(QueueFree));
}
}
// 使用方式
public static class ScorePopupHelper
{
public static void ShowPopup(Node parent, int score, Vector2 position)
{
var popup = new ScorePopup();
popup.Position = position;
parent.AddChild(popup);
popup.Show(score, position);
}
}# score_popup.gd - 分数弹出特效
extends Node2D
var _label: Label
var _lifetime: float = 1.0
func show_score(score: int, pos: Vector2) -> void:
position = pos
_label = Label.new()
_label.text = str(score)
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_label.add_theme_font_size_override("font_size", 16)
_label.add_theme_color_override("font_color", Color.WHITE)
add_child(_label)
# 向上飘动并淡出
var tween = create_tween()
tween.tween_property(self, "position:y", position.y - 32, _lifetime)
tween.parallel().tween_property(_label, "modulate:a", 0.0, _lifetime)
tween.tween_callback(queue_free)
# 使用方式
static func show_popup(parent: Node, score: int, pos: Vector2) -> void:
var popup = load("res://scripts/effects/score_popup.gd").new()
popup.position = pos
parent.add_child(popup)
popup.show_score(score, pos)金币收集特效
金币被收集时,弹出一个 "+200" 的分数文字,同时播放金币旋转上升的动画。
下一章预告
音效和特效系统完成了!游戏现在有了完整的视听反馈。最后一章,我们将进行最终的打磨和发布准备——多关卡管理、动画优化和导出设置。
