1711 字
9 分钟
Godot C# 零基础教程 08:音效、粒子、屏幕震动与调试

教程一 · 第 8 章:游戏”果汁感”——音效、粒子、震动、调试与性能#

静态功能正确的游戏和”手感好”的游戏之间,差的是一层反馈:起跳有尘土、命中有顿帧、爆炸有震屏。这层东西行业里叫 game feel / juice。本章把性价比最高的手段全给你,再讲调试与性能。

8.1 音效:AudioStreamPlayer 三兄弟#

节点用途
AudioStreamPlayer非位置相关:BGM、UI 音效
AudioStreamPlayer2D声音随离摄像机距离衰减:敌人吼声、爆炸
AudioStreamPlayer3D3D 空间声

基本用法:

  1. .wav(短音效,无解码开销)或 .ogg(BGM,体积小)拖进文件系统
  2. 节点的 Stream 属性拖入音频
  3. 代码:
private AudioStreamPlayer _sfx = null!;
public override void _Ready() => _sfx = GetNode<AudioStreamPlayer>("JumpSound");
private void Jump()
{
// ...跳跃逻辑
_sfx.PitchScale = (float)GD.RandRange(0.95, 1.05); // 音高随机,避免连听疲劳
_sfx.Play();
}

复用同一个音效播放器的技巧:把音效做成 [Export] AudioStream 参数,一个播放器按场景传不同 clip;或者常见做法是每个一次性音效自己一个节点,Play 后 Finished += QueueFree 自动清理。

BGM 与音量总线:BGM 播放器放 Autoload(切歌不断);输出 Bus 选 Music,音效选 SFX(在底部 Audio 面板新建这两条总线)。这样设置界面能分别调音量(第 7 章)。

一次性音效管理写法(Autoload):

public partial class AudioManager : Node
{
public static AudioManager Instance = null!;
public override void _Ready() => Instance = this;
public void PlaySfx(AudioStream clip, float pitchRange = 0.05f)
{
var player = new AudioStreamPlayer
{
Stream = clip,
Bus = "SFX",
PitchScale = (float)GD.RandRange(1.0 - pitchRange, 1.0 + pitchRange)
};
AddChild(player);
player.Play();
player.Finished += player.QueueFree;
}
}

免费音效来源(注意许可):Kenney.nl(CC0 素材全家桶)、Freesound(逐条看许可)、Sonniss GDC 包。

8.2 粒子:GPUParticles2D#

Godot 4 主推 GPUParticles2D(GPU 算,几千粒不费劲;老项目可能见到 CPUParticles2D,功能类似)。

做跳跃尘土:

  1. Player 下加 GPUParticles2D
  2. 检查器:
    • Emitting 先关掉(代码触发)
    • Amount 12
    • Lifetime 0.4
    • Particle → Process Material 新建 ParticleProcessMaterial:
      • Gravity = (0, 0, 0)(尘土不上浮)
      • Initial Velocity Min/Max 40/90,方向朝侧下方展开(Spread 60)
      • Scale 曲线:用 Curve 从 1 缩到 0(在 Scale 子属性里建 Curve)
  3. Textures:一张 8×8 软圆白图(Kenney 有 particle 素材包)
  4. One Shot 勾上、Explosiveness 1(一次性爆发而不是持续喷)

代码触发:

private GpuParticles2D _dust = null!;
private void Land()
{
_dust.Restart(); // 重新发射一次
}

命中火花、金币闪光、死亡碎块全是同一个节点换参数。粒子是游戏里投入产出比最高的视觉投资。

3D 对应 GPUParticles3D;要在粒子里发多个图片(不同形状的碎片)用 Animator 或换 Mesh。

8.3 屏幕震动#

摄像机挂一个小脚本:

public partial class CameraRumble : Camera2D
{
private float _trauma; // 0..1 创伤值
[Export] public float MaxOffset = 12f;
[Export] public float NoiseSpeed = 2f;
private FastNoiseLite _noise = new();
private float _seed;
public override void _Ready() => _noise.Seed = (int)GD.Randi();
public void AddTrauma(float amount)
=> _trauma = Mathf.Clamp(_trauma + amount, 0f, 1f);
public override void _PhysicsProcess(double delta)
{
if (_trauma > 0f)
{
// trauma 平方:轻伤几乎不抖,重伤才剧烈(业界标准做法)
float shake = _trauma * _trauma;
Offset = new Vector2(
_noise.GetNoise2D(_seed, Time.GetTicksMsec() * 0.001f * NoiseSpeed, 0),
_noise.GetNoise2D(_seed, 0, Time.GetTicksMsec() * 0.001f * NoiseSpeed)
) * MaxOffset * shake;
_trauma -= (float)delta; // 衰减速度自己调
if (_trauma <= 0f) Offset = Vector2.Zero;
}
}
}

调用:GetNode<CameraRumble>("...").AddTrauma(0.4f)。用噪声而不是随机数,抖动才顺滑连续。

