Axıs
技能目录

参考 04:工程实现管线(素材 → 运行时的完整通道)

参考 04:工程实现管线(素材 → 运行时的完整通道)

本文档回答"知道该做什么之后,怎么把它变成游戏里真正跑起来的东西"。 前面所有参考文档讲的是标准与目标,这一份讲的是链路、工具、脚本与协作机制。


1. 全链路总览

①需求 → ②选型(创作方式) → ③制作 → ④编辑批处理 → ⑤导入中间件
      → ⑥引擎集成 → ⑦运行时调制 → ⑧构建打包 → ⑨剖析验证 → ⑩迭代
阶段 输入 输出 主要工具 负责人
① 需求 玩法描述 音频清单 + 基调 表格 / 会议 音频 Lead
② 选型 清单条目 创作方式决策 本文档 02 声音设计师
③ 制作 决策 原始素材 录音棚 / 合成器 / 素材库 声音设计师
④ 编辑批处理 原始素材 规范化交付资产 DAW + 脚本(ffmpeg / Python) 声音设计师
⑤ 导入中间件 交付资产 中间件工程对象 Wwise / FMOD + WAAPI 脚本 音频程序
⑥ 引擎集成 事件清单 引擎调用点 引擎编辑器 + 代码 音频程序
⑦ 运行时调制 游戏状态 RTPC / State 驱动 中间件 + 游戏逻辑 音频程序 + Gameplay
⑧ 构建打包 中间件工程 SoundBank / 资源包 CLI + CI 音频程序 / 构建工程师
⑨ 剖析验证 构建产物 Profiler 报告 + QA Profiler / 响度表 音频 Lead
⑩ 迭代 报告 修改清单 — 全体

2. 阶段 ④:编辑与批处理(最容易被忽视的产能杠杆)

2.1 DAW 会话模板(强制)

每个项目建一套标准会话模板,包含:

  • 采样率 48kHz / 24bit(源素材可用 96k 项目但交付导出 48k)
  • 预置轨道组:Transient / Body / Tail / Detail / Sweetener / Ref
  • 预置总线:Bus_Sub / Bus_Low / Bus_Mid / Bus_Hi / Bus_ReverbSend
  • 预置测量表:响度表(LUFS)+ 真峰值表 + 频谱仪常开
  • 统一的 导出预设(命名自动拼接、格式、峰值 -3dBFS)

没有模板 = 每个人交付的东西都不一样 = 后期混音要重做。

2.2 批处理脚本(必备)

响度与格式归一化(ffmpeg):

# 音效:目标 -18 LUFS,真峰值 -3 dBTP,重采样 48k
ffmpeg -i in.wav -af "loudnorm=I=-18:TP=-3.0:LRA=11:linear=true" -ar 48000 -ac 1 out_mono.wav

# 音乐:-20 LUFS
ffmpeg -i in.wav -af "loudnorm=I=-20:TP=-3.0:LRA=14:linear=true" -ar 48000 -c:a libvorbis -q:a 5 out.ogg

# 语音:-16 LUFS + 轻度压缩
ffmpeg -i in.wav -af "acompressor=threshold=-18dB:ratio=3:attack=10:release=250,loudnorm=I=-16:TP=-3.0" -ar 48000 out.wav

linear=true(两遍测量更准)需先跑一遍 print_format=json 取 measured 值再套用。批量脚本应实现两遍流程。

首尾淡入淡出去 click:

# 起点 2ms fade-in,终点 8ms fade-out
ffmpeg -i in.wav -af "afade=t=in:st=0:d=0.002,afade=t=out:st=<DUR-0.008>:d=0.008" out.wav

循环无缝校验(Python + numpy):

# 检查首尾采样是否接近,避免循环 click
import soundfile as sf
x, sr = sf.read(path)
d_head = abs(x[0] - x[1])
d_loop = abs(x[-1] - x[0])
print("loop delta:", d_loop, "OK" if d_loop < 0.02 else "NEEDS CROSSFADE")

