第 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 数值不变

边界 1世界层

Arena、玩家、僵尸与 worldView

边界 2界面层

Text、血条、弹药与默认 View

边界 3阶段层

首页、游戏中、升级与重启画面

可验收结果

世界滚动,血条和文字保持固定窗口位置

实验二:逐步执行状态轨迹

依次执行 设置 worldView → 绘制世界 → 切换 HUD view → 更新 HUD → 绘制界面。每一步只选中一个阶段,并持续检查“世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪”。

Deterministic state trace

第 13 章:分层 View 与 HUD:状态执行轨迹

逐步执行当前 1 / 5
始终保持

世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪

本步证据

当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图

实验三:单一故障与同输入恢复

注入“绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放”,定位第一项不一致;撤销后用完全相同的“进入升级界面”重放。只有中间状态和最终输出一起恢复才算修复。

Fault injection · clean replay

第 13 章:分层 View 与 HUD:故障注入与恢复

单一故障:绘制 HUD 前忘记切回默认 View,导致文字随相机移动或缩放

1. 固定构建与输入一致

第 1 次重放使用同一源码、资源、初始状态和输入序列

2. 程序状态一致

世界对象只在 worldView 下绘制,HUD 只在固定界面 View 下绘制,每个 draw 调用前 View 状态可追踪

3. 诊断证据一致

当前 View 标识、draw 调用序列、世界坐标、HUD 像素位置和窗口截图

4. 恢复判断一致

同输入重放后,所有权、状态更新、可见输出和诊断证据重新一致

易错边界与工程取舍

从“哪些东西应随相机移动”开始

第 8 章建立双 View,第 13 章把它扩展为完整

地图、Player、Zombie、Bullet 属于世界;弹药、分数、生命和波次属于 HUD;Home 与 LevelUp 面板属于阻塞世界输入的屏幕状态。

一次 clear,一次 display,中间切 View

先绘制。世界血条、爆炸和地面标记若绑定实体位置,也在此层。随后切换 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 对象不是业务状态

不应保存“真正分数”并让 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:格式与布局一起刷新

不能只 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,因此不会显示“子弹没发出但弹药已扣”的中间状态。

可减少工作,但状态比较必须完整;漏标记比每帧更新更糟。

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 与重启路径

读取最终 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。

产生的黑边不是 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 策略。

资料与写作方式声明

本章以Beginning C++ Game Programming, Third Edition, Chapter 13权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…