コミュ障だから明日が僕らをよんだって返事もろくにしなかった

何かを創る人に憧れたからブログをはじめたんだと思うよ

Unityで2Dシューティングつくったよの巻 ~中編

Unityで2Dシューティングをつくろう

2Dとは言わずもがな二次元のことである。すなわち二次元のシューティングということである。ここで言うシューティングは、現実社会すなわち三次元に対しての鬱屈した感情・暴力性のはけ口のことを指す。人は誰しも高位の次元に対し尊敬の念を抱き、低位の次元に対しては憎悪の念を抱くものである。つまりは……。



えぇ……、ちょっと何を言っているのかわかりませんね。


そうしたわけでまずは2D シューティングを作りたいんですよ

はい、前回記事の続きやっていきます。
inujini.hatenablog.com



どこまでやったかは覚えていませんが、自機の弾と敵の弾は実装したけどあたり判定がないって状況だったと思います。なのであたり判定を実装していきます。Unityでは幸いなことに「Collider」をつかうことであたり判定は簡単に実装できるそうです*1

f:id:andron:20180529043121p:plainf:id:andron:20180529043122p:plain
てことで、こう。今までUnity機能って丸と四角しかできないものだとか思っていたんですけど、「Polygon Collider 2D」使えば複雑なあたり判定(右:緑枠)も簡単にできるんですね。同じようにして弾にも判定をつけてスクリプトを書いていきます。接触判定の発火フラグが「is Trigger」らしいのでチェック忘れると虚無を味わうことになるっぽいぞ。

まずは爆発エフェクト描画用のスクリプトを 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標準機能でなく勝手に追加したやつなのでそれを実行するための紐づけを行います。そうすると、ようやく接触判定ができます。やっぱり画像が入るとひと手間かかりますね。
f:id:andron:20180529052654p:plain
Animationウィンドウから指定のフレームでイベント実行するように設定するとできるらしいです。
f:id:andron:20180529053732g:plain


んで、このまま画像を無限に呼び出すと画像が消えずに画面が無駄に賑やかになるので画面外の弾は削除されるようにします。まずは、除去用の画面とスクリプトを書きます。
画面外除去.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);
        }
    }

f:id:andron:20180529063243g:plain
この辺の削除フラグ管理って座標で管理するものだと思っていたんですけど、こんな除去方法もあるんですね。とりあえず、動画キャプってみたけど分かりづらいし地味……。


さて、自機やったので敵機のスクリプトも同じような感じやっていきます。
敵.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);
  }
}

f:id:andron:20180529070005g:plain
相討ちぃぃぃ……。オブジェクト消す時間もう少し早くてもいいかもしれない。

Destroyについて参考
Object.Destroy - Unity スクリプトリファレンス




それじゃあ、判定できましたし今度は背景をつけていきます。ゲームオブジェクトをQuadから配置しーので、遠景・中景・近景がさくっとおけるらしいですね。今回は用意されているの使っているから楽ちんじゃんとかなっているけど、Quadに利用するファイルmat形式か……。
f:id:andron:20180529085434p:plain

まあ、今後のことを考えてもアレですし、背景スクロールを追加していきます。まずはスクリプトを記述。
背景.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);
  }
}

そして、各背景に紐づけ。アニメーションの基本は近いものほどゆっくり、遠いものは速く動くが基本ですのでそれに倣ってスクロールスピードをカスタマイズしていきましょう。
f:id:andron:20180529095018g:plain




さて、背景もできたことですしゲームらしくなってきました。次は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;
      }
    }
  }
}

f:id:andron:20180529180336g:plain
ループ確認したいだけなので背景を青バックに戻します。



それじゃあ、機能はだいたいできてきましたのでBGMの挿入方法を確認していきます。ここから先は画像で確認する手段がないのでなんとも言えない感じになってしまう……。とりあえず、BGMに関してはシーンに投げて愚直にループしとけばいいんじゃない?
f:id:andron:20180529180348p:plain

次はSEをつけていきます。まずはプレイヤーショット音をいじります。プレイヤーにSEを紐づけて、スクリプトを追記していきます。
プレイヤー.cs_追記

public class Player : MonoBehaviour
{
  IEnumerator Start ()
  {
// 略
    while (true) {
// 略 
      // ショット音を鳴らす
      GetComponent<AudioSource> ().Play();
    }
  }
}

爆発音は爆発オブジェクトにそのままSEを紐づければいけるらしいね。

AudioSource.Playについて*2
AudioSource.Play - Unity スクリプトリファレンス


てことで今回はこの辺でまとめたいと思います。今回は茶番が入る余地がなかったね。まあ、仕方ない。次の回(移動制御)は色々と手戻りが発生する修正になるので次回を後編の開始点にしていこうと思います。そんなわけで次回後編となります。


続く。

*1:今までコリダーって呼んでたんですけど、コライダーって読むらしい……

*2:非推奨ながら遅延パラメータ持ってるんですね