命名合规校验(CI 里跑):

import re, sys
PAT = re.compile(r'^(sfx|mus|vo|amb|ui|cin)_[a-z0-9]+(_[a-z0-9]+){2,}_[0-9]{2}\.wav$')
bad = [p for p in files if not PAT.match(p.name)]
sys.exit(1 if bad else 0)

2.3 资产元数据

每个资产在交付目录旁生成同名 .meta.json:

{
  "asset_id": "A0003",
  "name": "sfx_weapon_rifle_fire_outdoor_01",
  "category": "SFX/Weapon/Rifle",
  "material": "Outdoor",
  "loop": false,
  "lufs": -18.0,
  "true_peak": -3.0,
  "channels": 1,
  "sample_rate": 48000,
  "license": "OEM-Internal",
  "author": "",
  "source_method": "Foley+Synth Hybrid",
  "notes": "需室内版本;连发时降调 60 cents"
}

中间件导入时读元数据自动填字段——避免手写几百条属性。


3. 阶段 ⑤:中间件导入与工程组织

3.1 工程结构约定

WwiseProject/
├─ Actor-Mixer Hierarchy/
│  ├─ Characters/{Player, NPC, Enemy, Creature}
│  ├─ Weapons/{Rifle, Shotgun, Melee}
│  ├─ Ambience/{Forest, Cave, Urban}
│  ├─ UI/
│  └─ Vehicles/
├─ Interactive Music Hierarchy/
├─ Audio Devices (总线)
├─ Attenuation/      ← 共享衰减曲线,禁止每个音一套
├─ Switches/States/RTPCs/
├─ SoundBanks/
│  ├─ Bank_Init         (常驻)
│  ├─ Bank_Level_XX     (按关卡)
│  └─ Bank_VO_en / _zh  (按语言)
└─ Work Units/          ← 按人拆,减少合并冲突

铁律:

  • 衰减曲线、总线、Switch 集中管理,禁止复制多份(改一处要改 N 处是灾难)
  • Work Unit 按人或按模块拆分,配合版本控制的独占检出
  • 所有对象写 Notes:不写的人 3 个月后自己都看不懂

3.2 自动化导入(WAAPI 示例)

from waapi import WaapiClient
client = WaapiClient()

# 批量导入音频文件到指定路径
client.call("ak.wwise.core.audio.import", {
    "importOperation": "createNew",
    "default": {"importLanguage": "SFX"},
    "imports": [
        {"audioFile": r"D:\Src\sfx_weapon_rifle_fire_01.wav",
         "objectPath": "\\Actor-Mixer Hierarchy\\Weapons\\Rifle\\<Sequence>Rifle_Fire"}
    ]
})

# 批量设置属性
client.call("ak.wwise.core.object.setProperty", {
    "object": "\\Actor-Mixer Hierarchy\\Weapons\\Rifle",
    "property": "Priority", "value": 80
})

# 生成 SoundBank
client.call("ak.wwise.core.soundbank.generate", {"soundbanks": [...]})

几百个资产靠手点是不现实的。WAAPI(Wwise Authoring API)与 FMOD 的 Studio Script API 都应尽早接入管线。

3.3 命令行构建(CI 用)

# Wwise(路径与参数随版本变化,以官方文档为准)
"%WWISEROOT%/Authoring/x64/Release/bin/WwiseCLI.exe" ^
  "GameAudio.wproj" -GenerateSoundBanks -Platform Windows ^
  -Bank Init Level1 Level2 -Verbose

# FMOD Studio
"FMODStudio.exe" -project "GameAudio.fspro" -build -batch -quit
# 或直接用 fsbank 命令行工具构建单个 bank
"fsbank.exe" -o "Build/Master.bank" -platform mobile "Assets/*.wav"

CI 流水线建议:

提交资产 → 命名/格式/响度校验(失败即阻断)
        → 自动导入中间件(脚本)
        → 生成 SoundBank
        → 自动跑冒烟测试(加载所有 Bank,检查无缺失引用)
        → 产物上传 + 内存/CPU 基线对比(超阈值告警)

