7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-10

1.gif
ターン制の移動とダメージ・非ダメージを実装。ひとまずはローグライクの基礎の基礎部分は自力で作れた。

#基本的な作り

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

#Player.cs

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);
        //HP0以下になったら敵を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

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); //絶対値を計算

        //5マス以上離れいている時はエネミーは移動停止
        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);
        //HP0以下になったら敵を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

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

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や効果音など
  • パラメータ:レベルの概念の追加
  • アイテム:回復アイテムや攻撃アイテム・装備アイテム等
  • セーブ&ロードの実装
  • マップやフロアの移動
  • ストーリー要素:練習用なので最低限で良い

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

7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?