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

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

ウィザードリィライクのダンジョンゲームを作る!(7):カスタマイズ可能なコマンドメニューを作る。



前回までで移動は一段落したので、いよいよ戦闘ができるようにしていきます。

ウィザードリィライクという事で、ターン制のコマンド型の戦闘になるのですが、
そのためには、当然ですが、コマンドを選択するためのコマンドメニューが必要です。

基本形は、しまづさんのYouTube
youtube.com
の#15で説明されている方法になりますが、
こちらを応用して、汎用性を高めていこうと思います。

尚、
上記のしまづさんのYouTubeシリーズは、一部チャンネルメンバー限定になっています。
ウィザードリィライクのダンジョンゲームを作る! 』 を書くに当たって、チャンネルメンバー限定部分の内容まで踏み込んで書く事を、しまづさんからお許し頂いています。🙇


さて、応用の内容ですが、
今後、色々な機能やシステムを追加していく事になり、色々な選択項目がでてくるのですが、その全てのコマンドメニューを個別に作成するのではなく、
スクリプトでメニューをカスタマイズできるようにして、後から使用用途に応じたコマンドメニューに簡単に変えられるようにしたいと思います。

というわけなので、メニュー側には自分がどこで使われても問題なく各Managerと連絡できるようにする手段を考える必要があります。

いろんな方法が考えられますが、今回は勉強中のリアクティブプログラミング(Reactive Programming)の実践として、UniRx、 せっかくなので、改版されたR3を使うことにしました。

R3の導入方法は、こちらで詳しく説明されています。
zenn.dev

UniRxからR3に変わったとはいえ、基本的な部分はあまり変わっていない様で
今回は、変数の値が変わると自動的に通知するReactivePropertyと、特定の型の変数を任意のタイミングで通知するSubjectを使いたいと思います。

ざっくりとした構成は、こんなイメージです。


しまづさんのコマンドメニューでは、各選択肢のテキストの色を変える指示をメニュー側のUpdate関数で毎フレーム行なっています。

これをUniRxを使って、選択されている項目が変わった時だけReactivePropertyによって指示が通知し、選択肢側で自分のindexと比較して色を変えるようにしました。
また、
必要な時に生成して使う予定なので、使い終わった時にメニューは破壊する前提にしているので、破壊時、購読関係を終了する

select_Button_index.OnCompleted();
Action_Buton.OnCompleted();

を加えてあります。
全体像としては、

using UnityEngine;
using R3;
using TMPro;
using UnityEngine.Events;

public class MenuController : MonoBehaviour
{

/// <summary>
/// メニューの設定用の変数群
/// </summary>

    bool Title = false;
    public UnityAction ButtonSetupAction;
    [SerializeField] int button_Count;
    [SerializeField] public int[] button_Index;//選択したボタンの番号として通知されるindex
    [SerializeField] public string[] button_Texts;
    [SerializeField] public bool[] button_Sleep;
    [SerializeField] public int[] button_Action_Types;//選択したボタンのindexに割り当てた機能番号
    RectTransform rectTransform;
    int button_index_max = 0;

/// <summary>
/// 連携システム
/// </summary>
    [SerializeField] public InputManager inputManager;
    [SerializeField] public SoundManager soundManager;

/// <summary>
/// メニューの表示・機能用の変数群
/// </summary>
    [SerializeField] CanvasRenderer canvasRenderer;
    [SerializeField] public Sprite OnClickSprite;//選択肢をクリックした時の画像
    [SerializeField] public Sprite NormalSprite;//選択肢をクリックしていない時の画像
    [SerializeField] public TMP_Text MenuTitle;
    public bool MenuSleep = true;
    //色を変えるボタンの番号を通知する。
    public ReactiveProperty<int> select_Button_index = new ReactiveProperty<int>(1);
    //上位のManagerに選択されたボタンの機能番号を通知する。
    public Subject<int> Action_Buton = new Subject<int>();


    public void Update(){
        if(MenuSleep)
        {
            return;
        }
        inputCheck();

    }

    public void inputCheck(){
        if(inputManager.IsPush)
        {
            inputManager.IsPush2 = false;
            inputManager.IsWait = true;
            Button_Action(select_Button_index.Value);
        }
    
        if(!inputManager.IsWait)
        {
            int cursor = select_Button_index.Value;
            bool isChanged = false;

            if(inputManager.LeftDirction.y > 0.4f)
            {
                cursor --;
                if(cursor < 1)
                {
                    cursor = button_index_max ;
                }
                isChanged = true;
            }
            else if(inputManager.LeftDirction.y < -0.4f)
            {
                cursor ++;
                if(cursor > button_index_max)
                {
                    cursor = 1;
                }
                isChanged = true;
            }

            if(isChanged)
            {
                this.select_Button_index.Value = cursor;
                inputManager.IsWait = true;
                soundManager.PlaySE(0);
            }
        }
    }

