Unityで2Dシューティングをつくろう
2Dとは言わずもがな二次元のことである。すなわち二次元のシューティングということである。ここで言うシューティングは、現実社会すなわち三次元に対しての鬱屈した感情・暴力性のはけ口のことを指す。人は誰しも高位の次元に対し尊敬の念を抱き、低位の次元に対しては憎悪の念を抱くものである。つまりは……。
えぇ……、ちょっと何を言っているのかわかりませんね。
そうしたわけでまずは2D シューティングを作りたいんですよ
はい、前回記事の続きやっていきます。
inujini.hatenablog.com
どこまでやったかは覚えていませんが、自機の弾と敵の弾は実装したけどあたり判定がないって状況だったと思います。なのであたり判定を実装していきます。Unityでは幸いなことに「Collider」をつかうことであたり判定は簡単に実装できるそうです*1。
まずは爆発エフェクト描画用のスクリプトを spaceship.cs に追記。爆発エフェクトを紐づけ。
使いまわし.cs_追記
public class Spaceship : MonoBehaviour { // 略 // 爆発のPrefab public GameObject explosion; // 爆発の作成 public void Explosion () { Instantiate (explosion, transform.position, transform.rotation); } }
次にあたり判定スクリプトをプレイヤーに追記
プレイヤー.cs_追記
public class Player : MonoBehaviour { // 略 // ぶつかった瞬間に呼び出される void OnTriggerEnter2D (Collider2D c) { // 弾の削除 Destroy(c.gameObject); // 爆発する spaceship.Explosion(); // プレイヤーを削除 Destroy (gameObject); } }
爆発オブジェクトが無限ループするので「Animation Clip」からループ(Loop Time)のチェックを外す。爆発アニメ終了後もアニメが残るのでその削除スクリプトを追加。
爆発.cs
using UnityEngine; public class Explosion : MonoBehaviour { void OnAnimationFinish () { Destroy (gameObject); } }
OnAnimationFinishメソッドはUnity標準機能でなく勝手に追加したやつなのでそれを実行するための紐づけを行います。そうすると、ようやく接触判定ができます。やっぱり画像が入るとひと手間かかりますね。
Animationウィンドウから指定のフレームでイベント実行するように設定するとできるらしいです。
んで、このまま画像を無限に呼び出すと画像が消えずに画面が無駄に賑やかになるので画面外の弾は削除されるようにします。まずは、除去用の画面とスクリプトを書きます。
画面外除去.cs
public class DestroyArea : MonoBehaviour { void OnTriggerExit2D (Collider2D c) { Destroy (c.gameObject); } }
これを書いても除去用画面とプレイヤーの接触判定がかぶるので、開始と同時にプレイヤーが自爆します。そこでレイヤーをいじって、不要な判定の取り除いていきます。「Edit ⇒ Project Settings ⇒ Physics 2D」の Layer Collision Matrix からいらないチェックを外します。ついでにレイヤ名もいじって、判定に関して特定状況に合わせて除去イベントが発火するようにスクリプトをいじります。
プレイヤー.cs_変更
// ぶつかった瞬間に呼び出される void OnTriggerEnter2D(Collider2D c) { // レイヤー名を取得 string layerName = LayerMask.LayerToName(c.gameObject.layer); // レイヤー名がBullet (Enemy)の時は弾を削除 if (layerName == "Bullet (Enemy)") { // 弾の削除 Destroy(c.gameObject); } // レイヤー名がBullet (Enemy)またはEnemyの場合は爆発 if (layerName == "Bullet (Enemy)" || layerName == "Enemy") { // 爆発する spaceship.Explosion(); // プレイヤーを削除 Destroy(gameObject); } }
この辺の削除フラグ管理って座標で管理するものだと思っていたんですけど、こんな除去方法もあるんですね。とりあえず、動画キャプってみたけど分かりづらいし地味……。
さて、自機やったので敵機のスクリプトも同じような感じやっていきます。
敵.cs_追記
public class Enemy : MonoBehaviour { // 略 void OnTriggerEnter2D(Collider2D c) { // レイヤー名を取得 string layerName = LayerMask.LayerToName(c.gameObject.layer); // レイヤー名がBullet (Player)以外の時は何も行わない if (layerName != "Bullet (Player)") return; // 弾の削除 Destroy(c.gameObject); // 爆発 spaceship.Explosion(); // エネミーの削除 Destroy(gameObject); } }
これで、ようやくそれっぽくなってきました。ついでに、自機弾が残って邪魔なので画面外に消えるぐらいの良い感じの秒数で消えてもらうようにスクリプトをいじります。
public class Bullet : MonoBehaviour { // ゲームオブジェクト生成から削除するまでの時間 public float lifeTime = 5; void Start () { // 略 // lifeTime秒後に削除 Destroy (gameObject, lifeTime); } }
相討ちぃぃぃ……。オブジェクト消す時間もう少し早くてもいいかもしれない。
Destroyについて参考
Object.Destroy - Unity スクリプトリファレンス
それじゃあ、判定できましたし今度は背景をつけていきます。ゲームオブジェクトをQuadから配置しーので、遠景・中景・近景がさくっとおけるらしいですね。今回は用意されているの使っているから楽ちんじゃんとかなっているけど、Quadに利用するファイルmat形式か……。
まあ、今後のことを考えてもアレですし、背景スクロールを追加していきます。まずはスクリプトを記述。
背景.cs
using UnityEngine; public class Background : MonoBehaviour { // スクロールするスピード public float speed = 0.1f; void Update () { // 時間によってYの値が0から1に変化していく。1になったら0に戻り、繰り返す。 float y = Mathf.Repeat (Time.time * speed, 1); // Yの値がずれていくオフセットを作成 Vector2 offset = new Vector2 (0, y); // マテリアルにオフセットを設定する GetComponent<Renderer>().sharedMaterial.SetTextureOffset ("_MainTex", offset); } }
そして、各背景に紐づけ。アニメーションの基本は近いものほどゆっくり、遠いものは速く動くが基本ですのでそれに倣ってスクロールスピードをカスタマイズしていきましょう。
さて、背景もできたことですしゲームらしくなってきました。次はWAVEの設定をやっていきます。この管理もプレハブでまとめることで視覚的に管理できるっぽいね。いいね。まずはWave用空オブジェクトをつくりーの、敵を配置しーの、Wave呼出スクリプト書きーの、Waveと呼出スクリプトをEmitterオブジェクトに紐づけしーのでいけるっぽいです。ここだけの話ですが僕は、EmitterオブジェクトつくらずにWaveオブジェクトそのものを無限に再帰して呼び出すなどして、期待した挙動にならなくて小一時間詰みました。こいついつも詰んでるな。
wave用スクリプト
using UnityEngine; using System.Collections; public class Emitter : MonoBehaviour { // Waveプレハブを格納する public GameObject[] waves; // 現在のWave private int currentWave; IEnumerator Start () { // Waveが存在しなければコルーチンを終了する if (waves.Length == 0) { yield break; } while (true) { // Waveを作成する GameObject wave = (GameObject)Instantiate (waves [currentWave], transform.position, Quaternion.identity); // WaveをEmitterの子要素にする wave.transform.parent = transform; // Waveの子要素のEnemyが全て削除されるまで待機する while (wave.transform.childCount != 0) { yield return new WaitForEndOfFrame (); } // Waveの削除 Destroy (wave); // 格納されているWaveを全て実行したらcurrentWaveを0にする(最初から -> ループ) if (waves.Length <= ++currentWave) { currentWave = 0; } } } }
ループ確認したいだけなので背景を青バックに戻します。
それじゃあ、機能はだいたいできてきましたのでBGMの挿入方法を確認していきます。ここから先は画像で確認する手段がないのでなんとも言えない感じになってしまう……。とりあえず、BGMに関してはシーンに投げて愚直にループしとけばいいんじゃない?
次はSEをつけていきます。まずはプレイヤーショット音をいじります。プレイヤーにSEを紐づけて、スクリプトを追記していきます。
プレイヤー.cs_追記
public class Player : MonoBehaviour { IEnumerator Start () { // 略 while (true) { // 略 // ショット音を鳴らす GetComponent<AudioSource> ().Play(); } } }
爆発音は爆発オブジェクトにそのままSEを紐づければいけるらしいね。
AudioSource.Playについて*2
AudioSource.Play - Unity スクリプトリファレンス
てことで今回はこの辺でまとめたいと思います。今回は茶番が入る余地がなかったね。まあ、仕方ない。次の回(移動制御)は色々と手戻りが発生する修正になるので次回を後編の開始点にしていこうと思います。そんなわけで次回後編となります。
続く。