
前回までで移動は一段落したので、いよいよ戦闘ができるようにしていきます。
ウィザードリィライクという事で、ターン制のコマンド型の戦闘になるのですが、
そのためには、当然ですが、コマンドを選択するためのコマンドメニューが必要です。
基本形は、しまづさんの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;
[SerializeField] public string[] button_Texts;
[SerializeField] public bool[] button_Sleep;
[SerializeField] public int[] button_Action_Types;
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);
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;
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;
this.ButtonSetupAction += buttonObj.GetComponent<SelectableText>().Button_Setup;
}
int sizeY = 20+(50 * button_Count)+(menuSetting.title ? 50 : 0);
rectTransform.sizeDelta = new Vector2(220, sizeY);
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;
}
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;
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;
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);
rectTransform.sizeDelta = new Vector2(220, sizeY);
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];
}
}
}
そして、タイトルや選択肢の数によってメニューのサイズを変えています。
今後、戦闘やらアイテムやら魔法やらでメニューは色々と出てくることになりますが、これでメニューの基本形は出来上がりました。
次から、いよいよ戦闘。最初は殴り合いができるようにしていきたいと思います。