インベントリ / スロットクリック時の動作

はじめに

今回の作業は大きく2つあり、まずはアイテムの削除機能、次にItemUtilityクラスの作成です。設計図では、下図の「」印を付けた箇所に相当します。



アイテムの削除

早速作業をはじめます。単純にアイテムを捨てるためだけでなく、アイテムを使用した際に数を減らすためにも必要になるため、重要な機能です。

ItemBagクラスに次のメソッドを追加してください(コンパイルエラーが発生します)。
ItemBag.cs
1/// <summary>
2/// アイテムを削除する
3/// </summary>
4/// <param name="itemId">削除するアイテムのId</param>
5/// <param name="number">削除する個数</param>
6/// <returns></returns>
7public bool RemoveItem(int itemId, int number)
8{
9    // 十分な数を所持しているか
10    bool haveEnough = Data.GetQty(itemId) >= number;
11
12    if (haveEnough)
13    {
14        // 十分持っている場合
15        Data.Remove(itemId, number);
16
17        UpdateItem();
18        return true;
19    }
20    else
21    {
22        //不足している場合
23        return false;
24    }
25}
ここで10行目のGetQty(itemId)にコンパイルエラーが生じます。これはItemBagDataクラスにGetQty()が定義されていないためですが、ここでは「itemIdのアイテムをいくつ所持しているか」を返すメソッドだと考えてください。後で作成します。よって10行目の変数は、アイテムの書次数が削除したい数以上か否かを表します。
仮に十分な数を持っていた場合、15行目でRemove()メソッドを呼ぶことで情報を更新し、UpdateItem()メソッドで表示も変更します。
一方数が足りない場合は、情報を一切変化させることなくfalseを返します。

次にGetQty()ItemBagDataクラス(ItemBagクラスのインナークラス)に定義します。次のコードを挿入してください。
ItemBag.cs (ItemBagDataインナークラス)
1/// <summary>
2/// 特定のアイテムをいくつ所持しているか
3/// </summary>
4/// <param name="id">アイテムid</param>
5/// <returns>所持数</returns>
6public int GetQty(int id)
7{
8    int index = Ids.IndexOf(id);
9    return index < 0 ? 0 : Qty[index];
10}
実装は非常にシンプルです。
8行目で、Ids(所持しているアイテムのidを保持するList)からidを検索します。存在している場合(所持している場合)はidが格納されている配列番号、存在しない場合(未所持の場合)は-1となります。仮にindexが負の場合、所持していないため0が返され、正の場合はQty[index]が返されます。

動作確認をするためInventoryTestクラスのStart()メソッドを次のように変更して下さい。
InventoryTest.cs
1void Start()
2{
3    // id1のアイテムを1つ追加
4    bag.AddItem(1, 1);
5    Debug.Log(bag.ToJson());
6
7    // id1のアイテムを2個削除
8    bag.RemoveItem(1, 2);
9    Debug.Log(bag.ToJson());
10
11    // id1のアイテムを1個削除
12    bag.RemoveItem(1, 1);
13    Debug.Log(bag.ToJson());
14}
まず id=1 のアイテムを1つ追加し、次に id=1 のアイテムを2個削除しようとします。所持数を上回る数を削除しているため、何も起こらないはずです。最後に id=1 のアイテムを1つ削除します。この時正常に削除が行われるため、最終的にBagの中身は空になると正解です。

全てのスクリプトが保存されていることを確認し、Unityに戻り実行してください。下の写真のような実行結果になったでしょうか。





ItemUtilityクラス

では次に、アイテムの扱いが楽になるように「ItemUtility」スクリプトを作成します。「Inventory/Scripts」フォルダ内に「ItemUtility」スクリプトを作成してください。

