リソース分野で縁の下の力持ち molangを使いこなす

 カスタムモブやカスタムパーティクルをつくるときに、モブの体のパーツの動きやパーティクルの軌道はどうやって決めているのでしょうか。実はmolangというminecraft独自の言語を用いていくつかのパラメータからいろんなものの動きなどを計算しています。
 molangはアニメーション(ビヘイビア/リソース)、アニメーションコントローラ(ビヘイビア)、レンダーコントローラ、パーティクルなどに使われています。アドオンを主に構成するjsonはプログラミング言語ではなくただのデータの塊なので計算はできません。そのためmolangというものを用いてjsonにデータとして計算式を格納しておき、mimecraftのシステム側で計算をします。とにかくアドオンにおいて計算が必要なところにmolangは使われています

 計算、というほどなのでもちろん多少の数学力は問われます高校で習う三角関数(三角比じゃなくて)のサイン、コサイン、タンジェントは特に必要です。ですが今回は数学が得意でなくてもできるだけ理解できるよう簡単にしようと思います。簡単と言ってもアドオンにおいてちゃんと戦力になるぐらいには説明します。

■ 使える演算子

・四則演算(+, -, *, /)

 だいたいのプログラミング言語と同じような感じです。念のため言っておくとこの世界では「かける」は「*」で「わる」は「/」です。

・カッコ

 カッコは普通に「()」です。もちろん半角なので気をつけてください。

・比較演算子(!, &&, ||, <, <=, >=, >, ==, !=)

 「!変数 == 値」と書いて否定です。
 「条件1 && 条件2」で「条件1 かつ 条件2」、「条件1 || 条件2」で「条件1 または 条件2」というふうになります。記号「|」はわりとレアだったりするのでリファレンスからコピーしてユーザー辞書に登録するのも手です。
 「<」と「>」は分かりますよね。比較する値自身は含みません。「<=」は「≦」、「>=」は「≧」です。比較する値自身も含みます。順番は「!=」みたいにイコールが必ず後と覚えてもいいですし、JavaScriptをやっているならアロー関数と混ざらないようにと覚えてもいいでしょう。
 「==」は「等しい」です。ふたつ書かないとダメです
 「変数 != 値」としても否定になりますが、コマンド出身のひとは「=!」と逆にしないように気をつけてください。

・二項演算子(?)

 「二項演算子」という名前に自信がないわけではありません。実際に「?」を使ってif文が書ける、すなわち条件分岐ができます。「条件 ? 満たすときの処理 : 満たさないときの処理」という感じで書きます

・戻り値(return)

 molangでは計算ができますが、変数へ値を格納することもできるので、変数に値を格納しただけで最終的な値が必要なのに値を出せていないというときに使います。あまり多用するものではないのでよく分からなかったら無理に使う必要はありません。

・this

 このthisを含む式の処理をする前の現在の値です。

■ 関数

・冪(べき)乗 math.pow(底, 指数)

 3²(3の二乗)とかです。底を指数回かけます。3の二乗くらいだったら2回かけても別にいいかもしれませんが、ゴツい足し算とかを2回書くのはキツいのでこれを使うのをおすすめします。
 3の二乗ならmath.pow(3, 2)ですね。

・平方根(ルート) math.sqrt(値)

 √(ルート)です。二乗したらその中身の値になる数。高校で指数をやったひとは分かるかもしれませんが別にmath.pow(値, 0.5)でも通用すると思います。

・絶対値 math.abs(値)

 その値が0からどれだけ離れているか。入力した値が全部プラスになって出てくるだけです。全部マイナスにしたかったら「-math.abs(値)」と書けばいいですよね?

・床関数 math.floor(値)

 入力した値を越えない最大の整数です。プラスの値なら小数部分の切り捨て、マイナスの値なら切り上げです。1.8なら1.0、0.3なら0.0、-1.2なら-2.0、1.0なら1.0──といった感じです。迷ったら初めに書いた定義をよくよく噛み砕いてください。
 学校では「ガウス記号」として学ぶ概念です。

・天井関数 math.ceil(値)

 入力した値を下回らない最小の整数です。プラスの値なら小数部分の切り上げ、マイナスの値なら切り捨てです。1.8なら2.0、0.3なら1.0、-1.2なら-1.0、1.0なら1.0──といった感じです。迷ったら初めに書いた定義をよくよく噛み砕いてください。
 気づいたひとがいるかもしれませんが、

