Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save pocketberserker/b98664514798d97cbb01d0cb6de1068f to your computer and use it in GitHub Desktop.
Save pocketberserker/b98664514798d97cbb01d0cb6de1068f to your computer and use it in GitHub Desktop.

Cluster Creators Guideに掲載されたスクリプト付きアイテムの解説(非公式)

メリークリスマス! この文章は クラスター Advent Calendar 2022 (2枚目) 25日目というパワーによって生み出されました。

本資料は「スクリプト付きアイテム」のためのJavaScript入門(非公式)の続編ということで、Cluster Creators Guideで公開されているサンプルコードを読み解いていきたいと思います。 読みにくい部分やわかりにくい点が多々あるかもしれませんが、継続的にメンテナンスして徐々に改善していけたらと思っています……。

  • cluster のアイテムに関する基本的なことは説明しないので、別途調べてください
  • 2022/12時点のスクリプト付きアイテムの挙動を元に記述しています
  • 業務上において得られる知識は用いていないため、非公式とつけています

解説前のひとこと

このサンプルについては、vinsさんが【clusterスクリプト】改変の仕方と読み方という素敵な記事を公開しています。 Creators Guide のサンプルコードをよりわかりやすい形に改変した上で解説されているので、先にこちらの記事を読むと良いかもしれません。

// https://creator.cluster.mu/2022/10/26/script-sample/#X
const speed = 1.0;
const range = 2.0;
const direction = new Vector3(1.0, 0.0, 0.0).normalize();

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = $.getPosition();
    $.state.time = 0.0;
  }

  $.state.time += deltaTime;

  $.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));
});

まずはこのサンプルを読み解いてみましょう。

定数定義 - 数値

スクリプト内の最初のほうには、後ほど使用する定数が定義されています。

const speed = 1.0;
const range = 2.0;

ScriptableItem ではスクリプトの変数を含む内部状態は定期的に再初期化されることが公式ドキュメントの注意点に書かれていますが、定数は変化することのない値なのでこの影響を受けません(仮に再初期化されたところで同じ値です)。 そのため(後述する) $.state に状態を書き込む必要はなく、トップレベルに定数を定義しても問題ありません。

speed は速さ、 range は幅を表す定数です。 これらの定数の数値を変更することでアイテムの動く速さや移動する範囲が変わります。

定数定義 - ベクトル

このサンプルコードにはもうひとつ定数が用意されています。

const direction = new Vector3(1.0, 0.0, 0.0).normalize();

direction は移動する向きを指定するための定数です。 このの x , y , z 成分を変更することで移動する向きを変えられるようになります。 このサンプルでは new Vector3(1.0, 0.0, 0.0) 、つまりX軸方向に1、 Y軸Z軸方向に0という指定なのでX軸方向にのみ移動するようになっています。

new Vector3(1.0, 0.0, 0.0).normalize()normalizeVector3 が持つメソッドです。 まずはCluster Creator Kit Script ReferenceのVector3.normalizeの記述を見てみましょう。 この Script Reference に限らず、一般的に多くのプログラミング関係の公式リファレンスには公式が提供するクラスやメソッドには説明が用意されており、重要な情報が書かれていることも多いのでまずはリファレンスを確認する癖をつけておくと良いでしょう。

https://docs.cluster.mu/script/classes/Vector3.html#normalize normalize

自身の値を正規化します。

Returns Vector3

シンプルな記述ですがなかなか重要な情報が書かれていますね。

自身の値を~しますというのは、この normalize メソッドを呼び出した Vector3 インスタンスの中身が書き変わることを意味しています。 どういうことかというと、前述の direction 定義は以下のように書き換えることもできるよという話になります。

const direction = new Vector3(1.0, 0.0, 0.0);
direction.normalize();

JavaScriptでは const を用いることでその変数への再代入をできないようにしますが、オブジェクトや(クラスの)インスタンスの中身を変更することまでは制限されていません。 なので、 direction.normalize() を実行した後に direction を参照しても direction は正規化した値になっているはずです。

