
前回までで移動は一段落したので、いよいよ戦闘ができるようにしていきます。
ウィザードリィライクという事で、ターン制のコマンド型の戦闘になるのですが、
そのためには、当然ですが、コマンドを選択するためのコマンドメニューが必要です。
基本形は、しまづさんの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.csのselect_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.csのMenuSetUp関数を使って設定しています。
とは言うものの、やってることはかなり力技ですが・・・😅
this.ButtonSetupAction += buttonObj.GetComponent<SelectableText>().Button_Setup;
選択肢を作成するタイミングで、SelectableText.csのButton_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]; } } }
そして、タイトルや選択肢の数によってメニューのサイズを変えています。
今後、戦闘やらアイテムやら魔法やらでメニューは色々と出てくることになりますが、これでメニューの基本形は出来上がりました。
次から、いよいよ戦闘。最初は殴り合いができるようにしていきたいと思います。