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

アイテムの削除
早速作業をはじめます。単純にアイテムを捨てるためだけでなく、アイテムを使用した際に数を減らすためにも必要になるため、重要な機能です。
ItemBagクラスに次のメソッドを追加してください(
コンパイルエラーが発生します)。
ItemBag.cs
/// <summary>
/// アイテムを削除する
/// </summary>
/// <param name="itemId">削除するアイテムのId</param>
/// <param name="number">削除する個数</param>
/// <returns></returns>
public bool RemoveItem(int itemId, int number)
{
// 十分な数を所持しているか
bool haveEnough = Data.GetQty(itemId) >= number;
if (haveEnough)
{
// 十分持っている場合
Data.Remove(itemId, number);
UpdateItem();
return true;
}
else
{
//不足している場合
return false;
}
}
ここで10行目の
GetQty(itemId)にコンパイルエラーが生じます。これはItemBagDataクラスにGetQty()が定義されていないためですが、ここでは「itemIdのアイテムをいくつ所持しているか」を返すメソッドだと考えてください。後で作成します。よって10行目の変数は、アイテムの書次数が削除したい数以上か否かを表します。
仮に十分な数を持っていた場合、15行目で
Remove()メソッドを呼ぶことで情報を更新し、
UpdateItem()メソッドで表示も変更します。
一方数が足りない場合は、情報を一切変化させることなく
falseを返します。
次に
GetQty()を
ItemBagDataクラス(
ItemBagクラスのインナークラス)に定義します。次のコードを挿入してください。
ItemBag.cs (ItemBagDataインナークラス)
/// <summary>
/// 特定のアイテムをいくつ所持しているか
/// </summary>
/// <param name="id">アイテムid</param>
/// <returns>所持数</returns>
public int GetQty(int id)
{
int index = Ids.IndexOf(id);
return index < 0 ? 0 : Qty[index];
}
実装は非常にシンプルです。
8行目で、
Ids(所持しているアイテムのidを保持するList)から
idを検索します。存在している場合(所持している場合)は
idが格納されている配列番号、存在しない場合(未所持の場合)は-1となります。仮に
indexが負の場合、所持していないため0が返され、正の場合は
Qty[index]が返されます。
動作確認をするため
InventoryTestクラスの
Start()メソッドを次のように変更して下さい。
InventoryTest.cs
void Start()
{
// id1のアイテムを1つ追加
bag.AddItem(1, 1);
Debug.Log(bag.ToJson());
// id1のアイテムを2個削除
bag.RemoveItem(1, 2);
Debug.Log(bag.ToJson());
// id1のアイテムを1個削除
bag.RemoveItem(1, 1);
Debug.Log(bag.ToJson());
}
まず id=1 のアイテムを1つ追加し、次に id=1 のアイテムを2個削除しようとします。所持数を上回る数を削除しているため、何も起こらないはずです。最後に id=1 のアイテムを1つ削除します。この時正常に削除が行われるため、最終的にBagの中身は空になると正解です。
全てのスクリプトが保存されていることを確認し、Unityに戻り実行してください。下の写真のような実行結果になったでしょうか。

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

作成後、このファイル内を次のコードで上書きしてください。
ItemUtility.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using UnityEngine;
namespace FlMr_Inventory
{
[CreateAssetMenu(menuName = "ItemUtility", fileName = "ItemUtility")]
public class ItemUtility : ScriptableObject
{
#region Singleton
private static ItemUtility instance;
public static ItemUtility Instance
{
get
{
if (instance == null)
{
var instances = Resources.LoadAll<ItemUtility>("");
// シングルトンなクラスのインスタンスは必ず1つでなければならない
instance = instances.Count() switch
{
0 => throw new System.Exception("ItemUtilityのインスタンスがResourcesフォルダ内に存在しません"),
1 => instances.ElementAt(0),
_ => throw new System.Exception("ItemUtilityのインスタンスがResourcesフォルダ内に複数存在します")
};
// 見つけた唯一のインスタンスを初期化
instance.Initialize();
}
return instance;
}
}
#endregion
/// <summary>
/// ゲームに登場させたい全アイテム
/// </summary>
[SerializeField] private ItemBase[] allItems;
/// <summary>
/// Idにアイテムを結びつける辞書
/// </summary>
public ReadOnlyDictionary<int, ItemBase> ItemIdTable { get; private set; }
/// <summary>
/// 全てのアイテムを保持した読み取り専用コレクション
/// </summary>
public ReadOnlyCollection<ItemBase> AllItems { get; private set; }
private void Initialize()
{
// ItemIdTableの初期化
Dictionary<int, ItemBase> idItemMap = new Dictionary<int, ItemBase>();
foreach (var item in allItems)
{
idItemMap.Add(item.UniqueId, item);
}
ItemIdTable = new (idItemMap);
// AllItemsの初期化
AllItems = new (allItems);
}
}
}
少々長いコードですが、やっていることは非常に単純です。
まず、このクラスはシングルトンな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行目)をもとに
ItemIdTableと
AllItemsを初期化します。
このUtilityクラスの存在により、
ItemBagクラスの
UpdateItemメソッドを改善することが出来ます。
ItemBag.cs
/// <summary>
/// スロットの表示と所持アイテムの情報を一致させる
/// </summary>
private void UpdateItem()
{
for (int i = 0; i < Data.Ids.Count; i++)
{
// 追加したいアイテムのid
int itemId = Data.Ids[i];
// 全アイテムからitemIdをもつアイテムを検索する
ItemBase addingItem = ItemUtility.Instance.ItemIdTable[itemId];
// アイテムを表示
AllSlots[i].UpdateItem(addingItem, Data.Qty[i]);
}
for (int i = Data.Ids.Count; i < slotNumber; i++)
{
// 残りは空
AllSlots[i].UpdateItem(null, -1);
}
}
変更は二か所です。メソッドの頭に
allItems変数を作成していましたが、不要になるので削除しました。そして13行目の
addingItemでは、
ItemIdTableを用いて、idからアイテムを見つけます。
ここで動作確認をします。全てのスクリプトが保存されていることを確認し、Unityに戻って下さい。「Inventory/ItemsTemp/Resources/」に
ItemUtilityクラスのインスタンスを作成します。

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

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

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

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