このように、メソッドによってはオブジェクトやインスタンス自身の中身を書き換える形で実装されていることもあるのですが、 定数として扱いたい direction はメソッドを実行して中身を書き換えられてしまって困ります。 そこで Vector3 にはインスタンスの中身の値をコピーした上で別のインスタンスを作成する clone メソッドが用意されています。 clone については後ほど登場するので改めて説明します。

続いては normalize のメソッド名にもなっている 正規化します について説明……といっても、これはベクトルの正規化というそのままの内容になります。 direction を用いた向きの計算ではベクトルの長さを1に揃えていたほうが都合が良い(今回は長さは重要ではない)ので正規化していると考えられます。

ベクトルの正規化って何、という場合にはベクトル 正規化単位ベクトルで調べてみましょう(この場で数学の話を始めると終わらなくなるので割愛します)。

最後に normalize メソッドの戻り値が Vector3 となっている点です。 リファレンスには 自身の値を正規化します と書かれていますが、より詳しく書くなら 自分自身の値を正規化し、自分自身を戻り値として返します となります。 Vector3 には自分自身を書き換えるメソッドがいくつか用意されていますが、これらをいちいち別の文で呼び出すのは面倒なことが多いので、自分自身を返して別のメソッド呼び出しを連続して記述できるようになっていると考えられます。

$ とは

ScriptableItem のコードにはよく $ が登場します。 $ についてもリファレンスに説明があるので、まずはリファレンスを確認してみましょう。

https://docs.cluster.mu/script/variables/_.html

Variable $ [Const]

$: ClusterScript

$ オブジェクトは、Scriptで動作する個々のアイテムを操作するハンドルのインスタンスです。

これだけだとなんのこっちゃなので、まずは1段目から分解してみます。

Variable $ [Const]

  • variable は変数を意味する英単語
  • const は定数を意味する英単語

つまり $ScriptableItem にあらかじめ用意された定数であることがわかります。

$: ClusterScript

ここでは $ がどのようなものなのか記述されており、$ClusterScript であることがわかります。

では ClusterScript とは……となるわけですが、これも公式リファレンスに説明があります。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#getPosition

Scriptで動作する個々のアイテムを操作するハンドルです。

interface についての説明はややこしくなるため省略しますが、重要なのは ClusterScript はアイテムをスクリプトで操作するために必要ないくつかのプロパティやメソッドを持つ存在で、 $ はその ClusterScript の唯一のインスタンスであるということです。 「スクリプト付きアイテム」は $ を通してアイテムとスクリプトを繋いでるのだな、となんとなく把握しておけばひとまず困ることはないでしょう。

ちなみに、唯一のインスタンスと明言できるのは ClusterScript を作成する方法が提供されていないからです。 例えば Vector3 の場合、 new Vector3(1.0, 0.0, 0.0) のようにインスタンスを生成する方法が定義されているのですが、 ClusterScript にはそれがありません。

$.onUpdate

続いてはこの部分。

$.onUpdate(deltaTime => {

});

https://docs.cluster.mu/script/interfaces/ClusterScript.html#onUpdate

onUpdate(callback: ((deltaTime: number) => void)): void

updateループ毎に呼ばれるcallbackを登録します。 スクリプトロード時に最後に登録したcallbackのみが有効になります。

updateループ毎に呼ばれるcallback を言い換えると 更新処理のたびに実行する関数 となります。 スクリプト内で onUpdate に渡した関数が更新処理ごとに呼び出されるからこそ、実行結果がアイテムに反映されなめらかに動いているように見えるわけですね( 実際なめらかかどうかは FPS 依存ですが)。

続いて スクリプトロード時に最後に登録したcallbackのみが有効になります ですが、これは JavaScript の仕様上、メソッドは制限なく呼び出し可能なため onUpdate も何度も呼び出せるのですが、 ScriptableItem の仕様として最後に実行したものだけ有効にしているよ、という話です。

$.onUpdate(deltaTime => {
  // これは実行されない
});