4. 版本控制与多人协作

资产类型 存储方式 说明
源素材 WAV Perforce / Git LFS 二进制大文件,必须 LFS,禁止直接进 git
DAW 工程 Perforce(独占检出) Reaper/Cubase/Logic 工程是二进制,无法 merge
中间件工程 Wwise .wproj + Work Units、FMOD .fspro 必须独占检出;Wwise 的 WWU 文件是可 XML diff 的,冲突可手工解
脚本 / 配置 Git 正常 code review
资产清单 CSV Git 策划与音频共同编辑

协作规则:

  1. 中间件工程同时只能一个人改,或用严格的 Work Unit 所有权划分
  2. 每周一次"工程同步 + 冲突检查",不要攒到月底
  3. 音频资产与代码用同一个 changelist / commit 提交(事件名改了,代码也要改)
  4. 禁止在别人正在改的 Bank 上加东西

5. 阶段 ⑥⑦:与游戏系统的接口

5.1 三个主要接入点

来源 接入方式 典型用途 注意
动画 Animation Notify / 时间轴标记 脚步、挥击、布料 通知点要对齐动画的接触帧,不是动作起点
物理 碰撞事件 → 冲击强度 → RTPC 撞击、滚动、破碎 必须做节流(同一帧多次碰撞只发一次)
Gameplay 状态 State / Switch / 自定义事件 战斗/探索、室内外 状态切换要有过渡时间,避免瞬变

5.2 物理联动示例(碰撞 → 音效)

// 节流 + 强度映射:避免同一物体连续碰撞刷屏
void OnHit(const FHitResult& Hit, float ImpactSpeed) {
    static TMap<UObject*, double> LastHitTime;
    const double Now = FPlatformTime::Seconds();
    if (Now - LastHitTime.FindRef(Hit.GetActor()) < 0.08) return;   // 80ms 节流
    LastHitTime.Add(Hit.GetActor(), Now);

    const float Norm = FMath::Clamp(ImpactSpeed / 1000.f, 0.f, 1.f); // 归一化
    AkSoundPosition Pos;                                             // 位置
    FAkAudioDevice::Get()->SetRTPCValue(nullptr, TEXT("Impact_Force"), Norm, 0, nullptr);
    FAkAudioDevice::Get()->PostEvent(TEXT("Play_Impact_Generic"), Hit.GetActor(), 0,
                                      FOnAkPostEventCallback(), { TEXT("Impact_Force") });
}

要点:节流、归一化、同一材质合并、限制单帧最大发声数。

5.3 数据驱动配置(强烈推荐)

把"游戏事件 → 音频事件"的映射放到数据表,而不是硬编码:

GameplayEvent,Surface,AudioEvent,RTPC_Intensity,CooldownMs,MaxVoices,Priority
OnFootstep,Dirt,Play_Footstep_Dirt,Speed,80,6,70
OnFootstep,Stone,Play_Footstep_Stone,Speed,80,6,70
OnHit_Generic,Metal,Play_Impact_Metal,Impact_Force,80,4,65

好处:策划可调、音频可改、不需要程序介入、可做自动校验。


6. 阶段 ⑦:程序化音频的落地架构

6.1 三种实现层次(按成本递增)

层次 做法 适用 成本
L1 伪程序化 多段采样 loop + 参数化交叉淡化 + 滤波/音高 引擎、风、脚步、水体 低,首选
L2 中间件内合成 Wwise/FMOD 内置振荡器/噪声/包络,参数驱动 充能、能量场、UI 参数化 中
L3 引擎内 DSP 图 MetaSounds / 自研 DSP 图(滤波、颗粒、模态) 载具、物理化碰撞、节奏系统 高

默认从 L1 起步,只有在 L1 出现明显"台阶感"时才升级。

6.2 L1 伪程序化:载具引擎(典型)

