第 9 章:引用、精灵图集与 VertexArray

第 9 章:引用、精灵图集与 VertexArray:保留第三版项目代码讲解,并以源码—状态—输出切片、确定性轨迹和故障重放完成验收。

学习目标

  • 能解释“第 9 章:引用、精灵图集与 VertexArray”如何用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景
  • 能逐项定位 c++ 引用(c++ references)、精灵图集(sprite sheets)、顶点数组(vertex arrays)、随机滚动背景(randomly generated scrolling background),说明它们位于源码、运行状态还是可见输出边界
  • 能按 绑定引用 → 选择 tile → 写入顶点 → 提交数组 → 验证寿命 重放“构造 Arena 背景”,持续检查“非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配”
  • 能注入“返回对局部 VertexArray 或临时纹理的引用,调用结束后继续绘制悬空对象”,从引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用找到第一个不一致并用同输入恢复

第三版来源、工具链与版本边界

“第 9 章:引用、精灵图集与 VertexArray”对齐 Packt 2024 年第三版的对应章节范围,并以官方公开代码仓库和 SFML 官方文档核对本页可公开验证的工程事实。本页是独立中文重写,不复现原书正文,也不把商品页、目录或代码仓库冒充完整原版。

在“第 9 章:引用、精灵图集与 VertexArray”中,第三版示例使用 SFML 2.6.1 时代 API。本页保留与本章有关的 2.6 系列合同;SFML 3 的事件、角度、时长和构造接口差异不会被静默回填。升级工具链时必须单独记录本章迁移补丁,不能把版本不匹配误判为“c++ 引用(c++ references)”概念错误。

  • Packt:Beginning C++ Game Programming, Third Edition:在“第 9 章:引用、精灵图集与 VertexArray”中,核对 2024 年第三版、C++20、SFML、四个项目、21 个正式教学章节及章节次序;不把商品页当作正文全文。
  • PacktPublishing:第三版官方代码仓库:在“第 9 章:引用、精灵图集与 VertexArray”中,核对 Timber、Pong、ZombieShooter、Run 四个项目的公开代码与资源组织;代码许可证不等于原书正文授权。
  • SFML 2.6 官方教程:在“第 9 章:引用、精灵图集与 VertexArray”中,核对本书使用的 SFML 2.6 系列 Window、Graphics、View、VertexArray、Shader 与 Audio API 语义。

正式概念与运行状态合同

c++ 引用(c++ references)

这个正式目录节点落在 数据所有权 边界:调用方对象、引用参数和资源寿命。在本页中,它参与“用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景”。验收时保存引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用,不能只凭最终画面判断。

精灵图集(sprite sheets)

这个正式目录节点落在 几何构造 边界:sprite sheet tile 到 VertexArray 顶点。在本页中,它参与“用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景”。验收时保存非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配,不能只凭最终画面判断。

顶点数组(vertex arrays)

这个正式目录节点落在 滚动输出 边界:纹理坐标、世界位置与批量绘制。在本页中,它参与“用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景”。验收时保存引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用,不能只凭最终画面判断。

随机滚动背景(randomly generated scrolling background)

这个正式目录节点落在 数据所有权 边界:调用方对象、引用参数和资源寿命。在本页中,它参与“用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景”。验收时保存非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配,不能只凭最终画面判断。

验收项本页合同
最小正常场景调用方预分配 VertexArray,并以引用交给背景构造函数
边界或恢复场景把局部纹理地址写入离开作用域后仍使用的对象
必须保持非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配
单一故障返回对局部 VertexArray 或临时纹理的引用,调用结束后继续绘制悬空对象
可观察证据引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用

先预测,再操作三个本页实验

实验一:从源码到可见结果

先预测“构造 Arena 背景”会怎样穿过 数据所有权 → 几何构造 → 滚动输出,再切换场景和正式概念。每次操作都必须能回到同一初始状态。

Source · state · visible result

第 9 章:引用、精灵图集与 VertexArray:可运行切片

用 C++ 引用传递世界数据,以 sprite sheet 和 sf::VertexArray 批量构造可滚动背景

