見出し画像

【Minecraft】グラップリングフックの物理演算をするコマンド

この記事は Minecraft Command Advent Calendar 2023 12 日目の記事です

↑のグラップリングの作り方解説、第2回です。
今回は、「コマンドで物理演算を実装する方法」について解説していきます。
バージョンはMinecraft JE 1.20.2です。

(↓第1回)


はじめに

前回はHow、どうやってプレイヤーを移動させかを解説しました。 この章ではWhere、どこにプレイヤーを動かすか物理演算する方法を解説していきます。
といっても、スコアボードを用いて三角関数や対数関数、微積分をゴリゴリに実装したりはせず、execute幾何学で動けばいいそれっぽい動きをするコマンドを書いていきます。
いきなりグラップリングフックの物理演算をするのは大変なので、単純なものから徐々に複雑なものを作っていきましょう。

等速直線運動

まず空気抵抗もない、加える力もない、等速直線運動から。
これは慣性をそのまま移動距離にすればいいです。
現在の座標+移動距離の場所にテレポートさせれば移動距離分移動させることができるので、コマンドは以下のようになります。

#エンティティ用コマンド。dataコマンドでNBTを変更しているのでプレイヤーには使えない
#毎tick実行する。エンティティの召喚時にスコアボードに移動速度を入れておく。

#移動前の座標
    data modify storage ckenja.ghook.__temp__: pig.data.Pos set from entity @s Pos
    execute store result score #pos.x ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[0] 10000
    execute store result score #pos.y ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[1] 10000
    execute store result score #pos.z ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[2] 10000

#移動前の座標と慣性の和
    scoreboard players operation #motion.x ckenja.ghook += @s ckenja.ghook.x
    scoreboard players operation #motion.y ckenja.ghook += @s ckenja.ghook.y
    scoreboard players operation #motion.z ckenja.ghook += @s ckenja.ghook.z
    scoreboard players operation #motion.x ckenja.ghook += #pos.x ckenja.ghook
    scoreboard players operation #motion.y ckenja.ghook += #pos.y ckenja.ghook
    scoreboard players operation #motion.z ckenja.ghook += #pos.z ckenja.ghook

#その座標にテレポート
    data modify storage ckenja.ghook.__temp__: marker.merge.Pos set value [0.0,0.0,0.0]
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[0] double 0.0001 run scoreboard players get #motion.x ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[1] double 0.0001 run scoreboard players get #motion.y ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[2] double 0.0001 run scoreboard players get #motion.z ckenja.ghook
    data modify entity @s Pos set from storage ckenja.ghook.__temp__: marker.merge.Pos

# 慣性としてこの移動前の座標と移動後の座標の差を保存する。
    scoreboard players operation @s ckenja.ghook.x = #pos.x ckenja.ghook
    scoreboard players operation @s ckenja.ghook.y = #pos.y ckenja.ghook
    scoreboard players operation @s ckenja.ghook.z = #pos.z ckenja.ghook
    scoreboard players operation @s ckenja.ghook.x -= #motion.x ckenja.ghook
    scoreboard players operation @s ckenja.ghook.y -= #motion.y ckenja.ghook
    scoreboard players operation @s ckenja.ghook.z -= #motion.z ckenja.ghook

等加速度直線運動

重力を考慮して、移動する座標を慣性で少し下にすれば自由落下運動になります。
Minecraftのプレイヤーが受ける下向きの重力は一定で、0.08block/tickの加速度で力が加わっています。結構強く、この値を使うとすぐ落ちて行ってしまいますね。

#移動前の座標
    data modify storage ckenja.ghook.__temp__: pig.data.Pos set from entity @s Pos
    execute store result score #pos.x ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[0] 10000
    execute store result score #pos.y ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[1] 10000
    execute store result score #pos.z ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[2] 10000

#移動前の座標と慣性の和
    scoreboard players operation #motion.x ckenja.ghook = @s ckenja.ghook.x
    scoreboard players operation #motion.y ckenja.ghook = @s ckenja.ghook.y
    scoreboard players operation #motion.z ckenja.ghook = @s ckenja.ghook.z
    scoreboard players operation #motion.x ckenja.ghook += #pos.x ckenja.ghook
    scoreboard players operation #motion.y ckenja.ghook += #pos.y ckenja.ghook
    scoreboard players operation #motion.z ckenja.ghook += #pos.z ckenja.ghook

#重力も足す
    scoreboard players remove #motion.y ckenja.ghook 80

