パート⑤:あたり判定をつけて壁を破壊できるようにする@2Dローグライク公式tutorial解説【Unity2018】

前回のパートではプレイヤーを十字キーで動かすことに成功した。
しかし、壁やアイテムをすり抜けてしまう状態だった。

このゲームでプレイヤーの移動を阻む物体は、壁と敵の2つであり、敵はまだ作っていないのでこのパートでは壁について対処していく。
まずは壁をすり抜けないようにし、さらに記事の後半ではプレイヤーが壁を破壊できるようにもする。

f:id:snoopopo:20180715114838g:plain


前回の記事はこちら。
パート④:プレイヤーを移動できるようにする@2Dローグライク公式tutorial解説【Unity2018】


※この記事は、unity公式チュートリアル「2D Roguelike tutorial」を解説した連載記事です。
この連載記事共通の事項(実施している環境等)は以下の記事を参照ください。
表紙:2Dローグライク公式tutorial解説【Unity2018】


壁をすり抜けないようにする

MovingObjectの修正

前回作成したMovingObjectPlayerの親クラスとした。
これは今後作る敵クラスでも同じように処理させたいためだったことを思い出そう。

このパートのはじめで壁と敵の2つが移動を妨げるものといったが、それはプレイヤー視点の話であり、
敵から見れば、壁とプレイヤーが移動を妨げるものとなる。

これは「xxと△△がぶつかったら動けない」という風に言い換えられる。
つまり、プレイヤーと敵の「ぶつかったら動けない」という処理は同じなので、 MovingObject に処理を書くことができる。


MovinObject の変更はStart()Move()、そしてMove()を呼び出すAttemptMove()である。

MovingObject .cs
public abstract class MovingObject : MonoBehaviour {

    public LayerMask blockingLayer;//←追加
    private BoxCollider2D boxCollider;//←追加

    //その他のフィールドは変更なしなので省略

    protected virtual void Start() {
        this.rb2d = GetComponent<Rigidbody2D>();
        this.boxCollider = GetComponent<BoxCollider2D>(); //←追加
        this.inverseMoveTime = 1f / moveTime;
    }

    protected bool Move(int xDir, int yDir, out RaycastHit2D hit) {
        Vector2 start = transform.position;
        Vector2 end = (start + new Vector2(xDir, yDir));

        this.boxCollider.enabled = false;
        hit = Physics2D.Linecast(start, end, this.blockingLayer);

        this.boxCollider.enabled = true;

        if (hit.transform == null) {
            StartCoroutine(SmoothMovement(end));
            return true;
        }

        return false;
    }

    protected virtual void AttemptMove(int xDir, int yDir) {
        RaycastHit2D hit;
        bool canMove = Move(xDir, yDir, out hit);
    }

    //その他のメソッドは変更なしなので省略
}

BoxCollider2Dをアタッチする

上記の修正を適応した状態で、一度unityエディタ側に戻ろう。

Start()メソッドでは、BoxCollider2D というコンポーネントを取得しているので、 BoxCollider2Dコンポーネントがアタッチされていることが前提でこのスクリプトは動いている。
これは前回のパートで登場したRigidbody2Dと同じだ。

さっそく「Player」プレハブにBoxCollider2Dをアタッチしよう。

コライダーとは物理衝突範囲を定義するコンポーネントである。
物理演算を用いて衝突したことを感知(=「当たり判定」)させたいゲームオブジェクトにつける、ということをまずは覚えておこう。

docs.unity3d.com

BoxCollider2Dは四角形に当たり判定範囲を指定することができるコライダーの1つであり、 当たり判定範囲は、Sceneビューで緑の線で囲まれた範囲となる。

f:id:snoopopo:20180715103024p:plain

緑の線に囲まれた範囲に何かが当たると当たったことを感知できるゾ!

BoxCollider2D#Offset#Size をXYともに0.9に変えて範囲のサイズを変え、回りに余裕を持たせておこう。

f:id:snoopopo:20180715103320p:plain

ちなみに「Edit Collider」を押すと、Sceneビューでもマウスカーソルでサイズを調整できる。

f:id:snoopopo:20180715103743g:plain

これで「Player」ゲームオブジェクトは当たったことを感知できる物体となった。

移動可能かチェックする