    public void Button_Action(int index){
        select_Button_index.Value = index;//ボタン選択状態を判定するindex

        int PushNumber = 0;
        //選択したボタンの番号から、対応する機能番号の位置を取得する。
        for(int i = 0; i < button_Count; i++)
        {
            if(button_Index[i] == index)
            {
                PushNumber = i;
            }
        }

        Action_Buton.OnNext(button_Action_Types[PushNumber]);
        MenuSleep = true;
    }

    public void MenuSetUp(MenuSetting menuSetting)
    {
        rectTransform = GetComponent<RectTransform>();

        this.Title = menuSetting.title;
        this.button_Count = menuSetting.button_Count;

        this.button_Texts = new string[button_Count];
        this.button_Index = new int[button_Count];
        this.button_Action_Types = new int[button_Count];
        this.button_Sleep = new bool[button_Count];

        this.button_Texts = menuSetting.button_Texts;
        this.button_Index = menuSetting.button_Index;
        this.button_Action_Types = menuSetting.button_Action_Types;
        this.inputManager = menuSetting.inputManager;
        this.soundManager = menuSetting.soundManager;


        canvasRenderer = GetComponent<CanvasRenderer>();

        if(menuSetting.title)
        {
            GameObject titleObj = Instantiate(menuSetting.textPrefab);
            titleObj.transform.SetParent(this.transform);
            MenuTitle = titleObj.GetComponent<TMP_Text>();
            MenuTitle.text = menuSetting.titletext;
        }

        for (int i = 0; i < button_Count; i++)
        {
            GameObject buttonObj = Instantiate(menuSetting.buttonPrefab);
            buttonObj.transform.SetParent(this.transform);
            buttonObj.GetComponent<SelectableText>().index = i + 1;
            buttonObj.GetComponent<SelectableText>().menuController = this;

            //選択肢を作成した時点で、ButtonSetupActionに選択肢のButton_Setup関数を登録する。
            this.ButtonSetupAction += buttonObj.GetComponent<SelectableText>().Button_Setup;

        }

        int sizeY = 20+(50 * button_Count)+(menuSetting.title ? 50 : 0);//タイトルがある場合は50を追加

        rectTransform.sizeDelta = new Vector2(220, sizeY);


        //button_indexに格納されている配列の中で最大値を取得する
        button_index_max = 0;
        for(int i = 0; i < button_Count; i++)
        {
            if(button_Index[i] > button_index_max)
            {
                button_index_max = button_Index[i];
            }
        }

        Button_SetUp();
    }

    public virtual void Button_SetUp(){
        ButtonSetupAction?.Invoke();

       MenuSleep = false;
    }

    //メニューが不要になったら、上位のManagerから呼ばれて、購読関係を終了する。
    public virtual void Menu_Close(){
        select_Button_index.OnCompleted();
        Action_Buton.OnCompleted();
        Destroy(this.gameObject);
    }
}