xが整数でないとき math.ceil(x) - math.floor(x) = 1 

xが整数であるとき math.ceil(x) - math.floor(x) = 0

を満たします。1と0が逆になるようにするにはどうすればいいんでしょうかね?? すぐにひらめいたらすごいと思います。

● 高校で習うもの

・正弦関数(サイン) math.sin(値)

 ここで詳しくは説明しません。学校で習うか独学で学んでください。よく分からなくてもグラフだけは知っておきましょうめちゃくちゃ使います。0で0、90で1、180でまた0、270で-1、360で0──となめらかに繰り返す周期関数です。

・余弦関数(コサイン) math.cos(値)

 ここで詳しくは説明しません。学校で習うか独学で学んでください。よく分からなくてもグラフだけは知っておきましょう。こちらもめちゃくちゃ使います。0で1、90で0、180で-1、270でまた0、360で1──となめらかに繰り返す周期関数です。math.sin(x + 90)でもありますね。
 タンジェントはありません。必要ないです。math.sin(値) / math.cos(値) でいいですね。(長いっちゃ長いけど)(そもそもあんまり使わない)

・指数関数(底がe) math.exp(値)

 eの正体は学校で習ってください。独学するひとは"ネイピア数"、"自然対数の底"などとググればいいでしょうか。値は2.71828182845904……と円周率のように循環しません。
 そしてeのナントカ乗がmath.exp(x)です。math.pow(e, 値)とは書けません。
 何に使うか?「コーヒーが冷めるときの温度」を底がeの指数関数で表せるんですよ。この身近に潜んでいるような関数を使えばものによってはリアルなものが作れると思います

・自然対数関数 math.ln(値)

 logの底がeのやつです。eのx乗の逆関数です。詳しくはまた高校数学で。あとたぶん理系を選択しないと教えてくれないと思います。ググるなら"自然対数"、対数が分からなければそれからググるといいでしょう。
 分からなくてもグラフの形は知っておきましょう。わりと使える形をしてます。

● 特殊な関数

・最大値関数 math.max(値1, 値2)

 ふたつのうちどちらか大きいほうを返す関数です。math.max(2,3)なら3、math.max(x,1)なら下のグラフのようになります。

・最小値関数 math.min(値1, 値2)

 ふたつのうちどちらか小さいほう返す関数です。math.min(2,3)なら2、math.min(x,1)なら下のグラフのようになります。

・乱数関数 math.random(最小値, 最大値)

 入力した最小値以上最大値以下の小数をランダムで出力します。

・四捨五入 math.round(値)

 入力した値を四捨五入します

・Trunc関数 math.trunc(値)

 入力した値の小数部分を切り捨てます床関数との微妙な違いに気をつけてください。1.8なら1.0(床関数と同じ)、0.3なら0.0(床関数と同じ)、-1.2なら-1.0(天井関数と同じ)、1.0なら1.0──といった感じです。
 無理やり床関数と天井関数を用いて表すなら

x > 0 ? math.floor(x) : math.ceil(x)

と書けます。

・モジュラー関数 math.mod(値, 除数)

 難しそうな名前ですが、要は値を除数で割った余りを出します。math.mod(13, 7)で6、math.mod(-22, 9)で5、math.mod(11, -3)で2(もしかしたらエクセルのように-1かもしれません)、math.mod(x,3)で下のグラフのようになります。

・Clamp関数 math.clamp(値, 最小値, 最大値)

 シェーダをやっていれば知っているかもしれません。値が最小値を下回ったときは最小値に、最大値を上回ったときは最大値になります

・Lerp関数 math.lerp(開始値, 終了値, 0_to_1)

 これはよく分からないですね……シェーダのhlslで出てきますがglslのmix関数と同じです。
 シェーダでは0_to_1のところが入力になっていて、その値が0のときに開始値、1のときに終了値となるものです。
 たぶんほとんど使わないです。

・Lerprotate関数 math.lerprotate(開始値, 終了値, 0_to_1)

 これはもはやわかりません。「0_to_1を経て開始値から終了値にかけて最小の円周方向へ線形補間します」

〇 molangを組み立てるコツ

●条件文を組み立てる

・bool型の条件

 「query.is_〇〇」なら中身はboolなのでそれをそのまま書くだけで、そのクエリを満たすかどうかの条件がつくれます。満たさないときに何かしたい場合は「!」を最初につけるんでしたね。ちなみにクエリには中身に数値を格納しているものもありますが、それをそのまま書いた場合「0でないかどうか」になります。

