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

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

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

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

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


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


最近シューティングをシェーディングに空目してしまう

てことで後編やっていきましょうか。前回どこまでいじっていたかわかりませんが、多分大まかな機能が実装されていたと思います。そうしたわけで、今回は細かい動作の修正を入れていきます。

前回記事
inujini.hatenablog.com


まずは、移動の制御をやっていきます。Unityですと移動制御に使えるメソッドに Mathf.Clamp なんてのがあるらしいですね。
参考
Mathf.Clamp - Unity スクリプトリファレンス


そんで、Rigidbody2D のコントロールから外れるので再度スクリプトを描きなおします。これやるんだったら、玉転がしゲームよろしく四隅に判定を置いたほうがどうにかなるのではと思ってしまいましたが、やっていきます。

※ うまくいかないバージョン(ボタンクリックで開閉します)

※ うまくいったバージョン(ボタンクリックで開閉します)


うまくいかない版のやり方では削除範囲を超えてしまうのでGetComponent().velocityを使用せず、transform.positionのみで行うようにするようです。
プレイヤー.cs_追記02

public class Player : MonoBehaviour
{
  void Update ()
  {
    // 省略
    // 移動の制限
    Move (direction); 
  }
  // 機体の移動
  void Move (Vector2 direction)
  {
    // 画面左下のワールド座標をビューポートから取得
    Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0, 0));
    // 画面右上のワールド座標をビューポートから取得
    Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(1, 1));
    // プレイヤーの座標を取得
    Vector2 pos = transform.position;    
    // 移動量を加える
    pos += direction * spaceship.speed * Time.deltaTime; 
    // プレイヤーの位置が画面内に収まるように制限をかける
    pos.x = Mathf.Clamp (pos.x, min.x, max.x);
    pos.y = Mathf.Clamp (pos.y, min.y, max.y);   
    // 制限をかけた値をプレイヤーの位置とする
    transform.position = pos;
  }
}

んでMoveメソッドを書き換えたことによってSpaseship.cs(使いまわし)、Enemy.csで不要になるので書き換えます。

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class Spaceship : MonoBehaviour
{
  // 移動スピード
  public float speed;
  // 弾を撃つ間隔
  public float shotDelay;
  // 弾のPrefab
  public GameObject bullet;
  // 弾を撃つかどうか
  public bool canShot;
  // 爆発のPrefab
  public GameObject explosion;

  // 爆発の作成
  public void Explosion ()
  {
    Instantiate (explosion, transform.position, transform.rotation);
  }
  // 弾の作成
  public void Shot (Transform origin)
  {
    Instantiate (bullet, origin.position, origin.rotation);
  }
}
using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour
{
  // Spaceshipコンポーネント
  Spaceship spaceship; 
  IEnumerator Start ()
  {   
    // Spaceshipコンポーネントを取得
    spaceship = GetComponent<Spaceship> ();   
    // ローカル座標のY軸のマイナス方向に移動する
    Move (transform.up * -1);
    
    // canShotがfalseの場合、ここでコルーチンを終了させる
    if (spaceship.canShot == false) {
      yield break;
    }
    
    while (true) {     
      // 子要素を全て取得する
      for (int i = 0; i < transform.childCount; i++) {       
        Transform shotPosition = transform.GetChild (i);       
        // ShotPositionの位置/角度で弾を撃つ
        spaceship.Shot (shotPosition);
      }     
      // shotDelay秒待つ
      yield return new WaitForSeconds (spaceship.shotDelay);
    }
  }
  
  // 機体の移動
  public void Move (Vector2 direction)
  {
    GetComponent<Rigidbody2D>().velocity = direction * spaceship.speed;
  }
  
  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);
  }
}

まあ、spaceship.Move()に関する事項を削除して書き換えているだけです。

ちなみに、Gravity Scaleにつけていた値*1がこの辺りで悪さをし始めてきて0にしないと境界をぶち抜くようになってしまいました。多分加速した数値が境界となる部分を超えてしまうからだと思う。やっぱり四隅を……。まあ、ちょっとずつ後退するスクリプトぐらいなんとかなるからいいか…。