$.onUpdate(deltaTime => {
  // これも実行されない
});

$.onUpdate(deltaTime => {
  // これは最後に登録された関数なので実行される
});

この性質を用いて interact したとき/していないときで callback を切り替える、といった手法を用いることもできそうですが、今回のサンプルコードのようにシンプルなものであれば onUpdate は1回しか呼び出すことはないので「そういうものか」くらいにとどめておいても大丈夫です。

さて onUpdate の説明をひと通り眺めたところで、読み飛ばしていた部分に着目していきます。

onUpdate(callback: ((deltaTime: number) => void)): void

これは onUpdate に渡す関数 callback(deltaTime: number) => void であるべき、と書かれています。 deltaTime は説明が書かれていないので想像するしかないですが、変数名からおそらく Unity の Time.deltaTime ではないかと推測できます。 つまり onUpdate には、前の更新処理から今回の更新処理までの経過時間を引数として受け取る関数を登録してあげれば良いわけですね。

そして戻り値の void は……このリファレンスでは関数が値を返さないことを示すキーワード、くらいに考えておいてください( JavaScript の void 演算子 とは別の話です )。

と、いうわけでようやく onUpdate までの説明が終わったので、ここからは実際に登録している関数の中身をみていきましょう。

$.state

if (!$.state.initialized) {
  $.state.initialized = true;
  $.state.pivot = $.getPosition();
  $.state.time = 0.0;
}

最初からいろんなことが書かれていますが、まずは $.state から確認していきましょう。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#state

state: StateProxy

アイテムごとのstateへのアクセスを提供します。 read/writeアクセスが可能です。stateのプロパティへのアクセスにより、そのプロパティ名をkeyとしてstateへアクセスすることができます。

未定義のプロパティをreadしたときはundefinedが初期値になります。

まず、リファレンスの説明から $.state がアイテムごとに状態を保持したり取得するためのプロパティであることが読み取れます。

続いて $.state がどのようなものか確認するために StateProxy をみてみます。

https://docs.cluster.mu/script/types/StateProxy.html

StateProxy: { [propName: string]: Sendable }

$.state に対して文字列でプロパティを呼び出すと Sendable が取得できる、と書かれています。 そして Sendable は次のように定義されています。

https://docs.cluster.mu/script/types/Sendable.html

Sendable: boolean | number | string | Vector2 | Vector3 | Quaternion | null

つまり、アイテムに状態として保存/読み出しできるのはこの7つであることがわかります。 配列やオブジェクトは対象外なわけですね。

ところで、なぜ $.state を用いて状態を保存しなければならないのでしょう? let で変数を用意し、それに値を代入することでも状態管理は可能なような気がしてなりません。

これについては、定数の節でも書いたように ScriptableItem の仕様が関係しています。

https://docs.cluster.mu/creatorkit/item-components/scriptable-item/

  • スクリプトの変数を含む内部状態は定期的に再初期化されます
    • 変数の変更を永続化したい場合は $.state を利用してください

定数と同じ場所に変数を用意しても cluster 側が変数を再初期化してしまい、あるタイミングから意図しない値が保持されてしまうわけですね。

初期化処理

さて、 $.state が何か確認したところで改めて if 文を見てみましょう。

if (!$.state.initialized) {
  $.state.initialized = true;
  $.state.pivot = $.getPosition();
  $.state.time = 0.0;
}

$.state.initialized で取得した値の論理否定 ( ! ) が true だったらこの処理を実行するようです。

ところで、 $.state.initialized は最初の update ループでは何も設定されていません。 ではいったい、この条件はどう判定されるでしょうか?

ここでヒントになるのが $.state の説明に書かれていた最後の一文です。

未定義のプロパティをreadしたときはundefinedが初期値になります。

つまりこの関数を初めて実行した際の $.state.initializedundefined を取得することになります。 そして undefined を条件式に使うと false と判定され ! で反転すると true になります。 つまり、この if 文の条件は初回 update ループでは必ず実行されることになります。

