本节摘要:声音与输入是实时性的极限考场:延迟超过极短阈值,玩家就会感知到"不同步"。本节拆解 XAudio2 的声音图与提交节奏、XInput 的手柄协议与震动反馈,并顺路梳理输入子系统的代际演化——为什么 DirectInput 退出、现代方案接棒。
图形管线的延迟以帧计,人的眼睛对几十毫秒的推迟还算宽容;耳朵与手指苛刻得多——按下按键到画面响应、触发音效到声音入耳,只要超出极短的窗口,手感立刻"发面"。这决定了音频与输入子系统的设计基调:队列要浅、路径要短、回调要稳。XAudio2 与 XInput 的 API 形态,处处透着这个基调。
XAudio2 的模型是一张声音图:源声音(Source Voice)持有音频数据,经子混音(Submix Voice)做效果与分组混音,最终汇入主声音(Mastering Voice)送往设备。这个三层结构对应真实的混音台:每个音效是一路推子,脚步声归一组、音乐归一组,各组可以独立调音量与效果——爆炸时压低音乐(侧链压缩)这类游戏标配,在子混音层实现干净利落。
// 源声音:加载一段 WAV 后建立播放通道 IXAudio2SourceVoice* source; xaudio->CreateSourceVoice(&source, &wfx); XAUDIO2_BUFFER buf{}; buf.AudioBytes = dataSize; buf.pAudioData = dataBytes; buf.Flags = XAUDIO2_END_OF_STREAM; source->SubmitSourceBuffer(&buf); // 提交缓冲入队 source->Start(0); // 开始发声
关键纪律在提交节奏:源声音内部是一个缓冲队列,播放消费队头,你要在队列见底前提交新缓冲,否则出现断音(underrun)。常规做法是回调驱动——注册缓冲结束回调,队列水位低于阈值就补货。这与图形侧的帧循环同构:生产与消费之间靠队列解耦,靠水位监测保供——只是音频的水位纪律比图形严格得多,断音比掉帧刺耳得多。3D 空间音效也只是把听者与声源的相对位置喂给 XAudio2,它负责按距离与方向做音量、音高与声像运算,不需要你手写空间化滤波。
XInput 是 Xbox 手柄在 Windows 上的标准协议,接口刻意保持极简——查询状态、设置震动,就这两件主事:
XINPUT_STATE state{}; DWORD ok = XInputGetState(0, &state); // 0 号手柄 if (ok == ERROR_SUCCESS) { float lx = state.Gamepad.sThumbLX / 32767.0f; // 左摇杆归一化 bool pressed = state.Gamepad.wButtons & XINPUT_GAMEPAD_A; if (crossedDeadzone(lx)) handleMove(lx); // 死区处理! } XINPUT_VIBRATION vib{ 30000, 15000 }; // 左马达强、右马达弱 XInputSetState(0, &vib);
三处工程细节值得展开。死区:摇杆静止时数值并不归零,直接映射会让角色漂移;要按死区半径归一化处理——这是手柄手感的第一课。轮询频率:输入以每帧轮询即可,但事件驱动的窗口消息(键盘鼠标)与轮询的手柄并存时,要统一到同一输入帧,否则两路设备时序错位。震动即反馈通道:震动强度映射到游戏事件(受击、爆炸、引擎转速),是廉价而有效的手感放大器;持续满强度会耗电且麻木,按事件脉冲式触发是更好的习惯。还有一个工程取舍值得一记:轮询与事件的选择。XInput 的轮询模型简单可靠,每帧一次状态查询的代价可忽略;但"按下瞬间"的判定要自己比对前后两帧的状态差——漏了比对,长按会被当成连发。事件模型省了比对逻辑,时序与帧循环的对齐又成了新问题。两种模型没有高下,选了哪种,就把它的边界条件收进固定的处理函数,别在业务代码里散落判断。
DirectInput 的退出值得记一笔:它诞生于手柄键位混乱的年代,想提供统一的输入抽象,把键盘、鼠标、摇杆全部装进一套接口。但它的抽象层逐渐跟不上设备演进——force feedback 规范碎片化、Xbox 手柄崛起后 XInput 以"少而稳"取胜,微软自己也把 DirectInput 列为遗留。现代方案是分层并存:手柄走 XInput(或其继任者 Windows.Gaming.Input,支持更多设备与更细的震动),键鼠走原生窗口消息或 Raw Input(绕过历史兼容层、拿到未加工数据),UI 文本另有专门的文本服务。统一大抽象输给了分层专业化——这个教训与 4.2 节"用对工具"一脉相承。
音频数据的搬运策略值得单独立一节,因为它跟图形资源的账法不同。短音效(枪声、脚步、界面点击)走常驻:解码成 PCM 一次装进内存,随叫随到,反正几 MB 的事。背景音乐与过场语音走流式:磁盘上一小块一小块读进环形缓冲,边播边补——一首三分钟的曲子常驻要吃几十 MB,流式只需两个缓冲区的水位管理,代价是读盘失败要兜底(卡顿处静音循环,别崩)。压缩则横跨两派:PCM 无压缩的带宽与内存开销都大,XAudio2 支持 ADPCM 与 xWMA 这类硬件友好的压缩格式,解码负担小、延迟低——音效批量压 ADPCM、音乐压流式压缩格式,是常见的组合拳。
三选的判断式其实就两个变量:这一响的体量,与它的触发实时性。体量小、要随时响——常驻;体量大、按序播——流式;两派都得压一压省内存。这里与 3.4 节的显存账本对照着看很有意思:图形资源讲究"放哪堆、什么状态",音频讲究"驻多久、怎么流",账本不同,记账思维相通——都是让稀缺的内存与带宽只装当下真正需要的东西。
三者齐备后,真正的工程题是对齐。一帧内的事件顺序应是:轮询输入 → 更新逻辑 → 触发音效提交 → 图形录制提交。反过来(先画再收输入)会让操控感迟滞一帧。音频与图形的同步点通常在事件层:爆炸那一帧提交音效缓冲,不必追求波形与像素的精确对齐——人耳对音频延迟敏感、对音画间的微小错位宽容,把延迟预算花在刀刃上。顺带记一条音频侧的独有陷阱:声音线程卡顿的表象是爆音杂音而非掉帧,别把它当渲染性能问题去优化——两套系统的故障表现完全不同,先分清再动手。输入则相反,每一步都要省:轮询放在帧首、避免在输入路径上做昂贵解析,是手感工程师的基本功。
本章油路电路全部接通。下一章装仪表盘:调试层、PIX、设备丢失排错实录——让所有隐性故障显形。