画面、界面与输入:玩家看见的世界怎样生成
本章讨论《红色警戒 1》的原始代码。核心事实是:战场由二维地块、按帧读取的精灵图、调色板和软件绘图操作组合而成;输入则经过控件、动作判定与命令队列,才进入世界模拟。渲染出的坦克位置不是游戏状态本身,鼠标光标也不是已经执行的命令。这两条边界,是理解 RTS 引擎的关键。
以下结论来自指定提交的静态源码阅读,未声称已运行原版游戏。WIN32、SORTDRAW、PREDATOR 等条件会改变实际路径。
11.1 一个叫 Map 的对象,为什么连侧栏和鼠标都管
全局 Map 实际声明为 MouseClass Map。类的扩展顺序是 GScreenClass → MapClass → DisplayClass → RadarClass → PowerClass → SidebarClass → TabClass → HelpClass → ScrollClass → MouseClass。底部提供地图数据与画布,逐层叠加战术窗口、雷达、电力、生产侧栏、提示、滚屏和鼠标功能。它不是“地图数据里面偶然塞了 UI”,而是一条用于累加屏幕功能的继承链。全局 Map 声明、DisplayClass 定义、SidebarClass 定义、MouseClass 定义。
这种设计让上层 Draw_It 或 AI 可以调用父类处理,再补充自己的区域。这里许多 AI 函数指“每轮更新界面状态”,并非敌方人工智能。工程收益是复用共同画布和输入链;代价是 Map 同时承载大量职责、层与层共享状态,移植时很难只抽出一个侧栏控件。
GScreenClass::Flag_To_Redraw(false) 表示“有东西变了,请运行绘制分发”;传入 true 还表示“需要完整重建”。Render 只在相应标记存在时工作,临时把 LogicPage 指向 HidPage,调用虚函数 Draw_It,补画控件和多人消息,最后送到可见画面并清标记。Flag_To_Redraw 与 Input、Render。
三个名词必须分清:LogicPage 是当前绘图目标,HidPage 是隐藏画面,SeenPage/SeenBuff 是玩家最终看见的输出。所谓“双缓冲”不是两份游戏世界,而是先在不可见目标拼好图,再提交成品。Windows 路径还先把软件鼠标画进隐藏页,Blit 后擦掉;DOS 路径使用 Shadow_Blit。因此不能把所有版本概括成同一种 GPU 页面交换。Blit_Display。
11.2 如何少画:脏格、对象重绘与滚屏复制
屏幕更新的主要成本不是“算出坦克有多少血”,而是搬运大量像素。代码保存 CellRedraw 位集合:地图格需要刷新时置位;对象另有 IsToDisplay。地块被重画后,覆盖它的对象也要重新绘制,否则地面会擦掉坦克的一部分。
CellClass::Redraw_Objects 将格子标脏,遍历实际占据该格的对象及重叠对象,按编译分支传播显示变化。这里“占据”与“画面覆盖”不是同一概念:建筑基座占若干格,上半部分的图像还可能覆盖别的格。脏区不能只等于寻路占地。Redraw_Objects。
DisplayClass::Draw_It 处理滚屏时,先比较期望视口与当前视口的像素位置。如果两个视图还有共同矩形,就把旧画面的共同区域搬过去,再标记新露出的边缘格;如果跳得太远,便强制全部重画。Windows 在隐藏页位于显存时从可见页拷贝,以避免同一表面重叠拷贝的问题;对小幅滚动还有专门的上下左右边缘扫描。视口移动和复制、边缘格标脏。
这解释了一个当代读者容易忽略的工程取舍:当绘图依赖 CPU 和内存带宽时,复制已经画好的图像往往比重新解压、排序、绘制便宜。滚屏算法首先寻找“可以不重新计算的像素”。这是一项源码支持的设计解释,不是本次实测的性能结论。
脏区还包括临时 UI:拖选矩形先把旧边框覆盖的格子标脏,消息区也标记下面的地图。否则矩形移动或消息消失会留下残影。完整刷新则统一 CellRedraw.Set(),随后仍走相同的地块、对象和遮罩绘制流程。临时覆盖物处理、统一刷新与绘制顺序。
11.3 一张战场画面的叠加顺序
可以把主要阶段理解成下面的局部关系;实际函数还包括编辑器、调试和特殊效果分支。
- 标记脏格,并复用可保留的滚屏区域。
- 绘制地面模板与地表痕迹。
- 绘制地面对象及上方对象。
- 覆盖未探索区域遮罩。
- 补上拖选框与界面覆盖物。
地面模板由格子的 TType/TIcon 决定;没有特定模板时选择清空地块。CellClass::Draw_It 调用 Draw_Stamp 画地面,然后叠加 Smudge,再画墙、矿石等 Overlay。这里的模板是瓦片图数据,不是现代三维地形网格。CellClass::Draw_It。
对象按 LayerType 分为 SURFACE、GROUND、AIR、TOP。这些枚举表达显示顺序;不要仅凭 AIR 名字推断它等于“所有飞行单位”。枚举注释将飞机、子弹列在最上层,而爆炸、火焰可处于上方层。层枚举。非特殊路径通过 ObjectClass::Render 检查是否需要显示、是否已进入地图且不在 Limbo,转换坐标后调用具体对象的 Draw_It。ObjectClass::Render。
前后遮挡也不是深度缓冲。LayerClass::Sort 执行一遍相邻比较交换,比较依据来自对象 Sort_Y();它是增量调整,不能表述成“每帧完整排序”。新增对象则用 Sorted_Add 插入有序位置。LayerClass 排序、对象比较运算。
SORTDRAW 分支进一步改变地面对象处理:每个格收集 Occupier 和 Overlapper,两个对象只比较一次,三个对象使用三次比较,更多对象用 qsort;随后在 WINDOW_PARTIAL 中只画对应局部。普通地面 Techno 会从后续通用层循环跳过,非正常视觉状态另作处理。因而不能同时把“逐层整物绘制”和“逐格裁切绘制”都说成唯一运行算法。逐格收集、排序和部分窗口、通用层循环对 SORTDRAW 的避重。
11.4 坦克为什么可以车身朝北、炮塔朝东
UnitClass::Draw_It 读取车身的 PrimaryFacing 和炮塔的 SecondaryFacing,把方向转换到有限的方向帧。先用 Shape_Number() 画车身,再根据炮塔方向选择 BodyShape[tfacing] + 32 等帧号,计算炮塔偏移后再画一次。射击后坐力通过 Recoil_Adjust 改变绘制坐标。雷达车的旋转盘则选取随全局 Frame 变化的附加帧。UnitClass::Draw_It。
因此,肉眼看见的“转动”主要来自预制二维帧切换,不是运行时对完整坦克三维模型投影。车身与炮塔拆开,减少需要制作的组合:若各有多个朝向,不必为每种车身朝向乘每种炮塔朝向都画一整辆车。源码也保留旋转、缩放位图的通用接口,但不能据此把普通坦克路径描述成任意角度三维渲染。
通用 Techno_Draw_Object 再加入所有权重着色、高度、隐形视觉和铁幕效果。高度使绘制 y 坐标上移;离地时选择不同阴影表;间谍在特定观察关系下使用玩家自己的颜色;铁幕倒计时存在时改用红色重映射。PREDATOR 条件会影响隐形涟漪具体画法。Techno_Draw_Object。
最末端 CC_Draw_Shape 取得帧尺寸,调用 Build_Frame 解出指定帧,构造裁切窗口,再交给 Buffer_Frame_To_Page。参数可以指定透明、调色映射、阴影、涟漪,以及特殊的缩放/旋转。尤其要注意接口差异:这里 Windows 将 Build_Frame 返回值当指针使用,非 Windows 分支把返回值当解压尺寸检查。CC_Draw_Shape。帧压缩与缓存见资源章节。
11.5 阵营颜色、阴影、水波:改变颜色映射也能制造动画
像素保存的是调色板索引。RemapTable 将输入索引转成另一索引,允许同一坦克图给不同玩家使用。初始化甚至从 PALETTE.CPS 图中的特定像素行提取各玩家的 16 色对应关系:先建立恒等映射,再替换那组颜色。这是将美术资源兼作配置表的具体例子。调色映射初始化。
阴影并非现代实时光照。DisplayClass::Init_Theater 加载战区 .PAL,建立 FadingShade、单位阴影、半透明表等映射;雪地选择不同深浅。渲染时根据源像素和背景索引查表,得到视觉上的暗化或混色。它适合有限颜色系统,但“半透明”效果不应机械等同为现代 RGBA 浮点混合。战区调色和阴影表。
Color_Cycle 定时循环一小段水面颜色,同时调整雷达框脉冲色和余烬色,再提交新调色板。已有像素的索引没有移动,颜色变了,水便像在流动。这也是为何旧游戏的水面、指示灯可以用很小成本同时更新。Color_Cycle。
单位自己的阴影与未探索区域的黑色遮罩是两套概念。后者在对象之后通过 Redraw_Shadow 叠加,防止未探索区域漏出本不应看见的对象;它还涉及地图信息状态,不能仅作为美术阴影理解。最后绘制地图遮罩。
选中框和生命条也不是预烘焙在精灵图内。TechnoClass::Draw_It 从类型尺寸生成边角线;生命条宽度按 Health_Ratio 计算,颜色由 ConditionYellow/ConditionRed 阈值决定,并检查同盟或规则权限。这样图像动画与实时状态显示可以各自变化。生命条与选中框。
11.6 鼠标从像素坐标走到模拟命令
GScreenClass::Input 从键鼠缓冲取得输入,让 Gadget 链表处理,再调用屏幕 AI。Gadget 不是浏览器控件,而是引擎自身维护的按钮、战术窗口等对象;它既能处理输入,也能标记重绘。Input、控件入链。
战术区的 TacticalClass::Action 对按下/释放使用队列保存的鼠标坐标,其他情况使用当前坐标。这避免“点击事件发生后鼠标又移动了”而命令落错点。随后执行像素到世界坐标转换,检查地图是否已探索,查找靠近点击点的对象,还会过滤非己方隐形目标。选中对象的 What_Action 决定此时适合移动、攻击、进入、选择等操作。TacticalClass::Action。
重要区分是:光标表现一个候选动作,释放鼠标才进入通常的动作执行分支。按下时记住拖动起点;保持按下并移动到一定范围会进入拖选;右键则取消当前模式或选择。这使“单击”和“框选”能够共享一个按钮。输入事件分发、左键按下与拖动。
框选 Select_These 归一化两个角,扫描地面层,按对象中心是否位于矩形内选择玩家可控制、可选且非建筑对象;飞机另行扫描。它并不是检查精灵图片每一个可见像素是否与矩形相交。Select_These。
多选后执行动作时,Mouse_Left_Release 为每个对象重新调用 What_Action,再调用各对象的 Active_Click_With,因为步兵、工程师、坦克对同一目标可能有不同动作。建筑放置、维修、出售和超级武器则可在这里直接添加 EventClass 到 OutList。选择状态可以立即反馈,而改变游戏世界的命令要进入后续事件处理;联网设计因此能共享命令入口,后续处理见联网、锁步与回放。多选分派与阵型处理、维修、出售、超级武器事件。
11.7 侧栏和雷达并不是战场的截图
侧栏 StripClass 保存可建造列表、顶部索引和与工厂的关联。绘制时取类型的 Cameo 图,检查对应工厂是否忙、当前生产是否完成、完成阶段,再画灰暗或进度遮罩;超级武器用自己的 Ready 和动画阶段。它展示模拟状态,不在绘制过程中推进生产。StripClass::Draw_It。一个值得细读的细节:前面虽计算了 remapper,后面实际绘制前又将它清零,因此不能只看前半段就断言此版本的侧栏图标总会随阵营变色。
点击侧栏也不是直接 new Tank。右键在生产中发出 SUSPEND,已经暂停则发 ABANDON;左键可发 PRODUCE。建筑完成后先进入本地放置预览,真正点击地图才发 PLACE;能自行走出工厂的对象可直接排入放置事件。SelectClass::Action。这将“玩家正在考虑放在哪里”与“所有玩家都必须同步的建造事实”区分开来。
雷达 Plot_Radar_Pixel 根据格子、探索状态和干扰位计算颜色。放大倍率较高时可把 24×24 地面图缩到雷达像素块,再叠加地形、覆盖物和步兵;缩小时可能只取代表色。Radar_Pixel 把变化格压入重绘栈,它和主视图拥有不同的更新颗粒度。Plot_Radar_Pixel、Radar_Pixel。
雷达点击首先做区域和有效性检查,再把雷达坐标转回地图格,允许的动作也经过过滤;它不是向缩小版图片随意注入一个点击,而是另一种地图命令入口。RTacticalClass::Action。
11.8 一个完整例子:框选坦克,再向右侧开进
- 按下左键记录起点;移动后进入拖选,旧边框经过的格子被标脏。新矩形画在战场之上。
- 释放按钮,两个角转成世界坐标;
Select_These选中矩形内可控制单位,单位重绘时生成边角框、生命条。 - 将鼠标移到右侧地面,
What_Action(cell)给出候选移动动作;光标变化本身不改变单位坐标。 - 再次释放按钮,每个单位重新判断动作,通过自己的
Active_Click_With进入命令路径。若满足阵型条件,还会调整各自目标格。 - 随后模拟让单位位置和朝向改变;显示标记使旧位置地面与相关对象重画,车身取新的方向帧,炮塔仍可保持自己的朝向。
- 本帧画面在隐藏页完成,雷达按变化格更新,最终 Blit 提交给玩家。
这条链说明“顺手的操控”来自许多小协议共同成立:事件坐标不能漂移、拖选不能残影、光标不能替代动作验证、覆盖区不能漏刷新、界面不能擅自越过模拟同步边界。
源码阅读路线与尚未验证处
推荐依次读 GSCREEN.CPP::Input/Render,DISPLAY.CPP::TacticalClass::Action/Draw_It,CELL.CPP::Draw_It,UNIT.CPP::Draw_It,TECHNO.CPP::Techno_Draw_Object,最后读 CONQUER.CPP::CC_Draw_Shape 和底层汇编。侧栏从 SelectClass::Action 反向看 Factory,比先看绘图细节更容易理解。
本章未测量各分支帧率,也未证明本仓库所有历史构建启用同一组宏。SORTDRAW、PREDATOR、显存/系统内存路径、640 模式和 DOS 模式必须结合构建配置阅读。Windows 底层确有 DirectDrawCreate、独占全屏模式及显示模式设置,所以上面的“软件精灵绘制”并不表示完全没有 DirectDraw 支持。DirectDraw 模式建立。