カードゲーム基盤 / カードの追加と移動

はじめに

このページでは、基本となるメソッド「AddCard()」「MoveCard()」の実装を行います。設計図では、下図の「」印を付けた箇所に相当します。







AddCard()

では早速AddCard()の実装をはじめます。メソッドの動作イメージは次の通りです。


例えば左上の状況で、デッキにカードオブジェクトを一つ追加することを考えます。新しく追加するカードにはまだIdが割り振られていないため、どんな番号を与えるかを決める必要があります。この時基本は「既にあるIdの最大 + 1」を用います。この例では最大値は 4 なので、新しいIdは 5 となります。よって、デッキの欄に 5 を追加しました。次に左下の例です。今から説明することは「1 vs 1 のゲームの時のみ」に効果を発揮する手法で、先攻のIdを自然数、後攻のIdを負の整数とする設計です。フィールドにカードを追加するとき、最小値である -3 から 1 を引いた数である -4 を追加します。これにより、カードのIdを見るだけでどちらのプレイヤーのカードか判別することが出来ます。このようなデータ変更機能を持つメソッドをCardObjectDataインナークラス内に作成しましょう。
DataManager.cs CardObjectData
1/// <summary>
2/// カードを追加する。
3/// </summary>
4/// <param name="posType">カードの追加場所</param>
5/// <param name="index">カードを何番目に挿入するか</param>
6/// <param name="positiveCardId">Idとして、自然数を使用するか負の整数を使用するか</param>
7internal void AddCard(Type posType, int? index, bool positiveCardId)
8{
9    // posIdの取得
10    // (Type をstringに変換するだけ)
11    string posId = posType.ToString();
12
13    // カードオブジェクトのIdの計算
14    int newId = positiveCardId switch
15    {
16        // idが正の場合、使用済みのidの中で最大の整数 + 1
17        true => dataCore.Values.SelectMany(l => l).DefaultIfEmpty().Max() + 1,
18
19        // idが負の場合、使用済みのidの中で最小の整数 - 1
20        false => dataCore.Values.SelectMany(l => l).DefaultIfEmpty().Min() - 1,
21    };
22
23    // idを挿入
24    // 挿入位置(index)がnullの場合は最後尾
25    // nullでない(intの場合) indexをそのまま使用
26    dataCore[posId].Insert(index ?? dataCore[posId].Count, newId);
27}
まずは引数に注目してください。第1引数は追加したい位置です。第2引数は、その位置の中の何番目にカードを挿入したいかです。nullの場合は最後尾になります。第3引数はIdとして正の値を使用するか負の値を使用するかを決定するフラグです。

11行目はTypestringに変換してポジションIdとしています。14-21行目で、新しく使用するカードオブジェクトのidを計算しています。positiveCardIdtrueの場合は、現状使用済みのIdの最大値 + 1とします。falseの場合は反対で、最小値 - 1 です。

26行目はIdを挿入しています。indexnullではない場合はそのままindexを使用します。nullの場合はindexの代わりにdataCore[posId].Countを使用します。

繰り返しになりますが、このモジュールにとって、このゲームが大富豪なのか、ババ抜きなのか、はたまたトレーディングカードゲームなのかはどうでもいいことです。そのためここで作成しているIdとはカードの種類 (ダイヤのエース、ワイルドドロー4、ピカチュウ等といった情報) とは無関係です。ただ「オブジェクトと1対1の関係にある整数」を生成しているにすぎない、ということを理解してください。



MoveCard()

次に、主要なメソッドの二つ目である「カードの移動メソッド」を作成します。再掲になりますが、イメージは下図の通りです。


こちらは特殊なことは何もなく、ただ移動させているだけです。CardObjectDataインナークラス内に次のコードを挿入してください。
DataManager.cs CardObjectData
1/// <summary>
2/// 指定したカードの位置idを取得する
3/// </summary>
4/// <param name="cardObjId"></param>
5/// <returns>配置場所</returns>
6internal Type GetCardPos(int cardObjId)
7{
8    foreach (var item in dataCore)
9    {
10        if (item.Value.Contains(cardObjId)) return Type.GetType(item.Key);
11    }
12
13    throw new Exception($"カードId = {cardObjId} は存在しません");
14}
15
16/// <summary>
17/// 指定したカードの位置を変更する
18/// </summary>
19/// <param name="cardObjId">カードオブジェクトのId</param>
20/// <param name="posTo">移動先</param>
21/// <param name="afterCard">挿入位置の指定(挿入位置直後のカードを指定する) 最後尾の場合はnull</param>
22internal void MoveCard(int cardObjId, Type posTo, int? index)
23{
24    // 移動したいカードが現在どこに配置されているか
25    Type posFrom = GetCardPos(cardObjId);
26
27    // 移動対象の情報を削除する
28    dataCore[posFrom.ToString()].Remove(cardObjId);
29
30    // 挿入
31    string posToId = posTo.ToString();
32    dataCore[posToId].Insert(index ?? dataCore[posToId].Count, cardObjId);
33}
順番が前後しますが、まずは2つ目のメソッドを見てください。第一引数は移動したいカードのオブジェクトIdです。このIdを第2引数の位置の、第3引数番目に挿入します。例によって、第3引数がnullの場合は最後尾に挿入します。

