如何快速寻找史莱姆区块?
在 Minecraft 中, 平面上每 个方块的区域被称为一个区块。有一些区块在 的地方可以自然生成史莱姆,被称为史莱姆区块。
史莱姆区块是史莱姆农场的基础。为了效率最大化,史莱姆农场往往会在史莱姆区块密集的地方选址。例如 、 甚至 的相邻史莱姆区块。此外还有人想要寻找特定图案的史莱姆区块。
本文主要介绍 Java 版史莱姆区块的生成规律和如何快速寻找特定图案的史莱姆区块。



史莱姆区块的判定
史莱姆公式
史莱姆区块的判定由世界种子 和区块坐标 共同决定,相同的种子与坐标总是会得到相同的结果:先计算坐标项 ,与世界种子组合后初始化 Random,再调用 nextInt(10)。返回 0 就是史莱姆区块,返回 1 到 9 则不是。也就是说,史莱姆区块的概率是 。
import java.util.Random;
boolean isSlimeChunk(long worldSeed, int x, int z) {
return new Random(
worldSeed
+ (int) (x * x * 4987142)
+ (int) (x * 5947611)
+ (int) (z * z) * 4392871L
+ (int) (z * 389711)
^ 987234911L
).nextInt(10) == 0;
}
先用一个记号表示 Java 有符号 32 位整数的溢出处理:保留运算结果的低 32 位,再按有符号数解释。本文的 mod 一律取非负余数。
原始坐标项是:
的第一项在不考虑溢出时为 。按 Java 的 32 位整数计算,结果是 。这个溢出后的结果才参与后面的坐标项求和。
随机数种子的公式:
Random 的内部实现
下面的函数等价于 new Random(randomSeed).nextInt(10):
初始化得到 48 位状态 ;每次乘 、加 11、保留低 48 位得到 ,再取高 31 位得到 。通常返回 q % 10;若 q - value + 9 超过有符号 32 位整数的上限,就从当前状态继续重抽。
int nextInt10(long randomSeed) {
final long A = 0x5DEECE66DL;
final long mask48 = (1L << 48) - 1;
long u = (randomSeed ^ A) & mask48;
while (true) {
long v = (A * u + 11) & mask48;
int q = (int) (v >>> 17);
int value = q % 10;
if ((long) q - value + 9 <= 0x7FFFFFFFL) {
return value;
}
u = v;
}
}
合并史莱姆公式和 Random 初始化里的两次 XOR:
对单个区块 ,首次输出由三步得到:
如果首次输出被接受,nextInt(10) 返回 ;返回 0 表示史莱姆区块。因此首次输出分支的候选条件是 。
朴素的爆搜
我们暂时把 Random 的数学性质按下不表,基于上面的公式,采用朴素的手段进行爆搜。
CPU:按种子枚举位置
最直接的方法是枚举世界种子和区块坐标,调用上面的判定函数。要找一片全部满足要求的区域,就检查该位置及其周围各格;只要一格失败,这个候选便可提前退出:
for worldSeed in seeds:
for (x, z) in positions:
allSlime = True
for (dx, dz) in offsets:
if not isSlimeChunk(worldSeed, x + dx, z + dz):
allSlime = False
break
if allSlime:
record(worldSeed, x, z)
从这一节开始,会有一些实测数据。所有测试都在作者的同一台笔记本上进行,CPU 为 AMD Ryzen 7 5800H,GPU 为 NVIDIA GeForce RTX 3060 Laptop GPU。并非严谨测试,数据仅供参考。
CUDA:每个线程负责一个种子
同样的逻辑可以在 GPU 上实现。每个线程负责一个世界种子,扫描全部候选位置:
GPU thread(worldSeed):
for (x, z) in positions:
check and record this candidate
同样搜索 65536 个种子、257×257 个候选位置,目标都是实心 5×5。
| 版本 | 搜索时间中位数 |
|---|---|
| CPU,16 个逻辑线程 | 3550.398 ms |
| CUDA,每线程一个种子 | 111.110 ms |
约加速 31.95 倍。CPU 时间包含线程创建、搜索与结果收集;GPU 使用 scan 时间,含清零、同步及回传,不含一次性的上下文创建和显存 分配。
CUDA:固定坐标,在线程内遍历种子
还可以交换两层循环的分工:让一个工作单元固定一个坐标,在线程内部遍历种子。
之前:thread(worldSeed) -> 扫描全部坐标
之后:thread(x, z) -> 遍历全部种子
交换循环后,每个位置的坐标项 只需计算一次;遍历种子时,只改变与它相加的 ,就能省去重复的乘法计算。
同样搜索 65536 个种子、4097×4097 个位置、实心 5×5:
| 版本 | GPU kernel 时间中位数 |
|---|---|
| 每线程一个种子 | 31598.539 ms |
| 固定坐标并复用轴项 | 17600.387 ms |
约加速 1.795 倍。
Outer Sum:相邻线程共享坐标轴项
这里还有一个优化点:相邻位置需要的 X、Z 坐标大量重叠,可以将坐标项按变量拆开:
只依赖 X, 只依赖 Z,把所有 X 项排成一列、Z 项排成一行,每个交点就是对应两项之和。例如取三个 X 坐标和三个 Z 坐标,坐标项表就是:
| X 轴项 \ Z 轴项 | |||
|---|---|---|---|
表中每格都是对应坐标的 ,例如第二行、第三列为 。同一行复用一个 ,同一列复用一个 :只需计算 3 个 X 轴项和 3 个 Z 轴项,就能通 过 9 次加法组成 9 个坐标项。一般地, 个 X 坐标和 个 Z 坐标,只需计算 个轴项,再按需相加得到 个坐标项。
类比 outer product,这种“每个横轴项加上每个纵轴项”就是 outer sum: