1.はじめに
ワールド制作でコマンドを組んでいるとき、「ちょっと重いな...」「処理をもうちょっと軽くできないかな...」と思うことは少なくないと思います。そこでコマンドを軽量化するための基本的で簡単なテクニックをまとめてみました!これらの方法を用いて是非軽量化にチャレンジしてみてください!
2.軽量化の方法
コマンド実行量の軽量化をするためには、実行するコマンドの個数を減らすことが重要です。そのための手法として大きく分けて 3 つ存在します。
無駄なコマンドの実行を減らす
コマンドでワールドやデータパックを制作する際、無駄にコマンドが実行され続けていませんか?不要なコマンド実行をなくすことにより、処理量を大きく減らすことにつながります。古いデータ・没データといったコマンドの実行がされている可能性もあるので確認が必要です。
コマンド実行を最適化する
常に実行するべきであると思っているコマンドに関しても、よくよく考えると特定のタイミングで実行すればいいだけである可能性もあります。
例えばRPGなどで「経験値がたまればレベルアップする」という処理があります。常時経験値量を計算してレベルアップの処理を行っても問題はないのですが、よく考えたら
「経験値を得たタイミング以外でレベルが上がることはない」
ことがわかると思います。このような工夫で処理量を大きく減らすことができるのです。
エンティティの数を少なくする
エンティティが増えすぎることへの注意も必要です。エンティティを出現させるとワールドにおいて毎ティック以上の頻度で処理が行われるため、かなり負荷が大きくなります。高度なコマンドだと大量の防具立てを使いたくなる気持ちもわかるのですが、その防具立て、「本当に必要か?」と今一度考えてみてください。また、使わなくなったエンティティを消すことによっても処理を軽くすることができます。
ちなみに、テクニックとして
/execute as @e run say @s
(1.13以降)
/execute @e ~ ~ ~ say @s
(1.12以前)
を実行すると、現在ワールドにいるエンティティを一覧表示してくれるので、余計なものが残っていないか確認できます。
3.軽量化テクニック
全般
処理をまとめる
基本的ながらも難しい処理です。コマンドを書いていると「あれ、こことここって処理ほぼ一緒じゃない?」って思うことがあるかもしれません。例えば、複数種類のボスが同じような技を放ってくる場合などです。そう思ったらその瞬間コマンドをまとめてしまいましょう!要はそのコマンド列を 1 個にまとめ、それぞれをその共通コマンドを「呼び出すだけ」のコマンドにしてしまえばいいのです。
コマンドブロックの場合、処理をしたいようなエンティティに特定のタグをつけておきます。エンティティ以外が対象の場合はその場所に適当な防具立てでも出してしまえばよいです。常時実行されるコマンドに毎ティック
execute as @e[tag=p] run ...
(1.13以降)
execute @e[tag=p] ~ ~ ~ ...
(1.12以前)
を実行させ、総コマンド数を減らしましょう。コマンドブロックの個数は負荷に比例します。
データパックを使えば簡単で、共通コマンドを書いた function を用意してそれを呼び出せばいいだけです。もちろん前述のコマンドブロックの方法も役立ちます。
無駄なエンティティを消す
コマンドを書いていると「特定の場所をマークしたい」「移動する物体を作りたい」など、さまざまなシチュエーションでエンティティを出現させることがあると思います。しかし、余計なエンティティが残ったりしてはいませんか?一度出した防具立ての消し忘れはコマンド上級者でも頻発します。
例えば魔法弾。防具立てを少しずつ動かすことによって管理することも多くみられますが、防具立ての残留について「壁に当たれば消える処理をしているから大丈夫」と思っていても、壁に当たらず永遠に進み続けている防具立てがあるかもしれません...。以下のようにスコアボードで飛距離を管理するなどして消し忘れないようにしましょう。
scoreboard players add @e[tag=bullet] time 1
kill @e[tag=bullet,score={time=100..}
コマンドブロック編
コンパレーターを活用する
チェーンコマンドブロックを「条件付き」にすると直前のexecute
コマンドなどの処理が成功した時のみ以降の処理を行う、ということができます。しかし、実はこのチェーンコマンドブロック、条件を満たしていない時でも毎ティック判定が入ってしまいます。
そこで、条件のコマンドブロックにコンパレーターをつなげてみましょう。するとその先のコマンドは条件を満たしたとき以外に処理されなくなり、軽量化につながります。またコマンドの区切りがわかりやすくなり、簡潔なコーディングにもつながります。一方コンパレーターの多用も重くなる原因となり得るので、処理コマンドの数が多い時や条件を満たす頻度が少ないときにお勧めです。
コマンド部屋を置く位置を考える
コマンド部屋は異常が起こらないよう、スポーンチャンクに置かれることが一般的です。しかし、逆に言えばスポーンチャンクのコマンドは常時実行されるため、負荷の大きいコマンドを置いていると処理量が多くなってしまいます...。使う場面が限られている処理(例: RPGのダンジョンのギミック処理)はそのコマンドを使うタイミングのみ読み込まれるような場所にコマンド部屋を置くようにしましょう。
データパック編
データパックには関数を呼び出すという唯一無二の機能があります。データパックの軽量化テクニックは高難度なものも多いので勉強を重ねてさらなる高速化に生かしましょう。
再帰処理を効率的に使う
再帰処理はデータパックを使う利点の一つです。再帰処理を使うことによってコマンドを簡潔にすることができるだけでなく、軽量化につなげることもできます。具体的には、一度にループコマンドを実行する回数を必要な数にぴったりそろえることで、軽量化することができます。
例えば、ミニゲームで得た得点から順位を計算することを考えてみましょう。以下のような方法が考えられます。
execute as @a[scores={point=1..}] run scoreboard players operation max point >= @s point
execute as @a[scores={point=1..}] if score @s point = max point run scoreboard players set @s rank 1
scoreboard players set @a[scroes={rank:1}] point 0
execute as @a[scores={point=1..}] run scoreboard players operation max point >= @s point
execute as @a[scores={point=1..}] if score @s point = max point run scoreboard players set @s rank 2
scoreboard players set @a[scroes={rank:2}] point 0
execute as @a[scores={point=1..}] run scoreboard players operation max point >= @s point
execute as @a[scores={point=1..}] if score @s point = max point run scoreboard players set @s rank 3
scoreboard players set @a[scroes={rank:3}] point 0
...```
このコマンド列では「スコアが最も高い人の順位を確定させ、その人のスコアを 0 にする」という処理を繰り返しています。が、人数によっては余計な実行が出てしまうことがわかります。これを再帰処理を使うことで「必要な分だけ」の実行にすることができます。
```1.mcfunction
execute as @a run scoreboard players operation max point >= @s point
execute as @a if score @s point = max point run scoreboard players operation @s rank = max rank
scoreboard players add max rank 1
execute if entity @a[scores={point=1..}] run function 1
二分探索を活用する
これは正直言ってお勧めはしない方法ですが確実にコマンド数を減らすことにはつながります。というのも、計算量こそ増えますが書くコマンドの量が極めて多くなる方法だからです。
このテクニックは「特定のスコアボードの値によってまったく異なった処理をする」時に使えます。以下のようなコマンドを考えてみましょう。
execute if entity @s[scores:{id=1}] run summon zombie ~ ~ ~
execute if entity @s[scores:{id=2}] run summon skeleton ~ ~ ~
execute if entity @s[scores:{id=3}] run summon creeper ~ ~ ~
execute if entity @s[scores:{id=4}] run summon wither_skeleton ~ ~ ~
execute if entity @s[scores:{id=5}] run summon spider ~ ~ ~
execute if entity @s[scores:{id=6}] run summon blaze ~ ~ ~
execute if entity @s[scores:{id=7}] run summon enderman ~ ~ ~
execute if entity @s[scores:{id=8}] run summon wither ~ ~ ~
数値によって異なるモブを出現させるよくある処理です。この処理だと常時 8 個のコマンドを実行することになります。しかし、これを 6 個に減らす方法が二分探索です。
execute if entity @s[scores:{id=1..4}] run function 2
execute if entity @s[scores:{id=5..8}] run function 3
execute if entity @s[scores:{id=1..2}] run function 4
execute if entity @s[scores:{id=3..4}] run function 5
execute if entity @s[scores:{id=5..6}] run function 6
execute if entity @s[scores:{id=7..8}] run function 7
execute if entity @s[scores:{id=1}] run summon zombie ~ ~ ~
execute if entity @s[scores:{id=2}] run summon skeleton ~ ~ ~
execute if entity @s[scores:{id=3}] run summon creeper ~ ~ ~
execute if entity @s[scores:{id=4}] run summon wither_skeleton ~ ~ ~
execute if entity @s[scores:{id=5}] run summon spider ~ ~ ~
execute if entity @s[scores:{id=6}] run summon blaze ~ ~ ~
execute if entity @s[scores:{id=7}] run summon enderman ~ ~ ~
execute if entity @s[scores:{id=8}] run summon wither ~ ~ ~
...はい、何をしているのかぱっと見分からないと思います。これを図式化すると下のようになります。
これを見ればわかる通り、mcfunction を 3 段階に分けて半分ずつ候補を削っていっています。この結果として、3 個の関数で 2 個のコマンドが実行されるためコマンドの総和は 6 個になります。
えっ?「たった 2 個しか削減できていない」って?この場合は確かにそうです。しかし、この分岐先が 1000 通りになった時この手法を使うと実行コマンドを何個まで減らせるでしょうか?3 択で当ててみてください。
① 20 個
② 100 個
③ 900 個
実は正解は①です。分岐先が多くなればなるほど計算量は大幅に削減できるのです。上の画像が、大人数を少ない試合数で減らすことができるトーナメント戦の表に似ていると思いますが同じ原理です。もっとも mcfunction を 999 個も使うことになるのですが...。
4.最後に
ここまで基本的な軽量化テクニックを紹介してきました。これらの軽量化方法はほんの一握りで、工夫次第でさらなる軽量化も期待できます。みなさんも各自で「あれ、こうしたら軽くなるのでは?」という方法が思いつきましたらぜひ使ってみてください!
軽量化についてより詳しく・上達したいという方はぜひ「競技プログラミング」に挑戦してみてください!競技プログラミングはいわば「軽量化の競技」。プログラムをいかに軽くするか。知識と発想力が問われます。(唐突な宣伝)