インベントリ / アイテムスロットにアイコンを表示する

はじめに

今回はバッグの内容をスロットに伝え、スロットにアイコンを表示する機能を追加します。設計図では、下図の「」印を付けた箇所に相当します。



作業の概要は、
  1. ItemSlotクラスの作成
  2. ItemBagクラスからItemSlotクラスのUpdateItem()メソッドを呼ぶ
の2つです。



ItemSlotクラスの作成

今回はバッグ内の情報をもとにスロットの表示を変更させる機能を実装します。 「Inventory/Scripts/」の中に「ItemSlot」スクリプトを作成し、ItemSlotプレハブ(Inventory/Prefabs/ItenSlot)にアタッチしてください。

次に作成したスクリプトを下記のコードで上書きします。
ItemSlot.cs
1using UnityEngine;
2using UnityEngine.UI;
3
4namespace FlMr_Inventory
5{
6    /// <summary>
7    /// 所持しているアイテムのアイコンを表示
8    /// クリックでアイテムに対してアクションを行う
9    /// 機能を実装するクラス
10    /// </summary>
11    internal class ItemSlot : MonoBehaviour
12    {
13        /// <summary>
14        /// UI画像の表示を司るクラス
15        /// 所持しているアイテムのアイコンを表示する
16        /// </summary>
17        [SerializeField] private Image icon;
18
19        /// <summary>
20        /// このスロットに入っているアイテム
21        /// </summary>
22        internal ItemBase Item { get; private set; }
23
24        /// <summary>
25        /// アイテムのアイコンを表示する
26        /// </summary>
27        /// <param name="item"></param>
28        /// <param name="number"></param>
29        internal void UpdateItem(ItemBase item, int number)
30        {
31            if (number > 0 && item != null)
32            {
33                // アイテムが空ではない場合
34                Item = item;
35                icon.sprite = item.Icon;
36                icon.color = Color.white;
37            }
38            else
39            {
40                // アイテムが空である場合
41                Item = null;
42                icon.sprite = null;
43                icon.color = new Color(0, 0, 0, 0);
44            }
45        }
46    }
47
48}


17行目の変数には[SerializeField]属性が付与されており、この変数にはImageコンポーネントを持ったIconオブジェクトを登録します。22行目のプロパティーはこのスロットに入っているアイテムを保持するプロパティーで、ゲームが進行するとともに変動します。この変数のsetterはprivateとなっているため、外部から直接変更することはできません。そこで29行目のメソッドが役に立ちます。第一引数には追加したいアイテム、第二引数には個数を指定できます。ちなみに引数itemnullの場合や個数が0の場合は、このスロットが空と認識されます。

またこのItemSlotクラスはinternalなクラスであるため、現在作成しているInventoryモジュール外からは参照出来ない設計にします。外部からスロットの中身を知る場合、ItemBagクラスのインスタンスを通して情報を取得することになります。



ItemBagからItemSlot.UpdateItem()を呼ぶ

