【clusterスクリプト】改変の仕方と読み方
はい1/23にcluster本がでます、vinsです。
ついにclusterにもスクリプト(Java Script)がやってきました! 制限やクセもあるとはいえ、特にワールドクラフトで動きのあるアイテムを実現するには必須。
今回はclusterでスクリプトをどう使っていくか、説明やヒントを出していきたいと思います。
Unityなら、おなじみの「テンプレートワールド」に入っている「Minimal Sample」あたりをベースにスクリプトのテストをやるのが無難でしょう。
あと、そもそもスクリプトで何が新しくできるようになったかは、以前書いたこちらの記事を見てくださいね。
スクリプトを書く準備
スクリプトをどこにどう書いていけばいいかはcluster公式の記事に出ています。下記の記事を読んで、スクリプトを書く準備を済ませておいてください。
ワールドクラフトでやる場合
こちらの記事の前半、「F12を押すと左上にスクリプトエディタが表示されます」までやりましょう。なおこの時点では、スクリプトを書く欄がカラッポでもかまいません)
Source Codeはカラッポでもよい
Unityでやる場合
こちらの記事の前半、「Scriptable ItemコンポーネントのSource Code欄に以下のコードを入力しましょう」の前までやりましょう。
Scriptable Itemの子にハコを
なおワールドクラフトのほうの記事には明確に書かれていませんが、Scriptable Itemの子にはとりあえずCube(ハコ)を付けておくとよいでしょう。シンプルな形で、動かしたときわかりやすいです。
単純なハコがいちばんわかりやすい
「基本的な動きをつくることができるスクリプトのサンプルコード集」より引用
・空(から)のオブジェクトをつくる(上の例ではOther)
→Scriptable Itemをつける(上の例ではOtherに)
→その「子」に、見た目の部分(上の例ではCube(ハコ))
こういう形にしてください。
スクリプトの注意点
ここではいくつかの注意点を説明します。
ワールドクラフトでの注意点
まず「Scriptable Item」をUnity上でつけて「アイテムアップロード」する必要があるので、ワールドクラフトですべてを完結させることはできません。最初だけはUnityをすこし使わないといけないわけです。
そして、PCでしかワールドクラフトでのスクリプトの編集はできません(スマホ・VRではできない)。
また、ワールドクラフト上だと、スクリプトを修正して「アップロード」ボタンを押すたび新しいアイテム扱いになるのが少し面倒……ここは不要になった古いアイテムを非表示にできるなど、早く修正が入るといいですね。
ファイル「関連づけ」の注意点(Windows)
WindowsではJava Scriptのファイル(拡張子がjsのファイル)を開くと 「JavaScriptを編集する」ソフトではなく「JavaScriptをWindows上で実行する」ソフトが起動してしまうことも多いです。
(ワールドクラフトから「エディタを開く」を押してもそうなります!)
なので「js」ファイルを編集用のアプリに関連づけておきましょう。サクラエディタやVS Codeなどが有名ですから、ネットで検索して先にインストールしておくとよいですね。
(わからなければ「ファイル 関連付け」とかでGoogle検索してください! 「拡張子」ってなに? という人も検索してみてくださいね!)
Unityでの注意点・ポイント
一番大事なのは、「Unity上ではスクリプトは動かない」ということです。ワールドアップロードしてcluster上で確認する必要があります。
Unity上ではスクリプトは動かない
また、Scriptable ItemのSource Code Assetに直接ファイルを指定したい場合は、
・まずUnityのプロジェクトに拡張子が「js」のファイルを入れる。
→そしてSource Code Assetのところにドラッグ&ドロップなどで指定する
この流れを覚えておくと将来的に便利です。
jsファイルをUnityのプロジェクトに入れた例。慣れてきたら直接Scriptable Itemに書き込むより、この形のほうがJavaScriptのファイルを管理しやすい
それをScriptable itemのSource Code Assetに指定した例
最初は公開されているスクリプトのコピペ・改変から
どんなジャンルでもそうですが、コピペ・改変からはじめるのはムズかしいモノをはじめるときに有効ですね。
コピペ大事
フツーにモノが上下左右に動くスクリプトで十分、という方なら「基本的な動きをつくることができるスクリプトのサンプルコード集」に出てくるものをコピペすれば大体OKです!
ただ、ちょっとムズかしい書き方をしているところもあるので、今回は↑の「X軸方向に反復移動する」をvinsが改造したものを使うことにします。
const hayasa = 1.0;
const nagasa = 2.0;
const muki = new Vector3(1,0,0);
$.onUpdate(deltaTime => {
if (!$.state.shokika) {
$.state.shokika = true;
$.state.ichi = $.getPosition();
$.state.time = 0.0;
}
$.state.time += deltaTime;
let _ichi = $.state.ichi.clone();
let _muki = muki.clone();
const nagasa_now = Math.sin($.state.time * hayasa) * nagasa;
_ichi.add(_muki.multiplyScalar(nagasa_now));
$.setPosition(_ichi);
});
UnityならScriptable ItemのSource Code欄に。ワールドクラフトなら画面に出ているスクリプトエディタに貼り付けます。
UnityでScriptable ItemのSource Codeに貼りつけた場合。全体はスクロールしないと表示されない
Movable Itemも要る
動くモノにはMovable Itemを
このあと、Movable Itemをつけるのだけは忘れずに。スクリプトでアイテムを動かしたい場合は必須です。
自動でRigidbodyもつきますので、Use Gravity(重力を使用)をOFFに、Is KinematicをONにするとよいでしょう。重力で動かれると色々面倒になるので。
ちなみにMovable Itemをつけないとこんなことに。「そんな何度も言わんでも」的な……
さて、Unityならワールドアップロードし、ワールドクラフトなら「アップロード」を押して更新された新しいアイテムを配置してみましょう。
ちゃんとハコが左右に動いているでしょうか?
ワールドクラフトでもこのように動きを実現できる
改変するときの注意
さて、改変です。
まずはスクリプトの先頭近くにある数字を変えていくのがわかりやすいでしょう。
const hayasa = 1.0;
const nagasa = 2.0;
hayasa(速さ)を3.0に、nagasa(長さ、動く幅)を1.0にしてみましょう。
const hayasa = 3.0;
const nagasa = 1.0;
こんな風になるはずです。
注意ポイント
- とにかく「全角」で入力しないこと! エラーになります。全角英数字はまだ気づきやすいですが、「全角スペース」は後でミスを発見しづらいこともあって大変です!
- 元々あった、前後の部分まで変えてしまわないこと
- 半角スペースはあってもなくてもいい
改変するときは、この辺りに気をつけてください。
const hayasa = 3.0;
const nagasa = 2.0;
const hayasa 1.0;
const nagasa = 2.0
↑の2例のように失敗しないでくださいね。
ワールドクラフトなら、修正したらすぐに動きが変わるはずです。
Unityならまたワールドアップロードして、cluster上でプレイしてみましょう。
動きが速く、小さくなったでしょうか?
スクリプトの中身を見ていく
スクリプトは大きく「準備」「メイン」の2部分に分けて理解するとよいです。
const hayasa = 1.0;
const nagasa = 2.0;
const muki = new Vector3(1,0,0);
「準備」では、スクリプト内で使いたい「定数」を指定します。スクリプトの中で変わらない、ずっと同じ値のデータですね。
スピードや動きの大きさなどは、スクリプトの先頭にまとめて「定数」として書いておくとあとで改変しやすくなります。
ここで1行目・2行目の意味はさっきの改変でなんとなくわかると思いますが、3行目はちょっとややこしいですね。いまはとりあえずスルーしておいてください。あとで説明します。
あと ; というのは「この行はここで終わり!」という意味です。
JavaScriptでは書かなくても動くことが多いようですが、意識的につけたほうがミスが少なくなりやすいように思います。
(JavaScriptではない、C#などでは ; が必須なので、今からクセをつけておくと将来ほかのプログラムの勉強をはじめたときに役立つのもあります)
アイテムの子を取得することも多い
このスクリプトには書かれていないですが、「準備」の部分で「アイテムの子」を取得しておくことも多いです。$.subNodeというのを使います。
ここで取得しておけば、あとで「アイテムの子」の位置などをスクリプト内でいじれるようになります。アイテムの子を自由に動かせるのは、スクリプトの強みの1つですね。
メイン部分へ
そしてメインの部分、ここで実際に動かしていくわけです。
長く見えますが、パーツに分けてすこしずつ説明していくので大丈夫ですよ。
$.onUpdate(deltaTime => {
if (!$.state.shokika) {
$.state.shokika = true;
$.state.ichi = $.getPosition();
$.state.time = 0.0;
}
$.state.time += deltaTime;
let _ichi = $.state.ichi.clone();
let _muki = muki.clone();
const nagasa_now = Math.sin($.state.time * hayasa) * nagasa;
_ichi.add(_muki.multiplyScalar(nagasa_now));
$.setPosition(_ichi);
});
最初は
$.onUpdate(deltaTime => {
と書いてあり、終わりが
});
となっています。
このonUpdateというやつの始まりと終わりは、毎回このまま書いてください。コピペでOKです。
(他のプログラムやスクリプトに慣れた人だと、ちょっとヘンな書き方だと思うかもしれませんが……)
2種類のスクリプトつきアイテム
スクリプトつきアイテムは、
- A.「onUpdate(毎フレームの処理)」があるタイプ。
- B.「onInteract(クリック/タップしたら)」や「onRide(乗ったら)」や「onGrab(つかんだら)」や「onUse(使ったら)」があるタイプ
Bタイプの代表例、ドア
初心者のうちは、このAとBのアイテムに分けて考えるとよいでしょう。
フクザツな動きをさせたいならA。
ドアなど、クリックしたときに何かさせたいならBですね。
今回のアイテムはAのタイプですね。「onUpdate(毎フレームの処理)」が入っています。
clusterに限らず、アプリでは1/60秒や1/30秒など、非常に短い時間の間に画面を書き換えたり計算したりすることをつづけます。それの1回ぶんを「フレーム」といいます。
(3Dゲームをよくやる人にはおなじみかもしれません)
onUpdateが書いてあるスクリプトは、毎フレーム(つまり1/60秒に1回とか、1/30秒に1回とか)処理が行われます。これによって、非常になめらかな動きをさせることができるわけです。
onUpdateを使いこなすとなめらかでフクザツな動きが可能に
また、deltaTimeと書いてある部分も大事です。このdeltaTimeには前回のフレームから何秒たったかの情報が入っています。
deltaTimeの役割
1/60秒に画面を1回書き換える予定でも、とても処理が重くなったときは1/10秒に1回しか書き換えられないこともあります。例えば「1フレームに1m進む」という感じで書いてしまうと、元々は1秒に60m進んでいたモノが10mしか進まなくなってしまうのです。
それを避けるため、「前回から何秒経ったのか?」という数字がdeltaTimeでわかるようになっています。この数字をかけ算することで、ほぼズレずに「秒速○○mで進む」のような指定ができるようになるわけです。
(なお、deltaTimeには普通0.033とか0.016とかとても小さい数字が入っています。1/30秒とか1/60秒とか、とても短い間隔でonUpdateが呼ばれるわけですからね)
メインの処理を見ていく(if)
いよいよここからが本番ですね。
まずは「初期化」しているところです。
if (!$.state.shokika) {
$.state.shokika = true;
$.state.ichi = $.getPosition();
$.state.time = 0.0;
}
1行目、ifとあります。「もし」ということですね。
そしてカッコの中に「もし~~だったら」という条件が書いてあります。「!」のあとに「$.state.shokika」(初期化)となっていますね。
「!」はスクリプトだと「否定」という意味です。つまりこの場合、「もし~~でなかったら」になるわけです。
スクリプト内の ! はビックリしているわけではない。「否定」という意味
残っているのは「$.state.shokika」ですね。
これはさっき出てきた 「定数」ではなく、「変数」 です。
最初に改変したhayasaやnagasaのように、スクリプトの中でずっと値が変わらないものは定数と呼ばれます。しかし、
- 途中で変わる(変数)
- しかも値をずっと保存しておきたい
こういうものは 「$.state.」とつける必要があるのです。
この記事では、今後これを「state変数」と呼ぶことにします。
途中で中身が変わるもの、値を保存したいものには$.state.とつける
今回は「shokika」、つまり「初期化されたかどうか?」という値を記録しています。
つまり「!」とあわせて、「if (!$.state.shokika)」は 「初期化されていなかったら」という意味になるわけです!
初期化されていない場合のみ、{ }にくくられた3行を実行します。
次はその3行を見ていきましょう。
メインの処理を見ていく(初期化処理)
$.state.shokika = true;
$.state.ichi = $.getPosition();
$.state.time = 0.0;
さて{ }にくくられた3行、つまり初期化処理です。
まずは、 さっきも出てきた $.state.shokika に「true(ONという意味)」の値を入れています。
「イコール」というと「左右が等しいかをチェックしているのかな?」と思うかもしれませんが、スクリプトにおいて 「イコール1つ」は「値を入れる(代入する)」という意味です! 大事なので絶対におぼえてください。
この1行目によって$.state.shokikaがtrue(ON)になる、つまり「初期化された」ことが記録されます。
よって、次のフレーム以降は{ }の中身が実行されないようになります。
「if (!$.state.shokika)」、つまり「もし初期化されていなかったら~」が成立しなくなるからですね!
ちなみにイコール2つだと……
「左右が等しいかをチェック」するときには==とイコールを2つつなげます。
if(speed == 2.0) とかいう感じです。今回は出てきませんが。
$.state.ichi = $.getPosition();
次の行では、state変数のichiに $.getPosition() というものを入れていますね。
この$.getPosition()は、いまのアイテム位置の情報をゲットします!
初期化のときにこれが実行されるので、「一番最初の位置」を$.state.ichiの中に記録できることになります。これを記録しておくのは、あとで大事になってきます。
$.state.time = 0.0;
3行目で、state変数のtimeに0.0を入れています。
開始してから何秒たったかを記録するのにつかいます。
この3行をまとめると
- 初期化されたことを記録
- アイテムの初期位置を記録
- 時間を計測するための準備
これをやっている3行ということですね。
メインの処理を見ていく(時間の記録)
では次の行に行きましょう。
$.state.time += deltaTime;
ここはシンプルですが、次から来るややこしいパートのための重要な準備をしています。
時間の記録です。
+=は、「数を足して、結果を記録!」という意味。
この場合、deltaTimeの値を$.state.timeに足したものを記録することになります。
さっきの初期化処理で、$.state.timeには0.0が入っていますから、0.0から少しずつ数字が増えていくことになります。0.0 → 0.033 → 0.066 →0.1 → 0.133……みたいな感じで。
deltaTimeには「前のフレームからの経過時間」が入っているというのは説明しましたよね。毎フレーム「前のフレームからの経過時間」を足していくと、スクリプトが実行されてからの経過時間を記録できるのです。
例えばしばらくたって$.state.timeに3.5が入っていたら、「最初から3.5秒経過したんだな」ということがわかります。
+=以外なら
-=なら「引け」という意味に。
かけ算・割り算がちょっと特殊で、かけ算は*=、わり算は/=を使います。
$.state.time *= 2;
これなら、2をかけた結果を$.state.timeに記録する、みたいな感じです。
スクリプトの実行を始めてからの経過時間はなにかと便利に使えるので、どんなスクリプトでもここまでの内容はコピペで活用してよいのではないかと思います。
差がでるのは次の部分から、「アイテムを動かす」のところですね!
メインの処理を見ていく(アイテムを動かす・前半)
さあ、ここからがキモです。
let _ichi = $.state.ichi.clone();
let _muki = muki.clone();
const nagasa_now = Math.sin($.state.time * hayasa) * nagasa;
_ichi.add(_muki.multiplyScalar(nagasa_now));
$.setPosition(_ichi);
まず「let」というのは、「一時的にデータを記録したいから変数をつくって」というときに使います。
このフレームが終わったら捨てていいよ、というデータ用ですね。
letは一時的に記録したいときに使う
let _ichi = $.state.ichi.clone();
let _muki = muki.clone();
この2行では、「初期位置」や「動く向き」の「コピー(クローン)」をつくり、一時的に記録しています。
初期位置そのものに足し算などをしてしまうと、数字がズレて次に計算するときおかしなことになってしまうのです。
なのでclone()というものを使い、データのコピーをつくります。
「ベクトル」を使う
さて、hayasaやnagasaは3.0や1.0などフツーの小数が入っているだけなのでわかりやすかったのですが、$.state.ichiやmukiはちょっとちがいます。
const muki = new Vector3(1,0,0);
スクリプトの先頭にある3行目、こんな風になっていましたよね。
「初期位置」や「動く向き」は、XYZ(ヨコタテ奥行き)の3つの情報を使わないと表現できません。こういうものを「ベクトル」といいます。高校数学をやった方なら習ったかもしれませんね。
(最近は理系だけ教わるように変わったらしいですが……)
位置や向きを表現するのにどうしても使う「ベクトル」
メインの処理を見ていく(アイテムを動かす・中盤)
const nagasa_now = Math.sin($.state.time * hayasa) * nagasa;
さて、ここからいよいよ具体的な計算をしていきます。
「三角関数」のsinを使い、-1から+1まで行ったり来たりさせています!
三角関数という名前を忘れて、「-1から+1まで動くモノ」と考えたほうがいいかも!
三角関数は高校で習った人も多いと思います。三角形の辺の長さの計算とかに使うやつですよね。
ただ、三角関数のsinは「0 → 1 → 0 → -1 → 0」と行ったり来たりするという性質もあるのです!
三角関数を使うと、-1から1までをゆったり行ったり来たりさせられる
Math.sin($.state.time * hayasa)
とすることで、$.state.timeに入っている経過時間が増えるたびに「0 → 1 → 0 → -1 → 0」と行ったり来たりする計算を行っています。さらにhayasaをかけ算することで、この行ったり来たりのスピードも調整できるようにしています!
hayasaを2.0にした例。行ったり来たりのスピードが2倍になる
その計算結果に、さらにnagasaをかけています。例えばnagasaが2なら「0 → 1 → 0 → -1 → 0」ではなく「0 → 2 → 0 → -2 → 0」という風に数字が動くことになります!
hayasaもnagasaも2.0にした例
数学ニガテな人はだいたい理解すればいいです! Math.sinは-1から+1の範囲をゆっくり動く、というのだけおぼえて!
これによって、初期位置からどれくらい動かすかという計算ができました。
あとは、実際に位置を指定するだけです!
メインの処理を見ていく(アイテムを動かす・後半)
_ichi.add(_muki.multiplyScalar(nagasa_now));
$.setPosition(_ichi);
最後の部分、まずは1行目。_ichiというのは、初期位置のコピーが入っている「ベクトル」でしたね。そこに「add」、つまり足し算をしようとしています。
「ベクトル」は+=や*=などを使わず、特別な命令を使わないと足し算やかけ算ができないのです。
ベクトルの計算には特別な命令が必要
何を足すかというと、「_muki.multiplyScalar(nagasa_now)」と書いてあります。
_mukiは動かしたい方向のコピーが入っている「ベクトル」でした。
そしてmultiplyScalarはベクトルのかけ算なのです。
かける数は、さっき三角関数を使って一生懸命計算したnagasa_now。
ここもフクザツですが、がんばって!
カンタンに表すと、上の図のようになります。
_ichiに、muki × nagasa_nowを足しているわけです!
もちろんnagasa_nowは「三角関数」によって-1から+1までゆっくり動いているので、計算結果は毎回変わってきます。
$.setPosition(_ichi);
最後にその計算結果を使い、$.setPositionというアイテムの位置を指定する機能を使って、アイテムを動かします!
これを「毎フレーム」くり返すことにより、ハコがなめらかに左右移動するわけです!
お疲れさまでした!
計算の順番
_ichi.add(_muki.multiplyScalar(nagasa_now));
このように書いた場合、まず _muki.multiplyScalar(nagasa_now)のほうが先に計算されます。
そして、その計算結果を使って_ichi.addが計算されます。
まとめ
やはりスクリプトは中上級者向け機能ですし、スクリプトは人によって「向き・不向き」がとてもハッキリしているという説もあります。
かなり勉強が得意なのに、スクリプトを書くのは全くダメなんて人も多いようです。
スクリプトなんもわからん! となっても仕方ないと思います
とはいえスクリプトの 「コピペ」や「改変」をするだけなら、すべてを理解していなくても割とつかえます! 今回の記事の最初のほうを改めてチェックして、まずは「コピペ」「改変」を安定してできるようになりましょう。
そして慣れてきたら、数字を改変するだけでなく複数のスクリプトを組み合わせて1つにするなどの「高度な改変」にチャレンジしていってください!
$.logはだいじ
今回あまり説明できませんでしたが、$.logというものをつかうと定数・変数の中身を「コンソール」に表示することができます。
たとえば、
const nagasa_now = Math.sin($.state.time * hayasa) * nagasa;
$.log(nagasa_now);
_ichi.add(_muki.multiplyScalar(nagasa_now));
のようにnagasa_nowの計算のあとに$.logをつかってみましょう。
ものすごい勢いで-1から+1まで数字が動いている例
nagasa_nowの数字がなめらかに変化していることがよくわかります!
スクリプトにひそむ「バグ」を直すときに$.logは役立つ!
こうすることで、「なにか計算がおかしくなっていないか」とか「ほんとうにここの処理を実行しているのか」 とかをチェックできるのです。
スクリプトの問題点をチェックする「デバッグ」のとき、心強い味方になってくれます!
clusterの「設定」-「その他」から「コンソール」も表示しておこう
アイテムの回転に動きを対応させるには……
実は元のcluster公式さんのスクリプトは、アイテムを回転させるとその向きに応じてアイテムの移動方向が変わるようになっています。
これに対応させるには、_ichi.addの行を改変してください。
_ichi.add(_muki.multiplyScalar(nagasa_now).applyQuaternion($.getRotation()));
$.setPosition(_ichi);
だいぶムズかしい内容なのでここでは説明しませんが、「アイテムの向きに合わせて動きを変えろ」という命令だと思っておいてください。
あらためてまとめ
- 改変のときはぜったい全角で入力しない
- 改変するとき、元々のスクリプトの前後まで消さないように注意
- onUpdateはフクザツな動きに使える
- onInteractなどはドアとかのアイテムに使う
- 経過時間を記録しておくと何かとつかえる
- 三角関数は-1から+1までゆったり動く
- $.logをうまくつかう!
この辺はおぼえておいてくださいね!
あと、スクリプトをつかうときもUnityでやるならば「トリガー・ギミック」などのclusterのキホンをおさえておくことはとても大事。
よろしければ1/23にでるcluster本をよろしくお願いいたします~~
明日は龍飛さんの「clusterで人生を賭けたイベントをしたいあなたへ。」です。
Discussion