更に if 文の中で $.state.initialized = true; としていることから、2回目の呼び出し以降は必ず条件が成立せず実行されないようになっています。 このことをわかりやすくするために、 initialized (初期化済みかどうか)というプロパティ名が付けられているわけですね。

ちなみに、このサンプルくらいの短さであれば問題になりませんが、初期化処理がそこそこの分量になる場合は別の関数を定義して呼び出してあげると onUpdate に渡す関数内の処理が読みやすくなります。

const initialize = (deltaTime) => {
  $.state.initialized = true;
  $.state.pivot = $.getPosition();
  $.state.time = 0.0;
};

$.onUpdate(() => {
  // 未初期化状態なら初期化するんやな!
  if (!$.state.initialized) {
    initialize();
  }
});

最後に、 $.state.pivot はアイテムの初期位置で $.state.time は初回 update からの時間経過を記録するプロパティです。 $.getPosition については リファレンスを読んでください。

経過時間の更新

トータルでの経過時間は $.state.timedeltaTime を加えることで実現しています。

$.state.time += deltaTime;

位置の更新

アイテムの位置更新処理は次の一行で行われています。

$.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));

まとまりすぎていて説明しづらいので、すこし読みやすくしてみます。 JavaScript コード上で引数指定部分に改行や半角空白を挿入しても無視されるだけなので、可読性のために字下げすることがよくあります(全角空白はエラーになるので挿入しないでください)。

$.setPosition(
  $.state.pivot.clone().add(
    direction.clone()
      .multiplyScalar(Math.sin($.state.time * speed) * range)
      .applyQuaternion($.getRotation())
  )
);

このコードで最初に実行されるのは次の部分、アイテムの向きや時間経過に応じて位置を決める計算です。

direction.clone()
  .multiplyScalar(Math.sin($.state.time * speed) * range)
  .applyQuaternion($.getRotation())

Math.sin は三角関数 sin に従って結果を返してくれるものです。 Math.sin に与えられる値を x としたとき、 x = 0 のときは 0x が増加していくごとに 1 ~ -1 の値を行き来する特徴を持つ関数です。 サンプルのタイトルが反復移動になっているのはこの Math.sin の性質を表したものになります。

Math.sin に渡す値はトータルの時間経過と速度定数をかけたものになっています。 つまり speed 定数の値を増やせば増やすほどはやく反復移動し、逆に 0 に近づければ近づけるほど反復動作が遅くなります。 speed0 にした場合は Math.sin(0) になり計算結果が常に 0 になるのでアイテムが移動しなくなります。

次に、 Math.sin の結果に range を掛けることで実際に移動する距離を計算しています。 Math.sin から得られる最大値と最小値は先ほど述べた通り 1-1 なので、最大移動幅は range に依存します。 サンプルコードでは range = 2.0 なので -2.0 ~ 2.0 を行き来することになります。

続いて multiplyScalar がどのような処理を行うか確認してみましょう。

https://docs.cluster.mu/script/classes/Vector3.html#multiplyScalar

multiplyScalar(s: number): Vector3

スカラー値sを自身に乗算します。

自身に乗算します とある通り、これをそのまま direction に対して実行してしまうと direction の中身の値が変更されてしまい、期待する定数の振る舞いになりません。 そこで Vector3.clone を呼び出すことにより direction を複製したインスタンスを作成し、それに対して multiplyScalar を実行することで direction に影響を与えることなくベクトル計算を実行しています。 また、この clone().multiplyScalar で取得できる値は複製した Vector3 なので、後続の処理で自身に計算を適用するメソッドを呼び出しても direction には影響しません。 実際、このサンプルでは .applyQuaternion($.getRotation()) を実行することでアイテムの姿勢を考慮して移動方向を決めています。

https://docs.cluster.mu/script/classes/Vector3.html

Vector3

3Dベクトルです。

値を操作するメソッドは基本的に破壊的操作であるため、影響を与えたくない場合は明示的にclone()を呼び出してインスタンスを複製してください。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#getRotation

