カードゲーム基盤 / カードの配置場所

はじめに

このページでは、Positionクラスの中身を実装します。設計図では、下図の「」印を付けた箇所に相当します。



作業は大きく分けて3つあり
  1. この位置(Position)に存在するカードの取得
  2. カードの追加メソッドの作成
  3. カードの配置数の上限を設定
です。



この位置(Position)に存在するカードの取得

まずはPositionクラス(のサブクラス)のインスタンスを指定して、その位置の全カードを取得するプロパティを作成します。手順は次の通りです。
  1. CardObjectDataに、特定の位置に存在する全カードを返すメソッドを作成する
  2. DataManagerから「1」で作成したメソッドを呼ぶ
  3. Positionから「2」で作成したメソッドを呼ぶ
CardObjectDataに次のメソッドを追加してください。
DataManager.cs CardObjectData
1/// <summary>
2/// 指定した位置に存在するカードのId
3/// (読み取り専用)
4/// </summary>
5/// <param name="posId"></param>
6/// <returns></returns>
7internal List<int> GetCardsOfPos(Type posType) => new (dataCore[posType.ToString()]);
ただ辞書の中身を返すだけのメソッドです。しかし単純にdataCore[posType.ToString()]を返すのではなく、new ()をしています。これはListのクローンを作成することにより、外部でいくらリストを編集したとしてもコアデータには一切影響が及ばないよう保護するためです。

次にDataManagerからGetCardsOfPos()メソッドを呼びます。次のコードをDataManagerクラス内 (CardObjectDataインナークラスの外) に追加してください。
DataManager.cs
1/// <summary>
2/// 指定した位置に存在するカードidを返す
3/// (読み取り専用)
4/// </summary>
5/// <param name="posType"></param>
6/// <returns></returns>
7internal List<int> GetCardsOfPos(Type posType)=> Data.GetCardsOfPos(posType);
今度はnew ()することなくData.GetCardsOfPos()の結果をそのまま返しています。

最後に、このメソッドをPositionから呼び出しましょう。Positionクラスを次のコードで上書きしてください。
Position.cs
1using System;
2using System.Collections.Generic;
3using UniRx;
4using UnityEngine;
5
6namespace FlMr_CardBase
7{
8    public abstract class Position : MonoBehaviour
9    {
10        private DataManager Manager { get; set; }
11        protected Type PosType { get; private set; }
12
13        internal void Initialize(DataManager manager)
14        {
15            Manager = manager;
16            PosType = this.GetType();
17        }
18
19        /// <summary>
20        /// この位置に存在するすべてのカード
21        /// </summary>
22        public List<int> Cards => Manager.GetCardsOfPos(PosType);
23
24    }
25}
まずは22行目を見てください。ここで先ほど作成したDataManagerクラスのGetCardsOfPos()メソッドを使用しています。この時当然ですがDataManagerクラスのインスタンスが必要になります。これはInitialize()メソッドを通して、外部から教えてもらいましょう。この初期化メソッドで、ついでにPosTypeというプロパティも作成しました。これによりわざわざthis.GetType()を使う手間が省けます。

ここで一つの疑問と一つの問題が発生します。まず疑問について「なぜInitialize()メソッドを通してインスタンスを受け取るか」です。例えばManager変数に[SerializeField]属性を付与してインスペクター上から登録しても良いです。しかしこの場合「全Positionクラス(の派生クラス)」に対してインスペクター上から登録する必要が出てきます。この登録作業はパッケージの使用者に押し付けることになります。このような手間の発生する設計は良いとは言えないでしょう。この属性を付与する方法のほかにも、GetComponentInParent()を使用する方法も考えられます。これはパッケージ使用者に「Positionの親オブジェクトにDataManagerコンポーネントを付けること」というルールを押し付けることになり、これは避けるべきです。以上の問題を解決できるのが、紹介している「初期化メソッドを用いた方法」になります。

