🃏
続:UniRx + MVP でサンプルコードを作成してみた
やること
MVP っぽい設計で、比較的シンプルなプログラムを作成してみる。
MVP については こちらの記事 で、とても丁寧に説明されている。
- 作るプログラム(カードゲームの土台となるようなプログラム)
- 山札と手札を作成し、画面に表示する
- Draw ボタンを押すと、山札から手札にカードを移動し、画面に表示する
- カードをクリックすると、手札から山札にカードを移動し、画面に表示する
結果
良い感じに実装できた。
背景
元々はカードゲームを作ってみたいということがあり、始めた。
しかし、前回に UniRx でプログラムを組んでみたが、MVP というより、MVVM 寄りの作りになっていた。
そのため、下記のスクラップにて MVP 寄りのコードを検討したうえで、再度サンプルプログラムを作ってみることにした。
- 前回
- スクラップ
やってみる
GameObject, Prefab を作成する
- UI Canvas
- まずは UI を表示するための Canvas を作成
- 詳細は割愛します。他の方が自分より上手く解説してくれているはず
- 手札と山札
- カードに見立てているものは、ただのボタンであり、サイズを調整しただけです
- Prefab にすることで、スクリプトから Instantiate でインスタンスを生成できるようにします
スクリプトを作成する
作成したスクリプトは以下。
設計を明確にして組んでいるため、見やすいコードになっている・・・はず。
上記の MVP の参考記事の通り、全体を取りまとめているのは Presenter なので、こちらを軸に見ていくと良いと思います。
コードの解説は後日するかも?という感じで・・・。
List っぽいデータを扱う時は ReactiveProperty ではなく、ReactiveCollection を使う必要があるため、少々躓きました。
SamplePresenter.cs
using System;
using System.Collections.Generic;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
public class SamplePresenter : MonoBehaviour
{
[SerializeField] private SampleView _view;
private CardsModel _model;
void Start()
{
initializeModelAndView();
}
private void initializeModelAndView()
{
_model = new CardsModel();
initializeView();
bindModelDataToView();
bindViewEventsToModelData();
}
private void initializeView()
{
_view.Hands.UpdateView(_model.Hands);
_view.DrawPile.UpdateView(_model.DrawPile);
}
private void bindModelDataToView()
{
_model.ObservableHands.Subscribe(_ =>
{
_view.Hands.UpdateView(_model.Hands);
addListeners2HandButton(onPlayCardButton);
}).AddTo(this);
_model.ObservableDrawPile.Subscribe(_ =>
{
_view.DrawPile.UpdateView(_model.DrawPile);
}).AddTo(this);
}
private void bindViewEventsToModelData()
{
_view.DrawCardButton.onClick.AddListener(onDrawCardButtonClicked);
addListeners2HandButton(onPlayCardButton);
}
private void addListeners2HandButton(Action<int> onPlayCardButton)
{
for (int i = 0; i < _view.Hands.Buttons.Count; i++)
{
int index = i;
_view.Hands.Buttons[index].GetComponent<Button>().onClick.AddListener(() => onPlayCardButton(index));
}
}
private void onPlayCardButton(int index)
{
_model.PlayCard(index);
}
private void onDrawCardButtonClicked()
{
_model.DrawCard();
}
}
public class CardsModel
{
private ReactiveCollection<SampleCard> _hands = new ReactiveCollection<SampleCard>();
private ReactiveCollection<SampleCard> _drawPile = new ReactiveCollection<SampleCard>();
// 値が追加された時のイベントを公開するためのプロパティ
public IObservable<int> ObservableHands => _hands.ObserveCountChanged();
public IObservable<int> ObservableDrawPile => _drawPile.ObserveCountChanged();
// 公開する値は読み取り専用にすることで、外部からの変更を防ぐ
public IReadOnlyList<SampleCard> Hands => _hands;
public IReadOnlyList<SampleCard> DrawPile => _drawPile;
public CardsModel()
{
createDrawPile(10);
createHands(3);
}
public void PlayCard(int index)
{
if (index >= 0 && index < _hands.Count)
{
moveCardHands2DrawPile(index);
}
}
public void DrawCard()
{
if (_drawPile.Count != 0)
{
moveCardDrawPile2Hands();
}
}
private void createDrawPile(int count)
{
for (int i = 0; i < count; i++)
{
var name = "Card_" + i.ToString();
var num = i;
var card = new SampleCard
{
Name = name,
value = num
};
_drawPile.Add(card);
}
}
private void createHands(int count)
{
for (int i = 0; i < count; i++)
{
moveCardDrawPile2Hands();
}
}
private void moveCardHands2DrawPile(int index)
{
var pop = _hands[index];
_hands.RemoveAt(index);
_drawPile.Add(pop);
}
private void moveCardDrawPile2Hands()
{
var pop = _drawPile[0];
_drawPile.RemoveAt(0);
_hands.Add(pop);
}
}
public class SampleCard
{
public string Name { get; set; }
public int value { get; set; }
}
SampleView.cs
using UnityEngine;
using UnityEngine.UI;
public class SampleView : MonoBehaviour
{
public Button DrawCardButton;
public HandsView Hands;
public DrawPileView DrawPile;
}
HandsView.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class HandsView : MonoBehaviour
{
[SerializeField] private Canvas canvas;
[SerializeField] private GameObject cardPrefab;
public List<GameObject> Buttons = new List<GameObject>();
private void deleteAllButton()
{
foreach (var button in Buttons)
{
Destroy(button);
}
Buttons.Clear();
}
public void UpdateView(IReadOnlyList<SampleCard> Cards)
{
// 一旦すべて削除する
deleteAllButton();
for (int i = 0; i < Cards.Count; i++)
{
GameObject newSelectorObj = Instantiate(cardPrefab, canvas.transform);
Buttons.Add(newSelectorObj);
Buttons[i].GetComponentInChildren<TextMeshProUGUI>().text = Cards[i].Name;
RectTransform rect = newSelectorObj.GetComponent<RectTransform>();
var offsetX = -400;
var offsetY = -230;
rect.anchoredPosition = new Vector2(offsetX, i * 50 + offsetY);
}
}
}
DrawPileView.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;
// HandsView.cs と同じであり、異なるのは画面に表示するための Offsetのみ。
// 共通化したいがサンプルなので良いかという感じ。
public class DrawPileView : MonoBehaviour
{
[SerializeField] private Canvas canvas;
[SerializeField] private GameObject cardPrefab;
public List<GameObject> Buttons = new List<GameObject>();
private void deleteAllButton()
{
foreach (var button in Buttons)
{
Destroy(button);
}
Buttons.Clear();
}
public void UpdateView(IReadOnlyList<SampleCard> Cards)
{
// 一旦すべて削除する
deleteAllButton();
for (int i = 0; i < Cards.Count; i++)
{
GameObject newSelectorObj = Instantiate(cardPrefab, canvas.transform);
Buttons.Add(newSelectorObj);
Buttons[i].GetComponentInChildren<TextMeshProUGUI>().text = Cards[i].Name;
RectTransform rect = newSelectorObj.GetComponent<RectTransform>();
var offsetX = -250;
var offsetY = -230;
rect.anchoredPosition = new Vector2(offsetX, i * 50 + offsetY);
}
}
}
GUI のインスペクター上で必要なデータの紐づけをする
説明するより、見てもらった方が分かりやすそうなので、以下に画像をぺたぺた張りつけました。
まとめ
- いきなり全部を作ろうとすると混乱することが予想されるため、まずシンプルなプログラムを作った
- 考えがまとまってきたので、良かった
- 良い感じ?に C# のプログラムを作成することができた
- 毎回ボタンを削除する処理等、改善点はあるように思うが、基本的には問題ないと思っている
- 自分はまだまだ初心者であり、本記事を読んでいただいた方からのご意見・改善案等は大歓迎なので、是非お願いします
Discussion