四谷ラボ公式ブログ

四谷ラボはいつでも誰でも自由に参加・研究・交流・発信のできる街のオープンイノベーションラボ

【マイクラ】大量の特殊アイテムをマクロで軽量化する

Minecraftのデータパック制作、特に「魔法の杖」のような実装では、アイテムの種類ごとに判定を行う条件分岐処理が行われ、パフォーマンスのボトルネックになりがちです。

本稿では、バージョン1.20.2で追加された「マクロ」を活用し、アイテムの種類がどれだけ増えても処理負荷を増加させない仕組みについて説明します。

従来の実装

特定のアイテムを持った際に処理を実行させたい場合、多くのクラフターは以下のようなコードをtick関数に記述します。

execute as @a[nbt={SelectedItem:{tag:{Ability:"fireball"}}}] run function mypack:magic/fireball
execute as @a[nbt={SelectedItem:{tag:{Ability:"ice_wall"}}}] run function mypack:magic/ice_wall
execute as @a[nbt={SelectedItem:{tag:{Ability:"heal"}}}] run function mypack:magic/heal

アイテムが10種類なら問題ありませんが、100種類、1000種類と増えるにつれ、サーバーは毎tickごとに膨大な量の処理を強いられ、TPSの低下を招きます。

マクロによる動的実行

Ver 1.20.2で導入されたマクロを活用することで、この問題を解決できます。 マクロを使えば、コマンドの一部を動的に書き換えることができます。つまり、「実行すべき関数名」をアイテム自体に持たせ、それを読み込んで実行することが可能になります。

実装

名前空間は仮にmypackとします。

また、バージョン1.20.2のコマンドになります。

アイテムに関数名を埋め込む

実行したい関数名そのものをNBTに記録します。

# アイテム付与コマンド
give @s stick{AbilityFunc:{fn_name:"mypack:abilities/fireball"}}

ディスパッチ処理

tick関数で特殊アイテムを持っているプレイヤーを検出し、ディスパッチ関数を呼び出します。

# tick.mcfunction

execute as @a[nbt={SelectedItem:{tag:{AbilityFunc:{}}}}] run function mypack:core/dispatch

次に、呼び出されたディスパッチ関数でNBTデータをストレージにコピーし、マクロへ渡します。

# mypack:core/dispatch

#    手持ちアイテムの NBT (AbilityFunc) をストレージにコピー
#    AbilityFuncの中身は {fn_name:"..."} という構造になっている必要がある
data modify storage mypack:context temp set from entity @s SelectedItem.tag.AbilityFunc

#    マクロ関数を呼び出す
#    "with storage" でストレージの内容を引数として渡す
function mypack:core/macro_exec with storage mypack:context temp

マクロ関数の定義

受け取ったfn_nameを展開して実行します。

# mypack:core/macro_exec
# 引数: {fn_name: "function_name"}

# $(fn_name) が "mypack:abilities/fireball" などに置換されて実行される
$function $(fn_name)

アイテムがたとえ1000種類あっても、tickの処理はこれだけで完結します。

パフォーマンス

実際に1000種類のユニークなスキル(helloworld0001.mcfunctionhelloworld1000.mcfunction)を用意し、毎tick実行するベンチマークを作成して比較を行いました。

何も実行していない状態

比較の基準として、データパックによる処理を一切行わない状態の負荷を計測しました。

Tick durationの中央値(Med)は 2.6ms でした。これがこのサーバー環境における最小の負荷となります。

何も実行していない状態

愚直な実装

全1000個の条件分岐を総当たりでチェックする方法です。

実際の計測結果では、Tick durationの中央値(Med)が 17.8ms まで上昇しました。

アイドル時と比較して明らかにベースの負荷が増しています。今回は20TPSを割ることはありませんでしたが、プレイヤー数・アイテム数が増えればラグが発生することは明らかです。

愚直な実装

tick.mcfunction

マクロを使用した実装

アイテムからIDを取得し、動的に関数パスを生成して実行する方法です。

計測結果では、Tick durationの中央値は 2.9ms と、非常に軽量な状態を維持していました。

これはサーバーのアイドル状態とほぼ変わらない数値です。アイテムの種類の数に関係なく、このパフォーマンスを維持できることが確認できました。

マクロを使用した実装

結論

愚直な実装は明らかに多くの処理時間を消費しています。

一方マクロを使用した実装は、何も実行していない状態とほぼ同等のパフォーマンスを維持しており、その優位性は明白です。

特に、RPGサーバーや大規模なデータパック開発において、この最適化は必須と言えるでしょう。

他のアプローチ

マクロが導入されるより前のバージョンでも、少しトリッキーな方法で似たような動的実行を実現するテクニックが存在しました。

それは、「コマンドブロックを特定座標に設置し、その座標情報をアイテムに埋め込む」という手法です。

  1. 実行したいコマンドが書かれたコマンドブロックを配置します。
  2. そのコマンドブロックの座標をアイテムのNBTに記録します。
  3. プレイヤーがアイテムを使用した際、その座標にあるブロック(コマンドブロック)のデータを読み取り、実行します。

ただし、この方法は物理的なブロック配置が必要であり、チャンク読み込みの問題や管理の複雑さが伴うため、マクロの登場によってよりスマートに実装できるようになりました。

まとめ

この設計パターンを採用することで、サーバー負荷を抑えるだけでなく、新規アイテム追加時にtick関数を編集する必要がなくなり、データパックの保守性も向上します。

リソースの限られたMinecraftの実行環境において、こうした最適化は非常に効果的です。ぜひお試しください。