前言
本篇文章是我开发思论启迪(gitee,github)数据包时,为了提高自定义方块放置的精度而研究的内容,在此撰写文章作为研究记录。同时也为MC生态圈贡献一篇技术性资料。
利用实体视线模拟的方法[1],可以十分简单的得到指向方块的坐标,然而这种做法至少有两个弊端:
- 精度不高。在视线模拟算法中,辅助实体会一步步把自己朝着玩家朝向的方向传送过去,为了提高精度,不遗漏方块,每次传送的距离要尽可能小。然而,传送距离越小,所要递归(MC中没有循环语句)的次数就越多。如果每次传送 0.1 0.1 0.1 个方块距离,那么要获取 100 100 100 格外的方块坐标,至少需要递归 1000 1000 1000 次;
- 在获取角落的方块坐标时,由于精度问题,会直接穿过去,获得角落后面的方块的坐标。这在视线模拟算法中,只能通过提高精度的办法降低这种情况发生的可能性。要想完全避免这种情况,编程起来十分繁琐。
为了解决以上问题,本文提出“层穿透法”,旨在用数学方法解决上面的两个弊端。
为了增大受众,本文尽可能避免专业的数学词汇,只要求读者了解空间直角坐标系与向量。
全文共三个章节:
- 在第一章中,将复述问题,以便后文中更好的阅读;
- 在第二章中,提出一种基于空间几何的层穿透法指向方块求解算法;
- 在第三章中,实践第二章中提出的算法,给出在二维情况下,具体的实现代码与其相关逻辑的解释。
一、分析
玩家准星的起点和玩家的坐标是不同的。前者大概从玩家脚底出往上 1.35 1.35 1.35 格高(该数据并未经过精确验证,实际编程时可做调整)的地方,后者是玩家的脚底。因此在实际计算时要特别区分。
指向方块和选中方块是不同的,在MC Java版中,玩家只能选中五格以内(调研确定一下)的方块,而指向方块是比五格更远的。
为了获得玩家选中方块的坐标,主要需要两个参数:玩家的朝向向量与视线初始点。
有了这两个参数以后,就可以确定视线所在的直线了,此时就可以开始计算玩家的选中方块的坐标。
二、数学原理
2.1 二维平面下的方块世界
在进一步分析之前, 首先需要对MC建立模型。MC中的方块坐标全部都是整点,并且从连续的坐标系上看,方块坐标对应的位置都在方块的某个角上。
因此,可以建立如下二维模型:
虽然方块的坐标全都是整点,但是,玩家的坐标却是一个小数,并且玩家的坐标和玩家的视线起点也是不相同的,因此在实际计算时还有进行对应的转换。在下文中,我们如下的符号约定:
符号 | 意义 |
---|---|
l l l | 表示玩家的视线,本质是一条射线 |
P P P 或 ( a , b ) (a,b) (a,b) 或 ( a , b , c ) (a,b,c) (a,b,c) | 表示玩家视线的起点坐标 |
s ⃗ \vec{s} s 或 ( m , n ) (m,n) (m,n) 或 ( m , n , u ) (m,n,u) (m,n,u) | 表示玩家视线的方向向量,并且还是一个单位向量 |
k k k | 表示玩家视线的斜率 |
如何从数学的角度上理解视线经过了方块?从前面的定义我们,可以得到:
2.2 层穿透法
有了前文中的建模以后,接下来开始讨论层穿透法的核心内容。
“层穿透法”是根据算法的行为起名的——视线的路径穿透一层层墙面,每一层都有被穿过的方块。在层穿透法中,将原本二维的计算简化为一维中的计算。这里的“层”可以是水平层,也可以是竖直层。在本文中采用竖直层算法。
令 x 0 = ⌊ a ⌋ , x i = x 0 + i x_0 = \lfloor a \rfloor, x_i = x_0 + i x0=⌊a⌋,xi=x0+i,则第 i i i 层所表示的区域为:
x i ≤ x < x i + 1 x_i \le x < x_{i+1} xi≤x<xi+1
很容易发现,对于某一层来说,穿过的方块要么从上往下依次穿过,要么从下往上依次穿过,因此,我们只需要知道在某一层中,视线穿过的最上面那个方块和最下面那个方块就可以得到,视线在该层中穿过的全部方块坐标了。为了方便描述,本文把视线穿过的第一个方块称为“头方块”,穿过的最后一个方块称为“尾方块”。
有了上面的讨论,我们有了层穿透法。
2.3 投影——从三维到二维
二维情况很好解决,那么该如何解决三维空间下的情况呢?答案仍旧是一层一层穿透,只不过需要降维两次。不过,这样思考实际上十分繁琐,因为三维的坐标还不能直接应用前文中的层穿透法,有什么法子直接用呢?
投影可以解决这个问题。
对于一条视线,我们做出它在 x O y xOy xOy 平面上的投影:
这时候有一个十分有用的规律:投影经过的方块的 x x x, y y y 坐标与视线经过的方块的 x x x, y y y 坐标相同!不同的只是 z z z 坐标。
这就意味着,我们在一层一层计算时,可以把它当成是二维的层穿透法计算既可以,只不过在计算结束以后,我们还需要判断方块的 z z z 坐标。
2.4 三维空间下的层穿透法
接下来可以正式讨论三维空间下的层穿透法了。
令 z 0 = ⌊ c ⌋ z_0 = \lfloor c \rfloor z0=⌊c⌋, z i = z 0 + i z_i = z_0 + i zi=z0+i,则第 i i i 层所包含的区域为
z i ≤ z < z i + 1 z_i \le z < z_{i+1} zi≤z<zi+1
三、解决方案
在本文中,分别给出 Python 语言和 mcfunction 语言两种代码示例,并给出相应的说明。
3.1 Python
在Python语言中,主要分别三个主要函数:
draw_squares(ax, squares)
:绘制一个长度为 1 1 1 的方块;draw_line(ax, P, s, length)
:绘制视线,length
参数限制计算你的长度;impale_layer(P, s, length)
:计算所经过的方块,length
参数限制计算的长度。
前两个函数均由 ChatGPT3.5
生成。
这里主要解释出 impale_layer
中的算法:
impale_layer
函数代码如下:
def impale_layer(P, s, length):
"""
层穿透法,输入初始点 P 和方向向量 s,则可以得到视线所穿过的方块
:param P: 初始点
:param s: 方向向量
:param length: 计算的长度
:return: 所穿过的方块的坐标,是一个list
""" # 数据初始化
a,b = P[0], P[1]
m,n = s[0], s[1]
x_0 = math.floor(a)
y_0 = math.floor(b)
# 求出视线与每一层边界的交点
x = lambda i: x_0 + i
y = lambda x: math.floor(b + n/m * (x - a))
# 开始计算
result = []
for i in range(length):
# 先求出第 i 层的上下方块的坐标
y_up = y(x(i+1))
y_down = y_0 if i == 0 else y(x(i))
# 从下方块开始,朝着上方块遍历,得到每个视线经过的坐标
result += [(x(i) , yi) for yi in range(y_down, y_up+1)]
return result
源代码参见 Gitee
3.2 mcfunction
施工中……(mc的命令写起来实在是太麻烦了!Debug巨繁琐!)
附录
A 求出直线与水平面的交点
已知一条直线 l l l 经过 ( a , b , c ) (a,b,c) (a,b,c) ,且方向向量为 m ⃗ = ( i , j , u ) \vec{m} = (i, j, u) m=(i,j,u) ,那么直线 l l l 与平面 z = z i z = z_i z=zi 的交点满足
( a , b , c ) + w ( i , j , u ) = ( x i , y i , z i ) (a,b,c) + w(i,j,u) = (x_i, y_i, z_i) (a,b,c)+w(i,j,u)=(xi,yi,zi)
其中 w w w 为参数, ( x i , y i , z i ) (x_i, y_i, z_i) (xi,yi,zi) 为交点坐标。
解上述方程,有
{ x i = a + z i − c u ⋅ i y i = b + z i − c u ⋅ j \begin{cases} x_i = a + \frac{z_i - c}{u} \cdot i \\ y_i = b + \frac{z_i - c}{u} \cdot j \\ \end{cases} {xi=a+uzi−c⋅iyi=b+uzi−c⋅j
综上,交点坐标为
( a + z i − c u ⋅ i , b + z i − c u ⋅ j , z i ) (a + \frac{z_i - c}{u} \cdot i,\quad b + \frac{z_i - c}{u} \cdot j,\quad z_i) (a+uzi−c⋅i,b+uzi−c⋅j,zi)
B 向下取整
⌊ a ⌋ \lfloor a \rfloor ⌊a⌋ 是向下取整的意思。例如 ⌊ 1.3 ⌋ = 1 \lfloor 1.3 \rfloor = 1 ⌊1.3⌋=1.
- 不能四舍五入,因此: ⌊ 1.9999 ⌋ = 1 \lfloor 1.9999 \rfloor = 1 ⌊1.9999⌋=1
- 要注意,是向下取整,不是向零取整,因此对于负数而言: ⌊ − 1.5 ⌋ = − 2 \lfloor -1.5 \rfloor = -2 ⌊−1.5⌋=−2.
C 玩家的朝向向量
在MC中,玩家的朝向是由标签 Rotation
标记的,但改标签是一个由角度构成的数据,在本文中不能直接使用。
为了获得玩家的朝向向量,一种办法是通过 Rotation
标签提供的角度数据,通过三角函数关系直接计算得到,这对于MC来说十分繁琐,因为MC中没有直接的三角函数计算方法,只能手动模拟泰勒展开计算。
另一个更加有效的办法是关于局部坐标[2]的。先使用命令
summon ^-1 ^-1 ^-1 minecraft:marker
此时,MC会以玩家头部的朝向为参考,在
(
−
1
,
−
1
,
−
1
)
(-1, -1, -1)
(−1,−1,−1) 中生成一个实体。这时候用玩家的 Pos
减去 marker 的 Pos
,便可以得到执行体的朝向向量。
更具体的操作是建立记分板,将这些数据分别存储起来,再进行计算。这里不做进一步探讨。