しかし問題になるのが、誰がInitialize()メソッドを呼ぶかです。ここでは初期化担当クラスを作成して解決します。Unityに戻り「CardBase/Scripts/」内に「CardObjUtility」スクリプトを作成し、次のコードで上書きしてください。
CardObjUtility.cs
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEngine;
5
6namespace FlMr_CardBase
7{
8    [RequireComponent(typeof(DataManager))]
9    public class CardObjUtility : MonoBehaviour
10    {
11        /// <summary>
12        /// 全Position
13        /// </summary>
14        [SerializeField] private List<Position> positions;
15
16        private DataManager Manager { get; set; }
17
18        private void Awake()
19        {
20            Manager = this.GetComponent<DataManager>();
21
22            // DataManagerの初期化
23            Manager.Initialize(positions.ConvertAll(p => p.GetType()));
24
25            // Positionの初期化
26            foreach (var pos in positions)
27            {
28                pos.Initialize(Manager);
29            }
30        }
31    }
32}
まず14行目を見てください。positionsにはインスペクター上から、全ての位置が登録されます。ゲーム開始時にAwake()が呼ばれ、初期化作業が開始します。まずDataManagerクラスのインスタンスが解決します。GetComponent()を使用しているということは「CardUtilityDataManagerクラスを同一のオブジェクトにアタッチする」という制約がパッケージユーザーにかかります。しかし8行目の属性によりCardUtilityコンポーネントを追加した際に、自動的にDataManagerコンポーネントが追加されるようになるため、ユーザーはこの制約を気にする必要が無くなります。

次に23行目です。ここではDataManagerクラスの初期化を行います。引数には全ての位置の型が必要なのでpositionsの内容をTypeに変換したコレクションを渡します。

最後に26-29行目です。ここではPositionの初期化を行っています。引数はDataManagerなので、素直にインスタンスを渡しています。