メニューの作り方に自由性を持たせるため、選択肢自体の番号(button_Index)と選択肢に割り当てられる機能(button_Action_Type)を分ける仕組みにしています。
そのため、選択肢が押された際には、button_Indexからbutton_Action_Typeを特定して、上位のManagerに通知するようになっています。

    public void Button_Action(int index){
        select_Button_index.Value = index;//選択されたボタンに変更する。

        int PushNumber = 0;
        //選択肢の番号(button_Index)から、対応する機能番号(button_Action_Type)の位置を取得する。
        for(int i = 0; i < button_Count; i++)
        {
            if(button_Index[i] == index)
            {
                PushNumber = i;
            }
        }


メニューのカスタマイズ機能は後にして、先に選択肢にあたる SelectableText.csです。 さきほど記載した通り、MenuController.csselect_Button_indexを購読して、自分のテキストの色を変える機能と、
選択肢は、ボタンではなくComponentを少しでも減らしたかったので、UnityEngine.EventSystemsの、IPointerClickHandler,IPointerDownHandler,IPointerUpHandlerを使うようにしています。

using UnityEngine;
using R3;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System;

public class SelectableText : MonoBehaviour,
    IPointerClickHandler,IPointerDownHandler,IPointerUpHandler

{
    [SerializeField] public MenuController menuController;

    public bool button_sleep = false;
    [SerializeField] public int button_index = 1;//選択したボタンの番号として通知されるindex

    public Image image;

    [NonSerialized] public TMPro.TextMeshProUGUI tMp;
    [SerializeField] public int index;//ボタンのシリアル番号(不変)

    private void Awake(){
        image = GetComponent<Image>();
        tMp = GetComponentInChildren<TMPro.TextMeshProUGUI>();
    }

    public virtual void Start(){
        menuController.select_Button_index.Subscribe(index =>
        {
            if (index == this.button_index)
            {
                tMp.color = Color.yellow;
            }
            else
            {
                tMp.color = Color.white;
            }
        }); 
    }

    //選択肢のカスタマイズ機能
    public virtual void Button_Setup(){
        button_index = menuController.button_Index[index - 1];
        tMp.text = menuController.button_Texts[index - 1];
        button_sleep = menuController.button_Sleep[index - 1];
    }

    public virtual void OnPointerClick(PointerEventData eventData){  
        if(button_sleep || menuController.MenuSleep)
        {
            return;
        }

        menuController.Button_Action(button_index);
    }

    public virtual void OnPointerDown(PointerEventData eventData){
        if(button_sleep || menuController.MenuSleep)
        {
            return;
        }
        //クリックした時に画像を変える。
        image.sprite = menuController.OnClickSprite;
    }

    public virtual void OnPointerUp(PointerEventData eventData){
        if(button_sleep || menuController.MenuSleep)
        {
            return;
        }
        //クリックを離した時に元の画像に戻す。
        image.sprite = menuController.NormalSprite;
    }

}


最後に、今回の目玉機能のメニューのカスタマイズ機能です。
まず、メニュー設定を分かりやすくするため、専用Classを作成します。

using UnityEngine;

public class Menu_Blueprint
{
    public Menu_Blueprint(GameObject buttonPrefab, 
                        GameObject textPrefab,
                        bool title,//タイトルを作るか否か
                        string titletext,//作る時のタイトルテキスト
                        int button_Count,//作るボタンの数
                        string[] button_Texts,//ボタンのテキスト
                        int[] button_Index,//ボタンに割り当てられる番号
                        int[] button_Action_Types,//ボタンの機能番号
                        InputManager inputManager, SoundManager soundManager)
    {
        this.buttonPrefab = buttonPrefab;
        this.textPrefab = textPrefab;
        this.title = title;
        this.titletext = titletext;
        this.button_Count = button_Count;
        this.button_Texts = button_Texts;
        this.button_Index = button_Index;
        this.button_Action_Types = button_Action_Types;
        this.inputManager = inputManager;
        this.soundManager = soundManager;
    }

    public GameObject buttonPrefab;
    public GameObject textPrefab;
    public bool title;
    public string titletext;
    public int button_Count;
    public string[] button_Texts;
    public int[] button_Index;
    public int[] button_Action_Types;
    public InputManager inputManager;
    public SoundManager soundManager;
}


他のManagerなどから、メニューのカスタマイズをする時は、こんな感じで設定してやるようになります。

        string[] b_text = new string[5]{"攻撃","魔法・スキル","アイテム","防御","逃げる"}; ;
        int[] b_index = new int[5]{1,2,3,4,5};
        int[] b_type = new int[5]{1,2,3,4,5};

        Menu_Blueprint menu_Blueprint = 
            new Menu_Blueprint(button_prefab,
                        text_prefab,
                        true,player.Character.battlerName,
                        5,
                        b_text,b_index,
                        b_type,inputManager,
                        soundManager);

        Menu.MenuSetUp(menu_Blueprint);
}

ボタンの数や、タイトルをつけるかどうか、各ボタンにどういう機能(int)を割り振るか等を専用class Menu_Blueprint.csに登録して、MenuController.csMenuSetUp関数を使って設定しています。
とは言うものの、やってることはかなり力技ですが・・・😅

this.ButtonSetupAction += buttonObj.GetComponent<SelectableText>().Button_Setup;

選択肢を作成するタイミングで、SelectableText.csButton_Setup関数を自身のUnityActionに登録して実行しています。

        int sizeY = 20+(50 * button_Count)+(menu_Blueprint.title ? 50 : 0);//タイトルがある場合は50を追加

        rectTransform.sizeDelta = new Vector2(220, sizeY);


        //button_indexに格納されている配列の中で最大値を取得する
        button_index_max = 0;
        for(int i = 0; i < button_Count; i++)
        {
            if(button_Index[i] > button_index_max)
            {
                button_index_max = button_Index[i];
            }
        }
}

そして、タイトルや選択肢の数によってメニューのサイズを変えています。


今後、戦闘やらアイテムやら魔法やらでメニューは色々と出てくることになりますが、これでメニューの基本形は出来上がりました。

次から、いよいよ戦闘。最初は殴り合いができるようにしていきたいと思います。