インベントリ / アイテムをインベントリに追加する

はじめに

今回は大きく分けて3つの作業をします。アイテム図鑑のページの作成、データの核の作成、そしてAddItem()メソッドの作成です。設計図では、下図の「」印を付けた箇所に相当します。



ただし、ページを束ねて図鑑にする( ItemUtilityクラス )のは後の講になります。



アイテム図鑑のページの作成

まずはInventory/Scripts/フォルダ内に「ItemBase」スクリプトを作成し、下記のコードを記述してください。
ItemBase.cs
1using UnityEngine;
2
3namespace FlMr_Inventory
4{
5    [CreateAssetMenu(menuName = "Item/ItemTemp", fileName = "itemTemp")]
6    public class ItemBase : ScriptableObject
7    {
8        [SerializeField] private int uniqueId;
9        [SerializeField] private string itemName;
10        [SerializeField] private Sprite icon;
11        [SerializeField] private string description;
12
13        /// <summary>
14        /// アイテムの種類と1:1対応する整数
15        /// (データを保存するときや、将来通信機能を実装する際に真価を発揮する)
16        /// </summary>
17        public int UniqueId => uniqueId;
18
19        /// <summary>
20        /// アイテム名
21        /// </summary>
22        public string ItemName => itemName; 
23
24        /// <summary>
25        /// アイテムのアイコン
26        /// </summary>
27        public Sprite Icon => icon;
28
29        /// <summary>
30        /// プレイヤーに対するアイテムの説明
31        /// </summary>
32        public string Description => description; 
33    }
34}

コードの説明をします。

6行目でクラスの定義がされるわけですが、ScriptableObjectクラスを継承していることが分かります。このクラスはUnity側で予め用意されているクラスで、ゲームを実行する前からインスタンスを生成することができるという特徴があります。もう少し具体的に説明すると、例えばClassAというクラスのインスタンスを生成する際は、通常new ClassA()というコードを書きます。当たり前ですが、このコードはプログラムを実行するまで呼ばれることはありません。そのためClassAのインスタンスはプログラム実行後に生成されます。しかしScriptableObjectクラスを継承しているクラスのインスタンスはプログラムを実行する前に生成し、ファイルとして保存することができます。この「ファイルとして保存する」機能を使用するために必要な記述が5行目です。日本語に直すと「Create/Item/ItemTemp」という項目をクリックするとItemBaseクラスのインスタンスを生成し「itemTemp」という名のファイルに保存する、となります。プログラム実行前にインスタンスを生成するのですから、ゲームを通して変化しない値を保存したい状況とは非常に相性が良いです。例えば今回のようなアイテムの性質や、カードゲームのカード、タワーディフェンスで登場するユニットのステータス等です。

折角ですから、残りのコードの説明の前にインスタンスの生成を体験してみましょう。Unityに戻り、Inventoryフォルダ直下に「ItemsTemp」フォルダを、さらにその中に「Resources」フォルダを作成し、その中に移動してください(このフォルダ名は重要です。つづりなどに間違いがないようお願いします)。ここで「右クリック/Create/Item/ItemTemp」という項目を選択します。


※名前にtempと付けている通り、後にこのフォルダごと削除します。このフォルダ内に重要なファイルを配置しないでください。
この時に生成されたファイルがItemBaseクラスのインスタンスです。このファイルの中身はコードの説明の後に行います。

8~11行目がアイテムが持つ性質です。順に、アイテムid、アイテム名、アイテムのアイコン、アイテムの説明です。これらの値はゲームが進んでも変化することはありません。というより、間違ってこの値が変わってしまったら大変です。そのため全ての変数はprivateで定義しています。ただ、これだけでは(ItemSlotクラス内を除いて)誰も読み取ることが出来ないです。そこで13~32行目のコードが必要になってきます。これらにより値の読み取りだけはどのクラスからでも可能にしています。ただし値の変更は禁止されているので、ゲームが進行したとしても(ItemSlotクラスのインスタンスを直接変更しない限り)値が変化しないことが保証されます。

では初期値はどこで変更するのかというとItemSlotクラスのインスタンスをいじります。これが先ほどUnity上で生成したファイルとなります。

それぞれの変数にSerializeField属性を付けているため、アクセス修飾子がprivateであったとしてもインスペクター上から値を登録することが出来ます。適当に値を書き込んでください。

これでアイテム図鑑の1ページが完成したことになります。



データの核となるItemBagDataクラスの作成

