【チュートリアル】uGUIとPhysics2Dでシューティングを作ってみる

今度は簡単な2Dシューティングゲームを作りたいと思います。
今回もプログラム経験が浅い人向けに気を付けたいと思います。
コチラの記事も参考にしてください。
【チュートリアル】uGUIとPhysics2Dでブロック崩しを作ってみる
チュートリアルとして試しに作ってみました。 比較的プログラムをあまりできない人向けにできる限り崩して書くようには気を付けます。 また、画像が用意できない人向けに画像リソースは一切使わないようにしてます。 ※この記事はUnity...

この記事で作るもの

今回は超簡単な2D縦シューティングゲームを作りたいと思います。
例のごとくスマホ向けで画面サイズは640×960前提としたいと思います。
(最近は画面いっぱい使うのがだいぶ当たり前になってきましたが……)

プロジェクトの初期設定をすませる

以前の記事の`uGUIの設定をする`までは一緒です。
【チュートリアル】uGUIとPhysics2Dでブロック崩しを作ってみる
チュートリアルとして試しに作ってみました。 比較的プログラムをあまりできない人向けにできる限り崩して書くようには気を付けます。 また、画像が用意できない人向けに画像リソースは一切使わないようにしてます。 ※この記事はUnity...
  • Unityのプロジェクトを作る
  • フォルダの整理
  • uGUIの設定をする
この3つを終わらせて下さい。
プロジェクト名はShooting2Dで行きます。
終わった段階でGameAreaというゲームオブジェクトが完成しているはずです。

Hierarchyの整理

前回同様にHierarchyを整理します。
ただ、前回とゲームの構成要素が違いますのでここから手順が変わってきます。
シューティングでの構成要素だと、自機自機の弾敵の弾という感じでしょうか。
というわけでこれ用のゲームオブジェクトを作っておきます。
一番手間に表示されてほしい物をHierarchyの下に来るように並べています。
これらのRectTransformはStrechにしてLeft等はすべて0にしておきます。
この様な感じです。Hierarchyで4つ選択した状態で変更すれば全部まとめて変更出来ます。

壁を作る

以前のブロック崩しと同じ感じで壁を用意します。
Wallsを右クリックしてImageを生み出します。
あとは壁となる位置に配置します。これを上下左右4か所行います。
自機が外に出ないように衝突判定を付ける為、BoxCollider2Dを付けるのも忘れないように。
壁が出来ました。
BoxCollider2Dの大きさは、少し大きめにしています。
これで画面の外に壁が出来ている状態になりましたので、自機が画面の外に出ていかなくなります。

自機を作る

次に自機を作ります。
Playersを右クリックしてImageを生み出します。
大きさや位置は好みの場所に設定してください。
私は大きさ50 x 50と、位置(0, -400)にしています。
これに衝突判定用のCircleCollider2DRigidbody2Dを設定します。
※CircleColliderの大きさ(radius)は自機の大きさの半分が見た目通りになります。
AddComponentからPhysics 2DからCircle Collier 2DとRigidBody 2Dを設定します。
重力で下に勝手に落ちないようにGravity Scale0に設定します。

タップで動くようにする

この自機をタップ(クリック)した場所に移動するようにします。
ただし、移動するにあたってタップ位置に瞬間移動するのはさすがにまずいので、速度を決めてタップしている場所に少しづつ移動するようにします。

まずはタップ判定

では、スクリプトを用意します。
ScriptsフォルダにPlayer.csを用意します。
ブロック崩しでタッチ判定をしたようにタッチ時の処理を記述します。
今回はタッチしている間、タッチしている場所に動き続けるという処理にしたいので、タッチが終わった時を検知できるようにします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Player
    : MonoBehaviour
{
    public void OnTouch(BaseEventData arg)
    {
        PointerEventData e = arg as PointerEventData;
        Debug.Log( "Touch:" + e.position );
    }

    public void OnTouchEnd(BaseEventData arg)
    {
        Debug.Log( "End!" );
    }
}

 

これをGameObjectに登録します。
次に、ブロック崩しと同様に画面いっぱいのタッチエリアを用意して、この関数がよばれるように設定します。
Canvasを右クリックしてImageを生成します。
Strechで全て0に設定して画面いっぱいにし、不透明度を0にして見えなくします。
EventTrigger(Add Component → Event → EventTrigger)を追加します。
押したとき、動いたとき、離したときのイベントを設定します。
Add New Event Typeを押して、Pointer DownDragPointer Upを増やします。
Pointer DownDragにはOnTOuchを、Pointer UpにはOnTouchEndを設定します。
これで無事、クリックしたり、ドラッグしたらその座標がConsoleに表示されるようになりました。

