查看源码下载文档
CHAPTER / 12

资源、声音与电影:CD-ROM 时代的内容管线

一套 RTS 的代码不会凭空产生士兵图像、配乐和真人过场。它必须把制作好的素材变成可分发的文件,在磁盘、内存、解压缓冲和播放设备之间持续搬运。本章沿着“找文件—解码—呈现”的实际路径阅读,解释 MIX、SHP、战区图块、WSA、AUD 和 VQA 如何合作。

本章依据指定提交静态分析;资源名称来自加载代码,不等于仓库已经附带相应商业游戏素材。不同年代的工具、DOS 与 Windows 实现并存,不能仅因函数同名就视为同一版本。

12.1 MIX 是资源索引容器,不是统一图片格式

游戏初始化分别注册 REDALERT.MIXLOCAL.MIX、分辨率资源包,以及 MAIN.MIXCONQUER.MIX、影片、音乐和语音包。一部分包整体缓存,一部分只注册索引并留在磁盘读取。代码对 GENERAL.MIXMOVIES1/2.MIXSPEECH.MIX 有不缓存的安排,对 CONQUER/SOUNDS/RUSSIAN/ALLIES 有后续缓存调用。资源包注册主资源和媒体包缓存调用

这样安排很容易理解:枪声需要低延迟反复播放,整部电影则不值得占据有限内存。MIX 提供打包、索引和统一查找,包内文件仍各有格式。不能说“从 MIX 解压出一切”,因为 MIX 的读取路径并不要求其整个数据区经过通用压缩;SHP、AUD、VQA 会分别决定自己的压缩方式。

MixFileClass 的普通头记录文件数和数据总量,每个子项提供 CRC、偏移和大小。扩展格式先检测两个短整数:第一个为零代表扩展头,第二个中的位表示是否有摘要、头是否加密。构造函数通过 FileStraw 读取;需要解密时串接 PKStraw,读完头与索引后记录 DataStartMIX 构造字段含义

尤其要分清加密范围:头文件明确说明加密针对包头,内嵌文件数据本身不因此加密。Cache 在读取整个数据区时可以串接 SHAStraw,随后比较 20 字节摘要;这项验证出现在缓存路径,并不意味着每次任意流式读取都重新校验整包。头加密边界Cache 的读取与摘要比较。存档的加密压缩链是另一套问题,见存档与二进制格式

12.2 请求一个文件名,系统到底怎样找到它

Offset 把名称转大写,计算 CRC,依注册的 MIX 链表逐个检索,各包的索引使用 bsearch。找到条目后,如果包已缓存,返回 Data + Offset 指针;未缓存则返回磁盘偏移,加上该包的数据起点。MixFileClass::Offset。因此包注册顺序会影响同名资源的优先级;索引本身使用 CRC 标识而不是必须保存完整路径字符串。

CCFileClass::Open 再把三种情况统一起来:

实际位置 打开方式 后续 Read 做什么
磁盘上存在独立同名文件 优先按普通文件打开 调用底层文件读取
文件在已缓存的 MIX 保存内存 Buffer 与位置 从内存复制并推进位置
文件在未缓存的 MIX 打开包文件,设 Bias 起点与长度 把包的一段伪装成独立文件

独立文件优先是代码明确保留的升级覆盖机制。调用者可以继续写 CCFileClass("XXX.AUD"),而无需知道它藏在光盘哪个包里。CCFileClass::OpenCCFileClass::Read

MFCD::Retrieve 是另一种更窄的接口:它只是经 Offset 取驻留指针,不负责把任意缺失资源从磁盘分配加载进来。调用点使用它之前,通常已确保相应包处于缓存状态。把 Retrieve 和 CCFile 的能力混为一谈,会误解很多资源加载顺序。Retrieve