MovingObject#Move()の説明に戻ろう。
壁をすり抜けない=壁にぶつかりそうだったら移動しない、という処理はどのように行えば良いだろうか。

様々な方法があるとは思うが、
ここでは移動する前に移動する予定の位置に壁があるかどうかをチェックし、なければ移動する。あれば移動しない。という風に実装しよう。

移動する予定の位置に壁があるか?はPhysics2D#Linecastを用いてチェックすることができる。

        hit = Physics2D.Linecast(start, end, this.blockingLayer);

Physics2D#Linecastは引数で渡すstartからendの位置にレイ=光線を引き、 線上にコライダーがアタッチされたゲームオブジェクトがあるかどうかを返す。

f:id:snoopopo:20180623221101p:plain

物体に当たった場合は、戻り値のRaycastHit2Dにそのゲームオブジェクトの情報を詰めて返す。

最後の引数である blockingLayer の型はLayerMaskだ。これはインスペクターで指定するLayerの事である。
このLayerを設定したゲームオブジェクトのみ当たり判定対象とすることができる。

f:id:snoopopo:20180623120836p:plain


上記のコードは自分がいる位置から移動予定の位置まで線を引き、その間に指定したLayerが設定されている物体があるかどうかをチェックしていることになる。

Physics2D#Linecast()の前後で自身のコライダーをon/offしている点に注意しよう。 これは自分自身にアタッチしているコライダーも当たり判定に引っかかってしまうためだ。

this.boxCollider.enabled = false;
hit = Physics2D.Linecast(start, end, this.blockingLayer);
this.boxCollider.enabled = true;

f:id:snoopopo:20180613160351g:plain

上図は、 Physics2D#Linecastで引いている線がどのようになっているのかをDebug.DrawLineを用して表示してみたものだ。
移動する都度、赤く表示した線上に物体があるかどうかをチェックしていることがわかる。

Layer の設定

ここまでの説明で、まだ足りていない設定がある。unityエディタ側に戻ろう。

移動を妨げる物体は、プレハブの種類で言うと、プレイヤー・敵・内壁・外壁の4つだった。
まだ作成していない敵以外の3種類のすべてのプレハブのLayerを「BlockingLayer」にする。 移動を妨げる=ブロックする物体という意味合いだ。

f:id:snoopopo:20180715105956p:plain

そして、「Player」プレハブのPlayer#blockingLayerで「BlockingLayer」を選択し、
移動を妨げるレイヤーを指定する。

f:id:snoopopo:20180715110209p:plain

また、Physics2D#Linecast()で当たり判定ができるのはコライダーだけだった。
「Player」プレハブにははすでにつけていたので、残りの内壁・外壁プレハブにも BoxCollider2D をつけよう。 敵プレハブは作ったときに設定することとする。

f:id:snoopopo:20180715110343p:plain


ここまで壁をすり抜けないようにする準備が整った。ゲームを起動して確認してみよう。

f:id:snoopopo:20180715110539g:plain

内壁を破壊できるようにする

このパートはまだ終わらない。
このゲームではプレイヤーは壁を破壊できる仕様なのである。破壊できる壁は、内壁のみで外壁は破壊できない。

Wall

新規に「Wall」スクリプトを作成しよう。

プレイヤーが壁を破壊しようとした時に、Wall#DamageWall()を呼びだす想定である。

DamageWall()では、壁のHPを引数で受け取るダメージ量の分だけ減らしていく。
そして、HPが0になった時にこのオブジェクトは非アクティブにして壁を破壊してしまおう。

Wall .cs
using UnityEngine;

public class Wall : MonoBehaviour {

    public Sprite dmgSprite; //破壊されているときの画像
    public int hp = 4; //内壁のhp

    private SpriteRenderer spriteRenderer; 