押した場所に移動させる

次にこの押された座標へ自機を移動させます。
まずは押されたタイミングで目標地点の座標を保存するようにします。
public class Player
    : MonoBehaviour
{
    /// <summary>
    /// 移動する目標点
    /// </summary>
    private Vector2 targetPosition_;

    public void OnTouch(BaseEventData arg)
    {
        PointerEventData e = arg as PointerEventData;
        targetPosition_ = e.position;
    }

    public void OnTouchEnd(BaseEventData arg)
    {
        Debug.Log( "End!" );
    }
}

 

Updateで今の座標と比べて離れていればそこに速さ分近づきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Player
    : MonoBehaviour
{
    /// <summary>
    /// 移動する目標点
    /// </summary>
    private Vector2 targetPosition_;

    /// <summary>
    /// 速度
    /// </summary>
    [SerializeField]
    private float speed_ = 500;

    private void Start()
    {
        targetPosition_ = transform.position;
    }

    private void Update()
    {
        var canvas = GetComponentInParent<Canvas>();

        var nowPosition = new Vector2(transform.position.x, transform.position.y);
        var different = targetPosition_ - nowPosition;    // 目標との差

        // 速度をフレームレートや画面サイズ比で調整
        var speed = speed_ * canvas.transform.localScale.x * Time.deltaTime;

        // 今の速度で目標地点にたどりつくかを判定
        if (different.magnitude > speed) {
            // 遠いので速度分近寄る
            nowPosition = nowPosition + different.normalized * speed;
            transform.position = nowPosition;
        } else {
            // 近いのでそこに移動する
            transform.position = targetPosition_;
        }
    }

    public void OnTouch(BaseEventData arg)
    {
        // タッチされている場所を目標地点に設定
        PointerEventData e = arg as PointerEventData;
        targetPosition_ = e.position;
    }

    public void OnTouchEnd(BaseEventData arg)
    {
        // タッチが終わったので今の座標を目標地点に上書き
        targetPosition_ = transform.position;
    }
}

 

色々詰め込みましたが、まず速度を表す変数を用意しました。
これはエディター上から調整できるように[SerializeField]にしました。数値は自由に調整してください。
次にUpdateで目標地点に向かって移動します。
var different = targetPosition_ - nowPosition;    // 目標との差

 

ベクトルは引き算したら、どれくらいどの向きに離れているかが計算できます。(といっても数値引いただけでそうなるのですが……)
// 速度をフレームレートや画面サイズ比で調整
var speed = speed_ * canvas.transform.localScale.x * Time.deltaTime;

 

localScaleを書けているのは以前のブロック崩しの記事を参考にしてください。画面の大きさの違いによる相対的な速度の違いを吸収します。
Time.deltaTimeを掛けることで端末の処理能力による速度の違い(いわゆる処理落ち)を吸収します。
// 今の速度で目標地点にたどりつくかを判定
if (different.magnitude > speed) {
    // 遠いので速度分近寄る
    nowPosition = nowPosition + different.normalized * speed;
    transform.position = nowPosition;
} else {
    // 近いのでそこに移動する
    transform.position = targetPosition_;
}

 

magnitudeでどれくらい離れているかの距離が分かりますので、それが速度より大きいかを比べます。
速度より小さい時はこのフレームでそこに到達できるので、直接その座標を設定します。
速度より大きい時は、そのフレームではたどり着かないので、速度分近づきます。
targetPosition_ = transform.position;

 

今の座標に、向き(normalizedで向きが分かる)に速度を掛けたものを足します。
nowPosition = nowPosition + different.normalized * speed;

 

StartOnTouchEndで目標地点を今の座標に設定しています。
これでクリックした場所に自機がついてきてくれます。

ところが壁が……