次にこのアイテムをバッグに入れる機能を実装します。
といっても、アイテムが入ったことによる見た目の変更はまだ行いません。内部の情報の更新を行うところを実装します。前回作成したItemBagスクリプトを開いてください。ItemBagクラスの内部に以下のコードを挿入してください。
ItemBag.cs
1/// <summary>
2/// 核となるデータ
3/// </summary>
4[Serializable]
5private class ItemBagData
6{
7    /// <summary>
8    /// 所持しているアイテムのId
9    /// </summary>
10    public List<int> Ids = new List<int>();
11
12    /// <summary>
13    /// 所持数
14    /// </summary>
15    public List<int> Qty = new List<int>();
16}
ここで[Serializable]部分にコンパイルエラーが発生する方は、1行目に
ItemBag.cs
1using System;
を追加して下さい。バッグ内に、何がどれだけ入っているかを保持するリストです。インベントリシステムを実装するうえで最も大切な情報です。本当に重要であるためprivateなインナークラスの中にしまっておきます。またセーブ&ロードシステムが実装される可能性を考慮しSerializable属性を付与してあります。内容は非常に単純で
  • Ids : 何番目のスロットに何のアイテム(のid)が入っているか
  • Qty : それぞれのアイテムを何個持っているか
です。イメージは下図のような表です。


ではアイテムを追加するときの処理を、このインナークラスに実装します。イメージは下図の通りです。

次のコードをQtyの定義の下に挿入してください。
ItemBag.cs (ItemBagData class)
1/// <summary>
2/// バッグに追加する
3/// </summary>
4/// <param name="id">追加するアイテムのid</param>
5/// <param name="number">追加する個数</param>
6public void Add(int id, int number)
7{
8    // アイテム番号=id のアイテムが既にバッグ内に存在するか
9    // 存在するなら何番目のスロットに入っているか
10    int index = Ids.IndexOf(id);
11    if (index < 0)
12    {
13        // 未所持のアイテムの場合は、スロットを1つ消費する
14        Ids.Add(id);
15
16        //個数の更新
17        Qty.Add(number);
18    }
19    else
20    {
21        // 既に所持しているアイテムの場合は、所持数のみを追加
22        Qty[index] += number;
23    }
24}
このAddメソッドを呼ぶと、IdsQtyが適切に更新されます。例えばid=1のアイテムを3個バッグに入れたいときはAdd(1,3)と記述します。このメソッドが呼ばれると、まずは「既に持っているアイテムか否か」で場合分けされます(10行目)。もし持っていない場合(Idsに「1」が存在しない場合)index変数には-1が入り、持っていた場合はIdsの何番目の要素に「1」が入っているかが分かります。

バッグ内にid=1のアイテムが入っていない場合if文の内部が実行されます。ここではIdsに「1」を追加し、Qtyに3が追加されます。これによりバッグ内にid=1のアイテムが3個存在することが表現されます。

仮に、既にid=1のアイテムがいくつか入っていた場合、elseの内部が実行されます。ここではIdsには変更を加えず、Qtyの該当要素の値を+3します。


次はアイテムを削除するメソッドを追加します。Add()に続けて以下のコードを挿入してください。
ItemBag.cs (ItemBagData class)
1/// <summary>
2/// バッグから取り出す
3/// </summary>
4/// <param name="id">取り出したいアイテムのid</param>
5/// <param name="number">取り出す個数</param>
6public void Remove(int id,int number)
7{
8    // アイテム番号=id のアイテムが既にバッグ内に存在するか
9    // 存在するなら何番目のスロットに入っているか
10    int index = Ids.IndexOf(id);
11    if (index < 0)
12    {
13        // 未所持のアイテムをどり出すことは出来ない
14        throw new Exception($"アイテム(id:{id})の取り出しに失敗しました");
15    }
16    else
17    {
18        if (Qty[index] < number)
19        {
20            // 必要数所持していない
21            throw new Exception($"アイテム(id:{id})の取り出しに失敗しました");
22        }
23        else
24        {
25            //取り出す
26            Qty[index] -= number;
27
28            if(Qty[index] == 0)
29            {
30                // 0個になった場合はリストから削除
31                Qty.RemoveAt(index);
32                Ids.RemoveAt(index);
33            }
34        }
35    }
36}
基本となる考え方はAdd()と同様です。まずは取り出したいアイテムがバッグにあるか否かを判定します。存在する場合はindexに何番目のスロットにあるかが代入され、持っていない場合は-1が代入されます。しかし(Add()とは異なり)持っていない場合は例外を投げています(14行目)。

次に持っていた場合です。今度は十分な個数を所持しているかで場合分けです(18行目)。足りない場合ももちろん例外を投げます(21行目)。十分な量を持っている場合はデータの変更を行います。つまり該当アイテムのQtyの量を減らします(26行目)。最後に、もしアイテムの量が0個になった場合、リストからアイテムを取り除きます(31 , 32行目)。