ここでDataManagerクラスにも[RequireComponent()]属性を付与し、DataManagerコンポーネントを付けた際に自動的にCardObjUtilityが追加されるよう改良しておきます。
DataManager.cs
1[RequireComponent(typeof(CardObjUtility))]
2internal class DataManager : MonoBehaviour
3{
4    ...
一度動作確認をしたいのでCardBaseTestクラスを次のように変更します。
CardBaseTest.cs
1using System.Collections;
2using System.Collections.Generic;
3using UnityEngine;
4
5namespace FlMr_CardBase.Demo
6{
7    public class CardBaseTest : MonoBehaviour
8    {
9        [SerializeField] private DataManager manager;
10        [SerializeField] private Hand hand;
11
12        void Start()
13        {
14            //カードを4枚追加
15            manager.AddCard(typeof(Deck), 0, true); // 「Deck」の「0」番目の位置に「正」のidをもつカードを追加
16            manager.AddCard(typeof(Deck), 0, true);
17            manager.AddCard(typeof(Deck), 0, true);
18            manager.AddCard(typeof(Deck), 0, true);
19
20            // カードを2枚移動
21            manager.MoveCard(2,typeof(Hand), null); // CardObjectIdが「2」のカードを「Hand」の「最後尾」に移動
22            manager.MoveCard(3,typeof(Hand), null);
23
24            Debug.Log(string.Join(",",hand.Cards));
25
26            // 状況確認
27            Debug.Log(manager.GetJsonData());
28        }
29    }
30}
主な変更点は、DataManagerの初期化を消したこと、Handのインスタンスをインスペクター上から受け取るよう変更したこと、handCardsプロパティを出力することです。

Unityに戻ります。シーン上のDataManagerオブジェクトのDataManagerコンポーネントを削除してください。そして再度DataManagerスクリプトをアタッチします。このとき同時にCardObjUtilityコンポーネントも追加されることを確認してください。CardObjUtilityコンポーネントのPositions変数に「Deck」「Hand」オブジェクトを登録します。


またCardBaseTestオブジェクトのコンポーネントにも設定を行いましょう。DataManagerにはDataManagerオブジェクトを、HandにはHandオブジェクトを登録します。


ではゲームを実行し、出力を確認してください。「2,3」 と出力されているでしょうか。



カードの追加メソッドの作成

Positionクラスに、カードを生成するメソッドを作成します。次のコードを追加してください。
Position.cs
1/// <summary>
2/// カードを新しく生成する
3/// </summary>
4/// <param name="index">生成したカードの挿入位置</param>
5/// <param name="positiveCardId">カードのidを自然数とするか負の整数にするか</param>
6/// <returns>生成に成功したか否か</returns>
7public bool AddCard(int? index,bool positiveCardId)
8{
9    Manager.AddCard(PosType, index, positiveCardId);
10    return true;
11}
これだけです。フラグを返している理由は、次の話題である「枚数制限」を想定しているためです。

カードの配置数の上限を設定

この値は各配置場所によって様々です。Positionクラスに上限を表す整数型を定義しましょう。次のコードを追加してください。
Position.cs
1[SerializeField] private int cardNumberLimit = -1;
2public int CardNumberLimit => cardNumberLimit;
3public bool CanAdd => cardNumberLimit < 0 || Cards.Count < cardNumberLimit;
上限のデフォルト値として-1を設定しています。このように上限が負の値の時は「無制限」を表すこととします。3行目のCanAddプロパティは、まだカードを追加可能か否かを表します。これを用いるとPositionAddCard()メソッドを次のように改良することが出来ます。
Position.cs
1/// <summary>
2/// カードを新しく生成する
3/// </summary>
4/// <param name="index">生成したカードの挿入位置</param>
5/// <param name="positiveCardId">カードのidを自然数とするか負の整数にするか</param>
6/// <returns>生成に成功したか否か</returns>
7public bool AddCard(int? index,bool positiveCardId)
8{
9    // 枚数制限をオーバーしないことの確認
10    if (CanAdd)
11    {
12        // カードの追加
13        Manager.AddCard(PosType, index, positiveCardId);
14        return true;
15    }
16    else
17    {
18        return false;
19    }
20}
この修正によりPositionAddCard()を通して追加する場合は枚数制限が守られます。ところでDataManagerクラスのAddCard()に注目してください。
DataManager.cs
1internal void AddCard(Type posType, int? index,bool positiveCardId)
2{
3    Data.AddCard(posType, index, positiveCardId);
4}
このメソッドが実行されると枚数制限を無視して、必ずカードが追加されます。そのためカードの生成には必ずPositionAddCard()を使用する必要があります。そこでCardBaseTestクラスを修正しましょう。緑に着色された部分が修正箇所です。
CardBaseTest.cs
1using System.Collections;
2using System.Collections.Generic;
3using UnityEngine;
4
5namespace FlMr_CardBase.Demo
6{
7    public class CardBaseTest : MonoBehaviour
8    {
9        [SerializeField] private DataManager manager;
10        [SerializeField] private Hand hand;
11        [SerializeField] private Deck deck;
12
13        void Start()
14        {
15            //カードを4枚追加
16            deck.AddCard(0, true); // 「Deck」の「0」番目の位置に「正」のidをもつカードを追加
17            deck.AddCard(0, true);
18            deck.AddCard(0, true);
19            deck.AddCard(0, true);
20
21            // カードを2枚移動
22            manager.MoveCard(2,typeof(Hand), null); // CardObjectIdが「2」のカードを「Hand」の「最後尾」に移動
23            manager.MoveCard(3,typeof(Hand), null);
24
25            Debug.Log(string.Join(",",hand.Cards));
26
27            // 状況確認
28            Debug.Log(manager.GetJsonData());
29        }
30    }
31}
DataManagerAddCard()を用いていた部分を全てDeckAddCard()に修正しました。しかし二つの疑問が生じます。まずDataManagerAddCard()を野放しにするのは危険ではないか、ということ。もう一つの疑問はMoveCard()の時も判定する必要があるのではないか、ということです。これらの問題には対処する必要があります。

解決策として、DataManagerクラスに枚数制限の情報を伝え、DataManagerAddCard()でもチェックを行います。DataManagerクラスのInitialize()を修正し、さらにCardNumberLimitMapプロパティを新規に作成してください。
DataManager.cs
1/// <summary>
2/// 各Positionの枚数制限
3/// </summary>
4private Dictionary<Type, int> CardNumberLimitMap { get; set; }
5
6/// <summary>
7/// 全Position情報をもとに初期化する
8/// </summary>
9/// <param name="cardNumberLimitMap">各Positionの枚数制限</param>
10internal void Initialize(Dictionary<Type, int> cardNumberLimitMap)
11{
12    CardNumberLimitMap = cardNumberLimitMap;
13    Data = new CardObjectData(cardNumberLimitMap.Keys.ToList());
14}
Initialize()メソッドの引数を、Positionの型と制限の辞書に変更しました。受け取った変数をCardNumberLimitMapに保持しつつ、さらにこのKeyだけを用いてCardObjectDataのインスタンス化を行います。

この変更によりCardObjUtilityクラスのAwake()メソッド内でコンパイルエラーが生じます。ただしこの解決は簡単で、positions.ConvertAll(p => p.GetType())positions.ToDictionary(p => p.GetType(), p => p.CardNumberLimit)に修正するだけです。

では本題の、DataManager.AddCard()DataManager.MoveCard()の修正を行いましょう。追加可能かを判定する新たなメソッドの作成と、AddCard() , MoveCard()の修正を行います。
DataManager.cs
1/// <summary>
2/// カードを追加、又は移動可能かを判定する
3/// </summary>
4/// <param name="posType"></param>
5/// <returns></returns>
6internal bool CanAdd(Type posType)
7{
8    return CardNumberLimitMap[posType] < 0 || GetCardsOfPos(posType).Count < CardNumberLimitMap[posType];
9}
10
11/// <summary>
12/// カードを新しく生成する
13/// </summary>
14/// <param name="posType">生成位置</param>
15/// <param name="index">カードを何番目に挿入するか</param>
16/// <param name="positiveCardId">カードのIdとして自然数を使用するか、負の整数を使用するか</param>
17internal void AddCard(Type posType, int? index,bool positiveCardId)
18{
19    if (!CanAdd(posType))
20    {
21        throw new Exception($"ポジション[{posType}]のカード枚数の上限を超過します");
22    }
23
24    Data.AddCard(posType, index, positiveCardId);
25}
26
27/// <summary>
28/// カードを移動する
29/// </summary>
30/// <param name="cardObjId">移動対象のカードId</param>
31/// <param name="posTo">生成位置</param>
32/// <param name="index">カードを何番目に挿入するか</param>
33internal void MoveCard(int cardObjId,Type posTo, int? index)
34{
35    if (!CanAdd(posTo))
36    {
37        throw new Exception($"ポジション[{posTo}]のカード枚数の上限を超過します");
38    }
39
40    Data.MoveCard(cardObjId, posTo, index);
41}
CanAdd()Positionに作成したメソッドと同様「上限が0以下」又は「現在の枚数が上限未満」で判定しています。DataManager.AddCard()DataManager.MoveCard()は、移動や追加の実行前に上限をオーバーしないことを確認しています。MoveCard()に関してはもう少し修正する必要がありますが、これは次回に回します。ここで動作確認をしましょう。Unityに戻ってください。

シーン上のCardBaseTestコンポーネントのDeck変数に、Deckオブジェクトを登録してゲームを実行してください。特に問題がなさそう(出力が以前と変わらない)なら、DeckやHandの上限を変えて動作を比較してみてください。例えばDeckの上限を3にすると結果は次のようになります。
Unity Console
1{"dataCore":{"dataCore":{"Pair":[
2
3    {"key":"FlMr_CardBase.Demo.Deck","value":[1]},
4    {"key":"FlMr_CardBase.Demo.Hand","value":[2,3]}
5
6]}}}

次にHandの上限を1にするとエラーがでます。これは (Positionのように、超化することを確認することなく) 直接DataManagerMoveCard()を呼んでいるためです。この修正は次回に回します。




今回はここまでです。

現時点でのプロジェクトは  Github から確認できます。次回もよろしくお願いします。