動かしてみるとすぐに気づくと思いますが、このままでは壁をすり抜けます。
ブロック崩しの時は特に問題なかったのですが、おそらくここは物理演算していてつまずきやすいポイントな気がします。
上のソースではtransform.positionを直接変更しました。ところが物理演算の座標を管理しているのはRigidbody2Dです。
Transformの座標や回転を直接変更してしまうと、物理演算システムがGameObjectの位置を見失ってしまいます。
その結果衝突判定などをうまく計算できずに、すり抜けてしまったり、当たってないのに当たったことになったりというのが起こります。
つまり、座標を動かしたいなら物理演算の管理をしているRigidbody2Dのメソッドを使えばよいだけです。
というわけでソースを書き換えます。
座標の設定にはMovePositionというのが用意されています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Player
    : MonoBehaviour
{
    /// <summary>
    /// 物理演算用
    /// </summary>
    private Rigidbody2D rigidbody_;

    /// <summary>
    /// 移動する目標点
    /// </summary>
    private Vector2 targetPosition_;

    /// <summary>
    /// 速度
    /// </summary>
    [SerializeField]
    private float speed_ = 500;

    private void Start()
    {
        rigidbody_ = GetComponent<Rigidbody2D>();
        targetPosition_ = transform.position;
    }

    private void Update()
    {
        var canvas = GetComponentInParent<Canvas>();

        var nowPosition = rigidbody_.position;
        var different = targetPosition_ - nowPosition;    // 目標との差

        // 速度をフレームレートや画面サイズ比で調整
        var speed = speed_ * canvas.transform.localScale.x * Time.deltaTime;

        // 今の速度で目標地点にたどりつくかを判定
        if (different.magnitude > speed) {
            // 遠いので速度分近寄る
            nowPosition = nowPosition + different.normalized * speed;
            // transform.position = nowPosition;
            rigidbody_.MovePosition( nowPosition );
        } else {
            // 近いのでそこに移動する
            rigidbody_.MovePosition( targetPosition_ );
        }
    }

    public void OnTouch(BaseEventData arg)
    {
        // タッチされている場所を目標地点に設定
        PointerEventData e = arg as PointerEventData;
        targetPosition_ = e.position;
    }

    public void OnTouchEnd(BaseEventData arg)
    {
        // タッチが終わったので今の座標を目標地点に上書き
        targetPosition_ = rigidbody_.position;
    }
}

 

transform.positionrigidbody_.positionに変えました。
※変数名をrigidbody_にしています。rigidbodyだとエラーになるので気を付けてください。
もし、rigidbody_.positionでエラー(NullReferenceException)が出る場合は、PlayerのGameObjectにRigidBody2Dが設定されていませんので、設定をもう一度確認してください。
これだけで壁にぶつかるようになります。
回るのはこれはこれで面白いので放置します。(RigidBody2DのFleez Rotation設定するだけで回らなくなりますが……)

自機の弾を出す

自機の移動が出来ましたので、次は弾を作ります。
まずは、いきなり自機から生み出すのではなく、Hierarchyに作って動きを確認します。
適当に黄緑色で作りました。大きさも適当です。ColliderとRigidbody2Dを忘れずに。
RigidBody2DのGravityScale0にするのも忘れずに。
出来たらこれの動きをソースで設定します。
とりあえず上に動くだけで実装します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerBullet
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var canvas = GetComponentInParent<Canvas>();
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, 1 ) * canvas.transform.localScale.x * speed_;
    }

    void Update()
    {

    }
}
PlayerBullet.csを作ったら動きを記述します。
Rigidbodyに上向きの速度を設定して終わりです。
PlayerBulletに設定するのを忘れないようにしておきます。
天井に突き刺さりますが、とりあえず動きました。

CanvasのlocalScale掛けるのわかりにくい……

毎度速度の類にはCanvasのlocalScaleを書けていますが、分かりにくいので今の内に別に用意しておこうと思います。(それ以外に理由もなくは無いですが)
新たにソースを用意します。名前はGameManagerとしておきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager
    : MonoBehaviour
{
    /// <summary>
    /// sataticがついているとnewとかしなくても使える
    /// </summary>
    private static float canvasScale_;

    void Awake()
    {
        var canvas = GetComponentInParent<Canvas>();
        canvasScale_ = canvas.transform.localScale.x;
    }

    /// <summary>
    /// 画面の大きさで速度を調整する
    /// </summary>
    /// <param name="speed"></param>
    /// <returns></returns>
    public static float AdjustForCanvas(float speed)
    {
        return speed * canvasScale_;
    }
}

 