・数値が関わる条件

 「variable.number」という変数があったとしましょう。中身は数値です。
 「1.0に等しい」という条件は「variable.number == 1.0」と書きます。
 「1.0より大きい」という条件は「variable.number > 1.0」となります。「小さい」ときは「<」ですね。
 「1.0以上」という条件は「variable.number >= 1.0」、「以下」なら「<=」を使いましょう。
 右辺に変数があってもいいです「variable.number1 == variable.number2」し、左辺で演算をする「variable.number1 / variable.number2 >= 0.5」こともできます。

・複数の条件が関わってくる場合

 複数の条件が関わる、つまり「条件1と条件2を両方とも満たす」といいたような条件です。この場合は論理演算子「&&」「||」などを使います。
 「条件1と条件2を両方とも満たす」ときは「条件1 && 条件2」と書きます。
 「条件1と条件2のうち少なくともどちらかを満たす」ときは「条件1 || 条件2」と書きます。
 そして当然「条件1と条件2が等しい」ならば「条件1 == 条件2」と、「条件1と条件2が異なる」ならば「条件1 != 条件2」と書きます。

●関数を組み立てる

 アニメーションってどう動いているかわかっていますか?
 たとえば歩いているときの腕の動きなら、歩き始めると同時に腕が振り子のように前、後ろと運動します。それはもちろん「この部分のアニメーションは何のアニメーションにしますか?→腕のアニメーションにする」というただ用意されている選択肢を選ぶだけなんてことはありません。それぐらい簡単だろうとナメてアドオンにかかってくるひとが多数いますが、そんなことがあっては拡張性が全くありませんからね。
 では、どう動くかというと、「関数」を組み立てて動かしています。ナメてもらいたくないので難しそうなオーラ全開で言いますが、誰も手を出さなくなっては困るので簡単に説明します。さきほどの「歩き始めたと同時に腕が動く」例で説明すると、歩き始めてからの時間に応じて腕がどれだけ傾いてどれだけ動いたかを指定します。数tickで肩を軸に腕のパーツがこれくらい傾いて──といった感じです。真面目なことを言うと、時間によって腕の角度が決まるので腕の角度は時間の関数であると言えます。中学──2年ぐらいですか?もっと前かな、「yはxの関数である」という呪文を習ったと思います。さらに同時期くらいに関数をグラフにすることを学んだと思います。そのどちらも学んでないという場合はこれからイバラの道です。学校で習うのを待つか、中学生向けの教育サイト(?)などで独学で学んでください。さすがにこれ以上簡単にはできません。

 さて、このまま腕を動かす例で話を進めていきましょう。アニメーションを作るときに、回転度でキューブの傾きを直接決められるので「時間によって腕の角度がどれくらいであるか」を決める関数をつくりましょう。
 まず腕は右腕で、歩き始めたときに先に前へ動くものとします。スローモーションで時間によって動くさまを頭のなかでイメージしましょう。
 歩き始め、はじめの位置から角度を増やしていきます。ある程度傾いたら戻っていきますね。角度が減っていきます。そのまま胴体の横を通りすぎていって後方へ傾き始めます。そしてまた一定の角度(前の時と同じとしましょう)まで傾くと前方に向かって戻ってきます。そして、胴体の真横にくればふりだしに戻って繰り返しとなります。胴体の真横に来たときの腕の角度をゼロ、前に動いたときは角度がプラス、後ろはマイナスとすると角度は「0→+→0→-→0→……」と繰り返しますね。この角度の変化をグラフにします。横軸は時間、縦軸は角度でグラフにしましょう。つまり、グラフの右に進めば進むほど時間が進んでいき上下はまさに角度が増えたり減ったりを示します。
 さて、どんなグラフを思い浮かべたでしょうか? 下のようなグラフでしょうか?
 しかし、このグラフで関数をつくると、腕が上がりきったときにカクッと戻っていきます。そしてこのカクカクとしたグラフは書くのが非常に難しいです。
 ちなみに上図のグラフを、変数をtとしてmolangで書くとこうなります:

