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

作業は大きく分けて3つあり
- この位置(Position)に存在するカードの取得
- カードの追加メソッドの作成
- カードの配置数の上限を設定
です。
この位置(Position)に存在するカードの取得
まずは
Positionクラス(のサブクラス)のインスタンスを指定して、その位置の全カードを取得するプロパティを作成します。手順は次の通りです。
- CardObjectDataに、特定の位置に存在する全カードを返すメソッドを作成する
- DataManagerから「1」で作成したメソッドを呼ぶ
- Positionから「2」で作成したメソッドを呼ぶ
CardObjectDataに次のメソッドを追加してください。
DataManager.cs CardObjectData
/// <summary>
/// 指定した位置に存在するカードのId
/// (読み取り専用)
/// </summary>
/// <param name="posId"></param>
/// <returns></returns>
internal List<int> GetCardsOfPos(Type posType) => new (dataCore[posType.ToString()]);
ただ辞書の中身を返すだけのメソッドです。しかし単純に
dataCore[posType.ToString()]を返すのではなく、
new ()をしています。これは
Listのクローンを作成することにより、外部でいくらリストを編集したとしてもコアデータには一切影響が及ばないよう保護するためです。
次に
DataManagerから
GetCardsOfPos()メソッドを呼びます。次のコードを
DataManagerクラス内 (
CardObjectDataインナークラスの外) に追加してください。
DataManager.cs
/// <summary>
/// 指定した位置に存在するカードidを返す
/// (読み取り専用)
/// </summary>
/// <param name="posType"></param>
/// <returns></returns>
internal List<int> GetCardsOfPos(Type posType)=> Data.GetCardsOfPos(posType);
今度は
new ()することなく
Data.GetCardsOfPos()の結果をそのまま返しています。
最後に、このメソッドを
Positionから呼び出しましょう。
Positionクラスを次のコードで上書きしてください。
Position.cs
using System;
using System.Collections.Generic;
using UniRx;
using UnityEngine;
namespace FlMr_CardBase
{
public abstract class Position : MonoBehaviour
{
private DataManager Manager { get; set; }
protected Type PosType { get; private set; }
internal void Initialize(DataManager manager)
{
Manager = manager;
PosType = this.GetType();
}
/// <summary>
/// この位置に存在するすべてのカード
/// </summary>
public List<int> Cards => Manager.GetCardsOfPos(PosType);
}
}
まずは22行目を見てください。ここで先ほど作成した
DataManagerクラスの
GetCardsOfPos()メソッドを使用しています。この時当然ですが
DataManagerクラスのインスタンスが必要になります。これは
Initialize()メソッドを通して、外部から教えてもらいましょう。この初期化メソッドで、ついでに
PosTypeというプロパティも作成しました。これによりわざわざ
this.GetType()を使う手間が省けます。
ここで一つの疑問と一つの問題が発生します。まず疑問について「なぜ
Initialize()メソッドを通してインスタンスを受け取るか」です。例えば
Manager変数に
[SerializeField]属性を付与してインスペクター上から登録しても良いです。しかしこの場合「全
Positionクラス(の派生クラス)」に対してインスペクター上から登録する必要が出てきます。この登録作業はパッケージの使用者に押し付けることになります。このような手間の発生する設計は良いとは言えないでしょう。この属性を付与する方法のほかにも、
GetComponentInParent()を使用する方法も考えられます。これはパッケージ使用者に「Positionの親オブジェクトにDataManagerコンポーネントを付けること」というルールを押し付けることになり、これは避けるべきです。以上の問題を解決できるのが、紹介している「初期化メソッドを用いた方法」になります。
しかし問題になるのが、誰が
Initialize()メソッドを呼ぶかです。ここでは初期化担当クラスを作成して解決します。Unityに戻り「CardBase/Scripts/」内に「CardObjUtility」スクリプトを作成し、次のコードで上書きしてください。
CardObjUtility.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace FlMr_CardBase
{
[RequireComponent(typeof(DataManager))]
public class CardObjUtility : MonoBehaviour
{
/// <summary>
/// 全Position
/// </summary>
[SerializeField] private List<Position> positions;
private DataManager Manager { get; set; }
private void Awake()
{
Manager = this.GetComponent<DataManager>();
// DataManagerの初期化
Manager.Initialize(positions.ConvertAll(p => p.GetType()));
// Positionの初期化
foreach (var pos in positions)
{
pos.Initialize(Manager);
}
}
}
}
まず14行目を見てください。
positionsにはインスペクター上から、全ての位置が登録されます。ゲーム開始時に
Awake()が呼ばれ、初期化作業が開始します。まず
DataManagerクラスのインスタンスが解決します。
GetComponent()を使用しているということは「
CardUtilityと
DataManagerクラスを同一のオブジェクトにアタッチする」という制約がパッケージユーザーにかかります。しかし8行目の属性によりCardUtilityコンポーネントを追加した際に、自動的にDataManagerコンポーネントが追加されるようになるため、ユーザーはこの制約を気にする必要が無くなります。
次に23行目です。ここでは
DataManagerクラスの初期化を行います。引数には全ての位置の型が必要なので
positionsの内容を
Typeに変換したコレクションを渡します。
最後に26-29行目です。ここでは
Positionの初期化を行っています。引数は
DataManagerなので、素直にインスタンスを渡しています。
ここで
DataManagerクラスにも
[RequireComponent()]属性を付与し、DataManagerコンポーネントを付けた際に自動的にCardObjUtilityが追加されるよう改良しておきます。
DataManager.cs
[RequireComponent(typeof(CardObjUtility))]
internal class DataManager : MonoBehaviour
{
...
一度動作確認をしたいので
CardBaseTestクラスを次のように変更します。
CardBaseTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace FlMr_CardBase.Demo
{
public class CardBaseTest : MonoBehaviour
{
[SerializeField] private DataManager manager;
[SerializeField] private Hand hand;
void Start()
{
//カードを4枚追加
manager.AddCard(typeof(Deck), 0, true); // 「Deck」の「0」番目の位置に「正」のidをもつカードを追加
manager.AddCard(typeof(Deck), 0, true);
manager.AddCard(typeof(Deck), 0, true);
manager.AddCard(typeof(Deck), 0, true);
// カードを2枚移動
manager.MoveCard(2,typeof(Hand), null); // CardObjectIdが「2」のカードを「Hand」の「最後尾」に移動
manager.MoveCard(3,typeof(Hand), null);
Debug.Log(string.Join(",",hand.Cards));
// 状況確認
Debug.Log(manager.GetJsonData());
}
}
}
主な変更点は、
DataManagerの初期化を消したこと、
Handのインスタンスをインスペクター上から受け取るよう変更したこと、
handの
Cardsプロパティを出力することです。
Unityに戻ります。シーン上のDataManagerオブジェクトのDataManagerコンポーネントを削除してください。そして再度DataManagerスクリプトをアタッチします。このとき同時にCardObjUtilityコンポーネントも追加されることを確認してください。CardObjUtilityコンポーネントのPositions変数に「Deck」「Hand」オブジェクトを登録します。

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

ではゲームを実行し、出力を確認してください。「2,3」 と出力されているでしょうか。
カードの追加メソッドの作成
Positionクラスに、カードを生成するメソッドを作成します。次のコードを追加してください。
Position.cs
/// <summary>
/// カードを新しく生成する
/// </summary>
/// <param name="index">生成したカードの挿入位置</param>
/// <param name="positiveCardId">カードのidを自然数とするか負の整数にするか</param>
/// <returns>生成に成功したか否か</returns>
public bool AddCard(int? index,bool positiveCardId)
{
Manager.AddCard(PosType, index, positiveCardId);
return true;
}
これだけです。フラグを返している理由は、次の話題である「枚数制限」を想定しているためです。
カードの配置数の上限を設定
この値は各配置場所によって様々です。
Positionクラスに上限を表す整数型を定義しましょう。次のコードを追加してください。
Position.cs
[SerializeField] private int cardNumberLimit = -1;
public int CardNumberLimit => cardNumberLimit;
public bool CanAdd => cardNumberLimit < 0 || Cards.Count < cardNumberLimit;
上限のデフォルト値として-1を設定しています。このように上限が負の値の時は「無制限」を表すこととします。3行目の
CanAddプロパティは、まだカードを追加可能か否かを表します。これを用いると
Positionの
AddCard()メソッドを次のように改良することが出来ます。
Position.cs
/// <summary>
/// カードを新しく生成する
/// </summary>
/// <param name="index">生成したカードの挿入位置</param>
/// <param name="positiveCardId">カードのidを自然数とするか負の整数にするか</param>
/// <returns>生成に成功したか否か</returns>
public bool AddCard(int? index,bool positiveCardId)
{
// 枚数制限をオーバーしないことの確認
if (CanAdd)
{
// カードの追加
Manager.AddCard(PosType, index, positiveCardId);
return true;
}
else
{
return false;
}
}
この修正により
Positionの
AddCard()を通して追加する場合は枚数制限が守られます。ところで
DataManagerクラスの
AddCard()に注目してください。
DataManager.cs
internal void AddCard(Type posType, int? index,bool positiveCardId)
{
Data.AddCard(posType, index, positiveCardId);
}
このメソッドが実行されると枚数制限を無視して、必ずカードが追加されます。そのためカードの生成には必ず
Positionの
AddCard()を使用する必要があります。そこで
CardBaseTestクラスを修正しましょう。緑に着色された部分が修正箇所です。
CardBaseTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace FlMr_CardBase.Demo
{
public class CardBaseTest : MonoBehaviour
{
[SerializeField] private DataManager manager;
[SerializeField] private Hand hand;
[SerializeField] private Deck deck;
void Start()
{
//カードを4枚追加
deck.AddCard(0, true); // 「Deck」の「0」番目の位置に「正」のidをもつカードを追加
deck.AddCard(0, true);
deck.AddCard(0, true);
deck.AddCard(0, true);
// カードを2枚移動
manager.MoveCard(2,typeof(Hand), null); // CardObjectIdが「2」のカードを「Hand」の「最後尾」に移動
manager.MoveCard(3,typeof(Hand), null);
Debug.Log(string.Join(",",hand.Cards));
// 状況確認
Debug.Log(manager.GetJsonData());
}
}
}
DataManagerの
AddCard()を用いていた部分を全て
Deckの
AddCard()に修正しました。しかし二つの疑問が生じます。まず
DataManagerの
AddCard()を野放しにするのは危険ではないか、ということ。もう一つの疑問は
MoveCard()の時も判定する必要があるのではないか、ということです。これらの問題には対処する必要があります。
解決策として、
DataManagerクラスに枚数制限の情報を伝え、
DataManagerの
AddCard()でもチェックを行います。
DataManagerクラスの
Initialize()を修正し、さらに
CardNumberLimitMapプロパティを新規に作成してください。
DataManager.cs
/// <summary>
/// 各Positionの枚数制限
/// </summary>
private Dictionary<Type, int> CardNumberLimitMap { get; set; }
/// <summary>
/// 全Position情報をもとに初期化する
/// </summary>
/// <param name="cardNumberLimitMap">各Positionの枚数制限</param>
internal void Initialize(Dictionary<Type, int> cardNumberLimitMap)
{
CardNumberLimitMap = cardNumberLimitMap;
Data = new CardObjectData(cardNumberLimitMap.Keys.ToList());
}
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
/// <summary>
/// カードを追加、又は移動可能かを判定する
/// </summary>
/// <param name="posType"></param>
/// <returns></returns>
internal bool CanAdd(Type posType)
{
return CardNumberLimitMap[posType] < 0 || GetCardsOfPos(posType).Count < CardNumberLimitMap[posType];
}
/// <summary>
/// カードを新しく生成する
/// </summary>
/// <param name="posType">生成位置</param>
/// <param name="index">カードを何番目に挿入するか</param>
/// <param name="positiveCardId">カードのIdとして自然数を使用するか、負の整数を使用するか</param>
internal void AddCard(Type posType, int? index,bool positiveCardId)
{
if (!CanAdd(posType))
{
throw new Exception($"ポジション[{posType}]のカード枚数の上限を超過します");
}
Data.AddCard(posType, index, positiveCardId);
}
/// <summary>
/// カードを移動する
/// </summary>
/// <param name="cardObjId">移動対象のカードId</param>
/// <param name="posTo">生成位置</param>
/// <param name="index">カードを何番目に挿入するか</param>
internal void MoveCard(int cardObjId,Type posTo, int? index)
{
if (!CanAdd(posTo))
{
throw new Exception($"ポジション[{posTo}]のカード枚数の上限を超過します");
}
Data.MoveCard(cardObjId, posTo, index);
}
CanAdd()は
Positionに作成したメソッドと同様「上限が0以下」又は「現在の枚数が上限未満」で判定しています。
DataManager.AddCard()と
DataManager.MoveCard()は、移動や追加の実行前に上限をオーバーしないことを確認しています。
MoveCard()に関してはもう少し修正する必要がありますが、これは次回に回します。ここで動作確認をしましょう。Unityに戻ってください。
シーン上のCardBaseTestコンポーネントのDeck変数に、Deckオブジェクトを登録してゲームを実行してください。特に問題がなさそう(出力が以前と変わらない)なら、DeckやHandの上限を変えて動作を比較してみてください。例えばDeckの上限を3にすると結果は次のようになります。
Unity Console
{"dataCore":{"dataCore":{"Pair":[
{"key":"FlMr_CardBase.Demo.Deck","value":[1]},
{"key":"FlMr_CardBase.Demo.Hand","value":[2,3]}
]}}}
次にHandの上限を1にするとエラーがでます。これは (Positionのように、超化することを確認することなく) 直接
DataManagerの
MoveCard()を呼んでいるためです。この修正は次回に回します。

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