Straw 的比喻是“向上游吸取字节”;Pipe 则是把字节推向下游。组合后,文件访问、摘要、解密、解压可以使用相似接口。以 LCWStraw::Get 为例,它先消耗内部缓冲,空了再读取压缩块头与块体,解压出下一段。这证明项目具有可组合的数据流设施,不代表每种资源都必须穿过 LCWStraw。 SHP 和 VQA 的许多路径直接调用 LCW 解码。Straw 链接与读取LCWStraw::Get

12.3 素材种类与运行时用途

名称或类型 游戏中的用途 已确认的加载/解释入口
.MIX 多种资源的打包与查找 MixFileClass、CCFileClass
.SHP 单位、建筑、图标、光标等多帧图像 类型初始化 → Build_Frame → CC_Draw_Shape
战区模板、地形图像 地面瓦片、树木等 TemplateTypeClass::Init、各类型 Init
.PAL 战区调色板 DisplayClass::Init_Theater
.CPS 标题或界面图,也可承载调色映射数据 Load_Picture / Load_Uncompress
.WSA 界面、阵营选择或成绩页动画 Open_Animation / Animate_Frame
.AUD,部分 .Vxx/.Rxx 效果、EVA 语音、音乐、不同口音回应 Sound_Effect / Speak_AI / ThemeClass
.VQA,Windows 另见 .VQP 过场电影及其插值调色支持 Play_Movie → VQA 库

这里“模板”常被社区叫作 TMP 格式,但不能把格式简称当成固定文件后缀:本代码按 Theaters[theater].Suffix 组成资源名称,战区类型还限制哪些模板可用。Windows 在载入模板后调用 Register_Icon_Set,为图块缓存登记,再从图块集头取宽高。TemplateTypeClass::Init。单位普通图和附加炮塔等则直接请求 .SHPUnitTypeClass 图形加载

.PAL 载入后还会生成阴影和半透明查表,资源内容会影响实际渲染结果。战区调色初始化.CPS 的标题用途与调色映射用途可在 Load_Title_Page 相邻代码看到;低层压缩图读取有 NOCOMPRESS、LCW 等分支,而不是按扩展名简单照搬到屏幕。标题图入口通用图解压分派

12.4 SHP:一帧可以由另一帧恢复出来

多方向单位和多阶段动画需要很多帧,但相邻帧通常相似。Build_Frame 把帧分为关键帧与差分关联帧:关键帧通过 LCW 恢复完整图像;其他帧先定位参考关键帧,再依序应用 XOR 差分,直到得到所需帧。这是“共享基础图像 + 存变化”的压缩思路。Windows Build_Frame 解码DOS Build_Frame

LCW 和 XOR 在这里分工不同。LCW 消除字节流内部的重复:代码支持直接复制字面数据、从已输出数据引用、重复填充等操作,0x80 结束流。XOR 差分则表达“相对于已有画面哪些值需要改变”。不能把它们统称为一种压缩,也不应把 LCW 自动等同于 ZIP、LZW 或视频的运动预测。库内 LCW_Uncompress 操作码与解码分支

还要区分实现来源:Windows 图像库的 IFF/MAKEFILE 和电影辅助库的 VQM32/MAKEFILE 都列入各自目录的 lcwuncmp.obj,对应 LCWUNCMP.ASM。主游戏技术库则编译 CODE/LCW.CPP,其接口叫 LCW_Uncomp;仓库另存的 CODE/LCWUNCMP.CPP 未列入这份主清单。不能因为名字相似,就把它当作所有媒体调用的唯一实现。图像库对象清单电影辅助库对象清单主技术库 LCW 对象

Windows 的 2KEYFRAM.CPP 为已经解开的帧保存缓存。如果对应槽位已有数据,直接返回其位置;否则完成解码,复制到普通或战区专用的大形状缓冲,并保存绘图头及偏移。战区图像与通用单位分开,便于战区切换时管理资源寿命。帧缓存查找缓存写入

