パート④:プレイヤーを移動できるようにする@2Dローグライク公式tutorial解説【Unity2018】

このパートで行うこと

いよいよこのパートで自分の分身となるプレイヤーを作る。
このパートではキーボードの十字キーを使って、プレイヤーを動かすところまでやってしまおう。
これで一層ゲームらしくなるはずだ。

f:id:snoopopo:20180710112109g:plain


前回の記事はこちら。
パート③:ランダムに配置される物体(アイテム・内壁)を配置する@2Dローグライク公式tutorial解説【Unity2018】


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


MovingObject

プレイヤーの作成に入る前にクラスの構成を説明しておこう。

このゲームで作成しなくてはいけない物体も残りプレイヤーと敵のみとなった。
この2つに共通するのは動くということである。
動くための処理を行うクラス MovingObject を作成し、 プレイヤーと敵はこのクラスを継承して処理を共有させる。

f:id:snoopopo:20180709105740p:plain


まずは今回作成するMovingObject クラスの定義とメソッドの一覧だ。
各メソッドが行うことの概要はコメントに書いた。

MovingObject .cs
public abstract class MovingObject : MonoBehaviour {
    protected virtual void Start () {
        //各処理で必要な初期処理を行う。
    }   

    protected IEnumerator SmoothMovement(Vector3 end) {
        //実際に位置を変えて移動させる。
    }

    protected bool Move(int xDir, int yDir) {
        //移動先に何らかの物体があるかどうかチェックする。
        //何もない場合はSmoothMovementを呼んで移動する。
        //移動した場合はtrueを返す。
    }

    protected virtual void AttemptMove(int xDir, int yDir) {
        //MoveやOnCantMoveといった移動処理に関する一連の処理を呼び出す。
        //外部のクラスからこのオブジェクトを移動させるための入り口。
    }
}

MovingObjectクラスは abstract =抽象クラスであり、このクラスを直接インスタンス生成することはできない。
このパート後半で作成するPlayerクラスなどの子クラスでないと使うことができないし、 ゲームオブジェクトにアタッチすることもできない。

Playerゲームオブジェクトを作成する

ここまでの説明でunity側で作成できる部分は作ってしまおう。

新規に 「MovingObject」スクリプトと、「Player」スクリプトを作成する。

MovingObject.cs
using System.Collections;
using UnityEngine;

public abstract class MovingObject : MonoBehaviour {
    protected virtual void Start() {
        //各処理で必要な初期処理を行う。
    }

    protected IEnumerator SmoothMovement(Vector3 end) {
        //実際に位置を変えて移動させる。

        yield return null; //TODO 仮 コンパイルエラーをなくすために固定でnullを返す
    }

    protected bool Move(int xDir, int yDir) {
        //移動先に何らかの物体があるかどうかチェックする。
        //何もない場合はSmoothMovementを呼んで移動する。
        //移動した場合はtrueを返す。

        return true; //TODO 仮 コンパイルエラーをなくすために固定でtrueを返す
    }

    protected virtual void AttemptMove(int xDir, int yDir) {
        //MoveやOnCantMoveといった移動処理に関する一連の処理を呼び出す。
        //外部のクラスからこのオブジェクトを移動させるための入り口。
    }
}

先に説明した通りPlayerクラスは、MovingObjectを継承する。

Player.cs
public class Player : MovingObject { //MonoBehaviourではない
  //中身はあとで書いていくよ
}

新規に「Player」ゲームオブジェクトも作成して、「Player」スクリプトをアタッチしておこう。

またこの状態だとゲームを起動しても何も表示されないので、 SpriteRenderer#Sprite に「Scavengers_SpriteSheet_0」をつけ、 SpriteRenderer#SortingOrder を「Units」にして床やアイテムよりも手前に表示されるようにしておく。

プレハブ名 Sprite SortingOrder
Player Scavengers_SpriteSheet_0 Units

f:id:snoopopo:20180710101955p:plain

MovingObject # SmoothMovement メソッド

さっそくMovingObject#SmoothMovement()の実装から入る。
SmoothMovement() は、実際に位置を動かして移動を行うメソッドだ。

protected IEnumerator SmoothMovement(Vector3 end){
    //移動処理
}

まずこのメソッドがコルーチンとなっている点に注目しよう。

コルーチンとは?