static変数に保存して、staticのメソッドで利用できるようにしました。
GameAreaにスクリプトを設定します。
PlayerとPlayerBulletでlocalScale掛けてるところをこれに変えておきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Player
    : MonoBehaviour
{
    /// <summary>
    /// 物理演算用
    /// </summary>
    private Rigidbody2D rigidbody_;

    /// <summary>
    /// 移動する目標点
    /// </summary>
    private Vector2 targetPosition_;

    /// <summary>
    /// 速度
    /// </summary>
    [SerializeField]
    private float speed_ = 500;

    private void Start()
    {
        rigidbody_ = GetComponent<Rigidbody2D>();
        targetPosition_ = transform.position;
    }

    private void Update()
    {
        var nowPosition = rigidbody_.position;
        var different = targetPosition_ - nowPosition;    // 目標との差

        // 速度をフレームレートや画面サイズ比で調整
        var speed = GameManager.AdjustForCanvas( speed_ * Time.deltaTime );

        // 今の速度で目標地点にたどりつくかを判定
        if (different.magnitude > speed) {
            // 遠いので速度分近寄る
            nowPosition = nowPosition + different.normalized * speed;
            // transform.position = nowPosition;
            rigidbody_.MovePosition( nowPosition );
        } else {
            // 近いのでそこに移動する
            rigidbody_.MovePosition( targetPosition_ );
        }
    }

    public void OnTouch(BaseEventData arg)
    {
        // タッチされている場所を目標地点に設定
        PointerEventData e = arg as PointerEventData;
        targetPosition_ = e.position;
    }

    public void OnTouchEnd(BaseEventData arg)
    {
        // タッチが終わったので今の座標を目標地点に上書き
        targetPosition_ = rigidbody_.position;
    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerBullet
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, 1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {

    }
}
canvasをけして、localScaleを掛けていた部分をGameManager.AdjustForCanvasに変えました。
これで毎度覚えににくいGetComponentInParentしてtransform.localScale.xを掛けなくて済みます。

※副次効果

上にちょろっと書きましたが、これにはもう一つ効果があります。
それはGetComponentInParent<Canvas>()が出来ないところでこの計算がしたいときです。
staticで事前に数値を保存してあるのでどこでも使えます。

弾を1秒ごとに発射する

ボタンとかで弾を出すのは、マウスやタッチ操作では面倒なので、自動発射にします。

まず弾をPrefab化

Prefabにすると、ソースからそのGmaeObjectをコピーして量産できるようになります。
やり方は簡単で、HierachyからProjectへドラッグして持っていくだけです。
Hierarchyの方はいらないので消します。

プレイヤーにPrefabと召喚先を設定

プレイヤーにこの弾の元となるPrefabと、弾を生み出す先を設定します。
/// <summary>
/// 弾の元となるPrefab
/// </summary>
[SerializeField]
private GameObject originalBullet_;

/// <summary>
/// 弾を生み出す場所
/// </summary>
[SerializeField]
private RectTransform bulletArea_;

 

メンバ変数に設定項目を用意します。
1秒に一回弾を生み出すようにソースを書き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Player
    : MonoBehaviour
{
    /// <summary>
    /// 物理演算用
    /// </summary>
    private Rigidbody2D rigidbody_;

    /// <summary>
    /// 移動する目標点
    /// </summary>
    private Vector2 targetPosition_;

    /// <summary>
    /// 速度
    /// </summary>
    [SerializeField]
    private float speed_ = 500;

    /// <summary>
    /// 弾の元となるPrefab
    /// </summary>
    [SerializeField]
    private GameObject originalBullet_;

    /// <summary>
    /// 弾を生み出す場所
    /// </summary>
    [SerializeField]
    private RectTransform bulletArea_;

    private IEnumerator GenerateBullet()
    {
        while (true) {
            yield return new WaitForSeconds( 1 );   // 1秒町

            // Prefabのコピーを生み出す
            var bullet = GameObject.Instantiate( originalBullet_, transform.position, Quaternion.identity, bulletArea_);
        }
    }

    private void Start()
    {
        rigidbody_ = GetComponent<Rigidbody2D>();
        targetPosition_ = transform.position;

        // 弾を生み出す処理開始
        StartCoroutine( GenerateBullet() );
    }

    /* 略 */
}

 

GenerateBulletはコルーチンです。
コルーチンについては以下の記事を参考にしてください。
お手軽に並列処理が出来ると思ってもらって大丈夫です。
Unityのコルーチン機能を使う
Unityにはコルーチンという便利な機能があります。 (Unityの機能というか.Netの機能をUnityが使っているというか…) コルーチンとは? コルーチン自体はUnity特有のものではありません。 マイクロスレッドやフ...
無限ループで1秒に1回弾を生み出し続けるようにします。

弾がプレイヤーと壁にぶつかる……

さて、弾が生み出されるようになったのは良いのですが、弾がプレイヤーに当たりはじかれたり、天井に突き刺さったりするのが気になります。
というわけで、弾はプレイヤーや壁には当たらない、けど敵には当たるという風に設定をしたいと思います。
Unityではこういう時の為に物理演算の設定に何が何と当たるのか、という設定が存在します。
メニューのEditを選択します。
Project SettingsPhysics2Dの設定を開きます。
この一番下にあるLayer Collision Matrixがその設定です。Layer毎に当たる当たらないをチェックで設定出来ます。

まずレイヤーを作る

というわけで、自機自機弾敵弾のレイヤーを作ります。
右上のLayersからEdit Layers…を開きます。
User Layersとなっている部分は好きに設定してよいのでここを利用します。

GameObjectのレイヤーを変更する

変えたいGameObjectを選択したらInspector右上のLayerのところを押して選択します。
子供のGameObjectもまとめて変えるか聞かれますので、yesを選択します。
Prefabの設定も忘れずに変えておきます。

Collision Matrixを変える

Physics2D設定を再度開いて、対応表を設定します。
  • 敵 ⇔ プレイヤー
  • 敵弾 ⇔ プレイヤー
  • 壁 ⇔ プレイヤー
  • 自機弾 ⇔ 敵
このような関係で当たるとよさそうです。
これで大丈夫そうです。

弾は画面の外に行ったら消えるようにする

生み出した後画面の外に行っても健気に飛び続けるので画面の外の辺りに行ったら消えるようにします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerBullet
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, 1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if (transform.position.y > 1000) {
            GameObject.Destroy( gameObject );
        }
    }
}

 

