# 11　画面、界面与输入：玩家看见的世界怎样生成

本章讨论《红色警戒 1》的原始代码。核心事实是：战场由二维地块、按帧读取的精灵图、调色板和软件绘图操作组合而成；输入则经过控件、动作判定与命令队列，才进入世界模拟。渲染出的坦克位置不是游戏状态本身，鼠标光标也不是已经执行的命令。这两条边界，是理解 RTS 引擎的关键。

以下结论来自指定提交的静态源码阅读，未声称已运行原版游戏。`WIN32`、`SORTDRAW`、`PREDATOR` 等条件会改变实际路径。

## 11.1 一个叫 Map 的对象，为什么连侧栏和鼠标都管

全局 `Map` 实际声明为 `MouseClass Map`。类的扩展顺序是 `GScreenClass → MapClass → DisplayClass → RadarClass → PowerClass → SidebarClass → TabClass → HelpClass → ScrollClass → MouseClass`。底部提供地图数据与画布，逐层叠加战术窗口、雷达、电力、生产侧栏、提示、滚屏和鼠标功能。它不是“地图数据里面偶然塞了 UI”，而是一条用于累加屏幕功能的继承链。[全局 Map 声明](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GLOBALS.CPP#L436-L445)、[DisplayClass 定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.H#L60-L72)、[SidebarClass 定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SIDEBAR.H#L43-L52)、[MouseClass 定义](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/MOUSE.H#L40-L50)。

这种设计让上层 `Draw_It` 或 `AI` 可以调用父类处理，再补充自己的区域。这里许多 `AI` 函数指“每轮更新界面状态”，并非敌方人工智能。工程收益是复用共同画布和输入链；代价是 `Map` 同时承载大量职责、层与层共享状态，移植时很难只抽出一个侧栏控件。

`GScreenClass::Flag_To_Redraw(false)` 表示“有东西变了，请运行绘制分发”；传入 `true` 还表示“需要完整重建”。`Render` 只在相应标记存在时工作，临时把 `LogicPage` 指向 `HidPage`，调用虚函数 `Draw_It`，补画控件和多人消息，最后送到可见画面并清标记。[Flag_To_Redraw 与 Input](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GSCREEN.CPP#L248-L312)、[Render](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GSCREEN.CPP#L393-L447)。

三个名词必须分清：`LogicPage` 是当前绘图目标，`HidPage` 是隐藏画面，`SeenPage/SeenBuff` 是玩家最终看见的输出。所谓“双缓冲”不是两份游戏世界，而是先在不可见目标拼好图，再提交成品。Windows 路径还先把软件鼠标画进隐藏页，Blit 后擦掉；DOS 路径使用 `Shadow_Blit`。因此不能把所有版本概括成同一种 GPU 页面交换。[Blit_Display](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GSCREEN.CPP#L470-L487)。

## 11.2 如何少画：脏格、对象重绘与滚屏复制

屏幕更新的主要成本不是“算出坦克有多少血”，而是搬运大量像素。代码保存 `CellRedraw` 位集合：地图格需要刷新时置位；对象另有 `IsToDisplay`。地块被重画后，覆盖它的对象也要重新绘制，否则地面会擦掉坦克的一部分。

`CellClass::Redraw_Objects` 将格子标脏，遍历实际占据该格的对象及重叠对象，按编译分支传播显示变化。这里“占据”与“画面覆盖”不是同一概念：建筑基座占若干格，上半部分的图像还可能覆盖别的格。脏区不能只等于寻路占地。[Redraw_Objects](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CELL.CPP#L371-L445)。

`DisplayClass::Draw_It` 处理滚屏时，先比较期望视口与当前视口的像素位置。如果两个视图还有共同矩形，就把旧画面的共同区域搬过去，再标记新露出的边缘格；如果跳得太远，便强制全部重画。Windows 在隐藏页位于显存时从可见页拷贝，以避免同一表面重叠拷贝的问题；对小幅滚动还有专门的上下左右边缘扫描。[视口移动和复制](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L1720-L1818)、[边缘格标脏](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L1820-L1922)。

这解释了一个当代读者容易忽略的工程取舍：当绘图依赖 CPU 和内存带宽时，复制已经画好的图像往往比重新解压、排序、绘制便宜。滚屏算法首先寻找“可以不重新计算的像素”。这是一项源码支持的设计解释，不是本次实测的性能结论。

脏区还包括临时 UI：拖选矩形先把旧边框覆盖的格子标脏，消息区也标记下面的地图。否则矩形移动或消息消失会留下残影。完整刷新则统一 `CellRedraw.Set()`，随后仍走相同的地块、对象和遮罩绘制流程。[临时覆盖物处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L1670-L1718)、[统一刷新与绘制顺序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L1995-L2091)。

## 11.3 一张战场画面的叠加顺序

可以把主要阶段理解成下面的局部关系；实际函数还包括编辑器、调试和特殊效果分支。

1. 标记脏格，并复用可保留的滚屏区域。
2. 绘制地面模板与地表痕迹。
3. 绘制地面对象及上方对象。
4. 覆盖未探索区域遮罩。
5. 补上拖选框与界面覆盖物。

地面模板由格子的 `TType/TIcon` 决定；没有特定模板时选择清空地块。`CellClass::Draw_It` 调用 `Draw_Stamp` 画地面，然后叠加 `Smudge`，再画墙、矿石等 `Overlay`。这里的模板是瓦片图数据，不是现代三维地形网格。[CellClass::Draw_It](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CELL.CPP#L957-L1059)。

对象按 `LayerType` 分为 `SURFACE`、`GROUND`、`AIR`、`TOP`。这些枚举表达显示顺序；不要仅凭 `AIR` 名字推断它等于“所有飞行单位”。枚举注释将飞机、子弹列在最上层，而爆炸、火焰可处于上方层。[层枚举](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DEFINES.H#L1279-L1289)。非特殊路径通过 `ObjectClass::Render` 检查是否需要显示、是否已进入地图且不在 Limbo，转换坐标后调用具体对象的 `Draw_It`。[ObjectClass::Render](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1205-L1241)。

前后遮挡也不是深度缓冲。`LayerClass::Sort` 执行一遍相邻比较交换，比较依据来自对象 `Sort_Y()`；它是增量调整，不能表述成“每帧完整排序”。新增对象则用 `Sorted_Add` 插入有序位置。[LayerClass 排序](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/LAYER.CPP#L92-L161)、[对象比较运算](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.H#L131-L136)。

`SORTDRAW` 分支进一步改变地面对象处理：每个格收集 Occupier 和 Overlapper，两个对象只比较一次，三个对象使用三次比较，更多对象用 `qsort`；随后在 `WINDOW_PARTIAL` 中只画对应局部。普通地面 Techno 会从后续通用层循环跳过，非正常视觉状态另作处理。因而不能同时把“逐层整物绘制”和“逐格裁切绘制”都说成唯一运行算法。[逐格收集、排序和部分窗口](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CELL.CPP#L1188-L1286)、[通用层循环对 SORTDRAW 的避重](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L2031-L2069)。

## 11.4 坦克为什么可以车身朝北、炮塔朝东

`UnitClass::Draw_It` 读取车身的 `PrimaryFacing` 和炮塔的 `SecondaryFacing`，把方向转换到有限的方向帧。先用 `Shape_Number()` 画车身，再根据炮塔方向选择 `BodyShape[tfacing] + 32` 等帧号，计算炮塔偏移后再画一次。射击后坐力通过 `Recoil_Adjust` 改变绘制坐标。雷达车的旋转盘则选取随全局 `Frame` 变化的附加帧。[UnitClass::Draw_It](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UNIT.CPP#L2029-L2138)。

因此，肉眼看见的“转动”主要来自预制二维帧切换，不是运行时对完整坦克三维模型投影。车身与炮塔拆开，减少需要制作的组合：若各有多个朝向，不必为每种车身朝向乘每种炮塔朝向都画一整辆车。源码也保留旋转、缩放位图的通用接口，但不能据此把普通坦克路径描述成任意角度三维渲染。

通用 `Techno_Draw_Object` 再加入所有权重着色、高度、隐形视觉和铁幕效果。高度使绘制 y 坐标上移；离地时选择不同阴影表；间谍在特定观察关系下使用玩家自己的颜色；铁幕倒计时存在时改用红色重映射。`PREDATOR` 条件会影响隐形涟漪具体画法。[Techno_Draw_Object](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L4224-L4313)。

最末端 `CC_Draw_Shape` 取得帧尺寸，调用 `Build_Frame` 解出指定帧，构造裁切窗口，再交给 `Buffer_Frame_To_Page`。参数可以指定透明、调色映射、阴影、涟漪，以及特殊的缩放/旋转。尤其要注意接口差异：这里 Windows 将 `Build_Frame` 返回值当指针使用，非 Windows 分支把返回值当解压尺寸检查。[CC_Draw_Shape](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L3407-L3547)。帧压缩与缓存见[资源章节](12-assets-audio-and-video.md)。

## 11.5 阵营颜色、阴影、水波：改变颜色映射也能制造动画

像素保存的是调色板索引。`RemapTable` 将输入索引转成另一索引，允许同一坦克图给不同玩家使用。初始化甚至从 `PALETTE.CPS` 图中的特定像素行提取各玩家的 16 色对应关系：先建立恒等映射，再替换那组颜色。这是将美术资源兼作配置表的具体例子。[调色映射初始化](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INIT.CPP#L2550-L2600)。

阴影并非现代实时光照。`DisplayClass::Init_Theater` 加载战区 `.PAL`，建立 `FadingShade`、单位阴影、半透明表等映射；雪地选择不同深浅。渲染时根据源像素和背景索引查表，得到视觉上的暗化或混色。它适合有限颜色系统，但“半透明”效果不应机械等同为现代 RGBA 浮点混合。[战区调色和阴影表](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L324-L455)。

`Color_Cycle` 定时循环一小段水面颜色，同时调整雷达框脉冲色和余烬色，再提交新调色板。已有像素的索引没有移动，颜色变了，水便像在流动。这也是为何旧游戏的水面、指示灯可以用很小成本同时更新。[Color_Cycle](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/CONQUER.CPP#L1524-L1599)。

单位自己的阴影与未探索区域的黑色遮罩是两套概念。后者在对象之后通过 `Redraw_Shadow` 叠加，防止未探索区域漏出本不应看见的对象；它还涉及地图信息状态，不能仅作为美术阴影理解。[最后绘制地图遮罩](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L2058-L2079)。

选中框和生命条也不是预烘焙在精灵图内。`TechnoClass::Draw_It` 从类型尺寸生成边角线；生命条宽度按 `Health_Ratio` 计算，颜色由 `ConditionYellow/ConditionRed` 阈值决定，并检查同盟或规则权限。这样图像动画与实时状态显示可以各自变化。[生命条与选中框](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L1089-L1193)。

## 11.6 鼠标从像素坐标走到模拟命令

`GScreenClass::Input` 从键鼠缓冲取得输入，让 Gadget 链表处理，再调用屏幕 `AI`。Gadget 不是浏览器控件，而是引擎自身维护的按钮、战术窗口等对象；它既能处理输入，也能标记重绘。[Input](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GSCREEN.CPP#L274-L312)、[控件入链](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/GSCREEN.CPP#L328-L355)。

战术区的 `TacticalClass::Action` 对按下/释放使用队列保存的鼠标坐标，其他情况使用当前坐标。这避免“点击事件发生后鼠标又移动了”而命令落错点。随后执行像素到世界坐标转换，检查地图是否已探索，查找靠近点击点的对象，还会过滤非己方隐形目标。选中对象的 `What_Action` 决定此时适合移动、攻击、进入、选择等操作。[TacticalClass::Action](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L2783-L2898)。

重要区分是：光标表现一个候选动作，释放鼠标才进入通常的动作执行分支。按下时记住拖动起点；保持按下并移动到一定范围会进入拖选；右键则取消当前模式或选择。这使“单击”和“框选”能够共享一个按钮。[输入事件分发](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L2937-L2997)、[左键按下与拖动](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L3649-L3716)。

框选 `Select_These` 归一化两个角，扫描地面层，按对象中心是否位于矩形内选择玩家可控制、可选且非建筑对象；飞机另行扫描。它并不是检查精灵图片每一个可见像素是否与矩形相交。[Select_These](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L2613-L2690)。

多选后执行动作时，`Mouse_Left_Release` 为每个对象重新调用 `What_Action`，再调用各对象的 `Active_Click_With`，因为步兵、工程师、坦克对同一目标可能有不同动作。建筑放置、维修、出售和超级武器则可在这里直接添加 `EventClass` 到 `OutList`。选择状态可以立即反馈，而改变游戏世界的命令要进入后续事件处理；联网设计因此能共享命令入口，后续处理见[联网、锁步与回放](13-network-lockstep-and-replay.md)。[多选分派与阵型处理](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L3383-L3584)、[维修、出售、超级武器事件](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/DISPLAY.CPP#L3585-L3629)。

## 11.7 侧栏和雷达并不是战场的截图

侧栏 `StripClass` 保存可建造列表、顶部索引和与工厂的关联。绘制时取类型的 Cameo 图，检查对应工厂是否忙、当前生产是否完成、完成阶段，再画灰暗或进度遮罩；超级武器用自己的 Ready 和动画阶段。它展示模拟状态，不在绘制过程中推进生产。[StripClass::Draw_It](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SIDEBAR.CPP#L1638-L1844)。一个值得细读的细节：前面虽计算了 `remapper`，后面实际绘制前又将它清零，因此不能只看前半段就断言此版本的侧栏图标总会随阵营变色。

点击侧栏也不是直接 `new Tank`。右键在生产中发出 `SUSPEND`，已经暂停则发 `ABANDON`；左键可发 `PRODUCE`。建筑完成后先进入本地放置预览，真正点击地图才发 `PLACE`；能自行走出工厂的对象可直接排入放置事件。[SelectClass::Action](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/SIDEBAR.CPP#L1991-L2216)。这将“玩家正在考虑放在哪里”与“所有玩家都必须同步的建造事实”区分开来。

雷达 `Plot_Radar_Pixel` 根据格子、探索状态和干扰位计算颜色。放大倍率较高时可把 24×24 地面图缩到雷达像素块，再叠加地形、覆盖物和步兵；缩小时可能只取代表色。`Radar_Pixel` 把变化格压入重绘栈，它和主视图拥有不同的更新颗粒度。[Plot_Radar_Pixel](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RADAR.CPP#L1012-L1129)、[Radar_Pixel](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RADAR.CPP#L1142-L1151)。

雷达点击首先做区域和有效性检查，再把雷达坐标转回地图格，允许的动作也经过过滤；它不是向缩小版图片随意注入一个点击，而是另一种地图命令入口。[RTacticalClass::Action](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/RADAR.CPP#L1718-L1828)。

## 11.8 一个完整例子：框选坦克，再向右侧开进

1. 按下左键记录起点；移动后进入拖选，旧边框经过的格子被标脏。新矩形画在战场之上。
2. 释放按钮，两个角转成世界坐标；`Select_These` 选中矩形内可控制单位，单位重绘时生成边角框、生命条。
3. 将鼠标移到右侧地面，`What_Action(cell)` 给出候选移动动作；光标变化本身不改变单位坐标。
4. 再次释放按钮，每个单位重新判断动作，通过自己的 `Active_Click_With` 进入命令路径。若满足阵型条件，还会调整各自目标格。
5. 随后模拟让单位位置和朝向改变；显示标记使旧位置地面与相关对象重画，车身取新的方向帧，炮塔仍可保持自己的朝向。
6. 本帧画面在隐藏页完成，雷达按变化格更新，最终 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 模式建立](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/WIN32LIB/MISC/DDRAW.CPP#L451-L487)。
