Minecraft中,数据包获得指向方块坐标的数学原理及解决方案

前言

本篇文章是我开发思论启迪(giteegithub)数据包时,为了提高自定义方块放置的精度而研究的内容,在此撰写文章作为研究记录。同时也为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 层穿透法

有了前文中的建模以后,接下来开始讨论层穿透法的核心内容。

“层穿透法”是根据算法的行为起名的——视线的路径穿透一层层墙面,每一层都有被穿过的方块。在层穿透法中,将原本二维的计算简化为一维中的计算。这里的“层”可以是水平层,也可以是竖直层。在本文中采用竖直层算法。

![[Pasted Image 20240229222316_736.jpg|400]]

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} xix<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} ziz<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+uziciyi=b+uzicj

综上,交点坐标为

( 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+uzici,b+uzicj,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,便可以得到执行体的朝向向量。

更具体的操作是建立记分板,将这些数据分别存储起来,再进行计算。这里不做进一步探讨。

参考文献

1 Minecraft 原版模组入门教程-第6章《方块设计》
2 坐标 - Minecraft Wiki

  • 48
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值