ではここで作成したUpdateItem()メソッドをItemBagクラスから呼ぶことを考えます。まずItemBagクラスにAllSlotsという名のプロパティーを作成したことを覚えていますか。この変数は全てのItemSlotオブジェクトを保持する変数で、ItemBagAwake()メソッド内でインスタンス化されるItemSlotインスタンスが入っています。ここでGameObjectを保持するのではなくItemSlotを保持するよう変更します。変更すべきは二か所で、下記コードの4,18行目です。
ItemBag.cs
1/// <summary>
2/// 全てのスロットオブジェクト
3/// </summary>
4private List<ItemSlot> AllSlots { get; } = new();
5
6/// <summary>
7/// 現在所持しているアイテムの情報
8/// </summary>
9private ItemBagData Data { get; set; } = new();
10
11void Awake()
12{
13
14    for (int i = 0; i < slotNumber; i++)
15    {
16        //slotNumber の数だけスロットを生成し、ItemBagの子オブジェクトとして配置する
17        var slot = Instantiate(slotPrefab, this.transform, false);
18        AllSlots.Add(slot.GetComponent<ItemSlot>());
19    }
20
21}
4行目ではリストの要素の型を変更しており、18行目ではSlotオブジェクトのItemSlotコンポーネントを追加するように修正しています。以上で「どのアイテムをいくつ持っているかのデータ」と「バッグ内に存在する全てのスロットコンポーネント」が手に入りました。次にすることはこの2つを結びつけることです。具体的には、データの変更が行われた際にスロットの表示を変更します。ItemBagスクリプトに次のコードを挿入してください。
ItemBag.cs
1/// <summary>
2/// スロットの表示と所持アイテムの情報を一致させる
3/// </summary>
4private void UpdateItem()
5{
6    // プロジェクトに存在する全アイテム
7    ItemBase[] allItems = Resources.LoadAll<ItemBase>("");
8
9    for (int i = 0; i < Data.Ids.Count; i++)
10    {
11        // 追加したいアイテムのid
12        int itemId = Data.Ids[i];
13
14        // 全アイテムからitemIdをもつアイテムを検索する
15        // ※ 後に修正
16        ItemBase addingItem = Array.Find(allItems, item=>item.UniqueId == itemId);
17
18        // アイテムを表示
19        AllSlots[i].UpdateItem(addingItem, Data.Qty[i]);
20    }
21
22    for (int i = Data.Ids.Count; i < slotNumber; i++)
23    {
24        // 残りは空
25        AllSlots[i].UpdateItem(null, -1);
26    }
27}
このメソッドで行っていることは大きく二つで、①持っているアイテム分だけスロットを埋め、②残りを空にすることです。簡単に中身を見ていきましょう。
7行目でプロジェクトに存在する全てのアイテム(ItemBaseクラスのインスタンスファイル)を検索します。前回リンゴのアイテムを生成しInventory/Items/Resourcesに保存しましたが、これがItemBaseクラスのインスタンスファイルです。次にfor文があり、ここで所持アイテム数分だけループを回します。12行目で表示したいアイテムのidを確認し、16行目でこのidに一致するアイテムを検索。最後に(先ほど作った)ItemSlotUpdateItem()メソッドを使用します。
2つめのfor文では、余ったスロットすべてを空にします。

UpdateItem()メソッドは、データの変更の度、忘れずに呼ぶ必要があります(Rxを用いた設計ではこのような注意を払う必要はなくなるが、ここでは説明しない)。以下のようにコードの変更を行ってください。
ItemBag.cs
1void Awake()
2{
3    for (int i = 0; i < slotNumber; i++)
4    {
5        //slotNumber の数だけスロットを生成し、ItemBagの子オブジェクトとして配置する
6        var slot = Instantiate(slotPrefab, this.transform, false);
7        AllSlots.Add(slot.GetComponent<ItemSlot>());
8    }
9
10    UpdateItem();
11}
ItemBag.cs
1
2/// <summary>
3/// アイテムをバッグに追加する
4/// </summary>
5/// <param name="itemBase">追加したいアイテムのID</param>
6/// <param name="number">追加したい個数</param>
7/// <returns>バッグへの追加に成功したか</returns>
8public bool AddItem(int itemId, int number)
9{
10    if (!Data.Ids.Contains(itemId) && Data.Ids.Count == slotNumber)
11    {
12        // スロットが埋まっている状態では、未所持アイテムの追加は出来ない
13        return false;
14    }
15
16    // アイテムをバッグに追加する
17    Data.Add(itemId, number);
18
19    UpdateItem();
20    return true;
21}
22
では動作確認をします。前回InventoryTestクラスに動作確認用のコードを書きましたが、その中のStart()メソッドを以下のように修正してください
InventoryTest.cs
1void Start()
2{
3    Debug.Log(1 + "番目のアイテム:" + bag.AddItem(1, 1));
4
5    //カバンに入っているアイテムのデータを表示
6    Debug.Log(bag.ToJson());
7}
全てのスクリプトが保存されていることを確認し、Unityに戻ります。ItemSlotプレハブを開き、インスペクター上からItemSlot.Iconの登録を行ってください。

ゲームを実行すると、1つめのスロットにアイテムが表示されるはずです。

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