敵を作る

次に敵を作ります。
例のごとく、EnemiesImageを追加します。
座標や大きさは適当に設定します。Colliderのradiusは大きさの半分です。
これのも動きを付けます。
Enemy.csを用意して、とりあえずこいつもとにかく下に動くだけにします。
中身はプレイヤーの弾と一緒な感じです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, -1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if (transform.position.y < -100) {
            GameObject.Destroy( gameObject );
        }
    }
}

 

初期座標をのyを600くらいにしておくと画面上から現れて下に消えていきます。
プレイヤーとぶつかると飛んでいきます。

敵が自機とぶつかるようにする

とりあえずぶつかりはするので、ぶつかったらゲームをリセットするようにします。
Player.csに衝突時の処理を追加します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;

public class Player
    : MonoBehaviour
{

    /* 略 */

    private void OnCollisionEnter2D( Collision2D collision )
    {
        // ぶつかったら再度シーンを読み込む
        SceneManager.LoadScene( "Game" );
    }
}

 

using UnityEngine.SceneManagement;足しています。
これで敵に当たりに行ってみます。
無事シーンが再読み込みされたかと思います。

自機の弾が敵にぶつかるよにする

では自機の弾が相手に当たるようにします。
まずEnemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, -1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if (transform.position.y < -100) {
            GameObject.Destroy( gameObject );
        }
    }
    private void OnCollisionEnter2D( Collision2D collision )
    {
        // ぶつかったら自分を消す
        GameObject.Destroy( gameObject );
    }
}

 

次にPlayerBullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerBullet
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, 1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if (transform.position.y > 1000) {
            GameObject.Destroy( gameObject );
        }
    }

    private void OnCollisionEnter2D( Collision2D collision )
    {
        // ぶつかったら自分を消す
        GameObject.Destroy( gameObject );
    }
}

 

ほぼ一緒です。
自機の為を当ててみると弾と敵が消えるようになっていれば成功です。

弾を出すようにする

