尽我所能优化通用 Jigsaw 结构生成。这个模组的核心目标基本上是完全不改变结构外观。它所做的只是尽量让结构生成得更快、更高效。就是这样。
以下是这些优化的更技术性的细节。如果你在整合包中运行此模组时发现任何冲突或问题,请务必反馈!
- 将 VertexShape 中未优化的调用替换为 BoxOctree,使结构片段只检查附近片段的相交情况,而不是检查整个结构。
- 默认情况下,原版使用 VoxelShape 来存储正在为结构布局组装的各个片段的边界。当它尝试添加一个新片段时,需要判断这个片段能否放入布局中,并且不与其他任何片段相交。
问题在于,VoxelShape 并不适合这个用途,因为当检查一个 VoxelShape 是否与另一个 VoxelShape 相交时,会比较所有顶点。在拥有大量片段的 Jigsaw 结构中,随着越来越多的有效片段被加入布局,这种相交检查会显著变慢。对于递归式 Jigsaw 结构,在生成大量片段时可能会造成明显卡顿尖峰。
这里的优化会将普通的 VoxelShape 替换为一个“伪” VoxelShape,它内部持有一个 BoxOctree 实现,以便把 BoxOctree 传递到需要它的地方。这个 BoxOctree 在检查相交时,只会检查与传入包围盒相邻的片段。从而避免相交检查时间失控增长,因为大多数距离过远、不可能影响检查的片段都会被忽略。
- 检查 Jigsaw Block 是否被完全阻挡,以判断何时可以跳过对子刚性片段的检查。
- 在原版中,父片段中的每一个 Jigsaw Block 都会检查子片段中的每一个 Jigsaw Block,以判断是否尝试将这两个片段连接起来。问题在于,如果父片段中的某个 Jigsaw Block 正朝向结构边界的边缘,或者正朝向另一个独立片段,那么这个 Jigsaw Block 就根本没有空间生成一个刚性结构片段。因此,我们可以让游戏跳过对这个刚性子片段的检查,直接检查下一个片段。这样可以省去大量高开销的 Jigsaw Block 匹配检查,尤其是在拥有海量 Jigsaw Block 的结构中。
- 将 Jigsaw 的 target/facing 匹配替换为一个略微更优化的版本。
- 将方块属性获取次数减少了一半。原版会对每个 JigsawBlock 调用两次
getValue,以从同一个属性中获取 top 和 front 值,而实际上只获取一次就能拿到所需的全部值。还稍微提升了从 Jigsaw Block 的 NBT 中获取 joints、targets 和 name 字符串值的速度。
同时还简化了 joint 数据检查,不再需要通过 byName 转换为 Enum(性能不佳),并重新排列了检查顺序,以减少频繁执行的逻辑量。
这将有助于那些拥有极大量 Jigsaw Block 的 Jigsaw 结构,因为每个 Jigsaw Block 都要针对其他结构片段中的所有 Jigsaw Block 运行这套匹配代码。
- 让任何没有 finalizeProcessing StructureProcessor 的大型结构 NBT 加载得更快。
- 当一个 NBT 结构片段要在区块中生成时,整个 NBT 片段都会被加载进内存,然后为了应用 StructureProcessors,会对所有位置迭代多次,之后又会忽略当前生成区块之外的所有位置。这种做法效率并不高(相当浪费),并且会导致大型 NBT 文件出现很长的加载时间。
本模组的优化方式是:在把数据传给 StructureProcessors 之前,先提前进行边界检查,把不需要的位置剔除掉。不过,任何重写了 finalizeProcessing 方法的 StructureProcessor 都可能需要依赖完整的 NBT 位置数据才能正常工作,因此对于带有这类 StructureProcessor 的片段,此优化会被禁用,而这样的结构应该非常少。在原版中,只有 Trail Ruins 因为使用了重写 finalizeProcessing 方法的 Capped StructureProcessor,所以无法享受此优化。
- (1.21.1+) 将 SinglePoolElement 中 Jigsaw Block 列表的洗牌与优先级排序逻辑替换为一个稍快的版本。
- 主要收益是通过一个新方法更快地从 NBT 中获取
selection_priority 数据:它只获取一次条目,而不是获取两次。原版会先获取一次来检查数据类型,再获取一次返回值,这有点奇怪也有些浪费。
另外还尝试了一个新的优先级排序系统,但它只对少数真正使用 selection_priority 的结构有帮助,比如 Trials Chamber。总体来说,这可能是最弱的一项优化,但对于那些拥有极其夸张数量 Jigsaw Block 的 Jigsaw 结构,它确实能提供不少帮助,所以仍然值得保留。
它可能会破坏与原版种子在 Jigsaw Block 执行顺序上的一致性,不过目前我还没找到相关证据。
- 跳过对那些我们已经检查过、并确定无法在当前位置生成的 SinglePoolElements 再次运行逻辑。
- 这个优化来源于这样一个事实:原版的 StructureTemplatePool 会持有一个包含所有 SinglePoolElements 的列表,其中元素会根据其权重值重复出现。所以如果你在一个 Template Pool 中给某个房子指定了 100 的权重,那么这个房子就会在列表里被放入 100 次!而在生成布局时,游戏会复制这个列表、打乱它,然后遍历它,尝试生成找到的第一个适合当前位置的片段。这意味着,即使列表中的重复项之前已经被判断为不适合该位置,游戏仍会对它们重复运行检查逻辑。我的优化就是跳过那些我们已经检查过的片段,直接继续检查列表中的下一个片段。对于在 Template Pools 中大量使用高权重值的结构,这能带来相当不错的性能提升。
- 此外,你还可以开启
deduplicateShuffledTemplatePoolElementList 配置选项,以从 Template Pools 中的高权重元素获得更多性能收益。问题是,这个配置的代价是会改变结构布局。布局仍然是有效且合理的,只是会和关闭该配置时不同。也就是说,它会破坏结构布局层面的 seed parity,以换取额外的性能提升。
- *说得更明确一点,seed parity 的意思是:每次你使用种子 777,并且某个位置有一个村庄,里面有 2 个铁匠铺,那么每次使用这个相同种子时,那个村庄都会在同一个位置,并且依然有那 2 个铁匠铺。而如果开启了这个优化配置,使用同样的种子时,那个村庄仍然会出现在同一个位置,但这次可能只有 1 个铁匠铺。不过每次你在开启此配置的情况下使用同一个种子,这个村庄都会稳定地生成出那 1 个铁匠铺。也就是说,布局在种子上仍然稳定,但不再与关闭配置时的结果完全一致。
- (1.21.4 with v1.1.0+): 将 StructureTemplate$Palette 中的 StructureBlockInfo 对象列表替换为 StructureBlockInfo 对象调色板,以降低未生成该缓存 StructureTemplate 时的内存占用。
- 这个优化修改自 contaria 的 Glacier mod(适用于 1.16.1)。非常感谢他们允许我以他们的代码为基础来实现这项优化!
- 本质上,在原版中,每当加载一个结构 nbt 文件时,它都会被转换成一个 StructureTemplate 对象并缓存起来。而在这个对象内部,保存着该 nbt 文件中所有位置的列表,并将每个位置与对应的方块和该位置的 nbt 标签配对。这是 StructureTemplate 对象中最主要的内存占用来源。这个模组会在底层把这个列表替换成一种特殊的调色板数据结构对象,它会“伪装”成一个列表。这个调色板的内存占用明显小于原本的列表!然后,当世界生成需要查询它时,它会重新构建这个列表,并将其作为 WeakReference 保存,这样世界生成仍然能保持快速并正常工作。而当世界生成结束后,这个列表会被垃圾回收,以再次释放内存。这样一来,每个 StructureTemplate 只有在进行世界生成时才会占用完整的内存,而长期缓存时则会以更小的形式存在。缺点是,这个列表不能也不应该通过 add、remove、clear 调用来修改;如果这样做,会抛出异常。幸运的是,我认为应该没有模组会直接修改 StructureTemplate 上的这个列表,但如果真有,请告诉我。
- ModernFix 确实有一个 mixin,会让 StructureTemplate 对象本身成为 SoftReference,这样当游戏内存紧张时,它们会被完全从内存中移除。这是一个有效的方案,目标是在长期游玩中阻止结构 nbt 被加载进一个永不清理的缓存而导致的内存泄漏。它的缺点是:如果某个片段已被从内存中清除,那么之后就必须再次直接从文件中加载。这个 mixin 可以与我的模组优化良好兼容。大概会发生的情况是:我的模组先让 StructureTemplate 在内存中变得更小,这样 ModernFix 就能在内存中保留更多的 StructureTemplate,而不必将它们彻底移除。理论上,这应该能在长期运行中提高缓存命中率。