math.pow(-1,math.floor((t-1)/2)+3)*(t-2(math.floor( (t-1)/2 )+1))

 さらにおまけで、ちゃんと数学の式として書くとこうなります:
 やってられないですね。数学アレルギーの方、ごめんなさい。
 もちろんこれは間違いということになります。他にも、あの上図の関数のように上下を繰り返す関数はないでしょうか。心当たりがなければ上の関数紹介の節に戻って探してみましょう。実はあそこにあります。

 わたしが重要だと言ったあの関数、高校で教わるけど正体がイマイチよくわからないあの関数(数学好きなら分かってほしいものです)、数学が苦手だとイマイチ頭に入ってこないのにこういった界隈ではなにかと重要になってくるあの関数です!
 正解は、正弦関数、math.sin()です。上で紹介している通り、この関数単体で「0→+→0→-→0→……」を実現してくれます。それと同時に腕が上がりきってもカクッとせずなめらかに戻っていきます
 どうですか? 画期的な関数でしょう?

 これで以上です!──というわけにはいかないんです。実際「math.sin(query.modified_distance_moved)」とだけ書くと動かないようです。いいえ、動いてはいます。ただ動く角度が小さすぎただけなんです。
 上でもう一度math.sin()のグラフを見てみましょう。縦には-1から1までしか動いていません。要するに、math.sin()が出力する値は-1以上1以下ということです。アニメーションで指定しているのは腕の角度なので、腕は-1度から1度しか動いていないんです
 腕がちゃんと振れるように腕の最大角度をmath.sin()にかけてあげましょう。最大角度が30度なら「30*math.sin(query.modified_distance_moved)」ですね。グラフで言うなら縦に30倍伸びます
 さて今度は動くでしょうか。ちゃんと目につくほど大きく触れています──が、どうやら遅いですか?
 グラフで説明します。腕が振れる速度を上げる→単位時間あたりのグラフが上下する回数を増やす→グラフを横にA倍縮めるということですが、イメージはつきますか?
 グラフを縦にA倍伸ばすときは関数全体にAをかけたので、それと似たノリで、グラフを横にA倍縮めるには変数すべてにAをかけます。つまり腕の例で言えば「math.sin(38.17*query.modified_distance_moved)」ということです。もし変数が複数あっても全部の変数に、変数だけにかけてください。でないと変にグラフが変わってしまいます

× math.sin(A*t) + math.cos(t)
○ math.sin(A*t) + math.cos(A*t)

× A*(t+2)
○ A*t + 2

 このついでにその他のグラフ編集の方法を教えていきます。
 グラフを右にAだけずらしたいときは、すべての変数から、変数だけからAを引きます。左の場合は足せばいいですね。

× math.pow(t, 2) - A
○ math.pow(t-A, 2)

 グラフを上にAだけずらしたいときは、関数全体からAを足します。下なら引きます。

 他にも回転とかできますが、molangでできなかったりそもそも難しかったりするので紹介しません。できないということだけは述べておきます。マイクラ関係なしに気になった場合はググってみてください。

 これでだいたいすコツはすべて伝えきったと思います。グラフをイメージすればわりと簡単だったりします。あとは、アニメーションを一次元ずつ分けて考えられるかという空間把握能力(?)ですね。立体の問題が苦手だとかなりキツいです。
 あと、グラフを書くのも場合によっては難しかったりするので、そこはあなたの数学力次第です。数学用のグラフ描画ツールを持っておくといいと勧めようとしましたが、そのツールを使うにあたってやっぱり多少の数学力がいるかなと思いました。

 どうしても数学力がなくて、どうしても数学が苦手という場合は、すでにバニラで使われている関数をコピーするなりして対応しましょう。そもそもよほど変なモブを追加しない限り特殊なアニメーションはないでしょうし、アドオンに手を出してこの記事を読むほど意欲があるなら絶望的な数学力ということはあまりないと思っています。

 molangが余裕となったらこのパーティクルのオフセットを、二項演算子無しで書いてみてください。

MinecraftBEアドオン-ヒント倉庫

MimecraftBEのアドオン作りで困ったときにここで解決できるようにと個人によって作られたものです

2コメント

  • 1000 / 1000

  • Ryu uuu

    2022.01.15 02:07

    lerpは線形補間のことです 書かれている解釈とほとんど違いはありませんが、厳密にみていくと違いがあります 与えられた値から傾きを近似して、与えられた値全てにできるだけ近くなるようにな直線(1次関数)を与えることだったはずです
  • かたわれ

    2021.11.13 06:48

    二項演算子(?) ではなく 三項演算子(?:) ではないでしょうか?