Edited at

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

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や効果音など

  • パラメータ:レベルの概念の追加

  • アイテム:回復アイテムや攻撃アイテム・装備アイテム等

  • セーブ&ロードの実装

  • マップやフロアの移動

  • ストーリー要素:練習用なので最低限で良い

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