ごあいさつ
本記事はクラスター Advent Calendar 2024 6日目の記事です。
前日は@baku_dreameaterさんのCluster Scriptでワールドに追加エモート的なのを入れてみるでした!
実践編です
スタジオチームのn_mattunです。
今回のこの記事は、
2日前に書いた「ClusterScriptが今「ちょうどいい」ので覚えるなら今がチャンス!」の続編記事になっています。
対象読者は
ワールド作りはできるようになったので、次はclusterでゲームっぽい何かを作ってみたい!
でもスクリプトとかプログラムとかちょっとよくわからない・・何か覚えるきっかけがあればなぁ・・
と思っている方々です。そんな方向けに、今このちょうどいい時期を使って
プログラミング何もわからん状態から、ゲームっぽい何かを動かす最初のステップ
を紹介してみたいと思います!
なお本記事は解説内容や表現方法が筆者の個人的な見解に寄っていますので、
公式のクリエイターガイドやリファレンスとはニュアンスが異なる部分もあるかとは思いますが、何卒ご容赦くださいませ。
まずは前回の記事の大事な部分
何かを動かすことは「動き始めるきっかけ」と「動かし方」の組み合わせという話を改めて記述します。
例えば、もぐら叩き風のゲームを作ってみよう!と思い立った場合、
ゲームとして必要な仕組みはこのへんかなと思います。
▼ゲームの進行を監理する仕組み
・もぐらを押したら|ゲームが始まる
・ゲームが始まったら|得点と残り時間を最初の状態にする
・ゲームが始まったら|もぐらがどこかに動く
・ゲーム中はずっと|残り時間を減らし続ける
・残り時間がなくなったら|ゲーム終了して結果を出す
▼ゲームの核となる当たり判定と得点の仕組み
・もぐらを押したら|得点が足される
・もぐらを押したら|もぐらがどこかに動く
制御したい物事に応じて作るものは色々ありますが、
いずれも動き始めるきっかけと動き方の指示の組み合わせでできていることがわかると思います。
誤解を恐れず言うと、ClusterScriptでゲーム風のプログラムを書いていく作業は、この
動き始めるきっかけと動き方にあたる指示書をひたすら書き連ねるようなもの
だと思ってもらって大丈夫だと思います。
これからこの例に沿って、ClusterScriptのコードを紹介していきます!
下準備:Unity上で動けるもぐら(仮)を準備しておく
まず下準備の下準備として、シーン上でClusterのシーンプレビューが動く最もシンプルな状態となる
- y座標の0にxz周囲10mくらいの地面がある
- y座標の-10にDespawnHeightコンポーネントをセットしたGameObjectがある
- 座標(0,1,5)にSpawnPointコンポーネントをセットしたSpawnPointがある。
という状況を手元で用意しましょう。
それができたら、シーン上にもぐらに相当するものを置いていきます。以下手順です。
Unity画面内のHierarchy上で右クリック → 3D Object → Cube
を選んでCubeを作り、原点座標(xyzすべて0)に移動しておいて、「もぐら」という名前を付けておきましょう。
また、このもぐらは色んな所に動いてもらうことになるので、
Cubeのインスペクタ上のAdd Componentを押す → 検索バーにMovableと入力 → 出てくるMovableItemを選択
と進んで、MovableItem
コンポーネントをセット`します。
続いて、このもぐらに「これこれこういう風に動いてね!」と書かれた指示書にあたるコードを渡しておきたいので、再び
Cubeのインスペクタ上のAdd Componentを押す → 検索バーにScritableと入力 → 出てくるScritable Itemを選択
、と進んで、ScritableItem
コンポーネントをセットしておきましょう。
このScritableItem
にはコードを直接書き込める入力欄(SourceCode)があるので、これから解説するコードを全部ここに書いても動きはするのですが非常に見づらいし、コードの修正もやりにくいため、コード本体はテキストファイル単体にして、そのファイルの渡すようにしておきましょう。
続いて、ProjectのAssetsフォルダ上で「ClusterScripts」という名前のフォルダを作り、その中で右クリック → Create → cluster → ClusterScript
と進んでいって、新規のClusterScriptファイルを作ります。ClusterScriptファイルは役割に応じた名前をつけておくとわかりやすいので、ファイル名も「Mogura」にしておきましょう。
できあがったClusterScriptファイルは先ほど作成したScritableItem
コンポーネントのSourceCodeAsset欄にセットできますので、CubeのインスペクタとProjectパネルのClusterScriptsをフォルダ開いたままの状態にしてMoguraと名付けたClusterScriptをCubeのインスペクタのSourceCodeAsset欄にドラッグ&ドロップしましょう。
これでSourceCodeAsset欄にドラッグしたMoguraと名付けたClusterScriptがセットされたので、これから書いてくるコード(指示書)の内容は、すべてもぐらに伝わるようになりました。
これで準備完了です!!
え、もぐらの見た目がおかしい?これはもぐらじゃなくて豆腐じゃないか?
そこは気にしないでいきましょう。ゲームを作る時の最初の一歩は見た目はわりとこんなもんです、子供から大人まで遊んでるような有名ゲームだって最初は同じような見た目だったんだぞ!(本当です)
さて、ではいよいよコードの説明です。
これから解説するコードはすべてClusterScriptのMoguraファイルの中に書いていきます。
自分で書いていってもいいのですが、慣れてないときは文字の書き損じや見落としなどでエラーが出るなんて事は当たり前のように起こりますので、ステップごとに書いてるコードをまるっとコピペして、実際に挙動を確認してみる感じでも大丈夫です!
数字の部分とか書き換えてみるとわかりやすく変化したりもするので、慣れてきたら少しづつ中身をいじくりながらスクリプトの仕組みを覚えていきましょう。
もぐらを押したら|ゲームが始まる
このコードをMoguraファイル内に書くか、コピペしましょう。
/*このコードはコピペできます*/
//動き始めるきっかけ:押されたら
$.onInteract(() => {
//ステータスをゲーム中にする
$.state.IsPlaying = true;
});
動き始める最初のきっかけはonなんとか
という表記で統一されていて、ここでは物体を押すことを示す$.onInteract
を使います。onInteract命令から続く{}
カッコの中に書かれいてるものが、そのきっかけ内での動き方・・すなわち指示書の中身です。
やってもらうことは上から順に書いておけば、その順番に指示をこなしてくれます。
カッコの中に書かれている指示はここでは1つだけです。
//
欄に書いてある文字は、コードを後で自分が読んだ時に何をやってるのかをすぐ理解するために書く補足コメントで、行内で//
記号の後に書かれている文字はコードの進行上は無視されます。指示書に貼っておく付箋みたいなもんですね。
同じく最初の行に/*このコードは上書きコピペできます*/
というコメントもありますが、こちらは/*
から*/
でくくってる間であれば複数行にわたってコメント扱いにできる記号表現です。必要に応じて好みのほうを使うことができます。
次の$.state.IsPlaying = true;
の行がコードの本体です。
文末にある;
記号は日本語の「。」みたいなもんで、命令の区切り記号です。
これから書いていく指示書(コード)には
「あれをして、次にこれをして、その次はどれを・・」
とたくさんの命令を書くことになるのですが、命令を受け付ける側としては、その命令をひとつひとつに分解されていたり、逆に命令を一定のグループごとまとめておいてもらうなりしておいてもらわないと読めない(エラーになってしまう)ので、その命令1セットにつき、最後に ;
を付けるという決まりになっています。
続いて、このコードの中でやっていることの説明です。
ゲーム中は、得点や残り時間といった様々なステータスを、ゲームの進行状況に応じて増やしたり減らしたりしていくことになるのですが、このステータスは作者が自由に作ることができます。
ここでは、そのステータスとして新たに「ゲーム中かどうか?」を、様々なきっかけの中で判断や変更するためのIsPlaying
という名称の動態保存領域を作って、その中に「はい」に相当するtrue
というデータを保存しています。
・・という言い方だと回りくどくてピンと来ないと思うのでPCのフォルダ構成で例えてみると、今ここでやっていることは
state
という名前のフォルダの中に新規のテキストファイルを作って、中にtrue
っていう文字を書いてIsPlaying
というファイル名で保存した
くらいに思ってもらえればOKです。
このステータスひとつひとつが、プログラミングの話でよく聞く言葉である変数です。
ゲームが始まったら|得点と残り時間を最初の状態にする
先ほどのコードにステータスをさらに2つほど追加しましょう
/*このコードは上書きコピペできます*/
//動き始めるきっかけ:押されたら
$.onInteract(() => {
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする(追加)
$.state.LeftTime = 60;
//得点をリセットする(追加)
$.state.Point = 0;
});
追加された$.state.LeftTime
が残り時間、$.state.Point
が得点で、残り時間には60、得点には0を入れました。この得点を増やしたり、残り時間を減らしたりする指示をどこでどう書くかは後ほど説明しますので、次いきましょう、
ゲームが始まったら|もぐらがどこかに動く
先ほどのコードに、もぐらがどこかに動くコードを追加したのがこちらです。
/*このコードは上書きコピペできます*/
//動き始めるきっかけ:押されたら
$.onInteract(() => {
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする
$.state.LeftTime = 60;
//得点をリセットする
$.state.Point = 0;
//+-2.5mの範囲のどこかに移動する(追加)
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
});
見た目ちょっとややこしいですね、丁寧に順を追って見ていきましょう。
let newPosition = new Vector3();
追加部分の最初に書いてある=
の左側のlet newPosition
ですが、これはnewPositionという名前の、使い捨てのステータス保存領域(変数)を作成した、という指示になります。
何が使い捨てなのかと言うと、let
命令で作られたステータス保存領域は、$.state
のステータス保存領域と違い、Onなんとか命令の{}
のくくりの中でしか使えないもので、別のonなんとかのきっかけの中では使うことができません。
「えー、そんな不便というか能力が劣る変数をわざわざ使うのってなんか意味あんの?」と思った方はまぁ正解で、実は別にこれを$.state.newPosition
に書き換えても今回の例では実害は一切なく動きます。
ではなぜ$.state
のステータス保存領域に入れないのかと言うと、ごく単純にゲームの規模が大きくなるにつれてステータスの数がめちゃくちゃ増えていってしまうとそれぞれのステータスがどこで何の役割を果たしているか作っている本人がわからなくなってしまうからに他なりません。
少し前にPCのファイルとフォルダの例を出してましたが、それに例え直すとstateという名前のフォルダの中にファイルが100個も200個も置かれているとどれが何かわからなくなるようなものですね。
話を戻して、=
の右側の説明をしましょう。
new Vector3()と書かれてますが、これは座標情報を取り扱うことに特化したステータス保存領域で、ひとつの変数の中にさらにxとyとzの3つの値を保存することができるものです。
PCのファイルとフォルダに例えると、IsPlayingファイルは中にtrueとだけしか書けなかったのですが、こちらはファイルの中に
x=数字
y=数字
z=数字
と、情報を3つ書いて保存しておけるわけですね。
では、次のコードの説明いきましょう。
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
=
の左側はわかりやすいですね。先ほどの説明通りnewPosition
にはxとyとzの値を保存しておけるので、右側に書かれてる値を左側に保存しています。
で、右側がなんじゃこりゃって感じだと思いますが、これも順を追って見ていきましょう。
まずわかりやすいやつからやっつけていきましょう。
newPosition.y = 1;
これはわかりやすいですね、y座標には数字の1が保存されてます。
で、残りのxとzの右側は同じ内容が書かれてますね。
(Math.random() * 5) - 2.5;
よくわからないですね。ひとつづつ見ていきましょう。
まずMath.random()
の部分ですが、これは0~1の範囲の値を、命令の文字通りランダムに出してきてくれる命令です。
0か1のどちらかではなく0.1324746412
とか0.978756163
とか0.47898673
とか、小数点の範囲も込みでバラバラの数字が出てきます。
ランダムなのでこの命令をこなすたびに違う数字が出てくるので、x部分とz部分は見た目上は同じコードですが保存される数字はまったく別の数字になります。では例えば、ランダム箇所でそれぞれ以下のような値が出てきた場合をあてはめてみましょう。
(0.87456 * 5) - 2.5;
(0.46875 * 5) - 2.5;
ここに書かれている情報だけを見ると、やっていることは実は単なる算数です。
ランダムの数字の右にある*印は算数の×記号と同じ意味、()は算数()の役割とまったく同じ「この中は先に計算してね」の意味です。
なので、これを紐解いていくと・・
//()の中を計算する(わかりやすいよう*をxに書き換えてます)
(0.87456 x 5) - 2.5;
(0.46875 x 5) - 2.5;
↓
//()内の計算結果はこう
(4.3728) - 2.5;
(2.34375) - 2.5;
↓
//()をとる
4.3728 - 2.5;
2.34375 - 2.5;
↓
//それぞれ-2.5する
1.8728;
-0.15625;
↓
//右側の計算結果をそれぞれxとzに保存する
newPosition.x = 1.8728;
newPosition.z = -0.15625;
となります。
計算結果がこうなったのはわかったのですが、なんでこんなパッと見わかりにくい算式を書いているんでしょうか?
該当箇所のコードをコメントと共に見直してみましょう。
//+-2.5mの範囲のどこかに移動する
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 0;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
//+-2.5mの範囲のどこかに移動する
と書いてありますね。
実はこの算式はコメントの通り、Math.random()
で出てくる数値が0~1の範囲内であれば必ず-2.5~2.5の範囲内に収まる数字になる算式になっています。
ためしに値の最下限と再上限※である0と1をMath.random()
の部分に代入した算式を見てみましょう。
(0 x 5) - 2.5;
(1 x 5) - 2.5;
上の式の計算結果は-2.5で、下の式の計算結果は2.5になってることがわかると思います。
また、xzは+-2.5の範囲でランダムにしているけどyは0で固定しているのは、Unity上におけるyの位置は高さに相当する位置で、今回動いてもらう物体はもぐらになるので、もぐらは地面から頭出ているくらいの高さで固定しておきたいからです。
※おまけ:Math.random()
で帰ってくる値の範囲は厳密には0以上1未満なので、実は1が戻ってくることはありませんが、本件ではわかりやすさ優先ということで。ちなみに理論上の最上値は調べてみたところ0.99999999999999988897769753748434595763683319091796875
だそうです(これも実行エンジンによって若干違うかもだけど)
さて、では最後の行の説明です。
$.setPosition(newPosition);
今まで書いてきたコードは=
がありましたが、今回はないですね。
これは書くコード(指示書)の内容によりけりで、=
がある場合とない場合があります。
どういう場合であるなしが決まるかと言うと、何か物販をしている会社で誰かが誰かに見積もり作成を指示する例で見てみましょう。
-
パターン1:お願いだけする
「この商品の販売価格の見積もりお願いね!」 -
パターン2:お願いをして、結果報告を受ける
「この商品の販売価格の見積もりお願いね。終わったら結果の価格を教えてね!」 -
パターン3:参考情報を付けてお願いをする。結果報告は受けない
「この商品の販売価格の見積もりお願いね。見積もりに使える参考情報も一緒に渡しておくよ!」 -
パターン4:参考情報を付けてお願いをする。結果報告も受ける
「この商品の販売価格の見積もりお願いね。見積もりに使える参考情報も一緒に渡しておくよ!見積もりができたら最終的にいくらになったから教えてね!!」
で、この例でいう所の結果報告を受けないケース・・すなわちパターン1と3が=ががないケースで、結果報告を受けるパターン2と4が=
があるケースになります。
直近に説明していた=
があるコードも、この例でいうと
+-2.5の範囲の数字を求めてね!(結果は保存したいので)結果報告は受けつけるよ!
という解釈になります。
さて、=
がないので結果報告は受け付けないことはわかりました。
そして結果報告を受け付けないパターンでも、参考情報を渡すパターンと渡さないパターンの2つがあることを説明しましたが、今回のコードは直近で作っていたxyzの座標情報をまとめた数字を保存しているnewPosition
を使っています。なのでこれは参考情報を付けているパターンです。
そしてsetPosition
ですが、これはClusterScriptで用意されている物体を指定の座標に移動させる命令です。指定の位置は変数newPosition
の中に入っていますので、これで移動ができます。
移動ができる・・・うん、移動ができます。
・・・えっと、何を移動するの?ゲームを遊んでる人が動く?それとももぐら本体??
そうなんです。
ゲームでは「プレイヤー」「敵」「弾」「的」「移動床」などなど、動く命令を聞いてくれそうなものは思いつくだけ無数にあるので、ちゃんと指示書の中で 誰を(何を) 動かすかという情報もセットで書かなければいけません。
で、今回のコードでこの指示を受け付ける「誰」にあたる記述は、$.setPosition(newPosition);
の記述のなかのいちばん左の $記号 がその役割を担っています。
$記号は、ClusterScriptでは「コード(指示書)を持っている本人」を示す役割を持った特別な記号です。
今回、このコードを書いてるClusterScriptファイルは、シーン内のScritableItem
コンポーネント経由で「もぐら」に渡していますので、ちゃんともぐらが動いてくれるようになっています!
ちなみに、今回の説明で登場するもぐらは1匹だけなのですが、最初のもぐらの配置と同じ要領でシーン上に「もぐらB」「もぐらC」を用意して、ここで作ったClusterScriptファイルを渡してあげると、それぞれのもぐらが「あ、俺が動くのね」と解釈してくれて、それぞれのもぐらを押すと、それぞれのもぐらが+-2.5mの範囲の思い思いの位置にランダムに動くことも確認できると思います。
さて、これで何となくゲームっぽい動きが実現できたと思います。
続いてゲームの残り時間を管理する他の仕組みも追加していきましょう!
ゲーム中はずっと|残り時間を減らし続ける
/*このコードは上書きコピペできます*/
//動き始めるきっかけ:押されたら(この中身は先ほどまでと同じもの)
$.onInteract(() => {
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする
$.state.LeftTime = 60;
//得点をリセットする
$.state.Point = 0;
//+-2.5mの範囲のどこかに移動する
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
});
//動き始めるきっかけ:常に(この領域がごっそり追加)
$.onUpdate(deltaTime => {
//残り時間を減らす
$.state.LeftTime = $.state.LeftTime - deltaTime;
});
先ほどまで書いていたコードは、されたきっかけでやってもらいたい諸々の指示だったのですが、時間を減らす計算に関しては押されてようが押されてまいが、現実の時間の進行と同じく、まさに何もしていない時も刻々と時間が増えたり減ったりしてもらわなければいけないので、別のきっかけの中にコードを書いていきます。
今回使っている$.onUpdate
命令は、
そんな何もしていない時にも常に動き続ける働きものなきっかけ命令です。
で、この「常に」がどれくらいの頻度で動いてくれるかというと、clusterを動かしている端末のスペックやその時の状況によってどれくらいの頻度で動くかが変わります。
例えばPCで動かしている時は1秒間に100回以上くらい動くこともありますし、
明らかに調子の悪そうなスマートフォンで動かしている場合は、1秒間に10回以下しか動かないなんてこともありえます。端末がいわゆる「重い」と呼ばれる時は動く頻度が少ない、と思ってもらえればOKです。
さて、そんな常に動いてる$.onUpdate
指示書の中で時間の計算をするにはどうすればいいでしょうか。コードを読んでいきましょう。
//残り時間を減らす
$.state.LeftTime = $.state.LeftTime - deltaTime;
=
の右側から見ていきましょう。
$.state.LeftTimeはonInteract
命令の中で60という数字を保存していた変数ですね。
なので最初の段階ではここの文字は数字の60に読み替えてしまってOKです。
続いてのdeltaTime、これは何でしょう?
よく見ると$.onUpdate
が書かれている行にもdeltaTime
と書かれてますね、これは何かを説明します。
このonUpdate命令、常に動いてくれるのは便利でいいのですが、先ほど話をした通りで実行頻度が決まっていません。これはつまり、「直近に動いた時」と「その次に動く時」の間にかかる時間もバラバラになる、ということになります。早く動ける時は0.01秒後に動くこともあるでしょうし、重い時は0.09秒以上経ってから動くこともあるでしょう。
deltatimeは、そんな「直近に動いた時」から「今回動いた時」の間にかかった経過時間そのものが、最初は0.0297413
秒、その次は0.016433
秒、その次は0.024654
秒、、みたいな感じで呼び出されるたびに異なる数字が入っている変数になります。
というわけで、$.state.LeftTime
とdeltaTime
の中身がわかったのでdeltatime
の中が上記に書いた数字の例のまま0.0297413
だったと仮定して計算します。
//右側の変数を数字にあてはめて表現してみる
$.state.LeftTime = 60 - 0.0297413;
↓
//右側の計算結果はこうなります
$.state.LeftTime = 59.9702587;
で、=
は「右側の計算結果を左側に書いてる変数に保存する」相当の命令なので、$.state.LeftTime
の中身は60
だったものが59.9702587
になり、晴れて最初のコメントに書いてあった残り時間を減らすことができました!
さてこの残り時間、次に減らす時は何をどうすればよいでしょうか?
また数値を当てはめて考えてみましょう。
onUpdate命令の中に入ってくるdeltatime
の中身は「直近に動いた時」から「今回動いた時」の間にかかった経過時間そのもの入りますよね。なので先ほどとは異なる次の値として、deltatime
が0.016433
だったと仮定して計算します。
$.state.LeftTime = $.state.LeftTime - 0.0297413;
こうですね。では次の右側の$.state.LeftTime
ですが、
ひとつ前に$.state.LeftTime
の中身を60
から減算していましたよね。なので
$.state.LeftTime = 59.9702587 - 0.0297413;
こうなります。
そしてひとつ前と同じ手続きで右側の計算結果を左側に書いてる変数に保存するので・・・
//これが
$.state.LeftTime = 59.9702587 - 0.0297413;
↓
//計算結果の結果、こうなる
$.state.LeftTime = 59.9405174;
なんということでしょう!コードを書き換えなくても
$.state.LeftTime
の中の残り時間がちゃんと減るようになってます!!!
もちろん次に動いた時も、その次に動いた時も同じことを繰り返してくれるので、何もしなくても$.state.LeftTime
の時間がどんどん減っていってくれそうです。
このように、変数というのはコードの実行タイミングに応じて中身を適切に操作することで、その時に欲しい情報を随時作り出したり、時間切れを演出するためのきっかけ作りのための道具としても使ったりもできます。
残り時間がなくなったら|ゲーム終了して結果を出す
先ほどまでの処理で$.state.LeftTime
の中身はどんどん減っていきます。
これを残り時間がなくなったらゲームを終わらせられるようにしましょう。
/*このコードは上書きコピペできます*/
//動き始めるきっかけ:押されたら(この中身は先ほどまでと同じもの)
$.onInteract(() => {
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする
$.state.LeftTime = 60;
//得点をリセットする
$.state.Point = 0;
//+-2.5mの範囲のどこかに移動する
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
});
//動き始めるきっかけ:常に
$.onUpdate(deltaTime => {
//ゲームが始まってる状態の時に常にやっておくこと
// → 得点を加えたり場所を移動したりしてる所。(追加)
if($.state.IsPlaying == true){
//残り時間を減らす(先ほどまでの説明とここは変化なし)
$.state.LeftTime = $.state.LeftTime - deltaTime;
//残り時間がゼロ(ゲーム終了時間)に達したらやること(追加)
if($.state.LeftTime < 0){
//ステータスをゲーム中じゃない状態にする(追加)
$.state.IsPlaying = false;
//初期位置に戻しておく(追加)
$.setPosition(new Vector3(0,1,0));
}
}
});
コードが長くなってきましたが、ほとんどの場所は説明済みの場所だったり、いちど登場している命令と同じようなことをやっているものなのでそのへんはいったん無視して大丈夫です!
まずは新しい命令が出てきているのでそこを説明しましょう、onUpdate命令のくくりの中の最初の指示です。
//ゲームが始まってる状態の時に常にやっておくこと
// → 得点を加えたり場所を移動したりしてる所。(追加)
if($.state.IsPlaying == true){
//(中に色々かいてあるけどいったん省略)
}
こちらですね。
$.state.IsPlaying
はonInteract命令の中で使っていた変数ですね。
そしてこの中にはtrue
というデータを保存したと思います。
で、その右=
ではなく==
という記号を使っています。これは何でしょうか?
この==
記号は、記号の右のデータと左のものが同じものなのかどうかを比較する時に使う命令で、同じ時はtrue
、違う時はfalse
というデータがこの比較内の()の中に入る、という動きをしてくれます。
そして$.state.IsPlaying
ですが、前述の通り中にはtrue
というデータが入っています。なので==
の左と右を比較すると、今回は同じあるということがわかりそうです。
ということで、この()の中身はtrue
になるということがわかりました。
改めて順番に見ていくと
//コードの見た目上はこういう状態ですが、
if($.state.IsPlaying == true){ (略) }
↓
//変数の中身はこうなっている。で、==を使って左と右を比較して、その結果は同じなので・・
if(true == true){ (略) }
↓
//比較結果が同じ時に入るtrueが入ってる
if(true){ (略) }
こんな感じですね。
それでは次はこの()の外側の命令の意味について説明します。
()の左側にif
と書いてありますね。if
は日本語に訳すと「もしも・・」という意味を持っていますが、この命令はこの日本語訳とニュアンスが近い感じで、もしも()の中がtrueの場合は、これから先に書く{
から}
の間に書かれているコードを動かしてね!という命令です。
そういう意味を持ったコードであることをわかった上で、
元のコードとコメント部分を見返してみましょう、
//ゲームが始まってる状態の時に常にやっておくこと
// → 得点を加えたり場所を移動したりしてる所。(追加)
if($.state.IsPlaying == true){
//(中に色々かいてあるけどいったん省略)
}
$.state.IsPlaying
はゲームが始まっているかどうかを判断する役割として扱っている変数ですので、「中に色々書いてあるけどいったん省略」と書かれている部分がごっそり「ゲーム中にやって欲しいコード」になっていて、ゲームが終わったら(すなわち、`if()の中身がtrueじゃない状況になったら)中に色々書いてあるものは無視される、という指示をしていることがわかります。
こんな感じで、コードの総量が増えていくと、ゲーム中だけ動いてほしいコードや、その逆でゲーム中じゃない時だけ動いてほしいコードがあったりなかったりしますので、このif
文と、その中に入れる変数をうまく使いこなして、それぞれのコードが動いてほしいタイミングをコントロールできるようになります。
では続いて、ゲーム中にやってもらうことになるこの中身を見ていきましょう。
//残り時間を減らす(先ほどまでの説明とここは変化なし)
$.state.LeftTime = $.state.LeftTime - deltaTime;
//残り時間がゼロ(ゲーム終了時間)に達したらやること(追加)
if($.state.LeftTime < 0){
//ステータスをゲーム中じゃない状態にする(追加)
$.state.IsPlaying = false;
//初期位置に戻しておく(追加)
$.setPosition(new Vector3(0,1,0));
}
はじめの残り時間を減らす部分は解説済みなので割愛します。
その次にまたif
がありますが、さきほどと少し中身が違って、左右の値の比較に使っている記号が==
ではなく<
になっています。この記号の役割は左と右にある数字を右のほうが大きいかどうかを比較して、大きい時はtrue
、そうじゃない時はfalse
になる記号です。
そして比較に使っているデータですが、
まず右側はずっと0
ですね、そして左側は$.state.LeftTime
です。
$.state.LeftTime
の中身は今まで書いてきたコードの指示により、60から始まって、何もしなくても数字がどんどん減っていってくれます。なので、始まってからちょうど1分経った頃合いでここの比較対象が
if( 0より小さいマイナスの値 < 0 ){
になるのですが、これがすなわちコメント通りの
残り時間がゼロ(ゲーム終了時間)に達したタイミング(きっかけ)を捉えている
ことになります。
今までは「動き始めるきっかけ」は$.onInteract
や$.onUpdate
という、ClusterScript側で用意されていた命令を使っていましたが、このように if
と変数を駆使すると
自分が必要とする「動き出すきっかけ」を生み出し、その中で「動き方」を指示する
といったことも可能になります。
さて、では残り時間がゼロになった時にやることを見ていきましょう。
//ステータスをゲーム中じゃない状態にする
$.state.IsPlaying = false;
//初期位置に戻しておく
$.setPosition(new Vector3(0,1,0));
上のコードは簡単ですね、残り時間がゼロになったため、ゲーム中ではない扱いにしたいので$.state.IsPlaying
の中身にfalse
を入れます。
続いてのコードも説明済みの$.setPosition
命令なんですが、onInteract命令で使っていた時と()内の書き方が違いますね。
onInteract命令の時は期間限定の変数を使っていましたが、今度はその期間限定の変数を使うことすらせず、()の中に直接Vector3のデータを記述していますが、この書き方はコードの記述を短めにしたいような場合など使われる書き方です。
さらに座標情報の割り当ての書き方も簡略化しており、new Vector3の()
の中に直接、座標情報を書いています。それぞれの意味は,
区切りを作る記号になっており、数字は左から順にx
,y
,z
が割り当たります。なのでこのコードは
もぐらがx座標0、y座標1、z座標0に動くという指示をしていることになります。
さて、これでほほ説明が終わったので改めてonUpdate命令の中を見直してみましょう。
//動き始めるきっかけ:常に
$.onUpdate(deltaTime => {
//ゲームが始まってる状態の時に常にやっておくこと
// → 得点を加えたり場所を移動したりしてる所。(追加)
if($.state.IsPlaying == true){
//残り時間を減らす(先ほどまでの説明とここは変化なし)
$.state.LeftTime = $.state.LeftTime - deltaTime;
//残り時間がゼロ(ゲーム終了時間)に達したらやること(追加)
if($.state.LeftTime < 0){
//ステータスをゲーム中じゃない状態にする(追加)
$.state.IsPlaying = false;
//初期位置に戻しておく(追加)
$.setPosition(new Vector3(0,1,0));
}
}
});
最初のほうの$.state.IsPlaying
を参考情報にif
の分岐を使っている箇所から、改めてゲームの進行に応じて実行のタイミングによってどうなるかを見返してみましょう。
まずゲームを1回も始めてない時(もぐらを一度も押してない時)は$.state.IsPlaying
の値はどうなるでしょうか?
・・・すみません、ここの説明はまだしていませんでした。
その時はtrue
もfalse
も入れてない 、、というか
その時点では$.onInteract
の中身が一度も呼ばれていないので、その中にある$.state.IsPlaying
も一度も登場していないですよね。
このような時の$.state.IsPlaying
の状態はどうなるかというと、登場していない(プログラミング用語的には「未定義」と呼ばれる状態)ので、ClusterScriptではundefined
という特別な状態になっています。
そして、undefined
とtrue
を比較するとどうなるかというと、比較対象が何であれ違うものを比較している・・つまり同じものではないという比較結果になるので、if
の結果はfalse
になり、if
の{}
内のコードは無視されてくれます。
続いてプレイヤーの操作によってもぐらを一度押すと、$.onInteract
の中のコードにより、$.state.IsPlaying
の中身がtrue
になったとします。そうすると$.state.IsPlaying
の中身がtrue
になったことにより、if($.state.IsPlaying == true)
の{}
内のコードが常に動くことになります。
その中では$.state.LeftTime
が60
から始まって、時間が経つことで数字がどんどん減っていきます。そして1分経った頃合いで$.state.LeftTime
の中身がマイナスになり、そのタイミングでif($.state.LeftTime < 0)
の{}
内のコードが動きます。
その中で$.state.IsPlaying
の中身がfalse
になって、もぐらが初期位置に戻ります。
そうすると、最初のほうのif($.state.IsPlaying == true)
の{}
内のコードは比較結果がfalse
になることで動かない状態に戻るので、残り時間が減り続けることもなくなり、ゲームは終了したかのような状態になります。
これで$.onUpdate
内のサンプルコードは完成です!
あとは$.onInteract
内を編集していきます!!最終章!
「もぐらを押したら|得点が足される」と「もぐらを押したら|もぐらがどこかに動く」
この2つは一気にやりましょう。書き換えた完成系のコードはこちらです。
/*このコードは上書きコピペできます*/
//動き始めるきっかけ:押されたら
$.onInteract(() => {
//ゲーム中にやること(追加)
if($.state.IsPlaying == true){
//得点を1点加える
$.state.Point++;
}
//ゲーム中じゃない時にやること(元々書いてたものをif文の中に入れた)
if($.state.IsPlaying != true){
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする
$.state.LeftTime = 60;
//得点をリセットする
$.state.Point = 0;
}
//ゲーム中かどうか不問で、押されたらやること(ここの一連のコードは同じもの)
//座標の原点(0,0,0)から+-2.5mの範囲のどこかに移動する
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
});
//動き始めるきっかけ:常に(この中身は変わらず)
$.onUpdate(deltaTime => {
//ゲームが始まってる状態の時に常にやっておくこと
// → 得点を加えたり場所を移動したりしてる所。
if($.state.IsPlaying == true){
//残り時間を減らす
$.state.LeftTime = $.state.LeftTime - deltaTime;
//残り時間がゼロ(ゲーム終了時間)に達したらやること
if($.state.LeftTime < 0){
//ステータスをゲーム中じゃない状態にする
$.state.IsPlaying = false;
//初期位置に戻しておく
$.setPosition(new Vector3(0,1,0));
}
}
});
もぐらを押したら得点が足されるコードが追加されてますね。得点が足されるタイミングはゲーム中だけであってほしいので、$.onUpdate
で書いていたif
と同様の条件となるif($.state.IsPlaying == true){}
を書いています。
で、その中の得点の加算、これは新しい書き方ですね。
//得点を1点加える
$.state.Point++;
この++
記号は、左側に書いている変数の中身に1を足すことができる命令です。
今まで書いてきたものと同じような書き方で$.state.Point = $.state.Point + 1;
と書くこともできるのですが、プログラミングでは中身に1を足すという指示はわりと頻繁に行うため、こうやって簡略化して書くことができるようになっています。(今回は使っていませんが、1を足すのではなく1を引く--
という命令もあります)
次のif
のくくりを見ていきましょう。
//ゲーム中じゃない時にやること
if($.state.IsPlaying != true){
//ステータスをゲーム中にする
$.state.IsPlaying = true;
//残り時間をセットする
$.state.LeftTime = 60;
//得点をリセットする
$.state.Point = 0;
}
また中身に見たことのない比較記号がありますので説明します。
この比較記号!=
は、右と左を比較した結果が同一かどうかを比較する点は==
と同じなのですが、==
とは逆に、同じではない時にtrue
、同じ時にfalse
というデータになるという性質をもっています。
この書き方をしておけば、$.state.IsPlaying
の中身がfalse
でも、$.state.IsPlaying
自体が登場していないundefinded
状態でも比較結果がtrue
となるため、中に書いてあるゲーム中じゃない時にやりたい、各ステータスの下準備にあたる指示をこなしてくれるようになっています。
そして最後の部分です。
//ゲーム中かどうか不問で、押されたらやること
//座標の原点(0,0,0)から+-2.5mの範囲のどこかに移動する
let newPosition = new Vector3();
newPosition.x = (Math.random() * 5) - 2.5;
newPosition.y = 1;
newPosition.z = (Math.random() * 5) - 2.5;
$.setPosition(newPosition);
ここはコメントが増えただけでコードには変わりありませんね。
コードコメントの通り、ゲーム中、ゲーム中じゃない状態どちらのケースでも、
もぐらを押したらもぐらがどこか(+-2.5のランダムの範囲に)動いてくれるようになっています。
以上、お疲れ様でした!!!!!!!!!!!!!!
これにてサンプルコードが完成です!!!!!!!!!!!!!
足りないこと
サンプルコードは完成しましたが、実際にプレイしてみると足りない、というか、プレイヤーからするとわからないことがあります。
このゲーム、残り時間と得点をカウントはしているのですが、その得点と残り時間がプレイヤーは一切わからないというそれなりに致命的な問題が残っています。
なのですが、ここまで書いてきたコードで、ClusterScriptを作るうえで必要となる
- 動き始めるきっかけと動き方の組み合わせの考え方の経験
- プログラミングの基本的な記号や書式が何なのか、どういう意味を持っているのかの理解
- それらを通じて、
setPosition
やrandom
といった、具体的な何かをこなしてくれる命令(プログラミングの世界でこれはメソッドと呼ばれてます)の使い方
を知ることはできたとは思いますので、今回はここで筆を置きたいと思います。
本記事がClusterScriptをやってみよう!という動き始めるきっかけになってくれれば幸いです!