カードゲーム基盤 / カードの位置情報

はじめに

このページでは、最も大切な情報であるカードの位置情報を保持するDataManagerクラスの作成を進めます。設計図では、下図の「」印を付けた箇所に相当します。



具体的な作業内容は次の通りです。
  1. プロジェクトの準備
  2. コアとなるデータの作成




準備

作業に入る前に作業場所の確保やアセットの準備を行います。
まず作業するディレクトリを掘ります。次の図のように、好きな場所に「CardBase」ディレクトリを作成します。その中に「Demo」「Prefabs」「Scripts」ディレクトリを作成します。


「Demo/」にCardBase_Demoシーンを作成し、開いてください。今後、このシーン内で動作確認を行います。

次に、今後必要となるアセットを2つインポートします。
1つめは、オブジェクトを整列させるためのスクリプトです。公式が提供しているLayoutGroupはUIでないと整列させることができたいため、次のコードをダウンロードして使用します。



ダウンロードしたZipファイルを解凍し「CardBase/」ディレクトリにフォルダごとインポートしてください。

そして2つ目はUniRxです。公式サイトからダウンロード&インポートをお願いします。実際に触れることになるのは後半になると思います。



コアとなるデータの作成

下図は前回も紹介した、コアとなるデータ構造のイメージです。


これをコードで表現する際、辞書型を使います。Keyは位置、Valueはオブジェクト番号です。注意点として、これはプレイヤー1人分のデータです。そしてこれ以降も1人分の作業を続けます。複数人に拡張するのは、1人分の機能をコピーするだけで出来上がるためです。

では作業をはじめましょう。まずは次の作業を行ってください。
  1. Unityの「CardBase/Scripts」に「DataManager」スクリプトを作成
  2. シーン上に空のオブジェクトを作り「DataManager」と名付ける
  3. このオブジェクトにDataManagerスクリプトをアタッチ
そしてDataManagerスクリプト内を次のコードで上書きしてください。
DataManager.cs
1using System;
2using System.Collections.Generic;
3using UnityEngine;
4using System.Linq;
5using UniRx;
6
7namespace FlMr_CardBase
8{
9    /// <summary>
10    /// カードの位置情報を保持、更新するクラス
11    /// </summary>
12    internal class DataManager : MonoBehaviour
13    {
14        /// <summary>
15        /// カードの位置情報
16        /// </summary>
17        private CardObjectData Data { get; set; }
18
19        /// <summary>
20        /// 全Position情報をもとに初期化する
21        /// </summary>
22        /// <param name="positions"></param>
23        internal void Initialize(List<Type> positions)
24        {
25            Data = new CardObjectData(positions);
26        }
27
28        /// <summary>
29        /// カードの情報すべてをJson形式に変換する
30        /// </summary>
31        /// <returns></returns>
32        internal string GetJsonData() => JsonUtility.ToJson(Data);
33
34        /// <summary>
35        /// カードの位置情報を保持するクラス
36        /// </summary>
37        [Serializable]
38        private class CardObjectData
39        {
40            /// <summary>
41            /// どの位置にどのカードが存在するかを保持する変数
42            /// </summary>
43            [SerializeField] private Dictionary<string, List<int>> dataCore;
44
45            internal CardObjectData(List<Type> positions)
46            {
47                dataCore = new Dictionary<string, List<int>>();
48
49                foreach (var posInfo in positions)
50                {
51                    dataCore.Add(posInfo.ToString(), new List<int>());
52                }
53            }
54        }
55    }
56
57}
DataManagerクラス内にCardObjectDataインナークラスを定義しました。このインナークラス内にカードの位置情報を保持する辞書dataCore変数が定義されています。この変数をコンストラクタ内 (47-52行目) で初期化をしています。コンストラクタの引数はList<Type>型で、平たく言うと「位置を表すクラスの名前」のコレクションです。型をList<string>として、「{"手札","デッキ","フィールド"}」等のような引数を受け取っても良かったのですが、List<Type>を用いることでタイプミスを防止できる役割があります。さらにジェネリック制約の恩恵にもあやかることができるのですが、これは後の話です。

さて、コンストラクタが呼ばれると、まずは辞書型dataCoreを初期化し、それぞれの位置(Key)に対応する整数コレクション(Value)をインスタンス化します (49-52)。

ここで1つ注意点があります。dataCore変数に[SerializeField]属性を付与していますが、実は辞書型はシリアル化を行うことが出来ません。この点に関しては後程変更を加えます。

以上で、上記の「表」が完成しました。このクラスのインスタンスを保持しているのが17行目のDataプロパティで、23-26行目のInitialize()メソッドでインスタンス化されます。