コルーチンとは、戻り値がIEnumerator型となっているメソッドのことで、 複数のフレームをまたいだ処理を記述することができる。

右に5px移動→0.1秒待機→右に5px移動

のような時間経過を伴う処理を書きやすくする仕組みだ。 毎フレームよばれる Update()で時間の経過をカウントしながら処理を行うことも可能だがコルーチンを使った方がシンプルでわかりやすいコードとなる。

基本的なことはこちらの記事が非常にわかりやすい。

developer.wonderpla.net

今回のゲームでは1フレームで1マス分移動するのではなく、1マス分の距離を数フレームかけて少しずつ位置をずらして移動する。 そのためコルーチンを使用する。


SmoothMovement()のコードはこのようになる。

MovingObject .cs
using System.Collections;
using UnityEngine;

public abstract class MovingObject : MonoBehaviour {

    public float moveTime = 0.1f;
    private Rigidbody2D rb2d;
    private float inverseMoveTime;

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

    protected IEnumerator SmoothMovement(Vector3 end) {
        float sqrRemainingDistance = (transform.position - end).sqrMagnitude;

        while (float.Epsilon < sqrRemainingDistance) {
            Vector3 newPosition = Vector3.MoveTowards(rb2d.position, end, inverseMoveTime * Time.deltaTime);
            rb2d.MovePosition(newPosition);
            sqrRemainingDistance = (transform.position - end).sqrMagnitude;
            yield return null;
        }
    }
}

移動を行うには物理演算を用いる。 物理演算とは重力や摩擦といった自然現象を加味した処理を行える仕組みでunityの2DではRigidbody2Dコンポーネントを使うだけで その仕組みを利用することができる。

SmoothMovement()Rigidbody2Dを使用するため、アタッチされているRigidbody2Dを事前にStart()で取得する。 つまり「MovingObject」スクリプトがアタッチされたゲームオブジェクトには、必ずRigidbody2Dコンポーネントをアタッチしておかなければならない。 *1
SmoothMovement()ではなく、Start()で取得する理由は、 移動のたびに何度も呼ばれるSmoothMovement()で何度も取得する必要はなく、最初の一度だけ取得すればよいからである。

PlayerゲームオブジェクトにRigidbody2Dをアタッチする

ここで今説明したRigidbody2Dを「Player」ゲームオブジェクトにさっそくアタッチしてしまおう。
この状態でゲームを起動するとキャラクターが下に落ちていってしまう。

f:id:snoopopo:20180710103155g:plain

先に説明した通り、Rigidbody2Dをつけたゲームオブジェクトは物理演算用いた処理=重力の影響を受けるようになるため、下に落ちて行ってしまう。 このゲームでは意図した動きでないことは明白だろう。
重力の影響を受けなくするためには、Rigidbody2D#BodyTypeを「Kinematic」 に変更する。

f:id:snoopopo:20180710103424p:plain

f:id:snoopopo:20180710103429p:plain

落下しなくなった!

Vector3#sqrMagnitude

SmoothMovement()の説明に戻ろう。

Vector3.sqrMagnitude - Unity スクリプトリファレンス

Vector3#sqrMagnitudeは、ベクトルを二乗して足したものを返す。
ここではVector3を用いているが例を簡単にするためにVector2で説明すると、 Vector2(2,3).sqrMagnitude は13を返す、といった具合だ。 これによって現在位置と移動したい位置の差があるかどうかがわかる。同じ位置にいるなら0となる。

f:id:snoopopo:20180623220336p:plain

2点の距離を出すには、平方根の計算をVector3#magnitudeで行う必要があるが、平方根の計算は非常に重い処理だ。 ここでわかりたいことは、「現在位置と移動後の位置の差がどれくらいあるか」ではなく、「現在位置と移動後の位置の差があるかどうか」が知りたいので、 軽量なVector3#sqrMagnitudeを使う。

whileループで毎回、現在位置と移動後位置が同じになるかをチェックし、なってなければ移動を行う。

float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
while(float.Epsilon < sqrRemainingDistance) {
    //~ここに移動処理が入る~
    sqrRemainingDistance = (transform.position - end).sqrMagnitude;
}

Vector3#MoveTowards

public static Vector3 MoveTowards (Vector3 current, Vector3 target, float maxDistanceDelta);

Vector3.MoveTowards - Unity スクリプトリファレンス より

