ディスプレイでレーザービームを撃とう

# はじめに
ディスプレイエンティティって便利ですよね。
モデルも大きさも自由に変えられるし。

今回はそんなディスプレイを使って、こんなレーザービームを作ってみましょう。
![Replay 2023-12-10 16-24-56.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/bc5caa10-5fc8-c0bb-72db-ceff46f40d79.gif)

:::note info
この記事は自分の試行錯誤の過程を辿りながら解説していきます。
結論だけ知りたい方は [#上手くいく方法](#上手くいく方法) から見てください。
:::

# レーザー召喚
赤色の色付きガラスを細長く引き伸ばすとレーザーっぽく見えると思うので今回はそのモデルを使います。
このコマンドで`laser`というタグが付いた`赤色の色付きガラス`のブロックディスプレイが召喚されます。
```
summon minecraft:block_display ~ ~ ~ {Tags:["laser"],block_state:{Name:"red_stained_glass"}}
```
![2023-12-10_18.06.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/9a0d21d8-fc8f-5c6c-d326-0ea4063bfb36.png)

続いて、このコマンドでディスプレイの`scale`を変更して細長くしましょう。
```
data modify entity @e[tag=laser,limit=1] transformation.scale set value [0.1f,0.1f,64f]
```

![2023-12-10_18.07.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/a629b43c-a4ab-bcf1-77a0-ff1987365bd6.png)
レーザーっぽいですよね？
もっと凝るなら`item_display`でカスタムモデルを表示しても良いです。

# エンティティの向きで方向を合わせてみる
ディスプレイは普通のエンティティと同じ様にtpコマンドで位置や向きを変えることができます。
ディスプレイをプレイヤーの頭の位置と向きにtpさせてみましょう。
```
execute as @p at @s anchored eyes run tp @e[tag=laser] ^ ^ ^ ~ ~
```
![2023-12-10_18.12.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/c21e8c15-8ca0-31c5-1261-1a527672c7fa.png)
額からレーザーが出てるみたいになりましたね。

![2023-12-10_18.15.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/48aaa050-ed48-d756-05f5-a3b40c68c949.png)
試しに遠くのアマスタを狙いながらこのコマンドを実行してみると…

![2023-12-10_18.16.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/083beab6-b4f3-395b-e9c0-9b06aef0dd73.png)
あれ？外れました。もう一度…

![2023-12-10_18.17.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/c6ca61f2-e78e-fe61-87e8-21ad88c45a29.png)
…当たりません。

これは私がクソエイムだからではないのです。
その証拠に、ディスプレイの位置と向きから再帰ファンクションでパーティクルの線を表示してみると、ちゃんとアマスタに当たっています。
しかし、ディスプレイの見た目はそれから大きくズレてしまっています。
![2023-12-10_18.38.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/3d190ef3-a1ed-cb79-b9f4-4b1f20812f8c.png)

実はこの現象には、ディスプレイだけではないマイクラのエンティティ全般のとある仕様が関わっています。
ざっくり言うと、エンティティの表示上の角度は一周256分割しかされておらず、`360°/256≒1.4°`程度の分解能しかないのです。
（もう少し踏み入った話をすると、シングルプレイであってもマイクラのアプリケーションは内部的にサーバとクライアントに分かれて動作しており、サーバからクライアントへ通信する角度データが256通りの表現しかできないbyte型で表されていることが原因です。[^1]）

エンティティの向きで合わせる方法は上手くいかなさそうです。

# transformationで方向を合わせてみる
ディスプレイのNBTの`transformation`中の、モデルの回転を表す`left_rotation`と`right_rotation`ならエンティティの向きよりも正確に回転をさせられます。
これを利用して向きを合わせる方法を試してみましょう。

その前に一度、ディスプレイを召喚しなおしてください。
```
kill @e[tag=laser]

summon minecraft:block_display ~ ~ ~ {Tags:["laser"],block_state:{Name:"red_stained_glass"}}
```

`left_rotation`と`right_rotation`は、`軸角形式`と`四元数形式`の二種類の形式で回転を表すことができますが、今回は扱いやすい`軸角形式`の方をメインに扱います。
`軸角形式`は`{angle:0.785,axis:[0f,1f,0f]}`の様な形式で、`axis`のベクトルを回転軸とした`angle`の角度の回転を表します。
`axis`のベクトルは長さが1（単位ベクトル）である必要があること、`angle`の角度は`度数法`ではなく`ラジアン`であること、`軸角形式`で書き込んでも`四元数形式`に自動的に変換されて保存されること、などに注意してください。

ところで、プレイヤーを含むエンティティの向きは`Rotation:[45f,-20f]`の様に横回転と縦回転の二つのパラメータで回転を表す`オイラー角`の形式で保存されていますが、`オイラー角`を`軸角形式`に変換するためには本来計算が必要になります。
幸い`transformation`には`left_rotation`と`right_rotation`の二つの回転があるので、それぞれに横回転と縦回転を代入すれば計算を避けることができそうです。
`left_rotation.axis`を横回転の回転軸とし、`left_rotation.angle`にプレイヤーの向きの横角度`Rotation[0]`を代入、`right_rotation.axis`を縦回転の回転軸とし、`right_rotation.angle`にプレイヤーの向きの縦角度`Rotation[1]`を代入しましょう。
```
# 回転軸設定
data modify storage laser: left_rotation set value {axis:[0f,-1f,0f]}
data modify storage laser: right_rotation set value {axis:[1f,0f,0f]}

# 角度代入
execute store result storage laser: left_rotation.angle float 0.000001745 run data get entity @p Rotation[0] 10000
execute store result storage laser: right_rotation.angle float 0.000001745 run data get entity @p Rotation[1] 10000

# ストレージからディスプレイにコピー
execute as @e[tag=laser] run data modify entity @s transformation.left_rotation set from storage laser: left_rotation
execute as @e[tag=laser] run data modify entity @s transformation.right_rotation set from storage laser: right_rotation
```
`軸角形式`では`angle`と`axis`を同時に書き込む必要があるため、一度ストレージ上にデータを作ってからディスプレイにコピーをしています。
`store result`時の倍率`0.000001745`は、度数法からラジアンに変換するため掛ける`円周率/180≒0.01745`という数値を、`data get`時の倍率`10000`で割った値です。

![Replay 2023-12-10 19-36-33.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/5705fdf7-1dbb-b83a-3e32-1339fea0fa99.gif)
上のコマンドを常時実行すると、プレイヤーの頭の向きに綺麗に追従させることができました。

あとは`scale`を変更してレーザーらしく細長くするだけで…？
![2023-12-10_20.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/ce75f339-b894-602a-bf4d-175970babf2a.png)
あれ？形が変になってしまいました。

原因は`right_rotation`を使ってしまったことにあります。
そもそも何故`transformation`に`left_rotation`と`right_rotation`の二種類の回転パラメータが用意されているのかというと、`scale`に対する影響に違いがあるためなのです。
`left_rotation`は`scale`の変形の仕方に影響を及ぼしませんが、`right_rotation`は`scale`の変形の仕方に影響を及ぼします。
そのおかげで`せん断`という斜めに押しつぶす様な変形が可能になるのですが、今回は変形してしまうと困ります。

どうしたものでしょうか…？

# 上手くいく方法
とうとう本命の上手くいく方法です。

今までの方法では、初期状態で`Rotation:[0f,0f]`方向に伸びているディスプレイに、オイラー角に合わせて`縦横二回の回転操作`を行うことで方向を合わせていました。
発想を転換して、初期状態で真上方向に伸びたディスプレイを横に倒す`一回の回転操作`で方向を合わせてみましょう。

まず、このコマンドでディスプレイを真上方向に細長く変形しましょう。
```
data modify entity @e[tag=laser,limit=1] transformation.scale set value [0.1f,64f,0.1f]
```

プレイヤーの向きの真横方向が倒す回転軸となるので、下のコマンドで計算用マーカーがtpする座標から回転軸ベクトルが分かります。
```
# 計算用マーカー召喚
summon marker ~ ~ ~ {Tags:["laser_marker"]}

# 原点からプレイヤーの向きの真横1ブロックへマーカーを移動
execute rotated as @p positioned 0.0 0.0 0.0 run tp @e[tag=laser_marker,limit=1] ^1 ^ ^ ~ ~

# マーカーの座標をストレージに代入
data modify storage laser: left_rotation.axis set from entity @e[tag=laser_marker,limit=1] Pos
```

倒す角度は、プレイヤーの縦角度が-90°の時は0°、縦角度が0°の時は90°、縦角度が90°の時は180°であり、縦角度に`+90`すれば良いことが分かります。
スコアボード上で`+90`の計算をしても良いのですが、今回はせっかくなのでスコアボードを使わずに`execute幾何学`を使って計算してみます。
[`縦角度から横角度への変換テクニック`](https://qiita.com/komaramune/items/2d0532c44455b0e8ab15#%E7%B8%A6%E8%A7%92%E5%BA%A6-%E6%A8%AA%E8%A7%92%E5%BA%A6-%E5%A4%89%E6%8F%9B)を利用して`マーカーの縦角度[-90~90]`を`マーカーの横角度[0~180]`に変換し、その横角度を`left_rotation.angle`に代入しましょう。
```
# execute幾何学で、縦角度[-90~90]⇒横角度[0~180]に変換
execute rotated as @p rotated -90 ~ positioned 0.0 0.0 0.0 positioned ^ ^ ^1 rotated ~90 ~ positioned ^ ^1 ^ facing 0.0 ~ 0.0 run tp @e[tag=laser_marker,limit=1] 0.0 0.0 0.0 ~ ~

# マーカーの横角度をストレージに代入
execute store result storage laser: left_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000
```

ストレージ上のデータをディスプレイにコピーしましょう。
```
# ストレージからディスプレイに書き込み
execute as @e[tag=laser] run data modify entity @s transformation.left_rotation set from storage laser: left_rotation
```

忘れずに計算用マーカーの削除もしておきます。
```
# 計算用マーカー削除
kill @e[tag=laser_marker]
```
ここまでのコマンドを実行してみると
![Replay 2023-12-14 21-27-53.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/02e69ebe-1a6a-5fd8-a624-3955e7c64660.gif)
お！無事にレーザーが形を保ったままプレイヤーの頭の向きに追従しています！

しかし、よく見ると伸びている方向を軸に余計な回転をしてしまっているので、これも直してしまいましょう。

この様に`right_rotation`を利用することで回転を打ち消すことができます。
```
# マーカーの向きをプレイヤーと同じ向きに
execute rotated as @p positioned 0.0 0.0 0.0 run tp @e[tag=laser_marker,limit=1] ~ ~ ~ ~ ~

# マーカーの横角度をストレージに代入
execute store result storage laser: right_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000

# 回転軸設定
data modify storage laser: right_rotation set value {axis:[0f,-1f,0f]}

# ストレージからディスプレイに書き込み
execute as @e[tag=laser] run data modify entity @s transformation.right_rotation set from storage laser: right_rotation
```
あれ？`right_rotation`を使うと変形しちゃうんじゃなかったの？と思われたかもしれません。
今回の`right_rotation.axis`は`[0f,-1f,0f]`で`Y軸`方向を回転軸としているため、ブロックの頂点は`X軸`と`Z軸`方向にのみ移動します。
そして`scale`は`[0.1f,64f,0.1f]`で`X`と`Z`の倍率が同じです。
回転でXとZ軸方向に動きますが、XとZ軸方向の倍率が同じであるため、変形しないのです。

ここまでのコマンドを整理しつつ纏めるとこのようになります。
```
# 計算用マーカー召喚
summon marker ~ ~ ~ {Tags:["laser_marker"]}

# 原点からプレイヤーの向きの真横1ブロックへマーカーを移動 & 同じ向きに
execute rotated as @p positioned 0.0 0.0 0.0 run tp @e[tag=laser_marker,limit=1] ^1 ^ ^ ~ ~

# マーカーの座標をストレージに代入
data modify storage laser: left_rotation.axis set from entity @e[tag=laser_marker,limit=1] Pos

# マーカーの横角度をストレージに代入
execute store result storage laser: right_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000

# 回転軸設定
data modify storage laser: right_rotation.axis set value [0f,-1f,0f]

# execute幾何学で、縦角度[-90~90]⇒横角度[0~180]に変換
execute rotated as @p rotated -90 ~ positioned 0.0 0.0 0.0 positioned ^ ^ ^1 rotated ~90 ~ positioned ^ ^1 ^ facing 0.0 ~ 0.0 run tp @e[tag=laser_marker,limit=1] 0.0 0.0 0.0 ~ ~

# マーカーの横角度をストレージに代入
execute store result storage laser: left_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000

# ストレージからディスプレイに書き込み
execute as @e[tag=laser] run data modify entity @s transformation.left_rotation set from storage laser: left_rotation
execute as @e[tag=laser] run data modify entity @s transformation.right_rotation set from storage laser: right_rotation

# 計算用マーカー削除
kill @e[tag=laser_marker]
```

![Replay 2023-12-14 21-51-39.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/1bef2898-e0aa-a847-62ad-7c21b69f2849.gif)
これで完璧になりました。

# データパックとして実装する
せっかくなのでデータパックとしての体裁を整えてみます。
スニークすると目からビームが出る仕様にしました。

`tick.json`に登録された毎tick実行される`tick.mcfunction`です。
[`executeテクニックのスニーク検知`](https://qiita.com/komaramune/items/2d0532c44455b0e8ab15#anchored-eyes%E5%BC%8F%E3%82%B9%E3%83%8B%E3%83%BC%E3%82%AF%E7%8A%B6%E6%85%8B%E6%A4%9C%E7%9F%A5%E4%BC%8F%E3%81%9B%E6%B3%B3%E3%81%8E%E7%8A%B6%E6%85%8B%E6%A4%9C%E7%9F%A5)を利用しています。
```mcfunction:tick.mcfunction
# 計算用マーカー召喚
summon marker ~ ~ ~ {Tags:["laser_marker"]}

# スニークしているプレイヤーにファンクションを実行
execute as @a at @s anchored eyes positioned ^ ^ ^ positioned ~ ~-1.27 ~ if entity @s[distance=..0.0001] at @s anchored eyes positioned ^0.05 ^0.15 ^ positioned ~ ~-0.2 ~ run function laser:every_player

# 計算用マーカー削除
kill @e[tag=laser_marker]

# 処理されなかったディスプレイをkill
kill @e[tag=laser,tag=!laser_rotated]

# 処理済みタグ削除
tag @e[tag=laser] remove laser_rotated
```
各プレイヤーを実行者として、`tick.mcfunction`から実行される`every_player.mcfunction`です。
```mcfunction:every_player.mcfunction
# 近くにディスプレイが無ければ召喚
execute unless entity @e[tag=laser,tag=!laser_rotated,limit=1,distance=..1] run summon minecraft:block_display ~ ~ ~ {Tags:["laser"],block_state:{Name:"red_stained_glass"},interpolation_duration:1,teleport_duration:1,transformation:{right_rotation:{axis:[0f,-1f,0f],angle:0},scale:[0.1f,50f,0.1f],left_rotation:{axis:[1f,0f,0f],angle:0},translation:[0f,0f,0f]},brightness:{block:15,sky:15}}

# 近くのディスプレイにファンクション実行
execute as @e[tag=laser,tag=!laser_rotated,sort=nearest,limit=1,distance=..1] run function laser:every_display
```

プレイヤーの近くのディスプレイを実行者として、`every_player.mcfunction`から実行される`every_display.mcfunction`です。
```mcfunction:every_display.mcfunction
# ディスプレイをtp
tp @s ~ ~ ~

# execute幾何学で、縦角度[-90~90]⇒横角度[0~180]に変換して、倒す角度特定
execute rotated -90 ~ positioned 0.0 0.0 0.0 positioned ^ ^ ^1 rotated ~90 ~ positioned ^ ^1 ^ facing 0.0 ~ 0.0 run tp @e[tag=laser_marker,limit=1] 0.0 0.0 0.0 ~ ~
execute store result storage laser: left_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000

# 倒す軸方向ベクトル特定
execute positioned 0.0 0.0 0.0 run tp @e[tag=laser_marker,limit=1] ^1 ^ ^ ~ ~
data modify storage laser: left_rotation.axis set from entity @e[tag=laser_marker,limit=1] Pos

# right_rotationの角度を特定
execute store result storage laser: right_rotation.angle float 0.000001745 run data get entity @e[tag=laser_marker,limit=1] Rotation[0] 10000
data modify storage laser: right_rotation.axis set value [0f,-1f,0f]

# 変形反映
data modify entity @s transformation.left_rotation set from storage laser: left_rotation
data modify entity @s transformation.right_rotation set from storage laser: right_rotation

# ブロックディスプレイのアニメーション補完のバグ回避のためにshadow_radiusを書き換えている
# アイテムディスプレイの場合は必要ない
execute store success entity @s shadow_radius float -1 if data entity @s {shadow_radius:0f}

# アニメーション補完
data modify entity @s start_interpolation set value 0

# 処理済みタグ付与
tag @s add laser_rotated
```

![Replay 2023-12-10 16-24-56.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526721/bc5caa10-5fc8-c0bb-72db-ceff46f40d79.gif)
これでスニークすると目からビームが出せるようになりました。

# おわりに
今回作ったデータパックのサンプルは [ここ](https://github.com/komaramune/laser-example/releases) に置いておきました。
ご自由にお使いください。
お好みでモデルの変更をしたり、演出や攻撃判定の追加といった改良をしたり、またレーザービーム以外にも応用ができると思います。
皆さんのコマンド開発のお役に立てば幸いです。


[^1]: ソース：https://wiki.vg/Protocol#Definitions:angle