次に、辞書型のシリアル化を行いましょう。Unityの「CardBase/Scripts/」に「SerializableDictionary」スクリプトを作成し、次のコードを記述してください。
SerializableDictionary.cs
1using System;
2using System.Collections.Generic;
3using UnityEngine;
4using System.Linq;
5
6namespace FlMr_CardBase
7{
8    /// <summary>
9    /// シリアル化可能な辞書クラス
10    /// </summary>
11    /// <typeparam name="TKey"></typeparam>
12    /// <typeparam name="TValue"></typeparam>
13    [Serializable]
14    public class SerializableDictionary<TKey, TValue> :Dictionary<TKey, TValue>, ISerializationCallbackReceiver
15    {
16        [Serializable]
17        private class DataCore
18        {
19            public DataCore(List<Pair> pair) => Pair = pair;
20            public List<Pair> Pair;
21        }
22
23        [Serializable]
24        private struct Pair
25        {
26            internal Pair(KeyValuePair<TKey,TValue> pair)
27            {
28                key = pair.Key;
29                value = pair.Value;
30            }
31            public TKey key;
32            public TValue value;
33        }
34
35        /// <summary>
36        /// シリアル化されるKey,Valueペア
37        /// </summary>
38        [SerializeField] private DataCore dataCore;
39
40        /// <summary>
41        /// デシリアライズのコールバック
42        /// </summary>
43        public void OnAfterDeserialize()
44        {
45            // 初期値をもとに辞書を復元
46            dataCore.Pair.ForEach(x => Add(x.key, x.value));
47        }
48
49        /// <summary>
50        /// シリアライズのコールバック
51        /// </summary>
52        public void OnBeforeSerialize()
53        {
54            // 辞書の情報をもとに、シリアライズ用のDataCoreクラスのインスタンスを作成
55            dataCore = new ( this.ToList().ConvertAll(x=>new Pair(x)) );
56        }
57    }
58}
59
シリアル化可能なDictionary派生クラスの定義ですが、カードゲームとの関係が薄いためコードの説明は省略します。DataManager.CardObjectDataクラスのDictionarySerializableDictionaryに変更してください。
DataManager.cs CardObjectData
1[Serializable]
2private class CardObjectData
3{
4    /// <summary>
5    /// どの位置にどのカードが存在するかを保持する変数
6    /// </summary>
7    [SerializeField] private SerializableDictionary<string, List<int>> dataCore;
8
9    internal CardObjectData(List<Type> positions)
10    {
11        dataCore = new SerializableDictionary<string, List<int>>();
12
13        foreach (var posInfo in positions)
14        {
15            dataCore.Add(posInfo.ToString(), new List<int>());
16        }
17
18    }
19}
これで辞書型のシリアル化が可能になります。動作確認がしたいため、ここからさらに2つの簡単なクラスを作成します。

まず一つ目です。次の作業を行ってください。
  1. Unityの「CardBase/Demo/」に「Scripts」ディレクトリを作成
  2. この中に「CardBaseTest」スクリプトを作成
  3. シーン上に空のオブジェクトを作り「CardBaseTest」と名付ける
  4. このオブジェクトにCardBaseTestスクリプトをアタッチ


次に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 Position position;
11
12        void Start()
13        {
14            manager.Initialize(new() { position.GetType() });
15            Debug.Log(manager.GetJsonData());
16        }
17    }
18}
やりたいことは明らかで、managerを初期化した後に、データをログで表示することです。この時にPositionクラスが必要になるため、これもUnity上で登録します。しかし現状Positionが存在しないためコンパイルエラーが発生しています。

そこで2つ目のスクリプトの作成です。まずは次の作業を行ってください。
  1. Unityの「CardBase/Scripts」に「Position」スクリプトを作成
  2. シーン上に空のオブジェクトを作り「Position」と名付ける
  3. このオブジェクトにPositionスクリプトをアタッチ
そして「Position」スクリプトに次のコードで上書きしてください。
1using System;
2using System.Collections.Generic;
3using UniRx;
4using UnityEngine;
5
6namespace FlMr_CardBase
7{
8    public class Position : MonoBehaviour
9    {
10    }
11}
12
ではUnityに戻り、CardBaseTestオブジェクト/CardBaseTestコンポーネントの2つの変数に、シーン上のDataManagerオブジェクトとPositionオブジェクトを登録します。ここまで出来たら、ゲームを実行してみてください。


次にこのインナークラスをインスタンス化しましょう。DataManagerクラスに (インナークラス内ではないため注意) 次のコードを挿入してください。ログに
1{"dataCore":{"dataCore":{"Pair":[{"key":"FlMr_CardBase.Position","value":[]}]}}}
と表示されたと思います。少し見づらいので改行を入れると次のようになります。
1{
2    "dataCore":{"dataCore":{"Pair":[
3
4                {  "key":"FlMr_CardBase.Position","value":[]  }
5            
6    ]}}
7}
重要な箇所は、辞書のKeyValueが書かれた4行目です。"FlMr_CardBase.Position"という位置には[]のカードが存在する(つまり何も存在していない)ということを意味しています。今後もこのJson形式のログを頼りに動作確認をするため、見方を覚えていてください。

今回は以上です。
現時点でのプロジェクトは  Github から確認できます。エラーが解決しない場合などは参考になるかと思います。

また  Twiter でもご意見、質問、指摘などを受け付けています。どんな些細なことでも全く構いませんので、気軽に連絡していただければと思います。



以上で説明が終了です。次回以降もよろしくお願いします。