02 生成状态与光照系统初步
如果读者已经阅读了上一章节的 Advance 部分,则这部分引子可以直接跳过。
假设你是 Mojang 的工程师,现在你遇到了一个问题:如果在主线程上一边运行世界生成一边还得处理游戏各项业务,游戏需要在主循环里每 gt 都轮询一下 “要不要生成新区块?” 然后如果要生成新区块,生成新区块的过程又会堵塞主线程,把后续业务全部卡死。
这个时候会有一个非常自然而然的想法:把世界生成部分从主线程踹出去,变成一个异步线程。但这样做存在风险:如果生成线程和游戏主线程同时修改同一个区块,区块数据就可能出现错误。为了避免两个线程相互干扰,Mojang 决定加一个线程锁。区块必须完全生成完成才能被游戏主线程修改。因此,Mojang 设计了区块的两种实现:准生成区块与已生成区块。
现在,我们已经知道区块有准生成和已生成两种形式,但是准生成区块是如何从无到有,一步一步变成已生成区块的呢?答案就藏在本节的第一个研究:区块生成状态中。
1 区块生成管线
区块生成状态具体指 12 个状态,按顺序的话包括:
- 空区块,也就是区块生成的起点,不包括任何起始结构。
- 结构起始,确定可能在区块中起始的结构。
- 结构参照。用于生成跨区快结构。
- 群系。确定生物群系。
- 地形生成噪声。用于生成地形的大致轮廓与基岩层。
- 地表。替换地表附近的方块。
- 洞穴。生成洞穴与峡谷
- 地物。生成地物,包括树木,紫水晶,矿石,地牢等。
- 光照初始化。
- 光照计算。
- 生物生成。
- 生成完成。在此阶段,准生成区块会准备被转化为已生成区块。
每个阶段都有一个对应的生成任务或加载任务(取决于是否从磁盘加载),定义了从上一个状态推进到当前状态需要执行的运算。
2 外圈依赖
上文介绍了区块状态的逐步推进,但一个区块在生成时并非只依赖自身的数据。例如,要让当前区块生成出与周围连续的地形,周围区块必须先确定生物群系等基础信息。为了描述这类前置条件,我们需要引入外圈依赖的概念。
外圈依赖指:一个区块要推进到某个区块状态,其周围一定距离内的区块至少要达到某个最低状态。这个依赖范围由任务边距表示:它是从目标区块向外计算的最大区块距离,也就是该阶段需要检查多大范围内的周围区块。任务边距为 0 表示只涉及目标区块自身,为 1 表示还涉及紧邻目标区块的一圈区块,为 8 则表示涉及向外八圈的区块。
各个阶段的任务边距大致如下:
2.1 切比雪夫距离
上文所说的 “最大区块距离” 使用切比雪夫距离计算。对于区块坐标 (x₁, z₁) 到 (x₂, z₂):
例如,(0, 0) 到 (8, 0) 的距离 = 8,到 (8, 8) 的距离也是 8。因此,任务边距 = 8 表示需要检查一个以目标区块为中心、边长为 2×8+1 = 17 的正方形区域。
2.2 生成不可达时的处理
加载票(ticket,一种用于声明 “需要加载某个区块” 的轻量标记,详见第 06 章)会影响周围区块所能达到的生成状态。对于无法生成完成的外围区块,系统会根据它与最近准加载区块(加载等级 ≤ 33,见本章末尾的等级表)之间的距离,确定它最多能生成到哪个阶段:
这意味着,1.20.1 中距离准加载区块正好 1 区块远的区块,只会生成到地物——洞穴有了、地物也有了,但光照是不完整的。只有当它周围区块吃到更强的加载票,级别足够低时,生成才会继续向后推进。
3 光照系统
光照系统是区块生成管线中的特殊环节。与其它状态不同,它在实际上不更改任何方块的布局,仅计算已生成的方块的亮度。光照计算由光照提供器统一管理,分为光照初始化和光照两个阶段进行。本节只介绍光照如何嵌入区块状态生成管线,其余内容会在后文提及。
3.1 光照初始化阶段
光照初始化的运作流程大致为:
- 检查组成区块的各个子区块,标记其中哪些包含非空气方块,而不是完全由空气组成。光照系统据此确定哪些子区块需要参与后续的光照计算。
- 将每个非空子区块的光照系统计算状态设置为
false,并告知光照阶段需要计算 / 重计算这一子区块的光照。 - 这一阶段不会执行完整光照传播。只是告诉光照系统哪些子区块需要计算光照。
3.2 光照阶段
完成初始化后,光照阶段会执行完整的光照传播,并在计算结束后将目标区块标记为光照已就绪。由于光照可以跨越区块边界,一个光源可能影响周围多个区块,因此只计算目标区块本身并不能保证边界处的亮度正确。目标区块要完成光照阶段,其周围 8 个相邻区块也必须至少达到同一阶段。
光照计算由光照线程异步执行,不会阻塞区块生成的主流程。此外,从磁盘加载旧区块时,即使记录显示光照初始化和光照阶段均已完成,游戏仍会强制重新计算光照。
3.2.1 光照票的自动管理
具体机制将在后文讨论。目前只需要记住一个基本逻辑:当一个区块需要进行光照计算时,光照系统会自动为它添加一张临时的光照票,使目标区块及其相邻区块在计算期间保持加载。计算完成后,这张票会自动过期。这样可以避免光照尚未计算完毕,相关区块就被卸载。
4 生成完成之后:加载等级预览
区块走完 12 步管线到达生成完成,意味着它作为一个 WorldChunk 存在于内存中了。但存在于内存 ≠ 正在运算——区块是否实际参与刻运算,取决于另一个独立的整数:加载等级(loading level)。数值越低,加载程度越高:
加载等级由加载票(ticket)动态调控——玩家位置、传送门、强制加载等都会产生不同类型的票,票的传播和过期决定了每个区块的等级。这部分将在第 06 章详细展开。目前只需要记住一个基本逻辑:生成完成决定区块 “能不能在内存里”,加载等级决定区块 “能不能运算”。