8.4 其他高性价比反馈#

  1. 受击闪白:Shader 材质一个参数(hint 玩法:Sprite2D 的 Modulate 在几帧内 Tween 到白再回来;正式做法写个 flash shader)
  2. 命中顿帧(Hit Stop):命中瞬间全局 Engine.TimeScale = 0.05f,60–100 毫秒后恢复。动作游戏灵魂:
    public async void HitStop(float seconds = 0.07f)
    {
    Engine.TimeScale = 0.05f;
    await ToSignal(GetTree().CreateTimer(seconds, true, false, true),
    SceneTree.SignalName.Timeout);
    // ignoreTimeScale=true,否则计时器自己也被放慢
    Engine.TimeScale = 1f;
    }
  3. 压扁拉伸(Squash & Stretch):起跳时 Scale 改成 (0.85, 1.15),落地 (1.2, 0.8),Tween 弹回 (1,1)。卡通游戏的生命感来源
  4. 拖尾:Trail 插件或 Line2D + 每帧记录点位;GPUParticles2D 配跟随发射也能用
  5. 敌人受击后退:给 Velocity 一个远离玩家的冲量
  6. 音效先行原则:没有素材时先放”占位音”(哪怕是嘴唇嘟嘟声),反馈节奏先成立,素材后换

8.5 调试三板斧#

① Debug 菜单与可视化#

  • 调试 → 可见碰撞形状(蓝色)/ 可见导航区域 / 可见选择形状
  • 运行时场景树面板切”远程”:检查运行时真实结构
  • 运行时检查器直接改属性做试验

② 自定义调试绘制#

public override void _Draw()
{
DrawCircle(Vector2.Zero, 100f, new Color(1, 0, 0, 0.2f));
DrawLine(Vector2.Zero, _facing * 200f, Colors.Red, 2f);
}
// 属性变化后需要重绘时调 QueueRedraw()

仇恨范围、巡逻路径、射线,画出来比 print 快十倍。_Draw 只在 Node2D/Control 上可用。

③ 断言与调试输出#

Debug.Assert(_player != null, "玩家必须在组 player 中"); // Debug 包,发布版剔除
GD.PrintS("a", "b", 1); // 空格分隔
GD.PrintRich("[color=red]红字[/color]");
GD.PushWarning("可疑情况"); // 黄色
GD.PushError("出错了"); // 红色带堆栈

VS Code 里按 F5(Godot 启动配置由 C# Tools for Godot 生成)可以断点、看变量、逐句执行,这是选 C# 的红利之一,逻辑绕不出来时用调试器,不要用 print 硬猜。

8.6 性能:先测量,再优化#

打开调试器 → Monitors(监视器):FPS、绘制调用数、对象数、物理帧耗时,全都有。

高级别分析:debug/settings/stdout/print_fps 开控制台 FPS;C# 代码热点用 jetBrains dotTrace / VS 性能分析器附加进程;Godot 内置 Profiler(调试器底部)看引擎侧耗时。

常见瓶颈与处理(按新手项目中出现频率排序):

  1. 每帧 new 大量对象 / 字符串:_Process 里别做分配,血条文本每秒刷 60 次?加脏标记,变化才刷
  2. GetNode/GetNodesInGroup 写在每帧:_Ready 缓存
  3. Instantiate/QueueFree 风暴:弹幕用对象池
  4. Visible 但不在屏幕的节点也在跑:远处敌人用 VisibleOnScreenNotifier2D 休眠(关闭 Process)
  5. 日志刷屏:发布版 GD.Print 也要清,字符串拼接本身有成本
  6. 粒子过多/纹理过大:内存与 fill-rate 双杀;美术资源导入时设压缩、Maximum Size
  7. 物理层过多交叉:Layer/Mask 精确配置,不要所有东西都在第 1 层互相检测
  8. _Process 能不用就不用:靠事件、信号、Timer 驱动的逻辑,比每帧轮询省且清晰

8.7 C# 侧的两个工程习惯#

启用 Nullable:.csproj 的 PropertyGroup 加 <Nullable>enable</Nullable>,编译器帮你挡空引用,Godot 字段配合 null!:

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>

代码目录约定(项目大了找得到东西):

scripts/
├── player/ # 玩家相关
├── enemies/
├── ui/
├── systems/ # Autoload 管理器
└── resources/ # 自定义 Resource 类
scenes/ # 镜像同名结构
assets/art audio particles

第 9 章(最后一章):导出到 Windows/Web/安卓,以及之后的学习路线图。


《Godot C# 零基础》系列导航

⬅️ 上一篇:Godot C# 零基础教程 07:UI 与游戏流程 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:Godot C# 零基础教程 09:导出发布与学习路线图

Godot C# 零基础教程 08:音效、粒子、屏幕震动与调试
https://021028.xyz/posts/default/168/
作者
021028
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0