getRotation

https://docs.cluster.mu/script/classes/Vector3.html#applyQuaternion

applyQuaternion

$.state.pivot.clone().add も基本的には先ほどと似たような話で、初期位置 $.state.pivot は更新したくないので clone で複製し、複製した値に対してベクトルの加算処理を行うことで最終的な position を決定しています。

https://docs.cluster.mu/script/classes/Vector3.html#add

add

そして最後に、計算結果を $.setPosition に渡してアイテムに適用することでアイテムを指定の場所に移動させています。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#setPosition

setPosition

続いてY軸方向の反復移動です……が、実はこのサンプルコードはX軸方向に反復移動するスクリプトの direction 指定が異なるだけです。

// Y軸方向の
const direction = new Vector3(0.0, 1.0, 0.0).normalize();

// X軸方向移動のときのdirection
// const direction = new Vector3(1.0, 0.0, 0.0).normalize();

Z軸方向の反復移動も同様に const direction = new Vector3(0.0, 0.0, 1.0).normalize() に変更されているだけですが、少し指定が異なるだけで動きに変化が現れるのがおもしろいところですね。

ちょっと順番を前後して、先に乗り物サンプルの説明をします。

反復移動までは先ほどと同じですが、今回は「乗り物に乗ると」という条件が付きました。

// https://creator.cluster.mu/2022/10/26/script-sample/#Z-3
const speed = 1.0;
const range = 2.0;
const direction = new Vector3(0.0, 0.0, 1.0).normalize();

$.onRide(isGetOn => {
  $.state.isGetOn = isGetOn;
})

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = $.getPosition();
    $.state.time = 0.0;
    $.state.isGetOn = false;
  }

  if (!$.state.isGetOn) return;

  $.state.time += deltaTime;

  $.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));
});

ポイントは以下になります。

  • 「乗り物に乗ったかどうか」状態を記録する
  • 「乗り物に乗った/降りた」ときに状態を更新する
  • 「乗り物に乗っていない」なら移動させない

乗り物に乗ったかどうかを管理するプロパティはサンプルでは $.state.isGetOn で定義されています。 最初は乗り物に乗っていないので false で初期化されています。 そして、乗っていないときに移動させない処理は if (!$.state.isGetOn) return; のように update ループ内での処理を中断させることで実現しています。

なお、このサンプルでは $.state.time はトータルの経過時間ではなく その乗り物に乗っていた時間 であることに注意してください。 時刻加算処理( $.state.time += deltaTime; )と先の条件文を入れ替えると、乗り物に乗った瞬間に乗り物が瞬間移動してしまうはずです。

最後に乗り物にのったことを記録する方法ですが、これは $.onRide によって関数を登録することで実現できます。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#onRide

onRide(callback: ((isGetOn: boolean) => void)): void

乗ることができるアイテムに乗る・降りるときに呼ばれるcallbackを登録します。アイテムにはRidableItemコンポーネントが付いている必要があります。 スクリプトロード時に最後に登録したcallbackのみが有効になります。

callback として登録する関数には、乗るときに true 、降りるときに false が渡される isGetOn 引数が渡されるようになっています。 これをそのまま $.state.IsGetOn として記録してあげれば、 onUpdate 側で乗り降りの情報を参照できるわけです。

インタラクト起因でアイテムを動かすサンプルは乗り物に乗ったときのものと似たようなコードになっていますが、状態の取得方法が異なります。 具体的には以下の部分です。

$.onInteract(() => {
  $.state.enabled = !$.state.enabled;
});

インタラクトしたときに実行される関数を登録するメソッド onInteract には onRide に存在した isGetOn のような引数は存在しません。 ではどうやってインタラクトした/解除したを切り替えるかというと、前の状態 $.state.enabled の真理値を反転させることで実現します。 こうすることで初期化時( $.state.enabled = false; )はインタラクトしていない状態を表現し、インタラクトしたときに true に切り替わって反復移動実行、インタラクト解除で false に切り替わり反復移動が止まるようになります。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#onInteract

