unityほぼ知識ゼロから2日でunity1weekに投稿した時に勉強したこと
フィギュア目当てでUnityをはじめる
ある日twitterを見ていたらこんな記事があった。
1週間でunityを用いたゲームを制作して公開するイベント、unity1weekにゲーム投稿して、noteに記事を書くと、「ユニティちゃんフィギュア」貰えるという。
「ユニティちゃんフィギュアほしいなあ」と思った私は早速unity1week参加を決意した。
しかし、一つ問題がある。自分のUnity知識はほぼゼロなのだ。
一応3年前にunity1weekに参加したことはある。
その時も頑張ってUnityを勉強しつつ制作していたのだが、
「空から降ってくる残業で押しつぶされないように逃げ続けるゲーム」というUnity Asset Storeの無料3Dモデルを組み合わせただけのクソゲーが完成してしまった。
・【Unity1week】ダメな人向け1週間Unityゲーム制作過程
その時の記事を読んでもらうとわかるのだが、ゲームを制作したというよりは「Unityの表現力に操られて制作した」ものになった。
コードもほとんど書いていない。Unityのリッチな機能を全く制御できなかったのだ。荒れ狂うロデオから豪快に吹き飛ばされて、騎手のいないロデオだけが公開されている気分である。
そんな事件も過去の話。3年たった現在、当時の数少ない知識も記憶のかなたへ消えてしまった自分が、改めてUnityゲーム開発をはじめたときの理解手順をこの記事に書いていく。
この記事は自分と同じように「Unityで3Dわちゃわちゃ触って、物理演算とか見て楽しんでいるけど、スクリプトをあまり書いてない人」がゲーム制作に必要な知識を得るためにはピッタリの内容になっていると思う。
さて、この記事ではUnityのロジックコードをどのように書けばよいか、当時の私が理解した内容をそのまま書いていく。もしあなたがUnity熟練者でおかしい点に気が付いたら、こっそり連絡してもらえると嬉しい。
どんなゲームを作ろうとしたのか
仕事で平日は全く作業ができなかったのだが、unity1week締め切り2日前にようやく作業開始できたので、とりあえずゲームの構想を紙に書いてみた。
「Unityには物理演算があるからいけるっしょ!」の精神で、割とスムーズに完成しそうなゲームメカニクスを考えた。
物理演算を使うと「そこまで考えなくても予想外の面白い構図が生まれやすい」ので利用させてもらう。
さて、とりえずUnity上でそれっぽい画面だけ作ってみる。Unityの表面を触るだけならできたのでここまでは問題なく作ることができた。
RigidBody2DやBoxCollider2Dを使えば物理演算が行われて、爆弾がそれっぽく反射するところまで確認した。が、ここからどうやってUnityをスクリプトで操作していくのか全く分からない。
ゲームのロジックをどうやって作っていくのか全くわからないのだ。まずい、これではまた「Unityに操られて制作したゲーム」が完成してしまう。
スクリプトファイルの生成はできるが、これをUnity内に適用する方法がわからない。そもそもゲームでよく使う「メインループ」はどこに書けばよいのかわからない!
散々調べてピンとこなかったので、Unityに詳しい人から直接使い方を学ぶことにした。
Unityを操作するメインループの書き方
どうやら、空のGameObjectを生成して、そこにスクリプトを埋め込んでメインループを管理する方法が一般的のようだ。
MonoBehaviorクラス内でpublicなプロパティを生成すると、メインループ内で操作したいゲームオブジェクトをUnityのinspectorから設定できるようになる、とのこと。
以下のようにpublicなプロパティを定義していくと・・・。
GameManagerのinspectorからPrefabの指定ができるようになる。
今までは「ゲームに使うものは全てScene内に最初から全部配置する」ものだと持っていたが、Sceneに置かずにPrefabにしてディレクトリ内に格納しておく方が使い勝手がよいとのこと。
読み込んだPrefabは以下のようにしてScene内に生成することができる。
たぶんこの処理がUnityのコードで最も基本的な処理のようだが、教えてもらうまではこのコードのことは記憶からすっかり抜け落ちていた。
// [prefab名]を(x,y,z)=(0,0,0)に表示する
const gameObject = Instantiate([prefab名], new Vector3(0, 0, 0), Quaternion.identity);
ちなみにScene内から削除する時はDestroy関数を用いる。
Destroy(gameObject);
そういえば「3年前にこんなことしていた気がするな・・・」と当時の記憶がおぼろげながらに浮かんできた。やはりこのあたりのUnity独自の知識は3年も離れると忘れてしまうものだなあ。
指定した方向に飛ぶ爆弾を生成する
今回、「クリックしたら、矢印の方向に飛んでいく爆弾を生成する」という処理を入れるのだが、この時「生成時に初期値を代入する処理」と「RigidBody2Dの速度パラメータにアクセスする方法」の二つの処理が必要になる。
ここも詰まりポイントだったので書いていく。
オブジェクトの初期値を代入する時、C#ではconstructorの引数を取るのが一般的だが、Unityはこれをしてはいけないそうだ。どうもUnityが動作中に内部的に勝手にクラス削除・再生成を行うものらしく、constructor関数が何度も実行されてしまう恐れがあるとのこと。
なので、Unityでは生成直後に初期値を代入する方法が一般的らしい。
また、RigidBody2Dの速度パラメータのような追加のコンポーネントにアクセスするためには、GetComponent<[取得したいComponent]>と書けばよい。
上記の話をまとめると、以下のコードを書けば爆弾が生成されて飛んでいくことになる。
// radは投げる角度
GameObject bombObject = Instantiate(bombPrefab, vector3, Quaternion.identity);
Rigidbody2D rigidbody = bombObject.GetComponent<Rigidbody2D>();
rigidbody.velocity = new Vector3(Mathf.Cos(rad) * 10, Mathf.Sin(rad) * 10, 0);
かなりシンプルに書けるのね。Unityってすごいなあ。
爆弾の衝突処理を書く
壁と爆弾が衝突した時に、衝突カウントが1増える。衝突カウントが3以上になったら爆発エフェクトを出して消えるという処理を書く。この時、衝突時のイベントハンドラはどう設定するのか。
どうやらCollision2Dが勝手にスクリプト内にあるOnCollisionEnter2D関数を発動させる仕様のようだ。
OnCollisionEnter2D関数はCollision2Dコンポーネントを追加しても勝手に生成されず、自分で追加する必要があるようだ。
当初自分はスペルミスで「OnCollisionEnter」と書いてしまい、関数が実行されない原因を探るために2時間悩んだ。同じ罠を踏んだ人は大勢いそうな仕様だ。
onCollisionEnter2Dの引数には「衝突した相手のCollision2D」が入っているので、そこからたどれば「壁に衝突したか、ドアに衝突したか」の区別をつけることができる。
今回、壁として設置したGameObjectのタグにはWallというタグ名を追加した。そしてそれを判定に用いた。
void OnCollisionEnter2D(Collision2D collision2d)
{
if (collision2d.collider.tag == "Wall")
{
this.hitCount++;
if (this.hitCount == 3)
{
// 爆発アニメーションエフェクトを爆弾の位置に生成
GameObject explosionEffectObject = Instantiate(explosionEffectPrefab, this.transform.position, Quaternion.identity);
// 爆弾を消去
Destroy(this.gameObject);
}
}
}
コードも10行以内でシンプルにまとまった。Unityってすごいなあ。
扉が爆発で開く処理
上記衝突関数の爆発時に「メインループを書いているスクリプト内にあるクリア処理関数を実行する」処理を書きたい。
クリア時は、「クリアテキストの表示や扉の画像変更」の処理を実行する。これを爆弾のスクリプト内でクリア処理を行うのは、見通しが悪いと思ったからだ。
しかし、爆弾のスクリプトからSceneにあるGameManager内の関数を発火させる方法が見つからない。図解にするとこんな感じだ。
GameManagerスクリプトはprefab化していないので、prefab化しているBombオブジェクトからGameManagerを呼ぶことはできなかった。こんな時、皆どうしているんだろう。
今回は、リアクティブプログラミングの知識を用いて、UniRxの導入でこの問題を解決することにした。リアクティブプログラミングを用いれば、生成先のBombから生成元のGameManagerにイベントを通知することができる。
具体的にはこんな感じで書いた。
以下がBomb側のコードだ。
void OnCollisionEnter2D(Collision2D collision2d)
{
if (collision2d.collider.tag == "Door")
{
this.hitCount++;
if (this.hitCount == 3)
{
GameObject explosionEffectObject = Instantiate(explosionEffectPrefab, this.transform.position, Quaternion.identity);
Destroy(this.gameObject);
this.clearedSubject.OnNext(""); // GameManager.cs内に通知する
}
}
}
そしてGameManager側のコードは以下となる。
// さっき書いた、爆弾を生成して配置する処理
GameObject bombObject = Instantiate(bombPrefab, vector3, Quaternion.identity);
// 爆弾のスクリプトを取得
Bomb bomb = bombObject.GetComponent<Bomb>();
// 爆弾側でclearedSubject.next()が実行されたら以下の処理を行う
bomb.clearedSubject.Subscribe(text =>
{
this.stageCleard(); // ステージクリア関数を呼ぶ
});
リアクティブプログラミングは慣れないと全く理解が追い付かないもので、自分も書き始めた当初は「どうやって動いているんだ?ホントに動くの?」と思っていた。
「コード内でsocket通信みたいなことをしている」と考えると理解が早かったので、そのように考えると理解しやすいはず。
とにもかくにも、無事にGameManager内でクリア処理が書けるようになったので、ステージの基本フローは完成した。
時計を見たら締め切りまで残り二時間、急がないと!
ステージを量産して公開
残り二時間でステージ量産作業を行った。
時間がもうないので、ものすごく頭の悪い方法「各ステージごとに、壁のPrefabを生成して、scaleやposition変更して貼り付ける処理」を書くことにした。
// 直接座標値やscaleを設定して書いていく
// この値は、事前にSceneに手動で貼り付けて、その座標値をメモして転記した
if (stage == 1){
this.shooterObject.transform.position = new Vector3(-7.91f, -3.24f, 0);
this.shooterObject.transform.rotation = Quaternion.identity;
this.doorObject.transform.position = new Vector3(7f, -3f, 0);
this.doorObject.transform.rotation = Quaternion.identity;
}
Scene内に各ステージの壁をGUIで貼り付けて、その壁のscaleとpositionをメモしてコードに直接書いていった。たぶんもっとunityらしいステージの生成方法があったと思う。ぜひ教えてもらえると嬉しい。
ゲーム公開
上記学びを元にゲーム制作を進めた結果、無事に爆弾で扉を開けるゲームを公開することができた。
少ないながらも5ステージも実装したので、そこそこ遊べるボリュームになっているはず。久しぶりに挑戦したUnityだったが、基本的な書き方さえ理解すればすんなりと実装できた。Unityってすごいなあ。
・・・まあ、WebGLビルドに10分かかったり、ビルド後にゲーム画面が横にめっちゃ長くて不自然なことに気が付いたり、UIボタンのテキストが見えなかったりと色々問題はあったんですけどね。
まとめ
色々あったが無事にゲーム公開までありつけたので一安心。
そして、ユニティちゃんフィギュアをもらうために必要な条件「noteに記事を書く」もこの記事をもって達成したことになる。
せっかくUnityを覚えたので、また何か機会があれば制作してみようかと思う。この記事はあなたと「未来の自分」に対して贈る記事となる。おそらく未来の自分はUnityの知識が抜け落ちているだろうから。
おまけ 自己紹介
最後までお読みいただきありがとうございます。
普段はJavaScriptブラウザゲームを制作している作っちゃうおじさんと申します。
自分のサイトにてスマホでも遊べるブラウザゲームを公開しているので遊んでもらえると嬉しいです。
使用言語・ライブラリはTypeScript+Pixi.js です。
この記事が気に入ったらサポートをしてみませんか?