这里不是仅凭文件名猜测运行分支:主 MAKEFILE 的 WIN32 对象列表选 2KEYFRAM.OBJ/2KEYFBUF.OBJ,非 Windows 分支选 KEYFRAME.OBJ/KEYFBUFF.OBJ构建明确选择。同名 Build_Frame 的返回值约定在调用处也不同。移植时若随便把一个版本替换成另一个,可能把“解压长度”误用为“帧指针”。

具体观察例子:坦克第一次向某个方向转动时,要恢复对应 SHP 帧;之后再次使用该帧,在缓存启用且有效时可省去解码。画面上的阵营颜色不必各存一套帧,因为最后绘制仍可做调色重映射。这把存储压缩、内存缓存和颜色复用结合起来。实际节约多少内存、是否出现首次显示卡顿,本次未运行测量。

12.5 WSA:适合连续播放的小动画

Open_Animation 读取动画头、帧偏移和可选的 768 字节调色板,计算目标缓冲、差分缓冲与整个文件驻留需要的大小。它可依据用户标志和内存大小选择从磁盘读取或保留文件数据,还支持直接画到目标页和先在内部缓冲合成。Open_Animation内存布局和驻留选择

Animate_Frame 从当前帧到指定帧的距离出发,比较正向、反向或环绕需要应用多少个差分。XOR 的可逆性使同一差分可以帮助前进或后退,不必每次都从第一帧开始。初次调用还要区分“首帧相对黑底”与“首帧相对已有图片”的情况。Animate_Frame

游戏中的 CHOOSE.WSA、成绩页动画通过这个接口播放。因而“所有动画都是 SHP”并不成立:战场单位要按当前方向/动作任意取帧,界面动画则更适合围绕连续帧状态推进。阵营选择动画入口成绩页 WSA 入口

12.6 声音有三种不同的使用策略

短音效与单位回应。 Sound_Effect 先过滤静音和无音频状态,结合选项音量。常规文件用 .AUD;某些单位回应根据盟军/苏军、步兵/车辆及 variation 选择 .V00…V03.R00…R03。取得缓存资源指针后,用优先级和音量调用 Play_SampleSound_Effect 文件选择

坐标版本对不在当前视图的声源计算距离衰减及横向 pan_value,然后下传。注意具体源码边界:Windows Play_Sample_Handle 的声像参数没有名字,所读实现未使用该参数,因此不能据上层计算直接断言这一 Windows 路径真的实现了左右定位。它体现了接口保留与后端行为不完全一致的问题。坐标音效Windows 播放签名

EVA 语音。 Speak 只有一个等待槽 SpeakQueue,会排除正在播放或已等待的同一句话;它不是无限长度消息队列。Speak_AI 等当前缓冲停止后,加载下一条 .AUD 到轮换语音缓冲并以较高优先级播放。这让“建造完成”“资金不足”等播报不必全部并发。Speak / Speak_AI

这里还有一个值得读者练习查证的静态疑点:函数注释说会复用此前载入的语音,但 SpeechRecord 的搜索之后,局部 speech 仍为 NULL,没有看到为它赋缓存指针的语句,随后总进入重新加载分支。这是该函数体的事实;是否有特定版本修复、是否对实际播放造成可观察影响,尚未通过运行验证。

背景音乐。 ThemeClass 维护当前曲目与 Pending。每轮检查播放状态,必要时选择下一首;Play_Song 调用 File_Stream_Sample_Vol,音乐不需要整曲先装入内存。流式播放器分配可重复使用的文件缓冲,预载开头,并以回调继续供给数据。ThemeClass::AIPlay_Song文件流播放

12.7 AUD 解码与真实的播放设备

AUD 头带采样率、长度、标志和压缩方式。Windows 播放器根据这些值设置采样追踪结构;遇到声道数、位深或采样率不同,还会重新配置 DirectSound 缓冲。压缩算法不是由 .AUD 扩展名唯一确定的。Play_Sample_Handle