さて、敵も弾を生み出すようにします。まずはプレイヤーの時と同様に一度Hierarchyに作って、あとでPrefabにします。
弾の動きを付けます。ほぼ自機の弾と一緒ですが、こっちは少しランダム感を付けようと思います。
EnemyBullet.csを用意してPlayerBulletを元に動きを書いていきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyBullet
    : MonoBehaviour
{

    [SerializeField]
    private float speed_ = 400;

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        var random = Quaternion.Euler( 0, 0, Random.Range(-30, 30) );   // ±30°の角度をランダムでつける
        rigidbody.velocity = random * new Vector2( 0, -1 ) * GameManager.AdjustForCanvas( speed_ );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if ( transform.position.y < -100 ) {
            GameObject.Destroy( gameObject );
        }
    }

    private void OnCollisionEnter2D( Collision2D collision )
    {
        // ぶつかったら自分を消す
        GameObject.Destroy( gameObject );
    }
}

 

Quaternionで速度ベクトルを回転させます。回転量はRandom.Rangeで±30°が適当に決まります。
Quaternionでベクトルを回転させるときは掛けるだけ良いようにしてくれます。
ちゃんとGameObjectに設定するのを忘れないようにしておきます。

自機弾と同じようにPrefab化と弾の生成処理を追加

Prefabの元になったGameObjectはいらないので消してください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy
    : MonoBehaviour
{
    [SerializeField]
    private float speed_ = 400;

    /// <summary>
    /// 弾の元となるPrefab
    /// </summary>
    [SerializeField]
    private GameObject originalBullet_;

    /// <summary>
    /// 弾を生み出す場所
    /// </summary>
    [SerializeField]
    private RectTransform bulletArea_;

    private IEnumerator GenereateBullet()
    {
        while (true) {
            yield return new WaitForSeconds( 1 );   // 1秒待ち

            // 弾を生み出す
            var bullet = GameObject.Instantiate( originalBullet_, transform.position, Quaternion.identity, bulletArea_ );
        }
    }

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.velocity = new Vector2( 0, -1 ) * GameManager.AdjustForCanvas( speed_ );

        // 弾を生み出し始める
        StartCoroutine( GenereateBullet() );
    }

    void Update()
    {
        // 画面の外くらいで消える
        if (transform.position.y < -100) {
            GameObject.Destroy( gameObject );
        }
    }

    private void OnCollisionEnter2D( Collision2D collision )
    {
        // ぶつかったら自分を消す
        GameObject.Destroy( gameObject );
    }
}

 

自機と同様にコルーチンで1秒に1回弾をコピーして生み出します。
実行するたびに左右違う方向に弾を生み出していると思います。

弾が自機とぶつかるようにする

特にする事はありません。
Collision Matrixをあらかじめ設定しているので自機と敵弾は、特になにもしなくても当たります。
そして、自機は何かとぶつかるとゲームが再起動するようになっています。

壁が怪しい

上に「自機は何かとぶつかるとゲームが再起動するようになっています。」と記述しました。
これはつまり壁とぶつかってもゲームオーバーになってしまいます……
実際試してみるとそうなってしまうと思います。
というわけで、Player.cs内のぶつかったときの判定を追加し、相手が壁の時はLoadSceneしないように対応します。
    private void OnCollisionEnter2D( Collision2D collision )
    {
        var other = collision.gameObject;    // ぶつかった相手
        if ( other.layer == LayerMask.NameToLayer( "Wall" ) ) {
            // 壁相手なので何もせず、物理演算エンジンに任せる
        } else {
            // 壁以外にぶつかったら再度シーンを読み込む
            SceneManager.LoadScene( "Game" );
        }
    }

 

簡単です。引数にぶつかった相手の情報が入っているのでLayerが壁かどうかで判断しています。
これで壁にぶつかってもゲームオーバーになりません。

敵を配置する

自機と敵とのやり取りがなんとなくできたので、敵を配置します。

ソースに配置情報を書く

とりあえずソースにどういうタイミングで敵がでるかを記述します。
情報管理用のEnemyManager.csを作ります。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager
    : MonoBehaviour
{
    /// <summary>
    /// 敵の配置情報
    /// </summary>
    [Serializable]
    public class EnemyInfo
    {
        /// <summary>
        /// 出現する時間
        /// </summary>
        public float time;

        /// <summary>
        /// 出現位置
        /// </summary>
        public float x;

        /// <summary>
        /// 出現位置
        /// </summary>
        public float y;
    }

    /// <summary>
    /// 敵の配置情報をエディターで編集する
    /// </summary>
    [SerializeField]
    private List<EnemyInfo> enemyInfos_;

    void Start()
    {
    }

    void Update()
    {
    }
}

 

