【Unity2D】0からのローグライク作成記録その2～改良版ターン制移動Script～

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/370076/91918982-66de-34fd-76d6-c042ce1244db.gif)
ターン制の移動とダメージ・非ダメージを実装。ひとまずはローグライクの基礎の基礎部分は自力で作れた。

#基本的な作り
- [ローグライクを作ったので開発手順をまとめてみた](https://qiita.com/2dgames_jp/items/1730e7c4822091c3c320)
- [【Unity7】Unity公式チュートリアル2Dローグライクの導入【2Dローグライク1】](https://hiyotama.hatenablog.com/entry/2015/05/25/090000)

こちらが非常に参考になりました。アイテムや敵の自動生成は大変そうなので、今の段階では省略して、なおかつ自分流の簡単なアレンジも加えています。
重要なScriptは4つあるので参考に下に記載しておきます。

#Player.cs
```ruby:Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Player : MovingObject
{
    public SpriteRenderer Spr; // 表示スプライト
    public Animator anm; // アニメーションコンポーネント
    public GameObject GameManager;
    public GameState PlayerState;
    public GameObject HitEffect;
    public GameObject DeathEffect;

    //ここからステータス
    [System.NonSerialized] public int Hp = 100; //HP
    [System.NonSerialized] public int Atk = 30; //攻撃力
    [System.NonSerialized] public int Def = 25; //防御力

    //ここまでステータス

    void Update()
    {
        int horizontal = 0; //水平方向
        int vertical = 0; //垂直方向
        horizontal = (int)(Input.GetAxisRaw("Horizontal"));
        vertical = (int)(Input.GetAxisRaw("Vertical"));

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

    }

    //継承クラスMovingObjectのプレイヤー移動処理を実行
    protected override void AttemptMove(int Xdir, int Ydir)
    {
        GameManager = GameObject.FindGameObjectWithTag("GameManager");
        PlayerState = GameManager.GetComponent<GameManager>().CurrentGameState;
        
        if(PlayerState == GameState.KeyInput)
        {
            base.AttemptMove(Xdir, Ydir);
            GameManager.GetComponent<GameManager>().SetCurrentState(GameState.PlayerTurn);
        }
    }

    //エネミーに与えるダメージを計算して与える
    protected override void OnCantMove(GameObject hitComponent)
    {
        //衝突判定のあったオブジェクトの関数を取得し、変数を代入可能にする
        Enemy Script = hitComponent.GetComponent<Enemy>();
        //ダメージ計算
        int Damage = Atk * Atk / (Atk + Script.Def);
        //オブジェクトのHP変数にダメージを与える
        Script.Hp -= Damage;
        Instantiate(HitEffect, new Vector3(hitComponent.transform.position.x, hitComponent.transform.position.y), Quaternion.identity);
        //HPが0以下になったら敵をDestroyして死亡エフェクトを出すこと
        if (Script.Hp <= 0)
        {
            Destroy(hitComponent);
            Instantiate(DeathEffect, new Vector3(hitComponent.transform.position.x, hitComponent.transform.position.y), Quaternion.identity);
        }
        Debug.Log("あなたは" + Damage + "のダメージを与えた");
        Debug.Log("敵の残りHPは" + Script.Hp);
    }

}
```
プレイヤーのキー入力や移動・ダメージ処理などを入れています。
#Enemy.cs
```ruby:Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Enemy : MovingObject
{
    public SpriteRenderer Spr; // 表示スプライト
    public Animator anm; // アニメーションコンポーネント
    private GameObject Player; //プレイヤーの座標取得用
    private Vector2 TargetPos; //プレイヤーの位置情報
    public GameObject HitEffect;
    public GameObject DeathEffect;

    //ここからステータス
    [System.NonSerialized] public int Hp = 40; //HP
    [System.NonSerialized] public int Atk = 20; //攻撃力
    [System.NonSerialized] public int Def = 20; //防御力

    //ここまでステータス

    public void MoveEnemy()
    {
        // PLAYERタグの付いたオブジェクトを取得する
        Player = GameObject.FindGameObjectWithTag("Player");
        TargetPos = Player.transform.position;
        int Xdir = 0;
        int Ydir = 0;
        Xdir = (int)TargetPos.x - (int)this.transform.position.x;
        Ydir = (int)TargetPos.y - (int)this.transform.position.y;
        int AbsXdir = System.Math.Abs(Xdir); //絶対値を計算
        int AbsYdir = System.Math.Abs(Ydir); //絶対値を計算

        //５マス以上離れいている時はエネミーは移動停止
        if (AbsXdir > 5 || AbsYdir > 5)
        {
            return;
        }
        //プレイヤーとの座標差がX軸の方が大きいとき
        else if (AbsXdir > AbsYdir)
        {
            Xdir = Xdir / AbsXdir;
            AttemptMove(Xdir, 0);
        }
        //プレイヤーとの座標差がY軸の方が大きいとき
        else if (AbsXdir < AbsYdir)
        {
            Ydir = Ydir / AbsYdir;
            AttemptMove(0, Ydir);
        }
        //プレイヤーとの座標差がX軸・Y軸で等しい＝斜め45°にプレイヤーがいる
        else if (AbsXdir == AbsYdir)
        {
            Xdir = Xdir / AbsXdir;
            Ydir = Ydir / AbsYdir;
            AttemptMove(Xdir, Ydir);
        }
    }

    //継承クラスMovingObjectのプレイヤー移動処理を実行
    protected override void AttemptMove(int Xdir, int Ydir)
    {
            base.AttemptMove(Xdir, Ydir);
    }

    protected override void OnCantMove(GameObject hitComponent)
    {
        //衝突判定のあったオブジェクトの関数を取得し、変数を代入可能にする
        Player Script = hitComponent.GetComponent<Player>();
        //ダメージ計算
        int Damage = Atk * Atk / (Atk + Script.Def);
        //オブジェクトのHP変数にダメージを与える
        Script.Hp -= Damage;
        Instantiate(HitEffect, new Vector3(hitComponent.transform.position.x, hitComponent.transform.position.y), Quaternion.identity);
        //HPが0以下になったら敵をDestroyして死亡エフェクトを出すこと
        if (Script.Hp <= 0)
        {
            Destroy(hitComponent);
            Instantiate(DeathEffect, new Vector3(hitComponent.transform.position.x, hitComponent.transform.position.y), Quaternion.identity);
        }
        Debug.Log("敵はあなたに" + Damage +"のダメージを与えた");
        Debug.Log("あなたの残りHPは" + Script.Hp);
    }

}
```
こちらもPlayer.csと役割はほぼ同じ。敵のターンの挙動管理をしています。

#MovingObject.cs
```ruby:MovingObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class MovingObject : MonoBehaviour
{
    private Rigidbody2D rb;
    private BoxCollider2D boxCollider;
    private float MoveTime = 0.1f;
    private float InverseMoveTime;

    protected virtual void AttemptMove(int Xdir, int Ydir)
    {
        Vector2 StartPosition = transform.position;
        Vector2 EndPosition = StartPosition + new Vector2(Xdir, Ydir);
        //移動判定用　衝突するレイヤーはすべて入れる
        int LayerObj = LayerMask.GetMask(new string[] { "Chara", "Object" });
        //攻撃判定用　HPのあるオブジェクトを置くレイヤーは全て入れる
        int LayerCha = LayerMask.GetMask(new string[] { "Chara"});

        this.rb = GetComponent<Rigidbody2D>();
        this.boxCollider = GetComponent<BoxCollider2D>();

        //自身の衝突判定を無くしてPhysics2Dの誤確認を無くす
        boxCollider.enabled = false; 
        //移動先に障害物があるか判定する
        RaycastHit2D HitObj = Physics2D.Linecast(StartPosition, EndPosition, LayerObj);
        RaycastHit2D HitCha = Physics2D.Linecast(StartPosition, EndPosition, LayerCha);
        //衝突判定を戻す
        boxCollider.enabled = true; 

        //RaycastHit2Dで移動先に障害物が無ければMovementを実行
        if (HitObj.transform == null)
        {
            StartCoroutine(Movement(EndPosition));
        }
        //RaycastHit2Dで移動先にプレイヤーかエネミーがいればHitComponentに入れてOnCantMoveを実行
        else if (HitCha.transform != null)
        {
            GameObject HitComponent = HitCha.transform.gameObject;
            OnCantMove(HitComponent);
        }


    }

    protected IEnumerator Movement(Vector3 EndPosition)
    {
        float sqrRemainingDistance = (transform.position - EndPosition).sqrMagnitude;
        InverseMoveTime = 1f / MoveTime;
        //EndPositionまでスムーズに移動するらしい　UNITY公式コード丸パクリ
        while (sqrRemainingDistance > float.Epsilon)
        {
            Vector3 NewPosition = Vector3.MoveTowards(rb.position, EndPosition, InverseMoveTime * Time.deltaTime);
            rb.MovePosition(NewPosition);
            sqrRemainingDistance = (transform.position - EndPosition).sqrMagnitude;
            yield return null;
        }
    }

    protected abstract void OnCantMove(GameObject hitComponent);
}
```
これは継承クラスでプレイヤーと敵の移動処理を実装しています。「Unity 継承クラス」とかで調べると詳しく載っています。継承クラスなのでオブジェクトにアタッチせずとも良い。

#GameManager.cs
```ruby:GameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum GameState
{
    // 開始
    KeyInput, // キー入力待ち＝プレイヤーターン開始
    PlayerTurn, //プレイヤーの行動中
    EnemyBegin, // エネミーターン開始
    EnemyTurn, //エネミーの行動中
    TurnEnd,   // ターン終了→KeyInputへ変遷
};

public class GameManager : MonoBehaviour
{
    public static GameManager instance;
    public GameObject[] EnemyObj; //エネミーにアタッチしている関数を使うためのハコ
    public GameState CurrentGameState; //現在のゲーム状態
    float TurnDelay = 0.20f; //移動ごとの間隔

    void Awake()
    {
        SetCurrentState(GameState.KeyInput); //初期状態はキー入力待ち

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

    //現在のゲームステータスを変更する関数　外部及び内部から
    public void SetCurrentState(GameState state)
    {
        CurrentGameState = state;
        OnGameStateChanged(CurrentGameState);
    }

    void OnGameStateChanged(GameState state)
    {
        switch (state)
        {
            case GameState.KeyInput:
                break;

            case GameState.PlayerTurn:
                StartCoroutine("PlayerTurn");
                break;

            case GameState.EnemyBegin:
                SetCurrentState(GameState.EnemyTurn);
                break;

            case GameState.EnemyTurn:
                StartCoroutine("EnemyTurn");
                break;

            case GameState.TurnEnd:
                SetCurrentState(GameState.KeyInput);
                break;
        }
    }

    //キー入力後プレイヤーの移動中の処理
    IEnumerator PlayerTurn()
    {
        yield return new WaitForSeconds(TurnDelay);
        SetCurrentState(GameState.EnemyBegin);
        
    }

    //エネミーターンの処理
    IEnumerator EnemyTurn()
    {
        yield return new WaitForSeconds(TurnDelay);
        GameObject[] EnemyObj = GameObject.FindGameObjectsWithTag("Enemy");

        //EnemyObjの数だけEnemyにアタッチしている移動処理を実行
        for (int x = 0; x < EnemyObj.Length; ++x)
        {
            yield return new WaitForSeconds(TurnDelay);
            EnemyObj[x].GetComponent<Enemy>().MoveEnemy();
        }
        
        SetCurrentState(GameState.TurnEnd);
    }
}
```
GameManagerオブジェクトにアタッチする。ゲームの状態を管理する大事なScriptです。個人的に一番アレンジした箇所。enum GameStateでゲームの現在の状態を管理することにより、視覚的にも分かりやすくなっています。最初はIFとbool変数だけの管理で頑張っていたのですが、訳が分からなくなってしまったので改善しました。
#個人的に詰まったところ（初心者殺し）
- 継承クラス：この記事を書いている段階でも、正直理解しきれていない感があります。共通した処理を一つのScriptにまとめて、複数のクラスから参照出来るのが素晴らしい。ゲーム作成には必須だと思った。
- RaycastHit2D：指定した場所からビームを飛ばして、ビームに当たるものがあったら教えてくれるイメージ。当たったものが壁なら動かない。敵ならダメージ処理を開始するなどで使い分けれる。
- Enum：switch文との組み合わせが非常に効率的。正しい使い方をしているかは甚だ不明。
- コルーチン：ターンの変遷の間に待機時間を入れることが可能。待機時間を入れないと、プレイヤーと敵が同じ座標に移動してエラーが起こったりする。そもそもそんなエラーが起こらないようにすべきとも言う。

全体的に「良くわからんが動くからヨシ！」マインドで書いているので、後々大変になりそう。
#今後実装すべきもの
- UI：HPとか現在フロアとか所持アイテムとか
- 音：BGMや効果音など
- パラメータ：レベルの概念の追加
- アイテム：回復アイテムや攻撃アイテム・装備アイテム等
- セーブ＆ロードの実装
- マップやフロアの移動
- ストーリー要素：練習用なので最低限で良い

ローグライクは作っていて楽しいけど、先の長さを考えるとゾッとする。シューティングゲーとか横スクロールとかで経験を積むべきかとも思った。