第 13 章:分层 View 与 HUD
第 13 章:分层 View 与 HUD:保留第三版项目代码讲解,并以源码—状态—输出切片、确定性轨迹和故障重放完成验收。
学习目标
- 能解释“第 13 章:分层 View 与 HUD”如何在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间
- 能逐项定位 分层视图(layering views)、hud 对象(hud objects)、更新 hud(updating the hud)、首页与升级界面(home and level-up screens),说明它们位于源码、运行状态还是可见输出边界
- 能按 设置 worldView → 绘制世界 → 切换 HUD view → 更新 HUD → 绘制界面 重放“相机跟随战斗”,持续检查“世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪”
- 能注入“绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放”,从当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图找到第一个不一致并用同输入恢复
第三版来源、工具链与版本边界
“第 13 章:分层 View 与 HUD”对齐 Packt 2024 年第三版的对应章节范围,并以官方公开代码仓库和 SFML 官方文档核对本页可公开验证的工程事实。本页是独立中文重写,不复现原书正文,也不把商品页、目录或代码仓库冒充完整原版。
在“第 13 章:分层 View 与 HUD”中,第三版示例使用 SFML 2.6.1 时代 API。本页保留与本章有关的 2.6 系列合同;SFML 3 的事件、角度、时长和构造接口差异不会被静默回填。升级工具链时必须单独记录本章迁移补丁,不能把版本不匹配误判为“分层视图(layering views)”概念错误。
- Packt:Beginning C++ Game Programming, Third Edition:在“第 13 章:分层 View 与 HUD”中,核对 2024 年第三版、C++20、SFML、四个项目、21 个正式教学章节及章节次序;不把商品页当作正文全文。
- PacktPublishing:第三版官方代码仓库:在“第 13 章:分层 View 与 HUD”中,核对 Timber、Pong、ZombieShooter、Run 四个项目的公开代码与资源组织;代码许可证不等于原书正文授权。
- SFML 2.6 官方教程:在“第 13 章:分层 View 与 HUD”中,核对本书使用的 SFML 2.6 系列 Window、Graphics、View、VertexArray、Shader 与 Audio API 语义。
- SFML 2.6.1:sf::View 官方参考:在“第 13 章:分层 View 与 HUD”中,核对 source rectangle 到 target viewport 的映射、主视图、HUD 与雷达视图边界。
正式概念与运行状态合同
分层视图(layering views)
这个正式目录节点落在 世界层 边界:Arena、玩家、僵尸与 worldView。在本页中,它参与“在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间”。验收时保存当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图,不能只凭最终画面判断。
hud 对象(hud objects)
这个正式目录节点落在 界面层 边界:Text、血条、弹药与默认 View。在本页中,它参与“在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间”。验收时保存世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪,不能只凭最终画面判断。
更新 hud(updating the hud)
这个正式目录节点落在 阶段层 边界:首页、游戏中、升级与重启画面。在本页中,它参与“在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间”。验收时保存当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图,不能只凭最终画面判断。
首页与升级界面(home and level-up screens)
这个正式目录节点落在 世界层 边界:Arena、玩家、僵尸与 worldView。在本页中,它参与“在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间”。验收时保存世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪,不能只凭最终画面判断。
| 验收项 | 本页合同 |
|---|---|
| 最小正常场景 | worldView 中心移动而 HUD 数值不变 |
| 边界或恢复场景 | 从战斗阶段切换到 level-up 阶段 |
| 必须保持 | 世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪 |
| 单一故障 | 绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放 |
| 可观察证据 | 当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图 |
先预测,再操作三个本页实验
实验一:从源码到可见结果
先预测“相机跟随战斗”会怎样穿过 世界层 → 界面层 → 阶段层,再切换场景和正式概念。每次操作都必须能回到同一初始状态。
Source · state · visible result
第 13 章:分层 View 与 HUD:可运行切片
在同一 RenderWindow 中显式切换 worldView 与 HUD view,更新 HUD、首页和升级界面而不混淆坐标空间
选择输入或构建场景
定位正式概念
bcgp3-13 · 当前切片
分层视图(layering views):worldView 中心移动而 HUD 数值不变
Arena、玩家、僵尸与 worldView
↓
Text、血条、弹药与默认 View
↓
首页、游戏中、升级与重启画面
可验收结果
世界滚动,血条和文字保持固定窗口位置
实验二:逐步执行状态轨迹
依次执行 设置 worldView → 绘制世界 → 切换 HUD view → 更新 HUD → 绘制界面。每一步只选中一个阶段,并持续检查“世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪”。
Deterministic state trace
第 13 章:分层 View 与 HUD:状态执行轨迹
世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪
当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图
实验三:单一故障与同输入恢复
注入“绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放”,定位第一项不一致;撤销后用完全相同的“进入升级界面”重放。只有中间状态和最终输出一起恢复才算修复。
Fault injection · clean replay
第 13 章:分层 View 与 HUD:故障注入与恢复
单一故障:绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放
第 1 次重放使用同一源码、资源、初始状态和输入序列
世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪
当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图
同输入重放后,所有权、状态更新、可见输出和诊断证据重新一致
易错边界与工程取舍
从“哪些东西应随相机移动”开始
第 8 章建立双 View,第 13 章把它扩展为完整
↡按世界、世界叠加、固定 HUD 和全屏面板的顺序使用不同 View 绘制同一帧的技术。地图、Player、Zombie、Bullet 属于世界;弹药、分数、生命和波次属于 HUD;Home 与 LevelUp 面板属于阻塞世界输入的屏幕状态。
一次 clear,一次 display,中间切 View
↡在 worldView 下绘制地图和游戏实体的阶段,所有对象使用世界坐标并随相机滚动。先绘制。世界血条、爆炸和地面标记若绑定实体位置,也在此层。随后切换 hudView 绘制固定界面。
window.clear(sf::Color::Black);
window.setView(worldView);
drawTerrain(window);
drawEntities(window);
drawWorldEffects(window);
window.setView(hudView);
hud.draw(window);
if (phase != GamePhase::Playing)
screens.draw(window, phase);
window.display();切 View 不会清除后缓冲,只改变后续 draw 的坐标映射。若每层都 clear,前一层会消失;若中途 display,玩家可能看到半成品帧。
HUD 对象不是业务状态
↡显示生命、弹药、分数、波次和提示的一组 Text/Shape 对象,由游戏状态投影生成但不拥有规则。不应保存“真正分数”并让 Game 再读取。业务层产生只读
↡某一帧用于界面显示的生命、弹药、分数、波次和阶段值集合。, HUD 比较前后值并更新视图。
struct GameSnapshot {
int health;
int maximumHealth;
int ammo;
int score;
int wave;
GamePhase phase;
auto operator<=>(const GameSnapshot&) const = default;
};Snapshot 是值,不含 Player/Zombie 指针,避免 HUD 跨帧保存易失引用。若生命变化,更新生命文本和条宽;分数未变则无需重新格式化。先保证正确,再测量文本更新是否真是瓶颈。
更新 HUD:格式与布局一起刷新
↡把数值状态转换成可显示字符串、几何宽度和颜色,并写入对应 HUD 对象的过程。不能只 setString:文本宽度变化后,右对齐或居中原点也要重算。
void Hud::setScore(int score)
{
scoreText_.setString("Score: " + std::to_string(score));
const sf::FloatRect bounds{scoreText_.getLocalBounds()};
scoreText_.setOrigin(bounds.left + bounds.width, bounds.top);
scoreText_.setPosition(logicalWidth_ - margin_, margin_);
}比固定左上坐标更稳。getLocalBounds().left/top 可能不为零,设置 origin
时应包含偏移。Font 必须覆盖所有 Text 生命周期。
生命条与弹药
生命条宽度由 health / maximumHealth 归一化,先处理最大值非正,再钳到 0..1。业务层应已保证 health 合法,HUD 仍防御损坏 snapshot,避免负宽。
const float ratio{snapshot.maximumHealth > 0
? std::clamp(static_cast<float>(snapshot.health) /
static_cast<float>(snapshot.maximumHealth),
0.0F, 1.0F)
: 0.0F};
healthBar_.setSize({fullHealthWidth_ * ratio, healthBarHeight_});
healthBar_.setFillColor(ratio < 0.25F ? sf::Color::Red : sf::Color::Green);弹药显示整数,成功 shoot 后才扣减,Pickup 提交后增加。HUD 接收提交后的 snapshot,因此不会显示“子弹没发出但弹药已扣”的中间状态。
↡仅在对应状态值变化时重新格式化或布局 HUD 元素的更新策略。可减少工作,但状态比较必须完整;漏标记比每帧更新更糟。
Home 屏幕是状态而非背景图
↡游戏启动或返回菜单时显示标题与开始入口、停止战斗更新并接管输入的阶段。可继续绘制静态世界作为背景,但 Player/Zombie 不更新。Enter 的 KeyPressed 边沿开始新局,按住不能每帧重置。
if (phase == GamePhase::Home &&
event.type == sf::Event::KeyPressed &&
event.key.code == sf::Keyboard::Enter) {
resetGame();
frameClock.restart();
phase = GamePhase::Playing;
}状态提交前 reset 完整成功,最后才进入 Playing。退出 Home 时清除菜单选择和重复输入状态;恢复时钟,避免菜单停留时间成为第一帧 dt。
LevelUp 屏幕与一次选择
↡达到升级条件后暂停战斗、显示可选能力,并在一次合法选择后应用效果并恢复游戏的阶段。不能只盖面板同时让 Zombie 在背后继续伤害 Player。进入时冻结游戏时间、生成当前合法选项;选择时验证资源和上限,再提交属性。
if (phase == GamePhase::LevelUp && event.type == sf::Event::KeyPressed) {
const std::optional<Upgrade> choice{mapUpgradeKey(event.key.code)};
if (choice && player.canApply(*choice)) {
player.apply(*choice);
phase = GamePhase::Playing;
frameClock.restart();
}
}只在转换边沿触发。重复按键、已达上限和过期选项都不能重复消费点数。
GameOver 与重启路径
↡Player 死亡后冻结战斗、显示最终分数并只接受重启或返回 Home 的终局阶段。读取最终 snapshot,不继续加分。重启恢复 Player、horde、Bullet/Pickup、分数和波次;返回 Home 则释放关卡对象或保持明确静态预览。
Home、LevelUp、GameOver 的输入集合互斥。不要让全局射击查询在非 Playing 阶段运行,否则点击菜单会向世界发射子弹。输入路由先看 phase,再交给对应处理器。
首页与升级界面共享同一 hudView 和安全区域,但读取不同状态、接受不同命令;复用布局组件不能把两套转换规则合并成一个含糊的 showMenu 布尔值。
焦点丢失可自动转 Paused 或忽略持续输入;恢复时清方向并重启帧时钟。界面状态也是时间策略的一部分。
resize 与逻辑 UI 分辨率
hudView 可保持 1280x720 逻辑尺寸,在不同窗口使用 letterbox viewport,避免 UI 被非等比拉伸。resize 时计算目标宽高比和归一化 viewport;worldView 可使用相同视口或独立策略,但鼠标映射必须指定对应 View。
↡在窗口比例与逻辑比例不同时用空白边保留等比内容区域的 viewport 策略。产生的黑边不是 HUD 坐标。点击前先判断像素是否在有效 viewport 内,避免黑边映射到菜单按钮。
文本需测试最长本地化字符串。动态缩放字体不是首选,先允许换行、扩大面板或按安全区域布局。按钮和计数器尺寸稳定,状态变化不能推动其它元素跳动。
HUD 类的职责边界
Hud 拥有 Text/Shape 并借用 Font,提供 update(snapshot) 与 draw(target);不读取全局 Player、不修改分数、不处理升级规则。ScreenOverlay 可独立拥有 Home/LevelUp/GameOver 文本,或 Hud 根据 phase 绘制,但输入仍由状态机处理。
class Hud {
public:
Hud(const sf::Font& font, sf::Vector2f logicalSize);
void update(const GameSnapshot& snapshot);
void draw(sf::RenderTarget& target) const;
private:
GameSnapshot previous_{};
bool initialized_{false};
sf::Text scoreText_;
sf::Text ammoText_;
sf::RectangleShape healthBar_;
};Font 由资源层拥有。previous 只用于投影缓存,不成为业务来源。首次 initialized 强制全量刷新,避免默认 snapshot 恰好相等导致空文本。
验证层次、状态和布局
渲染测试记录 draw 阶段:worldView 下世界对象,hudView 下 HUD,面板最后,一帧一次 clear/display。让 Player 移动一屏,HUD 像素位置保持;World 血条跟 Zombie 移动。
状态测试 Home 不更新战斗,Enter 只重置一次;LevelUp 冻结 Player/Zombie/Bullet/Pickup 时间,一次合法选择修改一次属性并恢复;GameOver 分数不再变化,重启完整清理。每次恢复 frameClock 重新开始。
布局测试分数从 9 到 100000 后仍右对齐,生命 0/满值/损坏值宽度合法,最长消息不溢出。多个窗口比例下截图和 mapPixelToCoords 点击回放同时通过。
先预测:先画 HUD 再画世界会怎样?即使坐标正确,世界背景和实体会覆盖 HUD;绘制顺序也是层次契约的一部分。
小结
- 一帧只 clear/display 一次,中间按 worldView 世界、hudView HUD、全屏面板顺序叠加
- HUD 是 GameSnapshot 的只读投影,业务分数、生命和弹药仍由游戏状态拥有
- 文本变化后重算 local bounds,以锚点布局保持右对齐和居中
- Home、LevelUp、GameOver 是明确状态,决定输入和游戏时间,不只是视觉覆盖
- LevelUp 选择只提交一次,恢复前重启帧时钟;非 Playing 阶段不处理射击
- resize 使用一致 View/viewport 映射,letterbox 黑边不属于可点击逻辑区域
练习
问题 1 写出一帧的三层绘制顺序,为什么每层不能各自 clear/display?
问题 2 分数从 99 变 100 后,右对齐 Text 要更新哪些数据?为何 HUD 不能拥有真实 score?
问题 3 进入 LevelUp 后哪些系统冻结,一次选择如何安全恢复 Playing?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 分层视图
按世界、HUD 和全屏面板顺序使用不同 View 绘制一帧。
- 世界图层
在 worldView 下绘制地图、实体和世界特效的阶段。
- HUD 图层
在固定 hudView 下绘制分数、生命、弹药和提示的阶段。
- HUD 对象
由 Text/Shape 组成、从游戏状态投影但不拥有规则的界面对象。
- GameSnapshot
供界面读取的一帧生命、弹药、分数、波次和阶段值。
- HUD 更新
把数值状态格式化为字符串、几何与颜色并刷新界面的过程。
- 锚点布局
以边缘或中心为参考、在内容变化后保持对齐的 UI 放置方式。
- 脏标记
只在对应状态变化时重新格式化或布局 HUD 元素的策略。
- Home 屏幕
显示标题和开始入口、停止战斗并接管输入的初始阶段。
- LevelUp 屏幕
冻结战斗并提供一次能力选择的升级阶段。
- 升级提交
合法选择后对 Player 状态执行一次的能力修改。
- GameOver 屏幕
Player 死亡后显示最终结果并接受重启或回首页的终局阶段。
- letterbox
窗口比例不同时用空白边保留逻辑内容等比的 viewport 策略。