选择输入或构建场景

定位正式概念

bcgp3-09 · 当前切片

c++ 引用(c++ references)调用方预分配 VertexArray,并以引用交给背景构造函数

边界 1数据所有权

调用方对象、引用参数和资源寿命

边界 2几何构造

sprite sheet tile 到 VertexArray 顶点

边界 3滚动输出

纹理坐标、世界位置与批量绘制

可验收结果

函数返回后数组仍由调用方拥有且顶点可绘制

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

依次执行 绑定引用 → 选择 tile → 写入顶点 → 提交数组 → 验证寿命。每一步只选中一个阶段,并持续检查“非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配”。

Deterministic state trace

第 9 章:引用、精灵图集与 VertexArray:状态执行轨迹

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

非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配

本步证据

引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用

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

注入“返回对局部 VertexArray 或临时纹理的引用,调用结束后继续绘制悬空对象”,定位第一项不一致;撤销后用完全相同的“临时资源误用”重放。只有中间状态和最终输出一起恢复才算修复。

Fault injection · clean replay

第 9 章:引用、精灵图集与 VertexArray:故障注入与恢复

单一故障:返回对局部 VertexArray 或临时纹理的引用,调用结束后继续绘制悬空对象

1. 固定构建与输入一致

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

2. 程序状态一致

非拥有引用的生命周期短于被引用对象,VertexArray 的 primitive type、顶点数、纹理坐标和世界坐标保持匹配

3. 诊断证据一致

引用绑定对象地址、所有者作用域、顶点数量、position/texCoords 和 draw 调用

4. 恢复判断一致

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

易错边界与工程取舍

从“函数怎样把一整片地形交回调用者”开始

第 8 章的 worldView 能在大世界滚动,但背景仍是占位对象。第 9 章用

让构建函数填充调用者拥有的 VertexArray,同时读取图集和随机引擎。核心问题是修改权限和生命周期,不是“引用永远更快”。

值、可写引用与 const 引用

用于行列数、tile 尺寸等小值。

用于 sf::VertexArray& output

用于 Texture 与配置。

bool buildBackground(sf::VertexArray& output,
                     const sf::Texture& atlas,
                     std::mt19937& random,
                     std::size_t rows,
                     std::size_t columns,
                     float tileSize);

随机引擎也是可写引用,因为抽样会推进其序列状态。函数不保存这些引用,调用结束后 output、atlas 和 random 仍由调用者拥有。引用非空,但不自动保证对象仍存活;返回引用或保存成员引用时必须额外证明生命周期。

引用一旦绑定不能改绑,给引用赋另一个对象执行的是“给被引用对象赋值”,不是让引用指向新对象。若依赖可缺失或运行期需要切换目标,应使用能表达空值的指针、optional<reference_wrapper<T>> 或更高层句柄。无论引用还是指针都不自动表示所有权;背景对象若长期保存 Texture 借用,拥有它的游戏资源层必须保证先销毁背景、后销毁纹理,并禁止资源热重载留下悬空地址。

API 文档应分别写“是否可空”“是否修改”“是否保存”“谁拥有”,不能用一个 & 让读者猜四件事。当前 buildBackground 只在调用期借用,因此最容易验证。

精灵图集:一张纹理保存多个 tile

减少纹理切换和文件管理。假设每格 64x64、图集每行 8 格,tile id 13 对应列 13 % 8 = 5、行 13 / 8 = 1,纹理左上为 (320,64)

struct AtlasLayout {
    std::size_t columns;
    std::size_t rows;
    unsigned tilePixels;
};
 
sf::Vector2f tileTexOrigin(std::size_t id, const AtlasLayout& atlas)
{
    const std::size_t tileCount{atlas.columns * atlas.rows};
    if (atlas.columns == 0 || id >= tileCount)
        throw std::out_of_range{"tile id outside atlas"};
 
    return {
        static_cast<float>((id % atlas.columns) * atlas.tilePixels),
        static_cast<float>((id / atlas.columns) * atlas.tilePixels)
    };
}