Sample_Copy 对未压缩数据直接复制;对 WESTWOOD/SOS 压缩块,先读压缩大小、展开大小和 magic。两者大小相等就直接复制,否则放入暂存缓冲后分别调用 Decompress_Frame 或 SOS 解码器。数据块可能跨两个输入缓冲,Simple_Copy 负责拼接,因此流式读取不必刚好落在压缩块边界。Sample_Copy

同时可播放音效槽位有限。Get_Free_Sample_Handle 先找空槽;没有空槽时,找优先级不高于新音效的槽并停止旧音效,若没有合适槽则放弃播放。代码还试图为音乐保留通道。战场越热闹,并不意味着所有声音都会无条件完整叠加。音效槽与抢占

Windows 音频通过定时回调维护缓冲,初始化处使用 timeSetEvent;该处创建音频线程的调用被注释掉,所以不能看到 Sound_Thread 函数便宣称此构建必然运行专用线程。回调实现定时器启用。相较之下,WWFLAT32 保留 DOS 寄存器保护和中断相关机制。共享高层 API,使游戏逻辑少关心设备;平台代码依然有实质区别。

12.8 VQA:电影不是一连串完整 BMP

Play_Movie 是游戏与电影库的边界:在通常 VQA 路径中排除编辑器和非单机模式,组成 .VQA 文件名,设置音频选项,初始化 VQAHandle,调用 Open/Play/Close/Free,并处理鼠标、调色板和中断退出后的屏幕恢复。Windows 还有 .VQP 插值调色支持;MPEGMOVIE 条件下,DVD 播放会更早尝试另一条电影路径,因此不能把所有历史发行配置都说成只有 VQA。Play_Movie

库不必自己了解 MIX。游戏把 MixFileHandler 作为 I/O 回调,库请求 OPEN/READ/SEEK/CLOSE,回调转给 CCFileClass。这正是为什么大型电影可以放在未缓存的 MIX 中顺序读取。MixFileHandler

VQA_Open 检查 FORMWVQA 标识;影片头携带帧数、宽高、块宽高、帧率、码本组大小、码本项数和音频规格。块尺寸、帧率来自头部,而不是应被写死为一种固定配置。VQA_OpenVQAHeader

VQ 即向量量化,这里的直观理解是:准备一份小图块字典,画面各位置只写“选字典第几项”。该字典叫 codebook,指向其条目的索引叫 pointers。相邻帧可以沿用同一码本,并用部分码本块渐进更新。源码按 IFF chunk 分派完整/部分码本 CBF*/CBP*、调色板 CPL*、指针 VPT* 及声音 SND*VQA_LoadFrame 的 chunk 分派完整码本加载

解码分两层:Prepare_Frame 在必要时用 LCW 解开码本、调色板和指针字节;随后 DrawFrame_Buffer 调用选定的 UnVQ,利用码本与指针生成实际像素。LCW 负责字节级重复,VQ 负责图块表示;它们可以叠加。 最后设置待展示标志、调用用户回调,并移到下一帧节点。Prepare_FrameDrawFrame_Buffer

示意例子:一帧中的大片墙面与军服区域重复,编码器可以让许多位置引用相同小图块。下一帧人物只动了一点,大部分码本仍可复用。这解释了这种表示适合低带宽播放;仓库中的播放器能够证明解码结构,但不能单凭它还原每部过场原始素材怎样拍摄、调色或选择码本。

12.9 音画同步、缓冲和丢帧

VQA_Play 启动音频和计时器,循环进行 LoadFrame、DrawFrame、User_Update。缓冲不足时暂不绘制,不把没有加载好的节点当有效画面。VQA_Play

Select_Frame 根据 VQA_GetTime 和绘制帧率算出应该显示哪一帧;太早则等待,落后则在规则允许时跳帧,同时照顾关键帧和调色板变化。源码还有至少维持一定显示频度的判断。它追求的是画面跟上媒体时间,不能简单理解成“游戏循环每跑一次就播下一张”。Select_Frame