#その座標にテレポート
    data modify storage ckenja.ghook.__temp__: marker.merge.Pos set value [0.0,0.0,0.0]
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[0] double 0.0001 run scoreboard players get #motion.x ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[1] double 0.0001 run scoreboard players get #motion.y ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[2] double 0.0001 run scoreboard players get #motion.z ckenja.ghook
    data modify entity @s Pos set from storage ckenja.ghook.__temp__: marker.merge.Pos

# 慣性としてこの移動前の座標と移動後の座標の差を保存する。
    scoreboard players operation @s ckenja.ghook.x = #motion.x ckenja.ghook
    scoreboard players operation @s ckenja.ghook.y = #motion.y ckenja.ghook
    scoreboard players operation @s ckenja.ghook.z = #motion.z ckenja.ghook
    scoreboard players operation @s ckenja.ghook.x -= #pos.x ckenja.ghook
    scoreboard players operation @s ckenja.ghook.y -= #pos.y ckenja.ghook
    scoreboard players operation @s ckenja.ghook.z -= #pos.z ckenja.ghook

こんな風に力(というか加速度)の分だけ移動させるアプローチで物理演算をしていきます。
ではグラップリングフックには何の力をかければいいんでしょうか?

グラップリングフックを動かすのに考慮する力

次に糸をつけるとどんな天井にフックを刺して、そこから吊り下がって静止している状態でかかっている力を考えてみます。
この図で言うおもり、プレイヤーに注目すると、かかっている力は二つです。

●慣性
●下向きの重力
●糸がおもりを引っ張る張力

糸がおもりを引っ張る張力は作用反作用が面倒でわかりにくいですが、要するにおもりと天井を一定の距離に保つだけの力がかかっています。この場合だと重力を打ち消すように重力と反対向きに同じ強さの力がかかっています。

https://rikamato.com/2017/11/27/3_2/ より

次に、重力で動いている状態のグラップリングフックにかかっている力を考えてみます。
※ここから物理を理解してないので雑になります

この状態でかかる力は、重力を、張力(作用)と復元力に分解して考えられます。
張力(作用)の部分の力は糸がプレイヤーを引っ張る張力(反作用)に打ち消されるので、おもりは復元力の向きに動きます。
でもそのまま復元力で計算した次の座標は、糸の長さの円より外側にあります。なので、単純にこの座標に移動させるとどんどんフックから離れてしまいます。あと計算が面倒です。

というわけで近似します。
プレイヤーより重力の大きさだけ下の座標とフックを結ぶ直線と、フックを中心とした糸の長さが半径の円の交点に動かすことにしました。
なんかすごいインチキをしている気がしますが、重力が十分に小さければ(本来の)張力の向き・大きさとこの(疑似)張力の向き・大きさはほぼ同じになるので、この近似が成り立ちます。

実際の実装

実際に実装する際には慣性を考慮したり、WASDキーで操作したりしたかったので、重力に加えてそれらの力も加えて、そこからexecuteコマンドで交点を調べてそこにテレポートしています。
交点を求める方法は図で説明してみました。

コマンドに書き起こすとこんな感じになります。

#> ckenja.ghook:feature/swing/tick

#慣性
	scoreboard players operation #intertia.x ckenja.ghook = @s ckenja.ghook.x
	scoreboard players operation #intertia.y ckenja.ghook = @s ckenja.ghook.y
	scoreboard players operation #intertia.z ckenja.ghook = @s ckenja.ghook.z

#空気抵抗として慣性を0.98倍する。
		scoreboard players operation $intertia.x ckenja.ghook *= #98 ckenja.ghook
		scoreboard players operation $intertia.y ckenja.ghook *= #98 ckenja.ghook
		scoreboard players operation $intertia.z ckenja.ghook *= #98 ckenja.ghook
		scoreboard players operation $intertia.x ckenja.ghook /= #100 ckenja.ghook
		scoreboard players operation $intertia.y ckenja.ghook /= #100 ckenja.ghook
		scoreboard players operation $intertia.z ckenja.ghook /= #100 ckenja.ghook

#追加の速度(プレイヤーのMotionから取ると重力とキー操作の両方が取れる)
    execute store result score #motion.x ckenja.ghook run data get storage ckenja.ghook.__temp__: player.data.Motion[0] 5000
    execute store result score #motion.y ckenja.ghook run data get storage ckenja.ghook.__temp__: player.data.Motion[1] 5000
    execute store result score #motion.z ckenja.ghook run data get storage ckenja.ghook.__temp__: player.data.Motion[2] 5000