onInteract

このサンプルはややこしいので、後日独立した解説記事を用意します(ここを期待していた方がいらっしゃったらごめんなさい……)

持っても というところがミソです。

// https://creator.cluster.mu/2022/10/26/script-sample/#i-4
const speed = 1.0;
const range = 0.5;
const direction = new Vector3(1.0, 0.0, 0.0).normalize();
const subNode = $.subNode("Cube"); // 説明用にサンプル記事のスクショに書かれているノード名へと書き換えています

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = subNode.getPosition();
    $.state.time = 0.0;
  }

  $.state.time += deltaTime;

   subNode.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range)));
});

手で持てる棒部分は移動せず、棒につながった移動させたいアイテムのみを移動させる必要があります。 これをするためにまず子要素(今回の子要素名は Cube)を取得しなければなりません。 そこで登場するのが $.subNode です。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#subNode

subNode(subNodeName: string): SubNode

アイテムの子・孫要素からsubNodeNameに指定した名前のオブジェクトを探索して取得します。 存在しないsubNodeNameを指定した場合は実行時に無視され、エラーにはなりません。

$.subNode によって取得した子要素は時間経過したところで取得対象が変化することはないので、 const による定数として保持して問題ありません。

残りの処理については、単純な反復処理だと $.getPosition$.setPosition によってルート要素の位置を取得・設定していた部分を subNode.getPosition 等に置き換えてあげるだけです。

ここからはアイテムを回転させるサンプルの紹介です。

// https://creator.cluster.mu/2022/10/26/script-sample/#X-2
const speed = 72.0;
const axis = new Vector3(1.0, 0.0, 0.0);

$.onUpdate(deltaTime => {
  $.setRotation($.getRotation().multiply(new Quaternion().setFromAxisAngle(axis, speed * deltaTime)));
});

回転は反復移動よりもコードがシンプルですね。

定数

speed は回転速度、 axis は回転軸を表す定数です。 axis のベクトル成分を変更することでY軸回転やZ軸回転に変更が可能です。

(ということでY軸回転やZ軸回転のサンプル解説は省略します)

$.getRotation

$.getRotation はアイテムの姿勢を取得するメソッドです。 反復移動のサンプルでも登場しましたが、改めてリファレンスを確認してみましょう。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#getRotation

getRotation(): Quaternion

アイテムの現在の姿勢を取得します。値はアイテムのあるワールド座標系で返されます。

setRotationで指定された値ではなく、移動中のアイテムの姿勢が返されることに留意してください。

今回は Vector3 ではなく Quaternion が取得できるようです。

https://docs.cluster.mu/script/classes/Quaternion.html

クォータニオンです。

値を操作するメソッドは基本的に破壊的操作であるため、影響を与えたくない場合は明示的にclone()を呼び出してインスタンスを複製してください。

はい。

3Dゲームの回転や姿勢計算に用いられるクォータニオンですが、これを扱い始めるとアドベントカレンダーの締め切りに間に合わないので割愛します。 気になった方は腰を据えて調べてみてください。

今回は「回転を制御するのに Quaternion なるものをつかぅているのだな」くらいに考えておけば大丈夫だと思います(?)

回転

new Quaternion().setFromAxisAngle(axis, speed * deltaTime) では回転を加えるための Quaternion を作成しています。

https://docs.cluster.mu/script/classes/Quaternion.html#setFromAxisAngle

setFromAxisAngle(axis: Vector3, degree: number): Quaternion

axisの周りをdegree度回転する値で自身を更新します。

今回のサンプルでは axis = new Vector3(1.0, 0.0, 0.0) なので X軸の周りを speed * deltaTime 度回転する Quaternion が出来上がります。 これを $.getRotation で取得した現在の姿勢を表す Quaternion に乗算することで回転後の姿勢情報が得られます。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#setRotation

setRotation

