メリークリスマス! この文章は クラスター 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()
の normalize
は Vector3
が持つメソッドです。
まずは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(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
までの説明が終わったので、ここからは実際に登録している関数の中身をみていきましょう。
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.initialized
は undefined
を取得することになります。
そして 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.time
に deltaTime
を加えることで実現しています。
$.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
のときは 0
、 x
が増加していくごとに 1 ~ -1
の値を行き来する特徴を持つ関数です。
サンプルのタイトルが反復移動
になっているのはこの Math.sin
の性質を表したものになります。
Math.sin
に渡す値はトータルの時間経過と速度定数をかけたものになっています。
つまり speed
定数の値を増やせば増やすほどはやく反復移動し、逆に 0
に近づければ近づけるほど反復動作が遅くなります。
speed
を 0
にした場合は 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
はアイテムの姿勢を取得するメソッドです。
反復移動のサンプルでも登場しましたが、改めてリファレンスを確認してみましょう。
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 番目の要素を取得することになります。
最後にちょっとした演習問題を用意してみたので、練習題材が思いつかなかったらやってみてください。
- アイテムをX軸方向かつY軸方向に反復移動させてみましょう
- X, Y, Zの組み合わせはどれでも良いです
- インタラクトするごとに逆回転に切り替わる、折り紙製のような風車を作成してください
持っても回転し続ける
サンプルに逆回転ギミックが加わる感じです- インタラクト時に
$.state.axis = $.state.axis.clone().negate();
を実行し、onUpdate
では$.state.axis
を初期化、参照する形にすれば良いはず?- 試してないので間違っていたらごめんなさい
- Vector3.negate
以下のものはサンプルの知識だけだと厳しいかも。
- 乗ったら平面上をゆっくりと円を描くように伊藤する乗り物を作成してください
- ゆっくりと、は酔い対策です
- インタラクトしたら真上に上昇していき、一定の位置に達したら初期位置に戻るアイテムを作成してください
- 上昇中かどうかを表すプロパティ
$.state.rising
に用意し、初期値をfalse
に設定しましょう - 初期化処理でアイテムの初期位置を記録しておきましょう
onInteract
の中で、$.state.rising
がfalse
のときのみtrue
に切り替えましょうonUpdate
の中で、一定の位置に達したら$.state.rising
をfalse
に設定し、アイテムの位置を初期位置に設定しましょう- 移動距離やスピードはお任せします
- 無理に経過時間を使う必要はないです
- 上昇中かどうかを表すプロパティ
- インタラクトすると真上に飛び上がり、一定の位置に達したら落ちてくるボールのようなものを作ってください
- 一定の位置にたどり着いたら、上昇と同じ速度で降りてくる、で構いません
- ちゃんとやるなら重力加速度を参考にすると良いでしょう
インタラクトするとZ軸方向に反復移動する
を改変するでも良いです- interact による停止ではなく、位置による停止に切り替える必要があります
- 一定の位置にたどり着いたら、上昇と同じ速度で降りてくる、で構いません