前書き
PocketMine-MPというオープンソースソフトウェアを用いるとプラグインという形でModのようなものが開発・導入できるようになり、それを用いた画期的なサーバーやプラグインが今、世界中に転がっています。そんなプラグインは、プレイヤーの動きを制御したり、オリジナルなMobを製作したりと、多種多様です。
今回はそのなかでもプレイヤーの動きを制御する座標関係の面にフォーカスして書き綴っていきます。
ここから先はPocketMine-MPのAPIの関数などを出しつつ解説していきます。
前提事項の確認
Minecraftの世界では三次元空間が用意されており、その空間内でプレイヤーが移動したりしています。
首の向きや角度は $x-z$ 及び $xz-y$ 平面に落とし込んだ際の極座標として考えられます。
X, Y, Z
数学などで学ぶ三次元空間は基本的に $z$ が上下の軸を示しますが、Minecraftでは $y$ が上下の軸です。
なお、画像に示した $xz$ 座標の増減は撮影時の状態の例です。
これらの値は以下コードより取得が可能です。
/** @var Player $player */
$location = $player->getLocation();
$x = $location->getX();
$y = $location->getY();
$z = $location->getZ();
Pitch, Yaw
Pitch(ピッチ)と、Yaw(ヨー)の二種類は、プレイヤーの首の垂直方向と水平方向を指した数値です。
このPitchとYawは下限と上限があり、それぞれPitchが -90から90 でYawが 0から360 です。
これらの値は以下コードより取得が可能です。
/** @var Player $player */
$location = $player->getLocation();
$yaw = $location->getYaw();
$pitch = $location->getPitch();
座標とYawの関係
プレイヤーやエンティティが動くワールドでは、上画像に示されているような座標とYawの関係性を持っています。
色分けされた矢印は、プレイヤーの向いている向きをわかりやすくしてます。
下記のリンク先では270が-90とされていますが、意味合いは同じです。
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();
}
deg2rad() ?
一般的な45°などといった角度(degree) をラジアン(radian) に変換するもの$y = -sin(deg2rad($this->location->pitch)); // 1
$xz = cos(deg2rad($this->location->pitch)); // 2
上画像のように $xz-y$ 平面を考えます。
-
Pitchを用いて、sin関数で高さを算出しています。
この高さはエンティティを中心とした単位円を考えるため、下限が -1、 上限が 1 です。
適切な値を求めるために最後に -1倍しています。 -
Pitchを用いて、cos関数より下限が 0、 上限が 1 の距離を算出しています。
負の値を考えないのは、私たち人間もそうですが、体の向きを変えずに真後ろを見ることは不可能だから。
純粋に上の処理の横バージョンだと考えたほうが分かりやすいかもしれません。
エンティティが真横を向いていれば 1、 真下や真上を向いていれば 0 になります。
1 で、なぜ -1倍 するのかというと、Pitchが真上を向いた状態で -90 だからです。
よって最後に符号を反転させる必要があるということです。
Pitchが-40のときの数学的計算
\begin{align}
y&=-sin(-40°)\\
&=-sin(-\frac{2}{9}\pi)\\\
&=-sin(-0.6981317)\\
&\fallingdotseq0.6427876.
\end{align}
\begin{align}
xz &= cos(-40°)\\
&=cos(-\frac{2}{9}\pi)\\\
&= cos(-0.6981317)\\
&\fallingdotseq0.7660444.
\end{align}
$x = -$xz * sin(deg2rad($this->location->yaw)); // 1
$z = $xz * cos(deg2rad($this->location->yaw)); // 2
次は上画像のように $x-z$ 平面を考えます。
- Yawを用いて、sin関数から増減を与えるべき方向を算出します。
得られた増減を -1倍 して一つ前の処理で得られた$xz$を掛け算します。 - Yawを用いて、cos関数から増減を与えるべき方向を算出します。
$z$はYawが0(360)の時に増加し、180の時に減少するので符号はそのままです。
1 で、なぜ -1倍 するのかというと、$x$の位置座標はYawが90 の時に減少し、270の時に増加するからです。
一般的な単位円で考えると、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
);
}
最後に
誤りがありましたら、ぜひ教えてくださると幸いです。
私自身数学に自信がない人間なので細かな表現ミスなどがあるかもしれません。
開発するうえで、上の項で貼った画像がきっと役に立つと思います。
頑張ってください。