この記事はマインクラフト(Java Edition) バージョン 1.21.4 に合わせて書かれています。
はじめに
GUI(Graphical User Interface)とは、アイコンやボタンなどを用いて視覚的に操作できるようにされた操作画面のことです。対して、文字によって命令を与えることで操作する、それこそマインクラフトのコマンドのようなものをCUI(Character User Interface)といいます。
マインクラフト上でGUIを作ろうと思った場合、
- マイクラのエンティティやブロックなどを直接使ったもの
- チャット欄と
/tellraw
を使ったもの - インベントリやチェストなどにアイテムを使ったもの
/title
コマンドを使ったもの- リソースパックを使ってマイクラ本来のGUIを拡張したもの
- MODで作成
など、列挙してみると意外とたくさんありますね。
そんな中、2023年にディスプレイエンティティやインタラクションが追加され、ゲーム内でのGUIはさらに作りやすくなりました。
そこで思い立って、こんなものを作ったことがあります。近未来映画とかでよくある、宙に浮かぶGUIです。
作ってみれば意外と面白く、本来はもっと別の目的(ゲームの補助)で作る予定でしたが、「このディスプレイエンティティで作るGUIをより汎用的にしていろんな場面で使えるようにしたい」「ディスプレイエンティティならそれができそうだ」と思い、今日までちまちまと作っては壊しての繰り返しで制作をしていました。
ディスプレイUIは自分の思いつくかぎりの様々な場面で使えるようにと、とにかく汎用性を持たせることを意識して作成しました。また、初見の人(と未来の自分)が直感的に扱えるような入力方法やファンクション構成を意識するようにもしました。とはいっても、コマンド(と私の頭)は限界もあるので、納得のできるものができたとはいいきれないですが...
この記事では、このディスプレイUIを作るにあたって考えたこと、およびコマンドとの向き合い方?的な部分を主に記します。ついでに、UIの使用例や、制作に際して使った小ネタなどを紹介していこうと思います。
1.ディスプレイUIをつくる
ここでは、ディスプレイUIのおおまかな仕様について説明します。ディスプレイUIにおいては、
1. ディスプレイエンティティを組み合わせて見た目を作る
2. プレイヤーの視線を入力として操作できるようにする
3. オブジェクトに機能を持たせる
ことの3つが軸にあります。順番に説明していきましょう。
1.1 ディスプレイエンティティを組み合わせて見た目を作る
ディスプレイUIにおいては、上の画像のように複数のディスプレイエンティティを組み合わせて見た目をつくります。
これらのエンティティを使う場合、バラバラのままでは非常に使いづらいです。テレポートをしようと思っても毎回全員を指定しなくてはいけなかったり、大小様々なグループを作りたい場合、タグやスコアを使って紐付けをしなくてはいけなかったりと、できなくは無いですが、なかなか大変です。
そこで、一体のエンティティにディスプレイエンティティをライドさせ、on passenger
とon vehicle
で紐付けをするという方法を使っています。これによって、テレポートしたい時は一番下のエンティティを移動させれば全体を動かすことができ、個々のディスプレイエンティティを実行者にしたい時はexecute on passenger
を使えばよくなります。
@eセレクターは負荷が高いの?
@eセレクターはエンティティを指定するときに使うセレクターですが、なにも指定していない状態では、ワールド全体(アクティブなチャンク)全てのエンティティを確認するので、エンティティの数に比例して負荷が高くなります。
そのため、使う時はなるべくtype
やcount
、distance
を設定することで、検索対象を絞ったり検索するチャンクを絞ったりすることで負荷を低減します。
しかし、今回のような同じ種類のエンティティが何十体も1箇所にまとまるような場面では、対象や範囲を絞っても負荷の低減が見込めません。もし、ディスプレイUIがバラバラだったら、エンティティを指定する時に毎回@eセレクターを使わないといけないため、負荷の観点でも欠点があります。
次に、エンティティをどのように乗せていくか考えます。真っ先に思いつくのは、一体のライド用エンティティに全てのディスプレイエンティティを乗せるという方法ですが、実際に使う時はディスプレイエンティティ同士をある程度のグループに分けて使いたいので、少し工夫する必要があります。
そのため、このディスプレイUIでは"レイヤー","オブジェクト","ユニット"などの概念を導入し、下図のような構造にしました。
-
「ユニット」
ディスプレイエンティティそのもの。 -
「オブジェクト」
ディスプレイエンティティにボタンなどの機能を持たせたもの。あるいはユニットのまとまりのこと。 -
「レイヤー」
オブジェクトのまとまりのこと。表示非表示の切り替えができたりする。
キーボードみたいに閉じたり開いたりさせたいものや、オブジェクトのまとまりをわかりやすくしておきたいときに使う。 -
「ディスプレイ」
レイヤーのまとまりのこと。
設定画面や対話GUI、魔法選択GUIなど、目的に準じた大きなくくり。 -
「ディスプレイUIエンティティ」
ディスプレイを構成するエンティティのまとまり、およびディスプレイUIを見かけ上ひとつのエンティティとして扱うためのくくり。狭義には、一番下層にいるディスプレイエンティティを乗せたエンティティのこと。
後述しますが、このディスプレイUIでは、オブジェクトにGUIの要であるボタンなどの機能をもたせます。ですが、いくらライドによってグループ化ができるといっても、オブジェクトが無制限にライドさせられていては、何回on passenger
を実行すればオブジェクトにアクセス出来るのかが決まらず、煩雑になってしまいます。そこで、容易なアクセスが求められるオブジェクトはすべて同じ階層に置き、見た目だけのユニットはいくらでもライドさせられる、という構造をとりました。
ライドを使うこの構造では、全てのエンティティが同じ座標にいるので、目的の配置を作るためには、ディスプレイエンティティのNBTであるtransformation
によってユニットを配置する必要があります。ただ、一体ずつNBTを設定して召喚するのでは冗長になってしまいますし、ユニット同士を相対的に配置したい時は位置を計算したり、ボタンのような機能を持たせるためにスコアやデータをいくつか設定したりするのも、手動でやるのはとても大変です。
そこで、ユーザー側は比較的簡単な入力でデザインできるようにし、コンパイルによってエンティティのnbtやスコア、ストレージなどに変換、それを呼び出してディスプレイUIを召喚する、という方法にしました。イメージとしては次のような感じです。
data modify storage display_ui:design displays.sample set value
{
objects:[
{type:"item",pos:[0f,0f,0f],size:[0.5f,0.5f,0.001f],item:{id:"stone"}}
{type:"button_item",pos:[0f,0.05f,0f],size:[0.2f,0.1f,0.001f],item:{id:"red_concrete"}}
]
}
↓
/function display_ui:compile
でコンパイルを実行する
↓
/function display_ui:summon
でディスプレイUIエンティティを召喚
といった感じで、ディスプレイUIの見た目をデザイン出来るようになりました。
1.2 プレイヤーの視線を入力として操作できるようにする
先述の方法によって、ディスプレイUIの見た目を作ったとしましょう。プレイヤーがボタンなどをクリックできるようにするためには、プレイヤーがどこを見ているかを取得出来るようにする必要があります。
インタラクションが追加された今であれば、ボタンの位置に当たり判定のサイズのインタラクションを置き、進捗(advancement)などでクリックしたときにファンクションが実行されるようにすれば実装できます。ただ、今回はなるべく汎用的なものにしたかったため、UIの大きさや向き、ビルボード設定に制限をつけたくはありません。そのため、ここではスコアボードによるベクトルの計算によって、プレイヤーがディスプレイUI上のどこに視点があるかを取得しています。
視点の計算
知りたいのは、ディスプレイの面をxy平面、法線方向をz軸とした座標系における、プレイヤーの視点(x',y',0)です。プレイヤーの視点の位置を計算し、それをディスプレイ面上の座標系に座標変換して求めます。
視点の計算には、ディスプレイの法線およびx,y軸と、プレイヤーの視線、ディスプレイとプレイヤーの相対位置の情報が必要です。計算の方針としては、ディスプレイの原点から視点までのベクトルを求め、そのベクトルとディスプレイのx,y軸との内積を計算して、ディスプレイ面上における視点の座標を求めます。単位ベクトルとの内積はその軸にベクトルを投影した時の長さとなるため、実質的に座標が求まるというわけです。
また、原点から視点までのベクトルは、原点からプレイヤーの目までのベクトルと、プレイヤーの目から視点までのベクトルを使って求めます。
では、計算の内容を順番に見ていきましょう。まずは、登場するパラメーターの一覧です。
\displaylines{
\begin{align}
NBTから取得する情報\\
&\vec{r_p} :プレイヤーの目のワールド座標\\
&\vec{p_p} :プレイヤーの視線ベクトル(単位ベクトル)\\
&\vec{r_d} :ディスプレイのワールド座標\\
&\vec{x_d} :ディスプレイ面上の座標系におけるx軸方向の単位ベクトル\\
&\vec{y_d} :ディスプレイ面上の座標系におけるy軸方向の単位ベクトル\\
&\vec{n_d} :ディスプレイの法線(z軸方向)の単位ベクトル\\
\\
計算する情報\\
&\vec{a} :ディスプレイからプレイヤーの目までのベクトル\\
&\vec{b} :ディスプレイの原点からディスプレイ上の視点までのベクトル\\
&k :プレイヤーの目からディスプレイ上の視点までの距離\\
&x' :ディスプレイ面上の座標系における視点のx座標\\
&y' :ディスプレイ面上の座標系における視点のy座標\\
\end{align}
}
ディスプレイからプレイヤーの目までのベクトル$\vec{a}$は、プレイヤーの目の座標$\vec{r_p}$からディスプレイの座標$\vec{r_d}$を引き算すれば求まります。
\vec{a}=\vec{r_p}-\vec{r_d}
プレイヤーの目からディスプレイ面に下ろした垂線の距離を視線のz軸成分(視線方向に1m進むと何mディスプレイ面に近づくか)で割るとプレイヤーの目からディスプレイ面上の視点までの距離$k$を求めることが出来ます。
プレイヤーの目からディスプレイ面に下ろした垂線の距離はディスプレイからプレイヤーの目までのベクトル$\vec{a}$とディスプレイの法線(z軸)の単位ベクトル$\vec{n_d}$との内積で求められ、プレイヤーの視線のz軸成分はプレイヤーの視線の単位ベクトル$\vec{p_p}$とディスプレイの法線(z軸)の単位ベクトル$\vec{n_d}$との内積で求められます。これらを式に表すと下記のようになります。
\vec{k}=(\vec{r_p}・\vec{n_d})/(\vec{a}・\vec{n_d})
距離$k$がわかれば、プレイヤーの視線の単位ベクトル$\vec{p_p}$に距離$k$を掛けて、プレイヤーの目から視点までのベクトル$k\vec{p_p}$を求めることが出来ます。
これで、原点から視点までのベクトルがわかります。
\vec{b}=k\vec{p_p}-\vec{r_d}
あとは、ディスプレイのx軸およびy軸との内積を計算すれば、面上の座標が求まります。
\displaylines{
x'=\vec{b}・\vec{x_d}\\
y'=\vec{b}・\vec{y_d}\\
}
これで、プレイヤーが見ている視点の、ディスプレイ面上の座標系におけるx,y座標が求まりました。
1.3 オブジェクトに機能を持たせる
プレイヤーの視点が計算できるようになったので、次はオブジェクトの当たり判定やイベントなどを実装していきます。ディスプレイUIでは、スコアボードによってオブジェクトに矩形の当たり判定を持たせています。
デザインの際に位置と当たり判定の大きさをボタンに設定すると、コンパイルによって"位置±当たり判定の大きさ/2"がx,y方向で計算され、合わせて4つのスコアがボタンに与えられます。
たとえば、 pos:[1f,0.5f,0f]
area:[0.2f,0.1f]
を設定した場合、
$(x_1,x_2,y_1,y_2)=(0.9,1.1,0.45,0.55)$
という範囲の情報がボタンに与えられるわけです。そして、プレイヤーの視点がボタンの当たり判定の中にあるかどうかは、スコア化されたプレイヤーの視点とボタンが持つ範囲を比較すれば簡単にできます。
そしたら次は、クリックしたとき、ホバーしたとき、ホバーをはずしたときなどに、任意のコマンドを実行できるようにしましょう。今のバージョンであればマクロがあるので、オブジェクトをデザインするときにコマンド文を設定しておき、クリックしたときやホバーしたときなどにそのコマンド文を呼び出し、下のようなマクロで実行させればよいです。
$$(command)
これさえできれば、好きなファンクションの実行もできるので、オブジェクトに複雑な機能を持たせることも可能になります。ディスプレイGUIでは、ボタンに限らず、よく使うようなオブジェクトについてはデータパック側で用意することにしています。トグルボタンや、数値入力、アイテムが置けるスロットなど、あると便利なオブジェクトを実装しています。
簡単な説明になりましたが、このようなやり方でディスプレイGUIを実現しています。
2.使用例
どんなディスプレイが作れるかはユーザーの想像力と技量次第ですが、とはいってもサンプルは必要だと思うので、いくつか作成してみました(ハリボテですが)ので紹介していきます。(Qiitaでは動画を貼り付けられないので、youtubeを経由しています。)
ゲームの設定画面
ミニゲームマップなどでつかるような、ゲームの設定画面です。トグルボタンだけでなく、リストや数値の選択もできるオブジェクトがあるので、このような設定画面を作ることができます。
また、ディスプレイUIの基本的な部分はマルチ対応させているので、複数人で操作できる画面を作ることもできます。
魔法選択GUI
RPGマップで使えるような、魔法選択のGUIです。ディスプレイが開くときや閉じるときのアニメーションも自由に設定できるので、それっぽい演出が可能です。
テーブルゲーム
手持ちのアイテムを置くことができるテーブルです。白黒のアイテムを使えば疑似的にオセロをすることもできます。シフトを押しながら設置で全設置できたり、アイテムが置かれる位置や角度を制限したりなどの細かい仕様もあります。
アイテムモデルなどを作成すれば、さらにいろんなテーブルゲームもできそうです。
会話GUI
RPGなどで使えそうな会話GUIです。メニューをクリックしたら別のウィンドウ(レイヤー)を出したり、といったことも可能です。ディスプレイUIやユニットには好きなタグをつけることもできるので、コマンドで好きに操作することができます。
3.小ネタ
ディスプレイUIを作る上で使った小ネタをいくつか紹介します。
3.1 数値型かどうかの判別
ストレージなどに格納されたNBTが数値型(byte型,int型,float型,double型など)かどうかを判別する方法です。data get
コマンドの仕様を利用します。
execute store success score <name> <objective> run data get storage <namespace> <path> 1.0
解説
スケールを指定したdata get ... <scale>
は数値型でない場合かデータが存在しない場合に失敗するという仕様があるので、execute store success
によって取得することで数値型の判別が行えます。
上記の例ではスコアに成否を格納しており、1
の場合数値型、0
の場合は数値型以外という結果になります。
注意点としては、データが存在しない場合をif data
などを用いて予め排除しておく必要があることです。
3.2 特定の型かどうかの判別
ストレージなどに格納されたNBTが、int型、byte型、float型、double型、string型、compound型、list型などあるデータ型のうちのどれなのか判別をする方法です。リスト型が持つ型制限を利用します。
# 例:double型かどうかの判別
data modify storage _: _ set value [0.0d]
execute store success score <name> <objective> run data modify storage _: _ append from storage <namespace> <path>
# 例:list型かどうかの判別
data modify storage _: _ set value [[]]
execute store success score <name> <objective> run data modify storage _: _ append from storage <namespace> <path>
解説
リスト型は格納するデータを全て同じ型とする制限があります。そのため、既にデータの存在するリストに異なる型のデータを追加しようとするか、追加するデータが存在しない場合に失敗となります。なので、判別したい型のデータを格納したリストを準備し、対象のデータをそのリストに追加するコマンドをexecute store success
によって成否を取得することで型判別が行えます。
上記の例ではスコアに成否を格納しており、1
の場合は型が合致、0
の場合は異なる型という結果になります。
注意点としては、対象のデータが存在しない場合をif data
などを用いて予め排除しておく必要があることです。
3.3 小数に対応した数値の範囲判定
ある数値があったとき、その数値の大きさが特定の範囲内かどうかを判別する方法です。整数(int型)であれば、スコアボードやプレディケートのvalue_check
を用いて判別することができますが、少数(float型やdouble型)では、その二つでは判別できる数値の桁に制限があります。そこで、やや負荷はかかりますが、マクロと座標を使って判別する方法を利用します。
data modify storage _: _ set value {value:<判定したい数値>,min:<下限>,max:<上限>}
function <namespace>:macro with storage _: _
$execute positioned 0.0 0.0 0.0 rotated 180 0 positioned ^$(value) ^ ^$(value) positioned ~$(min) ~ ~$(max) run function <namespace>:check
# 下限以下
execute if predicate {condition:location_check,predicate:{position:{x:{min:0.0}}}} run say less than or equal to min
# 上限以上
execute if predicate {condition:location_check,predicate:{position:{z:{max:0.0}}}} run say more than or equal to max
# 範囲内
execute if predicate {condition:location_check,predicate:{position:{x:{max:0.0},z:{min:0.0}}}} run say in range
解説
これは、マクロでexecute positioned x y z
に数値を代入することで数値の引き算min - value
とmax - value
を行っており(詳細は後述)、実行座標に引き算の結果が反映されるため、実行されるファンクション内でプレディケートのlocation_check
を用いて0より大きいか小さいかを確認して範囲の判定ができます。また、一つのコマンドで判別できるよう、x座標とz座標それぞれに限と上限の判別を担わせています。
macro.mcfunction
の内容を分解すると以下のようになります。
- 原点を基準にする(
positioned 0.0 0.0 0.0
) -
-value
の方向に移動する(rotated 180 0 potitioned ^$(value) ^ ^$(value)
) -
+min
および+max
の方向に移動する(positioned ~$(min) ~ ~$(max)
) - コマンドを実行すると、実行座標が
x = min - value
およびz = max - value
となる(run ...
)
初めに原点に移動するのは、positioned
は絶対座標に整数が入力されると実行地点が+0.5されてしまう(positioned 2 0 4 -> positioned 2.5 0.5 4.5
)仕様があり、value
などに入っている値が整数だったときにそうならないようにするためです。、
また、value
などには負の値も入るようにしたいのですが、単純に座標で引き算を行おうとして
# 失敗例
$execute positioned 0.0 0.0 0.0 positioned ^-$(value) ^ ^-$(value) positioned ~$(min) ~ ~$(max) run function <namespace>:check
と、マクロの変数の前にマイナスを付けてしまうと、負の値を入れた際にpositioned ^--2.3 ^ ^--2.3
のようにマイナスが連続してしまい、コマンドが失敗するので注意が必要です。ここでは、マイナスを付けずに負の方向に進むため、後ろを向いて(rotated 180 0
)向き相対(^x ^ ^z
)で移動させています。
また、最近のバージョンでプレディケートやアイテム修飾子、ルートテーブルなどはインライン記述(コマンドに直接書くこと)ができるようになりました。最後のcheck.mcfunction
では、それを用いて簡潔に実行地点の座標が0より大きいか小さいかを判定しています。
3.4 テキストディスプレイのアニメーション補間のバグ対策
ディスプレイエンティティには、変形や影、色などのNBTにアニメーションによる補完機能があり、滑らかな変化ができます。
普通の使い方としてはtransformation
とinterpolation_duration
を同時に変更することで変形にアニメーションをさせるというものですが、それをした後にinterpolation_duration
を変更すれば途中でアニメーションの速度を変えることもできます。しかし、なにかの不具合で、現在はアイテムディスプレイとブロックディスプレイは上記の動作になりますが、テキストディスプレイはinterpolation_duration
のみを変更するとアニメーションが最初からになってしまいます。(下の動画)
▲アイテムディスプレイの場合 ▲テキストディスプレイの場合
解決方法として、interpolation_duration
の影響を受ける別のNBTを同時に変更すれば途中からのアニメーションにすることが出来ます。どのような方法でもいいですが、私は今回shadow_strength
を変更することで対応しました(使っていないNBTなので)。
data modify storage _: _ set value {interpolation_duration:5,shadow_strength:1f}
execute store result storage _: _.shadow_strength int 1 store success score @s _ unless score @s _ matches 1
data modify entity @s {} merge from storage _: _
解説
上記の例では、エンティティごとに、実行するたび数値が変わるデータをスコアボードで生成してshadow_strength
に与え、interpolation_duration
と同時に変更させています。スコアボードが必要にはなりますが、このやり方の場合エンティティへの初期設定が不要なので扱い易いというメリットがあります。
1コマンドで実行する度に切り替わるスコア
実行する度にスコアを切り替える方法としては、シンプルに2コマンドで
scoreboard players add @s _ 1
scoreboard players set @e[scores={_=2..}] _ 0
とする方法があります。さらに、次のようにstore success
を使って1コマンドに納めるという方法もあります。
execute store success @s _ unless score @s _ matches 1
これは、unless score ...
はスコアが1以外の場合に成功、1の場合に失敗となるため、その成否を同じスコアボードに格納することで下記のようにトグルを実現させています。
- スコアが無いとき→
unless
が成功→スコアが1になる - スコアが1のとき→
unless
が失敗→スコアが0になる - スコアが0のとき→
unless
が成功→スコアが1になる
... (繰り返し)
3.5 プレイヤーやエンティティのアイテム個数の操作
プレイヤーやエンティティの特定のスロットのアイテムを特定の個数減らしたり、逆に増やしたりする方法です。clear
コマンドではアイテムを指定できますがスロットを指定することができないため不便です。アイテム修飾子を用いることで、一定の個数を増減させることや、スコアボードやストレージの数値で増減させることもできます。
# 手持ちのアイテムを1個減らす
item modify entity @s weapon.mainhand {function:"minecraft:set_count",add:true,count:-1}
# 手持ちのアイテムをスコアの数だけ減らす
item modify entity @s weapon.mainhand {function:"minecraft:set_count",add:true,count:{type:"score",score:<objective>,target:{type:"fixed",name:<name>}}}
解説
最近のバージョンでプレディケートやアイテム修飾子、ルートテーブルなどはインライン記述(コマンドに直接書くこと)ができるようになりました。そのため、別のファイルをわざわざ作らなくても、上記の例のようにアイテム修飾子を利用することができます。
アイテム修飾子にはアイテムの個数を設定するset_count
という機能があり、パラメーターでadd:true
にすると現在の値からの増減を設定できます。さらに、ナンバープロバイダー(スコアやストレージ、乱数などを設定できるやつ)が利用できるため、可変的な増減が可能です。
3.6 コンパウンド形式のアイテムデータをもとにアイテムを与える
チェストやエンティティなどから取得したアイテムのNBT(item:{id:<item id>,count:<count>, components:...}
)をそのままプレイヤーに与える方法です。エンティティであればNBTのままやり取りが可能ですが、プレイヤーのNBTは変更することができないので、必然的にgive
コマンドやitem
コマンド、loot
コマンドなどでアイテム与える方法に限られます。/item raplace ... from
であれば、エンティティやブロックが持っているアイテムを与えることができるので一見それでもよさそうですが、give
コマンドのように空いているスロットに渡すようなことはできません、そこで、ルートテーブルとマクロを用いて、NBTを元にプレイヤーにアイテムを渡す方法を考えます。
# 与えたいアイテムのNBT
data modify storage _: _ set value {id:"minecraft:stone",count:3,components:{...}}
# コンポーネントが存在しない場合は空データを設定
execute unless data storage _: _.components run data modify storage _: _.components set value {}
# マクロによってアイテムを与える
function <namespace>:give with storage _: _
$loot give @s loot {pools:[{rolls:1,entries:[{type:"item",name:"$(id)",functions:[{function:"set_components",components:$(components)},{function:"set_count",count:$(count),add:false}]}]}]}
解説
最近のバージョンでプレディケートやアイテム修飾子、ルートテーブルなどはインライン記述(コマンドに直接書くこと)ができるようになりました。そのため、マクロによってルートテーブルの中にデータを代入することができます。加えて、アイテムのデータがアイテムコンポーネントの実装によってシンプルになり、アイテムID(id
)、個数(count
)、コンポーネント(components
)の三つを設定さえできればアイテムを複製することができます。ルートテーブルにはそれぞれを設定する機能があり、対応するname
、set_count
、set_components
にマクロでNBTを代入すればよいです。
また、インライン記述の場合はJSONファイルではないので、今まで
"components":{"minecraft:custom_data":{"data_a":1,"data_b":"hoge"}}
のような記述をしていたのが、
"components":{"minecraft:custom_data":{data_a:1,data_b:"hoge"}}
という書き方ができるようになり、二重引用符を気にせずにコンパウンド型のデータを代入することができます。(minecraft:~
は初めからNBT上で二重引用符が付く)
3.7 エンティティの表示角度取得
エンティティは、内部的な角度と異なり、見た目上の向きは分解能が低いという仕様があります。そのため、内部的には1°を向いているのに見た目上は0°のまま、といったことが起こりえます。ディスプレイエンティティも例外ではなく、ディスプレイUIで視点などを計算する際は内部的な角度を用いて計算しますが、そのままでは実際の視点と計算した結果が異なるという問題が起きてしまいます。そのため、分解能を反映した角度を取得する必要があります。
execute store result storage _: Rotation[0] float 1.40625 run data get entity @s Rotation[0] 0.711111111
execute store result storage _: Rotation[1] float 1.40625 run data get entity @s Rotation[1] 0.711111111
解説
エンティティは見た目上では360°が256方向に制限され、角度の分解能は360°÷256=1.40625°
ということになります。そのため、内部的に1°のときは見た目は0°、2°の時は1.40625°、3°の時は2.8125°と、小さい方の一番近い1.40625°の倍数の角度になります。式にすると角度 ÷ 1.40625 → 整数に切り捨て → ×1.40625 = 見た目の角度
です。この計算を、data get
コマンドのスケールと整数切り捨ての仕様を用いて行って下記の流れで行っています。
- 角度を1.40625で除算:1.40625の逆数0.711111...を乗算 (
data get ... Rotation[0] 0.711111111
) - 整数に切り捨て:コマンドからの返り値は整数で切り捨てされる (
store result ... run data get...
) - 1.40625を乗算:返り値にスケールを設定する (
store result ... Rotation[0] float 1.40625
)
上記の例では、これをそれぞれの角度で行っています。縦角度は範囲自体は180°しかないですが、分解能は横角度と同じ1.40625°です。
データパックはまだ配布されていません。
身内でデバッグした後、ドキュメント等を整理して配布する予定なので、大体1,2ヶ月後になると思います。