行う操作は2つで
  1. 元々いた場所から取り除く
  2. 移動先に挿入する
です。24-28行目が「1」についてです。25行目はcardObjIdがどの位置に存在しているかを取得するコードです。ここで1つ目のメソッドを見てください。引数がカードオブジェクトのId、戻り値はその位置のTypeとなっています。核となるデータdataCoreを順番に見てcardObjIdが見つかったとき、その位置を返しています。見つからなかった場合は例外を投げます。では2つ目のメソッドに戻り、28行目を見てください。先ほど取得した位置posFromからcardObjIdを取り除く処理が書かれています。

次に「移動先に挿入する」処理です。これはAddCard()メソッドで行った後半のコードと全く同じなので、解説は省略します。



動作確認をするためCardBaseTestクラスからAddCard() , MoveCard()を使用したいです。しかしCardObjectDataクラスはprivateなので呼び出せません。そこでDataManagerクラス (CardObjectDataインナークラスの外) に次のinternalなメソッドを追加してください。
1/// <summary>
2/// カードを新しく生成する
3/// </summary>
4/// <param name="posType">生成位置</param>
5/// <param name="index">カードを何番目に挿入するか</param>
6/// <param name="positiveCardId">カードのIdとして自然数を使用するか、負の整数を使用するか</param>
7internal void AddCard(Type posType, int? index,bool positiveCardId)
8{
9    Data.AddCard(posType, index, positiveCardId);
10}
11
12/// <summary>
13/// カードを移動する
14/// </summary>
15/// <param name="cardObjId">移動対象のカードId</param>
16/// <param name="posTo">生成位置</param>
17/// <param name="index">カードを何番目に挿入するか</param>
18internal void MoveCard(int cardObjId,Type posTo, int? index)
19{
20    Data.MoveCard(cardObjId, posTo, index);
21}
一目見てわかる通り、ただAddCard() , MoveCard()を引っ張ってきているだけです。これにより他のクラスからアクセスできるようになりました。 では先ほど説明した通りCardBaseTestクラスからメソッドを呼び出したいのですが、現状ポジションが一つしかありません(Positionクラスのみ)。そこでUnityの戻り「CardBase/Demo/Scripts」に「Deck」スクリプトと「Hand」スクリプトを作成してください。それぞれ次のコードで上書きしてください。
Deck.cs
1namespace FlMr_CardBase.Demo
2{
3    public class Deck : Position
4    {
5    }
6}
Hand.cs
1namespace FlMr_CardBase.Demo
2{
3    public class Hand : Position
4    {
5    }
6}
現状、特に機能が不要なので空のクラスになっています。続けて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    }
11}
今後カードの配置場所に共通する特徴をPositionクラスに定義していきます。

では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 List<Position> positions;
11
12        void Start()
13        {
14            manager.Initialize(positions.ConvertAll(p=>p.GetType()) );
15
16            //カードを4枚追加
17            manager.AddCard(typeof(Deck), 0, true); // 「Deck」の「0」番目の位置に「正」のidをもつカードを追加
18            manager.AddCard(typeof(Deck), 0, true);
19            manager.AddCard(typeof(Deck), 0, true);
20            manager.AddCard(typeof(Deck), 0, true);
21
22            // カードを2枚移動
23            manager.MoveCard(2,typeof(Hand), null); // CardObjectIdが「2」のカードを「Hand」の「最後尾」に移動
24            manager.MoveCard(3,typeof(Hand), null);
25
26            // 状況確認
27            Debug.Log(manager.GetJsonData());
28        }
29    }
30}
10行目の位置の変数がコレクションになっています。この変更によりインスペクター上から複数の位置を登録できます。Start()メソッドで、カードの追加と移動のメソッドを確認し、動作を確認します。最後にコアデータをシリアル化して表示しています。

Unityに戻り、シーンの調整や設定をいじります。まずシーン上の「Position」オブジェクトのインスペクター上に警告が現れていると思います。これはPositionクラスを抽象化したため、オブジェクトにアタッチできなくなって発生しました。このコンポーネントを除去してください。代わりに「Position」オブジェクトの子として、空のオブジェクトを2つ作成し、それぞれ「Deck」「Hand」と名付けます。そしてそれぞれに「Deck」「Hand」スクリプトをアタッチします。


続けて、CardBaseTestオブジェクトのコンポーネントの「Positions」変数に、今作成した「Deck」「Hand」オブジェクトを登録します。


ゲームを実行して、ログを確認してください。次のような出力が得られた場合は成功です(読みやすさのため改行を挿入しています)。
Unity Console
1{"dataCore":{"dataCore":{"Pair":[
2
3    {"key":"FlMr_CardBase.Demo.Deck","value":[4,1]},
4    {"key":"FlMr_CardBase.Demo.Hand","value":[2,3]}
5
6]}}}
デッキには Id:4,1 のカードが存在し、手札には Id:2,3 のカードが存在することが分かります。



今回はここまでです。

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