规则、类型与数据驱动:修改一个数字为什么能改变游戏
分析版本:
0dc09bb1d6188d1ec79ab4765b22a48b75ffee32。本文讨论初代 Red Alert 源码中的实际读取路径;示例数值是教学输入,不代表发行版平衡数据。
1. 游戏由代码、类型数据库和当前世界共同组成
玩家看到一辆坦克,至少有三层东西在合作:C++ 决定坦克能够转向、移动、瞄准和射击;类型记录规定这一类坦克使用什么武器、多少钱、多快、多少生命;实例记录保存这一辆坦克现在的位置、所属玩家、剩余生命和命令。改动类型数据可以影响所有使用该类型的实例,却不会自动创造一种新的行为算法。
这是一种预先实现行为、用数据组合和调参的架构。它已经足以支持相当丰富的修改,但并不是现代意义上“任意定义组件、脚本与插件”的通用引擎。理解这个边界,才能分清哪些修改只需 INI,哪些需要重新编译。
UnitTypeClass::Init_Heap 按固定顺序复制代码里的类型原型;UnitTypeClass::From_Name 在既有类型范围内寻找名称。这意味着 2TNK 是程序知道的类型标识,随意增加一个 [MYTANK] 段落不会自动扩大车辆类型集合。武器和弹头也有明确的注册清单,虽然它们的参数后来从 INI 填充。车辆类型注册与名称解析、武器与弹头注册。
2. 配置读取不是一个步骤,而是一串有依赖的步骤
启动时先分配并建立单位、建筑、步兵等类型池,再加载 RULES.INI,满足条件时加载 AFTRMATH.INI,最后按规则分配运行期对象池。类型数量与战场上能同时存在多少实例是两类限制。启动顺序。
RulesClass::Process 将一次完整规则处理拆成 General、MPlayer、Recharge、Heap_Maximums、AI、Powerups、Land_Types、Themes、IQ、Objects、Difficulty。它的返回值最终直接为 true,不能据此判断每个段落存在、每个引用有效或每项内容都正确。Process 函数体。
对象规则的顺序也有意义:先弹头和弹体,再武器,再车辆、步兵、船只、飞机、建筑,最后国家类型和任务控制数据。下游类型会把名称解析为上游对象的指针。例如武器要引用弹头,坦克又要引用武器。把这些步骤任意并行化,可能在依赖尚未填好时计算派生属性。Objects 处理顺序。
查看流程图源文本
flowchart TD
A[编译时类型原型] --> B[类型池]
C[规则 INI] --> D[规则读取器]
D --> B
B --> E[弹头与弹体]
E --> F[武器引用]
F --> G[单位类型引用]
G --> H[战场实例]这里的箭头表示初始化与依赖关系,不表示每帧都解析文本。运行期主要使用已经解析的值和指针。
3. 一张地图也能覆盖规则,但它没有重做全部初始化
场景加载时依次重新应用基础 RuleINI、条件编译下的 AftermathINI、当前场景 INI 中的规则段落。场景不仅包含摆放位置,也可以携带局部规则,因而同名单位在特殊任务中可能具有不同属性。场景规则覆盖链。外层 Read_Scenario 在特定多人或资料片条件下还可能继续应用 MPLAYER.INI,因此不能说地图配置在所有模式下总是最后一层。多人规则附加层。
这里不能简化成“后一个文件完全替换前一个文件”。多数读取器把当前字段值作为缺省值,只改当前文件提供的项目。例如只覆盖 Cost 不会自动清除先前的 Primary。但并非每个字段都遵守相同默认策略:难度段落存在时,Difficulty_Get 的多项缺省直接为 1;弹头的 Verses 也有自己的缺省字符串。要判断局部配置是否继承,必须看具体 getter 的第三个参数。Techno 字段读取、难度缺省、弹头读取。
场景这条路径没有调用完整 Process,没有重新执行 Heap_Maximums。后者会重建武器与弹头池,是启动期操作,不能在已有大量对象引用的战斗中随意重做。也不能据启动时支持 [Maximums] 就推断地图可以安全扩大所有池容量。Heap_Maximums。
另一细节是所谓“重置规则”实际是重新套用配置,不是普遍地把所有类型重新构造一遍。源码对一些任务特殊值进行了显式清理。因此,若上一关修改了某字段而基础配置又没有提供该字段,是否遗留必须逐项追踪,不能凭注释认定绝无状态残留。场景前置清理。
4. INI 解析器:文本怎样变成可查询数据库
INIClass 持有 section 链表,每个 section 持有 entry 链表,同时为段名和键名建立 IndexClass 索引。链表保存顺序,索引支持定位。其索引 ID 来自 CRCEngine,不是现代标准库字符串 map。数据库结构。
读取过程寻找 [section],逐行拆分 key=value,处理注释和空白,并略过没有键或没有值的行。空段落不会留下 section 对象。行缓冲上限是 128,不能将现代配置库处理超长字符串的能力投射到这里。解析函数、缓冲定义。
几个容易影响修改结果的规则:
| 数据 | 实际读取行为 | 使用上的含义 |
|---|---|---|
| 字符串 | 有长度上限,末尾补零并去空白 | 超长内容不能按现代无限长字符串理解 |
| 整数 | 普通十进制,也识别 $ 前缀和 h 后缀十六进制 |
0x 形式不是这段实现明确支持的路径 |
| 布尔 | 按首字符 Y/T/1、N/F/0 判断 | 不识别的首字符回退默认,不一定报错 |
| 定点数 | 交给 fixed 的字符串构造器 |
小数和百分比的语义由它决定 |
| 游戏对象名称 | CCINI 转为枚举、位图或类型引用 | 拼错名字可能变为 NONE,而不是显式异常 |
依据:Get_String、Get_Int、Get_Bool、Get_Fixed。
不要把这些函数名理解成严格的 schema 校验器。例如 Get_Int 的普通路径使用 atoi,不能保证不合法文本必然获得你期望的报错。对外发布修改工具时,应在原有读取器之外加入自己的验证层。
5. 玩家叫它“重坦”,代码依赖的是多个标识
静态原型同时包含枚举标识、显示文本 ID、INI 名称、图像相关偏移和能力标志。例如 UnitMTank2 的规则名为 2TNK。游戏显示名称、数据键、C++ 变量名和玩家习惯称呼不必一致。尤其不能只看英文变量名或多年前的注释来判断某个单位在发行版里的中文名称。2TNK 原型。
一个单位的共同规则主要在 TechnoTypeClass::Read_INI:Strength 写入 MaxStrength,Cost 写入 Cost,Primary 和 Secondary 解析为武器指针,Armor 解析为护甲枚举,Prerequisite 转为建筑位图,Owner 转为可用阵营集合,Image 修改图形名称。共同规则字段。
派生类型再加专用字段。车辆读取 NoMovingFire、Tracked,并设置移动区域;建筑读取储量、相邻范围、可占领、需供电、地基、可出售等。建筑的负 Power 会拆成消耗 Drain,正值用于产电。读取动作也可能计算新状态,并非所有字段都是简单赋值。车辆扩展、建筑扩展。
6. 武器、弹体和弹头为什么要拆开
| 层 | 主要问题 | 典型字段 |
|---|---|---|
| 单位类型 | 谁使用武器?能生产吗? | Primary、Secondary、Cost、Owner |
| 武器类型 | 多久射一次?多远?多少基础伤害? | ROF、Range、Damage、Burst |
| 弹体类型 | 发射后怎样飞行、碰撞和瞄准? | 弹体的各种运动与命中能力 |
| 弹头类型 | 对护甲、墙、矿和死亡表现有什么效果? | Verses、Spread、Wall、InfDeath |
武器读取器明确分别绑定 Warhead 和 Projectile。因此同样的飞行形态可以配不同伤害性质,同样的伤害性质也可以由不同载具发射。这比把“每种炮弹全部行为”写进每种坦克更易复用。WeaponTypeClass::Read_INI。
弹头 Verses 按护甲枚举顺序填入 Modifier 数组,再派生 IsOrganic。这个有序数组本身就是一种协议:修改枚举顺序或增加护甲类型时,配置、序列化和消费者都要一起检查。增加一项数据不意味着所有使用它的代码自动扩展。WarheadTypeClass::Read_INI。实际命中计算见第06章。
7. 跟踪一次局部修改
下面是故意选择的教学配置,不是从游戏资源提取的原版数值,也不是完整可运行地图:
[2TNK]
Cost=900
Strength=420
Speed=50
Primary=105mm
[105mm]
Damage=40
Range=5
ROF=45其生效过程是:
- INI 解析器建立两个 section;不会创建新的坦克或新武器类型。
Objects遍历既有武器,105mm读取伤害、射程和间隔。- 遍历车辆类型时,
2TNK读取生命、成本和速度,并把 Primary 解析为同一个 105mm 类型记录。 - 后续生产实例、移动和射击算法消费这些已经解析的字段。
Range=5 经 Get_Lepton 按格宽转换,采用一格 256 lepton 的单位时得到 1280。Speed=50 在 Techno 读取器按 0…100 缩放到 0…255 的内部量,50 对应 128;这不是承诺“每秒移动50格”。实际位移还有地形、状态、逻辑步进等因素。距离转换、Techno 速度缩放、读取调用。
还要注意共享引用的后果:修改 [105mm] 会影响所有引用这份武器类型的对象,并非只影响 [2TNK]。想只影响一种单位,首先要确认是否有可复用、不会影响其他对象的既有武器槽;若必须新增类型,就进入代码、枚举和资源共同修改的范围。
8. 数据驱动的收益与代价
这套设计让策划能够调整平衡、地图能定制特殊战斗、程序员能复用武器和移动算法;它也留下了字符串引用、默认值依赖、固定注册表和隐含单位这些成本。没有统一 schema 时,查找“这个参数到底是什么单位”最可靠的方法是从 getter 追到消费者。
例如 Prerequisite 并不是任意逻辑表达式树:读取器把建筑名转成移位后的位图。若今天扩展建筑种类,必须核查位宽和消费者,而不能仅在 INI 写更多名称。建筑前置条件读取。难度也不只是让 AI 想得更聪明;读取器明确包含火力、装甲、价格、生产速度等倍率,以及是否扫描内容、拆墙等开关。难度字段。
如果要为自己的 RTS 复用思想,可以保留“类型与实例分离、数据复用、关卡覆盖”三件事,同时把单位写入 schema、让未识别字段产生诊断、为类型使用稳定 ID,并明确哪些覆盖允许发生在开局后。这些是基于阅读所得的现代设计建议,不是本项目已经实现的功能。
9. 阅读路线与验证边界
建议按 INIT.CPP 的类型初始化 → RULES.CPP::Process/Objects → TECHNO.CPP::Read_INI → WEAPON.CPP → WARHEAD.CPP → SCENARIO.CPP 的覆盖路径阅读,再选择一个参数追到使用点。
本章证实了规则如何进入程序,未取得完整发行版资源库,也未启动游戏验证示例,因此不声称给出了发行版全部默认数值。不同宏、资料片可用状态和场景覆盖都可能改变最终配置。精确的实际参数表应由指定资源版本加载后导出,并与源码提交一起记录。