出来ました。
Serializableを付けたクラスはエディタ上で編集することが出来ます。
エディターで設定できるのでこれは適当に入力します。
これで1秒あとと2秒あとの2体出現する想定です。

敵をPrefab化

量産する為に敵をPrefabにします。
ただし、1点まずい部分があります。Prefabはシーンとは関係ない独立した設定なので、Prefabの中にHierarchyの設定を含めることが出来ません。
つまり、この敵Prefabには敵弾を生み出す、EnemyBulletを設定できません。
これを解決する為にこの設定はEnemy.csにメソッドを用意してEnemyManagerから設定できるように変更します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy
    : MonoBehaviour
{
    /* 略 */

    /// <summary>
    /// 弾を生み出す場所を設定
    /// </summary>
    /// <param name="bulletArea"></param>
    public void SetBulletArea(RectTransform bulletArea)
    {
        bulletArea_ = bulletArea;
    }

}

 

EnemyManagerに敵のPrefabや弾の生成先などを設定できるようにしておきます。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager
    : MonoBehaviour
{
    /* 略 */

    /// <summary>
    /// 敵の配置情報をエディターで編集する
    /// </summary>
    [SerializeField]
    private List<EnemyInfo> enemyInfos_;

    /// <summary>
    /// 敵の元Prefab
    /// </summary>
    [SerializeField]
    private RectTransform originalEnemy_;

    /// <summary>
    /// 敵の出現先
    /// </summary>
    [SerializeField]
    private RectTransform enemyArea_;

    /// <summary>
    /// 弾の出現先
    /// </summary>
    [SerializeField]
    private RectTransform bulletArea_;

    /* 略 */
}

 

 
不要なHierarchyのEnemyは消します。

経過時間で敵を生成

次にゲームの開始からの経過時間で敵を生成するようにします。
やり方は単純で、Updateで経過時間を足していき、設定の時間と比べるだけです。
同じ敵を何度も生み出さないように、どこまで生成したかも保存しておきます。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyManager
    : MonoBehaviour
{
    /// <summary>
    /// 敵の配置情報
    /// </summary>
    [Serializable]
    public class EnemyInfo
    {
        /// <summary>
        /// 出現する時間
        /// </summary>
        public float time;

        /// <summary>
        /// 出現位置
        /// </summary>
        public float x;

        /// <summary>
        /// 出現位置
        /// </summary>
        public float y;
    }

    /// <summary>
    /// 敵の配置情報をエディターで編集する
    /// </summary>
    [SerializeField]
    private List<EnemyInfo> enemyInfos_;

    /// <summary>
    /// 敵の元Prefab
    /// </summary>
    [SerializeField]
    private RectTransform originalEnemy_;

    /// <summary>
    /// 敵の出現先
    /// </summary>
    [SerializeField]
    private RectTransform enemyArea_;

    /// <summary>
    /// 弾の出現先
    /// </summary>
    [SerializeField]
    private RectTransform bulletArea_;

    /// <summary>
    /// 経過時間
    /// </summary>
    private float time_;

    /// <summary>
    /// どこまで敵を生成したか
    /// </summary>
    private int index_;

    void Start()
    {
        time_ = 0;
        index_ = 0;
    }

    void Update()
    {
        if ( index_ < enemyInfos_.Count ) {     // 敵が最後まで出ていないか判定
            var current = enemyInfos_[index_];
            if ( time_ >= current.time ) {          // 出現時間を過ぎているか判定
                // 敵の出現時間を過ぎているので生成
                var enemy = GameObject.Instantiate( originalEnemy_, new Vector3( current.x, current.y, 0 ), Quaternion.identity, enemyArea_ );
                // 弾の生成先を設定
                var enemyComponent = enemy.GetComponent<Enemy>();
                enemyComponent.SetBulletArea( bulletArea_ );

                // 次の敵へ
                index_ += 1;
            }
        }

        // 時間加算
        time_ += Time.deltaTime;
    }
}

 

最終的な敵はこうなりました。
この配置情報をファイルなんかでバリエーションを増やせば、それがステージになりますね。

次の課題

  • 敵のバリエーション
  • 弾のバリエーション
  • 配置情報のバリエーション
次の課題としてはこのような感じになるかと思います。
余裕があればいずれも記事にしようと思います。
タイトルとURLをコピーしました