これを実行するとこうなります(引き撃ちに対する謎のこだわりはなんとか実装できました)。
f:id:andron:20180610162053g:plain



はい、といったわけで移動の制限ができましたので次はタイトルを作っていきます。まずはタイトルオブジェクトを作成しーの、GUI Textを選択しーので作っていきたいところですが最新版では非推奨要素ですのでここでオリチャーを発動します。「GameObject → UI → Text」を選択していきます。Canvasをカメラに紐づけしーの、テキストをいじりーとやっていきます。
f:id:andron:20180610162646p:plain

んでんで、このままだとタイトルが表示されただけなのでメインWaveが垂れ流しの状態になります。それを制御するスクリプトを書きます。まずは空(Title)オブジェクトを作成しーの、Manager.csを作りーの、それにプレイヤーを紐づけーのやっていきます。

using UnityEngine;

public class Manager : MonoBehaviour
{
  // Playerプレハブ
  public GameObject player;

  // タイトル
  private GameObject title;

  void Start ()
  {
    // Titleゲームオブジェクトを検索し取得する
    title = GameObject.Find ("Title");
  }

  void Update ()
  {
    // ゲーム中ではなく、Xキーが押されたらtrueを返す。
    if (IsPlaying () == false && Input.GetKeyDown (KeyCode.X)) {
      GameStart ();
    }
  }

  void GameStart ()
  {
    // ゲームスタート時に、タイトルを非表示にしてプレイヤーを作成する
    title.SetActive (false);
    Instantiate (player, player.transform.position, player.transform.rotation);
  }

  public void GameOver ()
  {
    // ゲームオーバー時に、タイトルを表示する
    title.SetActive (true);
  }

  public bool IsPlaying ()
  {
    // ゲーム中かどうかはタイトルの表示/非表示で判断する
    return title.activeSelf == false;
  }
}

これをつくることで任意にプレイヤーが呼び出せるようになるので、デフォにあるプレイヤーは不要となります。でも、とりあえず実行するとNullReferenceExceptionが出たりするので、「Edit → ProjectSetting → ScriptExcutionOrder」から実行順をManage.csが一番上にくるようにいじります。そしてManager呼んでも他のスクリプトの関係が適当になっているのでそこを修正していきます。
f:id:andron:20180610162513p:plain

Player.cs_追記

public class Player : MonoBehaviour
{
  void OnTriggerEnter2D (Collider2D c)
  {
    // レイヤー名がBullet (Enemy)またはEnemyの場合は爆発
    if( layerName == "Bullet (Enemy)" || layerName == "Enemy")
    {
      // Managerコンポーネントをシーン内から探して取得し、GameOverメソッドを呼び出す
      FindObjectOfType<Manager>().GameOver();
    }
  }
}

ゲームオーバー判定の追加です。

Emitter.cs_追記

public class Emitter : MonoBehaviour
{
  // Managerコンポーネント
  private Manager manager;

  IEnumerator Start ()
  {
    // Waveが存在しなければコルーチンを終了する
    // 略

    // Managerコンポーネントをシーン内から探して取得する
    manager = FindObjectOfType<Manager>();    
    while (true) {
      // タイトル表示中は待機
      while(manager.IsPlaying() == false) {
        yield return new WaitForEndOfFrame ();
      }
    }
  }
}

シーン遷移関連。

こんな感じのManager呼出追記修正していけばいけます。オリチャー発動してもUI関連に影響しないところだったから特に問題ありませんでした。実行するとこうなります。
f:id:andron:20180610162230g:plain



といった感じで制御できるようです。このまま後編部分で残りの内容全部書き切ろうかと思ったのですが予想よりも文字数が長くなってしまったので残りをPart 2に持っていきたいと思います。まあ、次の制御部分に「アニメーションのレイヤーの作成」ってのがあってそれについてはもう少しちゃんと覚えてから記事に残したいかなって思ったのと残りがスコア関連の管理とかで地味すぎてあまりやる気が起きないってのが主な理由です。あと、画像が重いってのもある。Youtubeにあげるほうがいいのかね……。

まあいいや。Part 2へ続く。

*1:意味もなく引き撃ち機能として残しておいたやつのことね