座標と向きについて

引言

 

通过使用名为PocketMine-MP的开源软件,可以以插件的形式开发和引入类似于Mod的内容,目前全世界正涌现出许多创新的服务器和插件。这些插件可以控制玩家的行动,创建独特的Mob等,种类繁多。

本文将着重介绍控制玩家行动方面的坐标相关内容。

在这里之后,我们会介绍PocketMine-MP的API函数并进行解释。

请用本地汉语进行改写,只需要一个选项:

There are many beautiful flowers in the garden.

确认前提条件

在Minecraft的世界中,有一个三维空间供玩家移动和探索。
头部的朝向和角度可以以极坐标形式投影到$x-z$平面和$xz-y$平面上来考虑。

X, Y, Z (X,Y,Z)

qiita1.png
/** @var Player $player */
$location = $player->getLocation();
$x = $location->getX();
$y = $location->getY();
$z = $location->getZ();

俯仰, 偏航

qiita2x.png
/** @var Player $player */
$location = $player->getLocation();
$yaw = $location->getYaw();
$pitch = $location->getPitch();

座標和偏航角的关系。

qiita3x.png

 

請以中文將以下內容進行同義轉述,只需要提供一種選擇:

“Can you help me with my homework? I’m really struggling with it.”

关于DirectionVector

前置きが長くなりましたが、今回紹介するのはこのDirectionVectorについてです。
読んで字の如く方向ベクトルを指すのですが、これを使ったプラグインの多くが
「向いている方向にTNT飛ばすよ!」だったり
「向いている方向に加速するよ!」だったりします。
要するに、ただモーションを付与しているだけのものが多いんですね。

/** @var Player $player */
$directionVector = $player->getDirectionVector();
$player->setMotion($directionVector->multiply(1.5));

で、この方向ベクトルを取得する関数がめっちゃ便利じゃないかと思ったので内部処理を見てみました。
以下がEntityクラスに設けられている方向ベクトル取得の関数です。
三角関数が出てきて頭がおかしくなりそうですが、詳しく見ていきましょう。

public function getDirectionVector() : Vector3{
    $y = -sin(deg2rad($this->location->pitch));
    $xz = cos(deg2rad($this->location->pitch));
    $x = -$xz * sin(deg2rad($this->location->yaw));
    $z = $xz * cos(deg2rad($this->location->yaw));

    return (new Vector3($x, $y, $z))->normalize();
}
将一般的的角度(degree),如45°,转换为弧度(radian)的函数“deg2rad()”。
qiita4.png
$y = -sin(deg2rad($this->location->pitch)); // 1
$xz = cos(deg2rad($this->location->pitch)); // 2

我们考虑如上所示的$xz-y$平面。

    1. 使用Pitch计算高度的方法是通过sin函数。这个高度是基于以实体为中心的单位圆,下限为-1,上限为1。

 

    1. 为了得到正确的值,最后乘以了-1。

使用Pitch计算距离的方法是通过cos函数,下限为0,上限为1。
不考虑负值是因为无论是人类还是动物,不可能不改变身体方向而看到身后。
可以将其看作是处理上述过程的水平版本。
如果实体直接朝向一侧,那么距离为1,如果朝向正下方或者正上方,距离为0。

所以为什么要乘以-1呢,原因是Pitch在朝上的情况下为-90。因此需要最后反转符号。
Pitch为-40时的数学计算:
\begin{align}
y &= -sin(-40°)\\
&= -sin(-\frac{2}{9}\pi)\\\
&= -sin(-0.6981317)\\
&\approx 0.6427876.
\end{align}\begin{align}
xz &= cos(-40°)\\
&= cos(-\frac{2}{9}\pi)\\\
&= cos(-0.6981317)\\
&\approx 0.7660444.
\end{align}

qiita5.png
$x = -$xz * sin(deg2rad($this->location->yaw)); // 1
$z = $xz * cos(deg2rad($this->location->yaw)); // 2

接下来我们考虑$x-z$平面,如上图所示。

    1. 使用Yaw计算应该给出的sin函数的增减方向。

 

    1. 将得到的增减乘以-1,并与前一个操作得到的$xz$相乘。

 

    1. 使用Yaw计算应该给出的cos函数的增减方向。

 

    当Yaw为0(360)时,$z$增加,而当Yaw为180时,$z$减少,因此符号保持不变。
所以,为什么要乘以-1呢?因为当Yaw为90时,x的位置坐标会减少,当Yaw为270时,x的位置坐标会增加。以一般的单位圆为例,当Yaw为270时,纵轴为负值,因此需要将最终的数值乘以-1。
当 Yaw 为60时的数学计算:\begin{align}
x&=-xz \times sin(60°)\\
&=-xz \times sin(\frac{\pi}{3})\\\
&=-xz \times sin(1.0471976)\\
&\fallingdotseq -xz \times 0.8660254.
\end{align}

\begin{align}
z&=xz \times cos(60°)\\
&=xz \times cos(\frac{\pi}{3})\\\
&=xz \times cos(1.0471976)\\
&\fallingdotseq xz \times 0.4999999.
\end{align}

请为以下句子提供中文本地化的释义(只需要一种选项):

return (new Vector3($x, $y, $z))->normalize();

最终将该向量规范化为单位向量,并返回。
有了这个过程,我们可以通过任意倍率相乘来实现像威力之类的效果。

除了运动之外的用途范例

さて、この方向ベクトルはそもそもモーションに使いやすいようにAPIの関数として存在していると思いますが、見方を変えてみれば 「方向が分かっている増減1以下のベクトル」 です。
つまりこの方向ベクトルを座標としてとりだして、プレイヤーの座標に加えると向いている方向へちょっと動くわけです。
これを用いることで例えばプレイヤーの背中の座標がどんな状態でも取得できますし、プレイヤーの前方をどんな状態でも取得できるってことになります。
Hypixelとよばれる世界最大級のMinecraftサーバーなどにあるような羽根のパーティクルもこれで実装できそうですね。

样本

以下是我在我的服务器上实现水上行走附魔的插件代码。
由于可以始终获取到前方的数据,所以可以实现无明显延迟且流畅的水上行走。

public function onMoveOnWater(PlayerMoveEvent $event) : void
{
    $player = $event->getPlayer();
    $world = $player->getWorld();
    $directionVector = $player->getDirectionVector();
    $addV = $directionVector->multiply(0.5); // 0.5倍して進行方向の距離を半分にする
    $nextVector = $player->getLocation()->addVector($addV); // プレイヤーの位置に加算
    $nextPosition = Position::fromObject($nextVector, $world);
    $underPosition = $nextPosition->subtract(0, 0.2, 0); // 進行先足元の座標を取得
    $underBlock = $world->getBlock($underPosition); // 進行方向足元のブロックを取得
    if (!($underBlock instanceof Water)) return; // 足元が水じゃなかったら処理終了

    $world->setBlock($underPosition, VanillaBlocks::ICE()); // 氷を設置

    /** @var PluginBase $pluginBase */
    $pluginBase->getScheduler()->scheduleDelayedTask(
        new ClosureTask(function() use($world, $underPosition, $underBlock) {
            $world->setBlock($underPosition, $underBlock); // 2秒後に氷を溶かす
        }),
        2 * 20
    );
}

最后

如果有错误的地方,请务必告诉我,我将不胜感激。
由于我个人在数学上没有自信,可能会出现一些细微的表达错误。
在开发过程中,我相信上面附上的图片一定会很有帮助。
请加油!

广告
将在 10 秒后关闭
bannerAds