今作成したItemBagDataインナークラスのインスタンスを作成し、保持するコードを追加します。ItemBagクラス内(インナークラス内ではないため注意)に次のコードを追加してください。AllSlotsの定義の下あたりが妥当でしょうか。
ItemBag.cs
1/// <summary>
2/// 現在所持しているアイテムの情報
3/// </summary>
4private ItemBagData Data { get; set; } = new();
やっていることは単純で、ItemBagDataのインスタンスを保持する変数の定義しているだけです。



AddItem()メソッドの作成

今後アイテム情報を変更する際は、IdsQtyを直接いじるのは避け 先ほど作成したAdd() , Remove()メソッドを使用します。ItemBagクラス内(ItemBagDataインナークラス内ではないため注意)に下記のコードを挿入します。Awake()メソッドの下あたりに挿入すると良いと思います。
ItemBag.cs
1/// <summary>
2/// アイテムをバッグに追加する
3/// </summary>
4/// <param name="itemBase">追加したいアイテムのID</param>
5/// <param name="number">追加したい個数</param>
6/// <returns>バッグへの追加に成功したか</returns>
7public bool AddItem(int itemId, int number)
8{
9    if (!Data.Ids.Contains(itemId) && Data.Ids.Count == slotNumber)
10    {
11        // スロットが埋まっている状態では、未所持アイテムの追加は出来ない
12        return false;
13    }
14
15    // アイテムをバッグに追加する
16    Data.Add(itemId, number);
17    return true;
18}
行うことは、アイテムを追加したとしてバッグのキャパをオーバーしていないかの確認、そして先ほど作成したAdd()メソッドの呼び出しです。肝は9行目で、アイテムを追加することでカバンが溢れることを防いでいます。既に所持しているアイテムの追加であれば、所持数を増やすだけなのであふれることはありません。しかし新規のアイテムであれば追加でスロットが必要となるため確認が必要です。9行目のコードは2つのチェックが行われており、前半は「新規のアイテムか否か」後半は「現在スロットが埋まっているか」です。両方ともtrueであればカバンがあふれてしまいます。よってこの時はfalseを返します。この場合を除くとカバンがあふれることはないためData.Add()を呼びます。


少し多めの変更を行ったので、ここで動作確認をしておきます。

まずItemBagクラス内に以下のコードを挿入します。
ItemBag.cs
1/// <summary>
2/// ItemBagDataをシリアル化する
3/// </summary>
4/// <returns></returns>
5public string ToJson() => JsonUtility.ToJson(Data);
このメソッドを呼ぶとItemBagDataのインスタンスであるDataの中身を文字列に変換されたものが返されます。そのため正しく動作していることを確認することももちろん、ファイルに書き込んでセーブ機能を実装することも出来ます。

次にUnityに戻り「Inventory/Demo/」の中に「Scripts」フォルダを作成し、このフォルダ内に「InventoryTest」スクリプトを作成します。さらにヒエラルキー上に空オブジェクトを作成し「InventoryTest」と名付けます。そしてこのオブジェクトにInventoryTestスクリプトをアタッチします。


InventoryTestスクリプトの中を、下記のコードで上書きしてください。
InventoryTest
1using UnityEngine;
2
3namespace FlMr_Inventory.Demo {
4    public class InventoryTest : MonoBehaviour
5    {
6        /// <summary>
7        /// 動作確認の対象であるItemBag
8        /// </summary>
9        [SerializeField] private ItemBag bag;
10
11        void Start()
12        {
13            // 10種類が限界であるカバンに、15種類のアイテムを追加する
14            for (int i = 0; i < 15; i++)
15            {
16                //追加に成功したか否かを表示
17                Debug.Log(i+"番目のアイテム:"+bag.AddItem(i, 3));
18            }
19
20            //カバンに入っているアイテムのデータを表示
21            Debug.Log(bag.ToJson());
22        }
23
24    }
25}
9行目には、テストしたいItemBagオブジェクトがUnity上で代入されます。Start()メソッド内では、15種のアイテムをカバンに入れることを試みます。for文の中では、カバンへの追加に成功したか否かがログで表示されるように記述しています。最後に(21行目)カバン内のデータをJson形式に変換しログとして表示しています。全てのスクリプトが保存されていることを確認し、Unityに戻ってください。

ヒエラルキーのInventoryTestオブジェクトのインスペクターを開き、bag変数にItemBagオブジェクトを登録してください。

ここまでで動作確認の準備は完了です。ゲームを実行してみてください。UnityのConsole画面を見ると、0から9番目はTrueが出力され、それ以降はFalseとなっているはずです。そして最後の行にはIdsQtyの情報が出力されます。画像と同様の結果が得られたでしょうか?




今回は以上です。
ここまでの状況は   Github  から確認できます。