上記はVector3#MoveTowardsの定義である。
引数のstartからendまでの距離をmaxDistanceDelta ずつ移動した位置を返す。

f:id:snoopopo:20180710110852p:plain

maxDistanceDelta の値は1回呼ばれるごとに移動する距離を表している。

answers.unity.com

MovingObject # Move メソッド

Move()は、移動する量を受け取る。
移動できる場合はSmoothMovement()を呼び移動を開始し、trueを返す。
移動後の位置に何らかの物体があり移動できなかった場合はfalseを返す想定だが、 そのチェックは次のパートにして、このパートでは移動することだけを優先させる。

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

    StartCoroutine(SmoothMovement(end));
    return true;
}

現在位置から移動後の位置を計算し、SmoothMovement()を呼びだす。
コルーチンは普通のメソッドと違ってStartCoroutineで呼びだすことに注意しておこう。

MovingObject # AttemptMove メソッド

最後にAttemptMove()だ。AttemptMove()は最初に説明した通り、それぞれの移動処理を呼び出し一連の移動処理を行う。

MovingObject .cs
protected virtual void AttemptMove(int xDir, int yDir) {
    bool canMove = Move(xDir, yDir);
}

子クラスでoverride(上書き)することを許可するため、virtual をつけておく。

キーボードでプレイヤーを動かせるようにする

プレイヤーはキーボードの十字キーで動くようにする。
PlayerGameManager に処理を追加しよう。

Player.cs
using UnityEngine;

public class Player : MovingObject {

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

        int horizontal = 0;
        int vertical = 0;

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

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

        if (horizontal != 0 || vertical != 0) {
            AttemptMove(horizontal, vertical);
        }
    }

    protected override void AttemptMove(int xDir, int yDir) {
        base.AttemptMove(xDir, yDir);
        GameManager.instance.playersTurn = false;
    }
}

Update() で十字キーの入力を受け取り、

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

最後にAttemptMove() を呼んで、キーボードから取得した移動量を渡す。


GameManagerのソースは以下のようになる。追加したメソッドはUpdate()WaitTurn()だ。

GameManager .cs
using System.Collections;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager instance;
    private BoardManager boardScript;

    public float turnDelay = 0.1f;
    [HideInInspector] public bool playersTurn = true; //trueならプレイヤー移動可能
    private bool wait;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }
        DontDestroyOnLoad(gameObject);

        boardScript = GetComponent<BoardManager>();
        InitGame();
    }

    void InitGame()
    {
        boardScript.SetupScene();
    }

    void Update()
    {
        if (playersTurn || wait)
        {
            return;
        }
        StartCoroutine(WaitTurn());
    }

    IEnumerator WaitTurn()
    {
        wait = true;
        yield return new WaitForSeconds(turnDelay);
        yield return new WaitForSeconds(turnDelay);
        playersTurn = true;
        wait = false;
    }
}

playersTurnはプレイヤーが移動可能なタイミングで true となる。
通常publicなフィールドはインスペクターに表示されてしまうが、[HideInInspector]をつけることでインスペクターに表示されなくなる。
playersTurnはスクリプトからしか触らせないためだ。


プレイヤーが一度に動きすぎるのを防ぐため、プレイヤーが移動を開始したら
WaitTurn()コルーチンでWaitForSecondsを使ってturnDelayで定義した時間、処理を待つこととする。
本来このタイミングは、敵の行動ターンにとなり後のパートで敵の動作処理を行うようにする。そのためWaitTurn()の中身は敵の処理を作るまでの仮の実装であるのでここでの細かな説明は省かせて頂こう。


これでプレイヤーを動かす準備がすべて整ったので、ゲームを起動してみよう。

f:id:snoopopo:20180710112109g:plain

次回予告

このパートでは移動処理を行う抽象クラス、MovingObjectを作成し、プレイヤーを動かすことに成功した。

しかし、今みてもらったとおり、壁やアイテムをすり抜けてしまう状態である。
次のパートではフィールドに配置した物体にあたり判定をつけ、壁をすりぬけないようにし、さらにプレイヤーが壁を破壊する処理をつくろう。

⇒ 続きのパートはこちら。

www.snoopopo.com

*1:ここでは行っていないが、RequireComponent というAttributeをつけることで特定のコンポーネントをアタッチすることを強制することもできる。