影片声音与普通 AUD 有相通理念,但容器和读取函数不同:SND1 读取 ZAP 头并调用 AudioUnzapSND2 使用 ADPCM 状态和 VQA_sosCODECDecompressData。Windows VQA_OpenAudio 能使用传入的 DirectSound 对象,也有自行创建对象和缓冲的路径。SND1SND2电影 DirectSound 初始化

VQWINVQ 保存媒体库及观看工具相关代码,VQA32 是播放器主体,VQM32 含 LCW、音频和图形辅助代码,VPLAY32/VQAVIEW 提供播放、查看相关程序。应把它们理解为支持游戏的媒体子工程,不要与主模拟逻辑混作一套,也不要把 OLD 或重复 INCLUDE 当成独立的新游戏功能。

12.10 从制作素材到游戏:仓库能证明多少

可以从源码直接串出一条生产链:音频工具读取 WAV 等输入,生成游戏可播放的音频块;MIX 工具将文件整理成索引和数据区;游戏按资源用途加载或流式读取;解码器将素材变成像素与采样;显示/声音后端把它们交给设备。

TOOLS/AUDIOMAK/AUDIOMAK.CPP 能读取 WAV 的 fmtdata 块,记录采样率/声道/位深,分小块调用 Compress_Frame,写出压缩大小、原大小、magic 和压缩数据,最后回填头。它还保留 Sonarc 历史分支;不要把这个旧工具枚举直接当作所有运行时 AUD 编码的完整清单。AUDIOMAK 转换

TOOLS/MIX/MIXFILE.CPP 则先安排数据顺序和偏移,再按 CRC 排序索引,最后写头及数据;这一顺序正好匹配运行时“二分查索引、按偏移取数据”的需求。MIX 工具写包。目录还包含 ANIMATE、KEYFRAME、ICONCOMP 等工具可执行文件,但仅凭文件存在不足以断言其每项能力、运行兼容性或完整构建方法。

对爱好者最有启发的制作原理是:最终游戏不直接消费所有美术原稿,而消费为目标硬件设计的运行时表示。多帧图像共享差分、阵营共享调色、地面共享瓦片、电影共享码本、长音频共享流式缓冲。素材管线和运行时引擎共同决定游戏的容量、速度与视觉风格。

源码阅读路线与尚未验证处

先读 CCFILE.CPP::OpenMIXFILE.CPP::Offset/Cache,解决“字节从哪里来”;再读 2KEYFRAM.CPP::Build_Frame 与 WSA 的 Animate_Frame,解决“图像怎样恢复”;然后从 AUDIO.CPPTHEME.CPP 追到平台库;最后读 Play_Movie → MixFileHandler → VQA_Open/LoadFrame → Prepare_Frame/UnVQ

本章未实际播放商业素材,未运行老工具,也未验证声卡、显卡驱动或光盘环境。SHP 帧缓存是否开启取决于初始化条件,具体 VQA 块尺寸、采样参数、压缩比例需要读取实际素材才能确认。Speak_AI 缓存赋值缺失和 Windows 声像参数未使用属于已指出的静态代码现象,其用户可见影响仍需要针对构建版本复现。边界检查也必须按实现区分:CODE/LCW.CPP::LCW_Uncomp 未使用长度参数;图像库和电影辅助库的汇编 LCW_Uncompress 则读取 length,并可见输出长度裁剪,不能把前者的行为推广到所有 SHP、WSA、VQA 解压路径。输出裁剪也不等于已经完整验证输入长度、回拷范围和所有损坏数据;本章没有将任何实现认定为经过安全验证的通用解析库。技术库 C++ 解码器图像库长度裁剪电影辅助库长度处理

红色警戒初代 · 源码剖析18 章正文 / 5 份附录
输入关键词,检索 24 份完整文档
全文检索 · 点击结果直达对应小节