    void Awake() {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    public void DamageWall(int loss) {
        spriteRenderer.sprite = dmgSprite;
        hp -= loss;

        if (hp <= 0) {
            gameObject.SetActive(false);
        }
    }
}

移動の妨げになった物体が何かを判別する

次に壁を壊すプレイヤー側の実装を行う。

MovingObjectに新規にOnCantMove()メソッドを追加する。
これは移動できなかった際に呼ばれる処理を書くメソッドでabstractだ。

protected abstract void OnCantMove<T>(T component) where T : Component;

abstract メソッドは親クラスで定義だけ行い、実際の処理は子クラスで実装することを強制するものだ。
つまり親クラスのMovingObjectでは「移動できなかった時に行う処理」とだけ定義しておき、「壁が邪魔で移動できなかった時は、壁を破壊する」という実際の処理は子クラスのPlayerで書くこととなる。

TComponent型である。Component型はゲームオブジェクトにアタッチされる全てのコンポーネントの基底クラスである。
このメソッドでは、移動できない要因となった物体の情報をComponent型で受け取り、子クラス側で処理を行う。

例えば、移動後の位置に壁があり移動できなかった、といった場合は、Component型に「壁」という情報がComponent型で渡ってくる。ということだ。

MovingObjectの変更はAttemptMove()と新規に追加するOnCantMove()だ。

MovingObject .cs
   protected virtual void AttemptMove<T>(int xDir, int yDir) where T : Component {
        RaycastHit2D hit;
        bool canMove = Move(xDir, yDir, out hit);

        if(hit.transform == null){
            return; //何も当たってない
        }
            
        T hitComponent = hit.transform.GetComponent <T> ();
            
        if(!canMove && hitComponent != null){ //移動できなかったし、当たったものがある
            OnCantMove (hitComponent);
        }
    }

    protected abstract void OnCantMove<T>(T component) where T : Component;

当たり判定を行ったRaycastHit2Dで当たったものがある場合、 以下のように当たったものを取得することができる。

この後、Playerクラスにて、T にはWallを指定する。
壁と当たった場合は、GetComponent()Wallが取れるという訳である。

T hitComponent = hit.transform.GetComponent <T> ();

次にPlayerだ。

親のMovingObjectに加えたメソッド定義の変更はPlayerにも適応する必要がある。
OnCantMove()Wall#DamageWall()を呼びwallDamageで定義したダメージ量を与える。

Player .cs
using UnityEngine;

public class Player : MovingObject {
    public int wallDamage = 1;

    private void Update() {
        if (!GameManager.instance.playersTurn) return;

        int horizontal = 0;
        int vertical = 0;

        horizontal = (int)(Input.GetAxisRaw("Horizontal"));
        vertical = (int)(Input.GetAxisRaw("Vertical"));

        Debug.Log(vertical +"," + horizontal);

        if (horizontal != 0) {
            vertical = 0;
        }

        if (horizontal != 0 || vertical != 0) {
            AttemptMove<Wall>(horizontal, vertical); //壁の当たり判定をチェックする
        }
    }

    protected override void AttemptMove<T>(int xDir, int yDir) {

        base.AttemptMove<T>(xDir, yDir);
        GameManager.instance.playersTurn = false;
    }

    protected override void OnCantMove<T>(T component) {
        Wall hitWall = component as Wall; //Tが壁なら
        hitWall.DamageWall (wallDamage);
    }
}

「Wall」ゲームオブジェクトの設定

ここまでで壁を破壊する準備が整ったのでUnityエディタ側に戻ろう。

内壁プレハブに作成したWallコンポーネントをアタッチする。

Wall#dmgSpriteには壁がダメージを受けた時の画像を設定しよう。 Wall#hpは変えても変えなくてもどちらでも良い。

プレハブ名 dmgSprite
Wall1 Scavengers_SpriteSheet_48
Wall2 Scavengers_SpriteSheet_49
Wall3 Scavengers_SpriteSheet_50
Wall4 Scavengers_SpriteSheet_51
Wall5 Scavengers_SpriteSheet_52
Wall6 Scavengers_SpriteSheet_52
Wall7 Scavengers_SpriteSheet_53
Wall8 Scavengers_SpriteSheet_54

ここまで出来たらさっそくゲームを起動してプレイヤーが壁を破壊できることを確認してみよう。

f:id:snoopopo:20180715114838g:plain

次回予告

このパートではRaycastHit2Dを用いて当たり判定を行い、壁とプレイヤーがぶつかる時の処理を実装した。

次のパートではプレイヤーがアイテムとぶつかる時の処理を実装する。
壁と違い、プレイヤーはアイテムを入手するので、このパートで行ったような移動の妨げは行わず、 今回とはまた違った方法で当たり判定を行う方法が登場する。

⇒ 次パートはこちら

www.snoopopo.com