作成後、このファイル内を次のコードで上書きしてください。
ItemUtility.cs
1using System.Collections.Generic;
2using System.Collections.ObjectModel;
3using System.Linq;
4using UnityEngine;
5
6namespace FlMr_Inventory
7{
8    [CreateAssetMenu(menuName = "ItemUtility", fileName = "ItemUtility")]
9    public class ItemUtility : ScriptableObject
10    {
11        #region Singleton
12        private static ItemUtility instance;
13        public static ItemUtility Instance
14        {
15            get
16            {
17                if (instance == null)
18                {
19                    var instances = Resources.LoadAll<ItemUtility>("");
20
21                    // シングルトンなクラスのインスタンスは必ず1つでなければならない
22                    instance = instances.Count() switch
23                    {
24                        0 => throw new System.Exception("ItemUtilityのインスタンスがResourcesフォルダ内に存在しません"),
25                        1 => instances.ElementAt(0),
26                        _ => throw new System.Exception("ItemUtilityのインスタンスがResourcesフォルダ内に複数存在します")
27                    };
28
29                    // 見つけた唯一のインスタンスを初期化
30                    instance.Initialize();
31                }
32
33                return instance;
34            }
35        }
36        #endregion
37
38        /// <summary>
39        /// ゲームに登場させたい全アイテム
40        /// </summary>
41        [SerializeField] private ItemBase[] allItems;
42
43        /// <summary>
44        /// Idにアイテムを結びつける辞書
45        /// </summary>
46        public ReadOnlyDictionary<int, ItemBase> ItemIdTable { get; private set; }
47
48        /// <summary>
49        /// 全てのアイテムを保持した読み取り専用コレクション
50        /// </summary>
51        public ReadOnlyCollection<ItemBase> AllItems { get; private set; }
52
53        private void Initialize()
54        {
55            // ItemIdTableの初期化
56
57            Dictionary<int, ItemBase> idItemMap = new Dictionary<int, ItemBase>();
58            foreach (var item in allItems)
59            {
60                idItemMap.Add(item.UniqueId, item);
61            }
62            ItemIdTable = new (idItemMap);
63
64
65
66
67            // AllItemsの初期化
68
69            AllItems = new (allItems);
70        }
71    }
72
73
74}
少々長いコードですが、やっていることは非常に単純です。
まず、このクラスはシングルトンなScriptableObjectクラスです。つまりインスタンスをプロジェクト上にファイルとして保存でき、そのファイルは唯一という制約が課されたクラスである、ということです。

8行目はクラスのインスタンスを作成しファイル出力するための記述です。11~36行目はただのシングルトンパターンの記述です。
38行目以降が本題になります。allItems変数が全てのアイテムを保持する変数であり、これはインスペクター上から初期化するため[SerializeField]属性が付与されています。仮にこの変数が書き換えられるとゲームに登場するアイテムが変化するため大きなバグに繋がります。そのため外部のクラスからは参照されないようprivateとし、代わりに46,51行目の読み取り専用プロパティーを公開しています。ItemIdTableはアイテムidをKey、アイテムをValueとした辞書です。id(int)とItemBaseの変換時に使用します。AllItemsは文字通り、アイテムのコレクションです。

これら2つの変数の初期化を行うのが53行目から始まるInitialize()メソッドです。このメソッドは13行目のInstanceプロパティーが初めて呼ばれた際に実行され、allItems(41行目)をもとにItemIdTableAllItemsを初期化します。

このUtilityクラスの存在により、ItemBagクラスのUpdateItemメソッドを改善することが出来ます。
ItemBag.cs
1
2/// <summary>
3/// スロットの表示と所持アイテムの情報を一致させる
4/// </summary>
5private void UpdateItem()
6{
7    for (int i = 0; i < Data.Ids.Count; i++)
8    {
9        // 追加したいアイテムのid
10        int itemId = Data.Ids[i];
11
12        // 全アイテムからitemIdをもつアイテムを検索する
13        ItemBase addingItem = ItemUtility.Instance.ItemIdTable[itemId];
14
15        // アイテムを表示
16        AllSlots[i].UpdateItem(addingItem, Data.Qty[i]);
17    }
18
19    for (int i = Data.Ids.Count; i < slotNumber; i++)
20    {
21        // 残りは空
22        AllSlots[i].UpdateItem(null, -1);
23    }
24}
25
変更は二か所です。メソッドの頭にallItems変数を作成していましたが、不要になるので削除しました。そして13行目のaddingItemでは、ItemIdTableを用いて、idからアイテムを見つけます。

ここで動作確認をします。全てのスクリプトが保存されていることを確認し、Unityに戻って下さい。「Inventory/ItemsTemp/Resources/」にItemUtilityクラスのインスタンスを作成します。


代わりに元々あった「Inventory/ItemsTemp/Resources/」のItemTempクラスのインスタンスを、「Inventory/ItemsTemp」直下に移動します。


そして、ItemUtilityのAllItems変数に、今移動させたItemTempクラスのインスタンスを追加します。


これで準備は整ったので、実行してみてください。先ほどと同様の実行結果となったでしょうか?



今回は以上です。

ここまでの状況は  Github から確認できます。