ゲーム制作勉強中!あこがれだったプログラマーに今からなろう!

昔、あこがれていたプログラマー。今からでも勉強してみようと思い立ち、チャレンジ開始! 勉強メモや、悪戦苦闘な日々の記録です。

ウィザードリィライクのダンジョンゲームを作る!(9):戦闘の流れをつくる。



今回から戦闘、BattleSystemを作っていきます。
基本的な形は、しまづさんの、
youtube.com
がベースになっています。

大きな違いは、ソロ戦闘ではなくパーティ戦なので、Player側もEnemy側も複数いるため、かなり処理が複雑になっている事と、各Manager・ControllerとUniRXを使って連携しているところでしょうか。
いわゆる、Model-View-Presenterパターンに似た構造を取っています。(厳密にはちょっと違うけれど・・・)
また、しまづさんのyoutubeは、作成時期のせいだと思いますが、Stateパターンの組み方をenumをSwitch分岐していますが、
今回作るゲームについては、最近、しまづさんに教わった、Battle SystemをOwnerにしたState単位に分けています。
かなり色々と変えていますが、基礎の考え方は、しまづさんのYouTubeが下敷きになっている事には変わりありません。

処理の流れは、ざっくりこんなイメージになっています。


まず、PlayerController.csから戦闘開始のSignalを受信したGameManager.csは、EnemyManager.csに敵パーティを生成させて受け取ります。
次に、 PlayerController.csのPlayerUnitと、生成された敵パーティをBattleSystem.csに渡して、戦闘開始する流れです。

ちょっとまどろっこしいですが、この様に、各Manager・Controllerは極力、互いに直接接続させず、GameManager.csに処理を集中させる作り方にしています。

さて、今回ですが、結局、まだ戦闘準備になります。
というのも、前まででCharacter.csは作成したものの、実際にそれをどのように運用するかを決めていないので、今回はその辺りになります。
まず、Playerですが、これはシンプルにPlayerController.csに持たせようと思います。

using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
using R3;

public class PlayerController : MonoBehaviour
{
    [SerializeField] public Character[] players = new Character[2];
    [SerializeField] public PlayerUnit[] playerUnits = new PlayerUnit[2];
・
・
・

次に、PlayerのCharacter.csが搭載されるPlayerUnit.csですが、

using System.Collections.Generic;
using UnityEngine;

public class PlayerUnit : CharacterUnit
{

    public List<Item> bkpack_items{ get => Character.bkpack_items; set => Character.bkpack_items = value; }  

    [SerializeField] TMPro.TextMeshProUGUI nameText;    
    [SerializeField] TMPro.TextMeshProUGUI hpText;
    [SerializeField] TMPro.TextMeshProUGUI mpText;
    [SerializeField] TMPro.TextMeshProUGUI StatusText;
・
・
・

この様に、UIの設定が格納されていますので、いっそ、UIに搭載してしまおうと思います。

どうも、僕が作ると面白味のないデザインになってしまうので、UIのデザインはお好みで🤣

次に、Enemy側の所在ですがEnemyManager.csを作って、そこに管理を任せてしまいます。
EnemyManager.csは、EnemyのListを持ち、そこから敵パーティを生成します。

using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    //EnemyのListを持つ
    [SerializeField] public Character[] enemys;

    public Character[] CreateEnemysParty(int floor)//現在はfloorは未使用。
    {
        int enemyCount = Random.Range(1, 4);//出現するEnemyの数を決める。

        Character[] enemysParty = new Character[enemyCount];

        for (int i = 0; i < enemyCount; i++)
        {
            int enemyIndex = Random.Range(0, enemys.Length);
            enemysParty[i] = enemys[enemyIndex].Clone(1);//将来的にはここでEnemyのレベルも変動させる。
        }

        return enemysParty;
    }
}

いずれはダンジョンの階層によって、Enemyの数や種類、Enemyのレベルも調整させようと考えてはいますが、現時点ではシンプルな作りです。
戦闘が開始されると、GameManager.csEnemyManager.csを使って敵パーティ(enemysParty)を生成し、BattleSystem.csに受け渡します。
Enemyも搭載されるべきCharacterUnit.cs(EnemyUnit.cs)が必要ですが、それは戦闘でしか使わないので、BattleSystem.csに持たせることにします。

メインのBattleSystem.csですが、正直、まだまだ開発途中なので、大幅に変わる可能性大です。😅

using UnityEngine;
using R3;
using System.Collections.Generic;
using System;

public class BattleSystem : MonoBehaviour
{
    public BatteleStateBase CurrentState;
    public BatteleState_Idle Idle;
    public BattleState_Setup Setup;
    public BattleState_ActionSelection ActionSelection;
    public BattleState_EnemyActionSelection EnemyActionSelection;
    public BattleState_ActionTurn ActionTurn;
    public BattleState_TurnEnd BattleTurnEnd;