来自地图数据或随机生成器。图集尺寸应与布局配置交叉验证,不能访问纹理外坐标;边缘过滤产生接缝时,还要检查采样方式、padding 和像素对齐。

VertexArray:四个顶点组成一个地块

设置为 sf::Quads 时,每个 tile 使用四个顶点。每个 Vertex 同时保存世界 position、纹理 texCoords 和可选颜色。

const std::size_t tileIndex{row * columns + column};
sf::Vertex* quad{&vertices[tileIndex * 4]};
 
const float x{static_cast<float>(column) * tileSize};
const float y{static_cast<float>(row) * tileSize};
quad[0].position = {x, y};
quad[1].position = {x + tileSize, y};
quad[2].position = {x + tileSize, y + tileSize};
quad[3].position = {x, y + tileSize};

按一致绕序排列。rows * columns * 4 前先检查 size_t 乘法溢出和合理上限,再 resize;否则恶意或损坏地图尺寸可导致小分配后越界写。

纹理坐标:从 tile id 选择图集区域

使用图块左上 (u,v) 和 tile 像素尺寸填四角。

const sf::Vector2f uv{tileTexOrigin(tileId, atlasLayout)};
const float pixels{static_cast<float>(atlasLayout.tilePixels)};
 
quad[0].texCoords = uv;
quad[1].texCoords = {uv.x + pixels, uv.y};
quad[2].texCoords = {uv.x + pixels, uv.y + pixels};
quad[3].texCoords = {uv.x, uv.y + pixels};

世界 tileSize 可以与纹理 tilePixels 不同,表示缩放;不要把两者混为同一变量。纹理启用平滑过滤时,相邻图块颜色可能在边界混合,图集可加 padding,或根据像素美术策略关闭 smoothing。

批处理:一次 draw 不等于零成本

让整个地面通过一个 VertexArray 和一张 atlas 绘制:

sf::RenderStates states;
states.texture = &terrainAtlas;
 
window.setView(worldView);
window.draw(terrainVertices, states);

VertexArray 不拥有 Texture,terrainAtlas 必须覆盖绘制期。一次 draw 仍要处理所有顶点;大地图若每帧提交全量不可见几何,会增加顶点处理与带宽。后续可按 chunk 分区并只绘制 View 可见块,但先测量,不要在地图很小时增加复杂度。

动态图块只修改对应四顶点,不必重建全部数组。地图数据仍应与渲染顶点分离,碰撞和寻路读取 tile id,不从 texCoords 反推地形类型。

随机背景:种子、规则与出生区

不是每帧随机。关卡开始时生成 tile id,再构建 VertexArray;相机滚动只改变 View,背景顶点保持世界坐标。

std::uniform_int_distribution<int> tileChoice{0, 99};
 
for (std::size_t row{0}; row < rows; ++row) {
    for (std::size_t column{0}; column < columns; ++column) {
        const bool inSpawnZone{row >= spawnTop && row <= spawnBottom &&
                               column >= spawnLeft && column <= spawnRight};
        const int roll{tileChoice(random)};
        const std::size_t tileId{inSpawnZone ? grassTile
                                             : (roll < 85 ? grassTile : wallTile)};
        writeTile(vertices, row, column, columns, tileSize,
                  tileId, atlasLayout);
    }
}

出生区强制可走,避免 Player 生成在障碍内。真实地图还要验证出口、连通性、僵尸生成距离和可见遮挡;概率落在范围不等于关卡可玩。固定种子用于测试,正式运行记录种子便于复现。

应进入日志或存档。生成器规则版本也要记录,否则算法更新后同一 seed 不再代表同一地图。

世界边界必须来自同一配置

竞技场宽度是 columns * tileSize,高度是 rows * tileSize。Player 边界、相机钳制、僵尸生成和 VertexArray 顶点都从这一配置派生,不能一处写 3000、另一处按 50 格乘 64 得 3200。

const float worldWidth{static_cast<float>(columns) * tileSize};
const float worldHeight{static_cast<float>(rows) * tileSize};
const sf::FloatRect arena{0.0F, 0.0F, worldWidth, worldHeight};