// https://creator.cluster.mu/2022/10/26/script-sample/#XYZ-2
const speed = 72.0;
const axes =
  [
    new Vector3(1.0, 0.0, 0.0).normalize(),
    new Vector3(0.0, 1.0, 0.0).normalize(),
    new Vector3(0.0, 0.0, 1.0).normalize()
  ];

$.onInteract(() => {
  $.state.case = ($.state.case + 1) % 3;
  $.state.axis = axes[$.state.case];
});

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.case = 0;
    $.state.axis = axes[$.state.case];
  }

  $.setRotation($.getRotation().multiply(new Quaternion().setFromAxisAngle($.state.axis, speed * deltaTime)));
});

単体の回転に比べて記述量が増えましたね……。

回転軸の定義

まずは回転軸をX・Y・Zの順で切り替えるために配列が用意します。

const axes =
  [
    new Vector3(1.0, 0.0, 0.0).normalize(),
    new Vector3(0.0, 1.0, 0.0).normalize(),
    new Vector3(0.0, 0.0, 1.0).normalize()
  ];

axes[0] で X、 axes[1] で Y 、 axes[2] で Z軸回転用のベクトルが取得できます。 つまりあとは、インタラクトするたびに axes[i]i を切り替えるコードを用意すれば良いことになります。

インタラクト時の切り替え

onInteract にインタラクト時の callback を登録すれば良いことは前のサンプルで理解していると思うので、あとは状態を更新していくだけです。

$.onInteract(() => {
  $.state.case = ($.state.case + 1) % 3;
  $.state.axis = axes[$.state.case];
});

現在どの軸を扱っているかを扱うプロパティ $.state.case に1加えれば次の要素を参照できるのですが、それだけだと3要素しか持っていない axes に対して axes[3]axes[4] のようなアクセスを行いエラーになってしまいます。

これを避けるためには 0, 1, 2, 0, 1, 2... のように配列の要素を指定する index を 0 ~ 2 までの整数でループさせる必要があります。 そこで、 % 3 という要素数で剰余を計算して index を指定する方法を用います。 これで仮に $.state.case + 1 が 3 だったとしても、 axes からは 0 番目の要素を取得することになります。

演習問題

最後にちょっとした演習問題を用意してみたので、練習題材が思いつかなかったらやってみてください。

  1. アイテムをX軸方向かつY軸方向に反復移動させてみましょう
    • X, Y, Zの組み合わせはどれでも良いです
  2. インタラクトするごとに逆回転に切り替わる、折り紙製のような風車を作成してください
    • 持っても回転し続ける サンプルに逆回転ギミックが加わる感じです
    • インタラクト時に $.state.axis = $.state.axis.clone().negate(); を実行し、onUpdate では $.state.axis を初期化、参照する形にすれば良いはず?
      • 試してないので間違っていたらごめんなさい
      • Vector3.negate

以下のものはサンプルの知識だけだと厳しいかも。

  1. 乗ったら平面上をゆっくりと円を描くように伊藤する乗り物を作成してください
    • ゆっくりと、は酔い対策です
  2. インタラクトしたら真上に上昇していき、一定の位置に達したら初期位置に戻るアイテムを作成してください
    • 上昇中かどうかを表すプロパティ $.state.rising に用意し、初期値を false に設定しましょう
    • 初期化処理でアイテムの初期位置を記録しておきましょう
    • onInteract の中で、 $.state.risingfalse のときのみ true に切り替えましょう
    • onUpdate の中で、一定の位置に達したら $.state.risingfalse に設定し、アイテムの位置を初期位置に設定しましょう
    • 移動距離やスピードはお任せします
      • 無理に経過時間を使う必要はないです
  3. インタラクトすると真上に飛び上がり、一定の位置に達したら落ちてくるボールのようなものを作ってください
    • 一定の位置にたどり着いたら、上昇と同じ速度で降りてくる、で構いません
      • ちゃんとやるなら重力加速度を参考にすると良いでしょう
    • インタラクトするとZ軸方向に反復移動する を改変するでも良いです
      • interact による停止ではなく、位置による停止に切り替える必要があります
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment