一款《红色警戒》是怎样被制作出来的
1. 先把战场看成一个随时间变化的数据世界
你点下一辆坦克,坦克绕过建筑、转动炮塔、发射炮弹、摧毁敌人。屏幕上的动画很连续,但程序处理的是离散变化:对象 A 的坐标变了,目标字段改了,开火计时器归零,创建了子弹 B,敌人 C 的生命减少,再创建爆炸动画。
这个世界可以抽象为以下关系式,它是解释模型,不是源码中的数学表达式:
这里 W 是当前世界状态,C 是该逻辑阶段被接受的命令,R 是需要时使用的确定性随机序列,D 是已加载的规则和场景数据。玩家看到的画面则由世界、视口、调色板、精灵和界面状态共同绘制。
这种拆分让几件事变得容易理解:两个玩家可以显示不同视口却共享同一场战争;关闭声音不应改变坦克伤害;存档需要保存世界关系而不只是截一张图;联机不能只同步鼠标坐标,还必须保证命令执行的时机和顺序。
在实际代码中,Main_Loop 协调输入、地图渲染、世界逻辑、命令队列和帧推进;LogicClass::AI 推进多种对象与系统。它没有一个现代名称为 World 的统一对象,而是由 Map、Logic、Scen、各对象池、House 和全局配置共同构成状态。主循环、世界逻辑入口。
2. 制作游戏需要六类东西共同成立
| 制作内容 | 本项目中的实现载体 | 玩家看见的结果 |
|---|---|---|
| 世界与对象模型 | Object、Techno、Foot、各兵种与对象池 | 坦克、步兵、建筑、子弹能持续存在并互相作用 |
| 规则与机制 | Rules、各类型数据、武器、伤害、生产 | 不同兵种有不同强弱、价格和能力 |
| 空间与行为 | Cell、Map、寻路、Mission、Team、House AI | 单位会移动、追击、回矿、编队进攻 |
| 内容编排 | 场景 INI、触发器、事件、动作、战役转换 | 指定时刻增援、目标完成后过关 |
| 视听与交互 | Shape、调色板、地图显示、Sidebar、音视频库 | 图像、爆炸、按钮、语音和过场 |
| 系统工程 | 资源容器、内存、网络、存档、平台库与工具 | 能加载内容、联机、保存、在目标电脑运行 |
如果仅实现“坦克能在窗口里走”,只完成了这张表的一小部分。成熟 RTS 的难度在于这些系统互相约束:建造影响占格,占格影响寻路,死亡影响引用,规则改变影响联机一致性,资源加载影响切换场景。
本仓库也因此不只有 CODE。WIN32LIB、WWFLAT32、IPX、VQ、WINVQ 和工具目录是这套系统的历史底座。全量盘点见文件索引,构建关系见第15章。
3. “游戏引擎”在这里不是一个外部产品
今天常把引擎理解为 Unity、Unreal 或独立框架。阅读这套源码时,更适合按职责划分:提供内存、文件、像素绘制和声音的通用底层;提供地图、对象、任务、命令和资源组织的 RTS 框架;承载坦克、矿车、电力与特定超武的游戏逻辑。
这些边界在代码中并不整齐。一些面向平台的条件编译直接进入游戏文件,UI 与模拟会通过共享状态关联,特殊单位也存在按类型分支的处理。所谓“引擎”和“游戏”主要是理解职责的视角,并非仓库已经实现了严格隔离的三个模块。
一个典型例子是 TechnoTypeClass::Read_INI:既处理通用生命、价格和武器,又依据武器能力和移动类型计算移动区域。这既体现数据复用,也说明配置、寻路和单位能力并非完全分离。类型读取与派生状态。
4. 先有“类型”,再有“这一辆坦克”
类型定义回答“这种坦克一般是什么样”,实例回答“它此刻发生了什么”。例如 2TNK 的静态原型提供类型名、炮塔和帧相关特征;启动时复制到类型池,再由 INI 覆盖共同属性。战场实例通过自己的 Class 引用其类型。类型原型示例、建立类型池。
这个分离节省重复数据,也让调参集中生效。更深的收益是,程序可以对“任何可战斗物体”统一询问距离、目标、开火资格或损伤,然后让具体兵种覆盖细节。这里采用的是继承、虚函数、组合字段与类型分支共同工作的方式,不是 ECS。
实例的生命也不是一次 new 和一次 delete 那么简单。ObjectClass 初始处于 Limbo,Unlimbo 负责进入有效世界,Limbo 负责脱离,另外还有跨系统的 Detach。装在运输工具里的单位、生产中尚未放置的对象、真正被销毁的对象不能被简单混为同一状态。对象初始状态、进入、离开及解除引用。详见第03章。
5. 单位不是每一帧从头“思考人生”
单位通常已有一个任务,如移动、攻击、采矿、卸载、驻守;任务内部还可能有多个阶段。一个矿车不会每帧运行通用规划器寻找“宇宙中最优策略”,而是根据当前阶段继续寻找矿、驶向目标、采集或返回。任务函数给出的等待时间控制下一次相应决策时机。
MissionClass::AI 按任务调度虚函数,具体兵种实现自己的任务逻辑。基类中很多默认任务函数只是长时间等待的占位。这也是为什么搜到 Mission_Harvest 就停下会误读:必须继续找到矿车所在派生类的实际实现。任务调度器、基类任务占位。
电脑对手又在另一层决定建什么、组织什么队伍、向哪里进攻。地图触发器则可以直接让一支队伍增援或执行预先安排的动作。因此必须区分单位的局部行为、电脑阵营的决策和关卡作者的脚本编排。这三者最终都能让坦克动起来,但来源不同。详见第09章和第10章。
6. “我点击了”不等于“世界立刻照做”
点击先产生交互动作,之后被转换成游戏命令,再在相应队列阶段执行。玩家对空地点击、对敌人点击、对运输车点击,同样是鼠标动作,却会经 What_Action、Active_Click_With 等路径解释为不同任务。
在 FootClass::Active_Click_With 的格子版本中,采矿、移动和攻击有不同映射;TechnoClass::Player_Assign_Mission 再进入命令安排层。格子点击解释、玩家命令入口。
把输入变成可记录的语义命令,为联机和录像提供共同表示。需要同步的是“哪个单位执行什么任务、目标是什么、何时执行”,而不是另一个玩家显示器上的某个像素。反过来,这也意味着延迟、命令帧和随机序列会影响世界一致性,详见第13章。
不要据这个概念图自行推断当前帧的准确执行顺序。本项目主循环有自己的历史顺序和等待期输入处理,必须以第02章的逐段分析为准。
7. 画面是模拟结果的表现,但也有自己的状态
坦克转向并不需要在运行时生成一个真正三维坦克模型:主体和炮塔可以选择不同方向帧,再按位置叠绘。队伍颜色可以通过调色表替换;损伤、闪烁、阴影、选择框和血条通过不同绘制路径呈现。画面中看似统一的对象,可能由多个精灵和附加元素拼出。通用 Shape 绘制入口。
表现层仍有必须维护的状态:哪些区域需要重画、哪个按钮按下、视口在哪里、鼠标悬停什么对象、动画进行到哪一帧。不能把“画面由世界产生”误解成界面完全无状态,更不能把某个动画对象都视为纯装饰;有的动画与伤害或生命周期有关,需要追踪它是否进入逻辑更新。
音乐、语音和过场还涉及异步播放、资源读取、设备缓冲及回调。它们不是一条 play() 就能涵盖的完整系统。第11章与第12章将从游戏层一直追到平台库。
8. 一条闭合的游戏因果链
用“采矿后生产坦克,坦克击毁建筑”串联整套系统:
- 关卡数据建立地图、矿、初始建筑与玩家阵营。
- 矿车任务根据地图与占格信息选择目标,移动模块尝试生成并执行路径。
- 采集改变地图资源和矿车载荷;卸矿改变阵营资金与储量。
- 玩家点击建造图标,形成生产事件;工厂推进阶段并扣钱。
- 完成的实例从未进入战场的状态转为有效世界对象,加入占格、逻辑和绘制系统。
- 攻击命令设置目标;单位移动、转向、检查射程与冷却后产生弹体。
- 弹体更新并触发伤害,目标可能死亡、解除引用并生成表现。
- 场景触发器或阵营胜负逻辑观察世界变化,决定是否完成任务。
这是跨模块因果链,并非严格逐帧调用栈。中间可能跨过数百逻辑帧,还可能因为堵路、资金不足、电力不足、目标消失而暂停、分支或失败。第16章会把它拆成可跟踪的入口、状态、出口与失败点。
9. 怎样判断自己已经读懂
如果你能回答一个机制的输入来自哪里、状态存在哪里、谁在何时推进它、失败后怎么办、结果影响哪些其他系统,就已经比只知道类名走得更深入。
阅读这类历史项目,还要多问三句:这段代码真的被当前构建选中吗?这个注释和函数体一致吗?这个数值是默认值还是已经加载配置后的最终值?这些问题贯穿整套文档,并在误读附录集中总结。
本章给出了制作原理的整体模型;它没有宣称一个抽象图就能替代全部实现。后续各章将把每个概念落到可查证的源码位置。