浮点转换前检查列行与 tileSize 的范围。地图数据索引使用 size_t,世界坐标使用 float;跨类型边界集中在构建阶段,运行循环不反复猜转换。

函数契约与失败提交

buildBackground 若中途发现 atlas 配置或尺寸非法,不应留下看似完整的半数组。可先构建局部 VertexArray,全部验证成功后 move 到 output;或返回 std::optional<Background>,同时携带 arena 与 tile 数据。

sf::VertexArray candidate{sf::Quads};
candidate.resize(vertexCount);
// Fill and validate candidate.
output = std::move(candidate);
return true;

可写引用容易产生部分修改,因此后置条件要明确。单纯返回 VertexArray 值通常借助移动语义足够高效,也更容易表达成功提交;本章使用引用是为了学习别名,工程设计仍应比较错误协议和所有权清晰度。

验证几何、纹理和滚动

对 2x2 小地图使用固定 tile ids,断言顶点数 16、世界范围正确、四个 UV 角落位于图集对应单元。用错误 id、零列数、过大乘积和纹理尺寸不匹配验证失败路径。

随机测试用固定 seed,断言重复布局和 checksum 一致、所有 id 合法、出生区全 grass;不同 seed 不要求必然不同每一格,只检查规则。连通性用 flood fill 从出生点确认最低可走比例。

视觉测试画 2x2 彩色标记图集,检查旋转、镜像、接缝和绕序;让 Player 跨越一屏,背景应通过 worldView 平滑滚动,HUD 保持固定。性能比较每 tile Sprite 与单 VertexArray 的 draw call 和 CPU 时间,而不是只看平均 FPS。

先预测:背景函数接收 sf::VertexArray output 而不是引用,内部填满后调用者为何仍是空?形参是副本;应返回值或使用明确可写引用,并定义失败后状态。

小结

  • C++ 引用是对象别名,不延长生命周期;可写引用表达输出,const 引用表达只读借用
  • 精灵图集用 tile id 映射纹理单元,世界 tile 尺寸与纹理像素尺寸是独立概念
  • 每个 tile 使用四个 Vertex,位置和 texCoords 按一致绕序写入 VertexArray
  • 批处理减少 draw 调用,但 Texture 仍由外部拥有,大地图还可能需要 chunk 可见性裁剪
  • 随机背景在关卡开始生成,固定种子和规则版本负责重放,出生区与连通性需要额外验证
  • 顶点几何、Player 边界和相机范围都从同一 rows/columns/tileSize 配置派生

练习

问题 1 buildBackground 的 output、atlas、rng、rows 分别应怎样传递?说明修改和生命周期契约。

问题 2 图集 8 列、tile 64 像素,id 13 的 UV 四角是什么?世界 tileSize 为 96 时位置如何计算?

问题 3 怎样证明随机滚动背景既可复现又可玩?

名词解释

本章出现的专业名词,用大白话再讲一遍。

C++ 引用

绑定现有对象的别名,不复制、不重新指向,也不延长对象生命周期。

值传递

形参取得副本,函数内修改不影响调用者对象。

可写引用
T& 形参绑定并允许修改调用者对象。
const 引用

const T& 只读绑定调用者对象,避免复制且禁止经该引用修改。

精灵图集

把多个子图排列在同一纹理、按图块矩形选择的资源。

图块编号

标识图集中一个子图的整数,经列数换算为纹理行列。

sf::VertexArray

按图元类型保存一批 Vertex 并可一次 draw 提交的 SFML 集合。

顶点位置
顶点在游戏世界中的二维几何位置。
纹理坐标

顶点在纹理图像中的采样位置,SFML 二维场景通常使用像素坐标。

批处理

合并相同渲染状态图元以减少 draw 提交次数的技术。

随机滚动背景

一次生成在世界坐标中、由 worldView 观察滚动的随机地形。

地图种子

初始化地形伪随机序列并使布局可重放的值。

资料与写作方式声明

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

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

讨论

评论区加载中…