#現在の座標
    data modify storage ckenja.ghook.__temp__: pig.data.Pos set from entity @s Pos
    execute store result score #pos.x ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[0] 10000
    execute store result score #pos.y ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[1] 10000
    execute store result score #pos.z ckenja.ghook run data get storage ckenja.ghook.__temp__: pig.data.Pos[2] 10000

#現在の座標と慣性と追加の速度の和
    scoreboard players operation #motion.x ckenja.ghook += #intertia.x ckenja.ghook
    scoreboard players operation #motion.y ckenja.ghook += #intertia.y ckenja.ghook
    scoreboard players operation #motion.z ckenja.ghook += #intertia.z ckenja.ghook
    scoreboard players operation #motion.x ckenja.ghook += #pos.x ckenja.ghook
    scoreboard players operation #motion.y ckenja.ghook += #pos.y ckenja.ghook
    scoreboard players operation #motion.z ckenja.ghook += #pos.z ckenja.ghook

#その座標にマーカーを出し、フックからロープ距離分マーカー方向に進んで、その場所の座標にテレポート
    data modify storage ckenja.ghook.__temp__: marker.merge.Pos set value [0.0,0.0,0.0]
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[0] double 0.0001 run scoreboard players get #motion.x ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[1] double 0.0001 run scoreboard players get #motion.y ckenja.ghook
    execute store result storage ckenja.ghook.__temp__: marker.merge.Pos[2] double 0.0001 run scoreboard players get #motion.z ckenja.ghook
		#糸の長さを設定
		scoreboard players operation #temp.long ckenja.ghook = @s ckenja.ghook.l
    summon marker ~ ~ ~ {Tags:["ckenja.ghook.marker"]}
    execute as @e[type=marker,tag=ckenja.ghook.marker] run function ckenja.ghook:feature/swing/marker

# 慣性としてこのtickの変位を計算して記録する(省略)
#> ckenja.ghook:feature/swing/marker

#PosのNBTに計算値を代入して計算した位置に移動
data modify entity @s Pos set from storage ckenja.ghook.__temp__: marker.merge.Pos
#ループで糸の長さだけマーカーの方向に移動して球上の位置を出す
execute positioned as @s as @e[type=bat,tag=ckenja.ghook.hook,distance=..120] if score @s ckenja.ghook = #temp.id ckenja.ghook facing entity @s feet positioned as @s positioned ^ ^ ^-0.125 as @e[type=marker,tag=ckenja.ghook.marker,distance=..120] run function ckenja.ghook:feature/swing/loop
kill @s
#> ckenja.ghook:feature/swing/loop
scoreboard players remove #temp.long ckenja.ghook 125
execute if score #temp.long ckenja.ghook ..0 run tp @p ~ ~ ~
execute unless score #temp.long ckenja.ghook ..0 positioned ^ ^ ^-0.125 run function ckenja.ghook:feature/swing/loop

終わりに

これで今日の記事はおしまいです。グラップリングフックの移動先を計算する機構部分解説したかったので。サクッと記事終わったけどあれ考えるの半年はかかってるぜ!?
ところで魅せコマに出したグラップリングフックの動画、ブロックにぶつかるシーンがありませんでしたよね…?
さっきの等加速度直線運動の例もブロックに落ちていきましたし…
そう、ブロックへの衝突処理はすごく難しく、グラップリングフックは2年放置されていたのです!
では次回(12/16)、「グラップリングフックの衝突処理編」でお会いしましょう。衝突処理が書けたらな!!!

参考リンク

  • グラップリングフックのソースコード: https://github.com/CKenJa/grappling_hook/

  • Entity - Minecraft wiki(英語)
    https://minecraft.wiki/w/Entity#Motion_of_entities
    エンティティの重力やMotionの仕様について詳細に記述されています。

  • Deltaの作者、BigPapi氏がグラップリングフック作ってた…すげぇ数式ゴリゴリで計算してる…
    https://github.com/BigPapi13/Grappling-Hook

  • ところでこれは宣伝なんですけど、Mimecraftの世界で棒人間が戦うアニメーションが作っている方の最新作、Animation vs. Physicsが面白かったので貼っておきますね。グラップリングフック(?)で超加速するシーンもありましたし。いつかシェーダーとかでブラックホールとか量子の世界の物理演算してみてぇ~



この記事が気に入ったらサポートをしてみませんか?