    [SerializeField] public GameObject EnemyImages;//Enemyの絵が表示される土台
    [SerializeField] public MenuController battle_Command_menu;

    public PlayerUnit[] playerUnits;//戦闘中のPlayerUnitへの参照を保存する。

    public GameObject[] enemyUnits;//EnemyManagerで作成された敵パーティを格納

    [SerializeField] public GameObject plafab;//EnemyUnitのPrefab

    private void Awake() 
    {
        Idle = new BatteleState_Idle(this);
        Setup = new BattleState_Setup(this);
        ActionSelection = new BattleState_ActionSelection(this);
        EnemyActionSelection = new BattleState_EnemyActionSelection(this);
        ActionTurn = new BattleState_ActionTurn(this);
        BattleTurnEnd = new BattleState_TurnEnd(this);
        CurrentState = Idle;
    }

    void Update()
    {
        CurrentState.OnUpdate();   
    }

    public void ChangeState(BatteleStateBase state)
    {
        if(CurrentState != null)
        {
            CurrentState.OnExit();
        }

        CurrentState = state;
        CurrentState.OnEnter();
    }

    public void Battle_Start(Character[] enemys,PlayerUnit[] get_playerUnits)
    {
        enemyUnits = new GameObject[enemys.Length];
        playerUnits = new PlayerUnit[get_playerUnits.Length];
        playerUnits = get_playerUnits;

        //同じ種類の敵がいた場合、名前の後ろにA,B,C...をつける
        {
            Dictionary<string, int> nameCount = new Dictionary<string, int>();

            foreach (var enemy in enemys)
            {
                string enemyName = enemy.CharacterName;
                if (!nameCount.ContainsKey(enemyName))
                {
                    nameCount[enemyName] = 0;
                }

                char plus = (char)('A' + nameCount[enemyName]);
                enemy.CharacterName = $"{enemyName}{" "}{plus}";
                nameCount[enemyName]++;
            }
        }

        //エネミーリストをBattlerNameBaseでソート
        System.Array.Sort(enemys, (a, b) => a.CharacterName.CompareTo(b.CharacterName));

        for (int i = 0; i < enemys.Length; i++)
        {
            enemyUnits[i] = Instantiate(plafab, new Vector3(0, 0, 0), Quaternion.identity);
            enemyUnits[i].transform.SetParent(EnemyImages.transform);
            enemyUnits[i].GetComponent<EnemyUnit>().Setup(enemys[i], i + 1, this);
        }

        ChangeState(Setup);
    }
}

細かい説明はおいおいしていくとして、今日のところはBattle_Start関数について
GameManager.csから、このBattle_Start関数が呼ばれ、同時にEnemyManager.csで生成されたenemysPartyと、PlayerContorollerが管理しているPlayerUnitへの参照がBattleSystem.csに受け渡されます。

Battle_Start関数は最初に受け取ったenemysPartyの敵の名前を確認して、敵の識別用に名前の後ろにA,B,C・・・を足しています。
まず、敵の名前と数という辞書型変数を作成して、

Dictionary<string, int> nameCount = new Dictionary<string, int>();

次に、enemysPartyに登録されているCharacter.csを一つずつ取り出し、名前を確認して、
辞書型変数にすでにその名前が登録されていれば、該当の名前の数を増やしています。

foreach (var enemy in enemys)
{
   string enemyName = enemy.CharacterName;
   if (!nameCount.ContainsKey(enemyName))//辞書型変数に名前が含まれているか確認。
   {
      nameCount[enemyName] = 0;//なければ、新たに、その名前と0を登録する。
   }

そして、辞書型変数の数を参照して名前の後ろにA,B,C・・・を足しています。
これはchar型の足し算という方法で、
ざっくり言えば、char型の「A」という文字に1を足してやると「B」になる。という、一瞬、頭が😵‍💫するような法則に則っています。

   char plus = (char)('A' + nameCount[enemyName]);
   enemy.CharacterName = $"{enemyName}{" "}{plus}";
   nameCount[enemyName]++;
}


敵パーティの名前の整理が終わったけれど、enemysPartyはEnemyManager.csがランダムにEnemyListから選んで作成しているので、名前ごとに並んでいるわけではないです。
今のところは、このままでも支障はないけれど、後々、魔法とかで範囲攻撃を導入する際に、敵が名前ごとに並んでいる方が都合が良いかもしれないので、並び替えることにします。

System.Array.Sort(enemys, (a, b) => a.CharacterName.CompareTo(b.CharacterName));

そして、最後にEnemyUnit.csに搭載してやり、これでようやく戦闘準備が完了です。

for (int i = 0; i < enemys.Length; i++)
{
    enemyUnits[i] = Instantiate(plafab, new Vector3(0, 0, 0), Quaternion.identity);
    enemyUnits[i].transform.SetParent(EnemyImages.transform);
    enemyUnits[i].GetComponent<EnemyUnit>().Setup(enemys[i], i + 1, this);
}


次から、各Stateの説明に行けるかなぁ・・・