录制:怠速 / 2000 / 4000 / 6000 / 8000 RPM 五段稳态 loop(各自无缝)
运行时:
  RPM(0..1) → 定位相邻两段 → 等功率交叉淡化(0.15s)
           → 播放速率微调(±30%)跟随真实 RPM
           → 负载 RTPC → 失真量 + 低频增益
           → 速度 RTPC → 风噪层音量

听感上"完全连续",成本只有程序化的 1/5。

6.3 L3 引擎内:Unreal MetaSounds 模式

MetaSounds 是节点图式程序化音源,音频设计师可在不写 C++ 的情况下搭建:

典型图:Wave Player(或 Noise)→ Filter(截止频率受参数驱动)
        → Envelope → Mixer → Output
参数输入:{ Speed, Load, Material, ImpactForce }

工程注意:

  • MetaSound 图在音频线程执行,节点越多 CPU 越高;复杂图要剖析
  • 用 MetaSound Parameter Interface 对外暴露参数,蓝图/代码只传参数不碰图
  • 与中间件共存时明确分工:MetaSounds 做程序化,Wwise/FMOD 做混音与音乐(或反过来,但只选一种)

7. 阶段 ⑧⑨:构建、剖析与验证

7.1 构建产物检查清单

  • 所有 Bank 生成成功,无 "missing source" 警告
  • Bank 体积在预算内(逐个记录,建立基线并对比趋势)
  • 语言 Bank 分离正确
  • 无未被引用的孤儿资产(定期清理)

7.2 剖析(Profiler)必测项

  • 峰值 voice 数、CPU 耗时(音频线程)、Stream 数量、内存占用
  • 最坏场景必须构造:大规模战斗 / 快速切场景 / 长时间挂机
  • 建立基线数值 + 趋势图,超阈值自动告警(而不是靠人感觉"好像卡了")

7.3 冒烟测试自动化

自动加载所有 Bank
  → 逐个 Post 所有 Event(检查无报错、无缺失引用)
  → 检查 voice 数峰值
  → 卸载所有 Bank,检查内存回落

这个脚本能在 5 分钟内抓出 80% 的低级错误。


8. 工程事故清单(血泪版)

事故 根因 预防
版本上线后某音效变成默认"哔" Bank 未包含该资产 / 平台编码失败 CI 跑冒烟测试 + Bank 引用校验
切场景后声音越叠越多 循环音未 Stop 所有循环音登记到"生命周期表",强制成对
音频线程爆掉、卡顿 卷积混响过多 / HRTF 全开 / Max Distance 未设 性能预算 + 剖析基线
打包体积莫名增大 有人把 96k/24bit WAV 直接拖进工程 CI 校验格式与响度,失败阻断
两个人都改了总线,冲突覆盖 无独占检出 Work Unit 所有权 + 独占检出
事件改了名字,代码没改 音频与代码分开提交 事件名进数据表 + 同一 changelist
内存缓慢增长 Bank 卸载不彻底 / 声源未销毁 挂机测试 + 内存基线对比
移动端首次播放卡顿 Stream 无预读 / 首帧解码 预加载 + 预热
音乐切换"卡不准拍" 未在节拍网格上切换 用音乐回调 / 小节对齐
外包交付的音乐无法分层 合同没写 stems 交付 委托合同写明交付项(见 02 文档第 2 节)

9. 交付契约:音频集成规格书

音频 → 程序之间必须有唯一一份契约文档,包含:

  1. 事件表(Event / 触发时机 / 3D / 循环 / Stop 事件 / 节流参数)
  2. RTPC 表(名称 / 范围 / 驱动源 / 更新频率 / 插值)
  3. Switch / State 表(名称 / 取值 / 设置者 / 默认值)
  4. Bank 加载矩阵(关卡 × Bank × 加载时机 × 卸载时机)
  5. 性能预算(voice / CPU / 内存 / Stream 数)

模板见 assets/templates/audio-integration-spec.md。

契约原则:事件名一旦发布即为 API,改名需要走变更流程并同步代码与数据表。