インベントリ / アイテムの種類を増やす

今回はアイテムの種類に応じてメニューを変化させます。例えば大切なアイテムは捨てられないようにしたり、武器の場合は装備の項目を追加する必要があります。このようにアイテムの種類に合ったアイテム詳細が表示されるように改良します。

作業に移る前にスクリプトファイルの整理をします。「Inventory/Scripts/」フォルダ直下に「Item」フォルダを作成し、ItemBaseスクリプトをItemフォルダに移動させてください。さらにItemフォルダの中に「StandardInterfaces」フォルダを作成してください。StandardInterfacesフォルダ内に「IUsable」「IStoreItem」「ICashable」「IDeletable」スクリプトを作成します。


これら4つのスクリプトファイル内を次のコードで上書きしてください。
IUsable.cs
1namespace FlMr_Inventory
2{
3    /// <summary>
4    /// スロットから「使用」することができるアイテムに実装するインターフェース
5    /// </summary>
6    public interface IUsable
7    {
8        /// <summary>
9        /// アイテムを使用したときに発動する効果
10        /// </summary>
11        void Use();
12
13        /// <summary>
14        /// アイテムが使用可能であるかを判定する
15        /// </summary>
16        /// <returns></returns>
17        bool Check();
18    }
19}
20
IStoreItem.cs
1namespace FlMr_Inventory
2{
3    /// <summary>
4    /// ショップで販売されるアイテムに実装するインターフェース
5    /// </summary>
6    public interface IStoreItem
7    {
8        /// <summary>
9        /// アイテムの値段
10        /// </summary>
11        int Price { get; }
12
13        /// <summary>
14        /// 現在のショップで販売してよいかを判定する
15        /// </summary>
16        bool CanBuy { get; }
17    }
18}
ICashable.cs
1namespace FlMr_Inventory
2{
3    /// <summary>
4    /// 売却可能なアイテムに実装するインターフェース
5    /// </summary>
6    public interface ICashable
7    {
8        /// <summary>
9        /// 売値
10        /// </summary>
11        int SellingPrice { get; }
12    }
13}
IDeletable.cs
1namespace FlMr_Inventory
2{
3    /// <summary>
4    /// スロットから削除可能なアイテムに実装するインターフェース
5    /// </summary>
6    public interface IDeletable
7    {
8
9    }
10}
スロット上から行える典型的な操作として「使用」「購入」「売却」「捨てる」を取り上げてみました。ここで注意していただきたいのは、インベントリ毎に表示するメニューは選択できることです。つまり、プレイヤーのバッグでは販売や売却は出来ないが、ショップでは可能といった設定は簡単に行えます。後で詳しく説明します。
まずIUsableインターフェースについて。このインターフェースを実装したアイテムは「使用可能なアイテム」となります。使用可能ということは、当然使用した際に発動する効果が定義されている必要があります。これを強制するためインターフェース内にUse()メソッドの定義が書かれています。また、アイテムの効果はいつ何時発動可能というわけではありません。例えば回復薬はプレイヤーのHPが満タンの時には使用不可ですし、室内で自転車が使用でいると不自然になります。そのため「現在アイテムが使用できるか」を判定するメソッドの定義も強制しています。

次にIStoreItemインターフェースについて。このインターフェースを実装したアイテムは、ショップでの販売対象となります。販売されるということは価格が定められている必要があります。そのためPriceプロパティの定義を強制しています。またRPGにおいてはプレイヤーランクや物語の進捗に応じて購入できるアイテムが増える(ラインナップが増える)ことがあります。そのため「現時点で販売可能かを判定する」プロパティの定義も要求しました。ここで注意点があります。始まりの村の店では低コストなアイテムが多い一方、終盤近くの店では高級アイテムが手に入る、などということが多いと思います。しかしこれを実装する際にはCanBuyプロパティを使用するのではなく、店の実装で何を売るかを定めてください。

次はICashableについてです。IStoreItemPriceと同様の理由で、SellingPriceプロパティを要請しています。一方で売却の場合は、任意のタイミングで行える場合がほとんどです。そのためCanBuyに相当するプロパティは要求しませんでした。

最後にIDeletableです。文字通り、対象を削除可能アイテムにします。しかし削除するために特別必要な情報はないため、インターフェースの中身は空(要求する情報はない)となります。

では、これらのインターフェースを実装したアイテムを定義してみます。ここから作るアイテムは動作確認のためだけに使用します。そのためInventory/Demo/Scripts/の中に「HealingItem」「Important」「Weapon」スクリプトを作成してください。まずはHealingItemスクリプトを開き、以下のコードで上書きしてください(コンパイルエラーが発生します)。
HealingItem.cs
1using UnityEngine;
2
3namespace FlMr_Inventory.Demo
4{
5    [CreateAssetMenu(fileName = "HealingItem", menuName = "Item/HealingItem")]
6    public class HealingItem : ItemBase,IUsable,IStoreItem,ICashable,IDeletable
7    {
8
9    }
10}
6行目のコロン( : )の後ろに注目します。まずItemBaseを継承しています。そのためItemBaseクラスの機能をそっくりそのまま受け継がれています。ItemBaseScriptableObjectクラスを継承しているため、HealingItemクラスのインスタンスも事前に生成することができます(その際に5行目の記述が必要となる)。
この後ろにインターフェースの実装が並んでいます。これらによりHealingItemは「使用可能」「購入可能」「売却可能」そして「削除可能」なクラスとなります。しかし、このままではHealingItemクラスのアイテムを使用しても、どんな効果が発動されるのかが不明です。というのもIUsableインターフェースがUse()メソッドを要請しているにもかかわらず、これを実装していないためです。その結果「IUsable」の実装箇所にエラーが発生しています。そしてこれと同様に、IStoreItemとICashableにもエラーが発生しています。IDeletableに関しては何も要求していないためエラーは発生していません。

ではHealingItemクラス内部に次のコードを挿入してください。
HealingItem.cs
1#region IUsableの要請
2
3/// <summary>
4/// 回復量
5/// </summary>
6[SerializeField] private int healAmount;
7
8public bool Check()
9{
10    // プレイヤーの体力が満タンの場合は使用不可
11    // if(player.Instance.Hp == player.Instance.MaxHp) return false;
12
13    return true;
14}
15
16public void Use()
17{
18    // player.Instance.Hp += heal;
19
20    Debug.Log($"体力を{healAmount}回復した!");
21}
22
23#endregion
まず4行目に回復量を定義しています。インスペクター上から設定できるように[SerializeField]属性を付与しています。6行目からはIUsableインターフェースが要請するメソッドを定義しています。

Checkメソッドはアイテムが使用可能か否かを判定するメソッドです。本来はプレイヤーのHpが満タンか否かで判定するのですが、このプロジェクトにプレイヤーがいないためとりあえずtrueを返しています。本来はコメントアウトしているような(9行目)記述が入ります。

14行目から始まるUse()メソッドは、アイテムを使用した際に発動する効果です。このプロジェクトではログを出すくらいしかできませんが、本来はプレイヤーのHpを回復する記述が入ります。

以上でIUsableインターフェースの要請に応えることができたため、エラーが1つ解決しているはずです。

続けて次のコードを追加してください。
HealingItem.cs
1#region IStoreItemの要請
2/// <summary>
3/// 価格
4/// </summary>
5[SerializeField] private int price;
6
7public int Price => price;
8
9public bool CanBuy
10{
11    get
12    {
13        // (例)プレイヤーランクが10以上の場合に購入可能
14        // return player.Instance.Rank >= 10;
15
16        return true;
17    }
18}
19#endregion
20
ここではショップでの価格と、購入制限に関して記述しています。値段はインスペクター上から設定できるようにし、購入制限は設けていません。
これでIStopItemのエラーが解決しました。

最後にICashableの要求に応えます。
HealingItem.cs
1#region ICashableの要請
2/// <summary>
3/// 買取価格
4/// </summary>
5[SerializeField] private int sellingPrice;
6
7public int SellingPrice => sellingPrice;
8#endregion
このコードに関してはそのまますぎて、特に説明することがありません。
以上でエラーがなくなったかと思います。ここでUnityに戻り、HealingItemクラスのインスタンスを作成してみましょう。Inventory/Demo/Items/フォルダを作成してください。このフォルダ内で「Create/Item/HealingItem」を選択し、インスタンスを作成してください。


名前を「Apple」とし、インスペクター上から適当に設定します。




ここで、今まで使用してきたアイテムを削除します。
前回までで使用してきたものは、動作確認のためだけの一時的なアイテムだったため、これからは不要となります。Inventory直下のItemsTempフォルダを削除してください。


この時ItemUtilityクラスのインスタンスも消えてしまったことに注意が必要です。そのためInventory/Demo/Items/フォルダ内にResourcesフォルダを新規作成し(つづりに注意)、その中で「Create/ItemUtility」を選択してインスタンスを再度作成してください。


そしてインスペクター上でHealingItemクラスのインスタンス「Apple」を追加します。


ここで一度ゲームを実行してみてください。エラー無く、以前と全く同じ動作をすることを確認してください。



ではWeaponスクリプト(Inventory/Demo/Scripts/Weapon.cs)を編集します。以下のコードで上書きしてください(コンパイルエラーが発生します)。
Weapon.cs
1using UnityEngine;
2
3namespace FlMr_Inventory.Demo
4{
5    [CreateAssetMenu(menuName = "Item/Weapon", fileName = "Weapon")]
6    public class Weapon : ItemBase, IUsable
7    {
8
9    }
10}
HealingItemクラスの時と同様、重要な箇所は4行目のコロン( : )の後です。まずはItemBaseクラスの継承、それに続いてIUsableの実装がされています。IStoreItem,ICashable,IDeletableが実装されていないため、店での売り買いや、捨てることが出来ません。つまり、戦闘でのドロップ等で入手することを想定していることになります。

例によって、IUsableを実装している箇所でコンパイルエラーが発生しています。これは先ほどと同様、要求されたメソッドが定義されていないためです。そこでクラス内に以下のコードを追加してください。
Weapon.cs
1/// <summary>
2/// 攻撃力
3/// </summary>
4[SerializeField] private int atk;
5
6/// <summary>
7/// 防御力
8/// </summary>
9[SerializeField] private int df;
10
11#region IUsableの要請
12
13public bool Check()
14{
15    // プレイヤーが装備できる武器の数に制限がある場合
16    // if(player.Instance.Weapons.Count >= 2) return false;
17
18    return true;
19}
20
21public void Use()
22{
23    // player.Instance.Weapons.Add(this);
24
25    Debug.Log($"{ItemName}を装備しました!");
26}
27
28#endregion
コードの内容は非常に単純です。1~9行目は武器のステータスを設定しています。11~28行目でIUsableインターフェースの要請に応えています。Check()メソッドは前述のとおり、現在アイテムを使用することができるかの判定です。例えば「プレイヤーは3つ以上武器を装備できない」というルールがある場合、16行目のような記述を加えることになります。
Use()メソッドではアイテムを使用した際の効果を記述しています。実際にプレイヤークラスが存在する場合、23行目のコードが挿入されます。

ここでは武器を例にコードを書きましたが、非常に大切なアイテム(ポケモンでは「学習装置」など)もIUsableのみを実装することになります。これにより買えない、かつ手放せないアイテムを作ることができます。
では武器のインスタンスを作成します。Unityに戻り、Inventory/Demo/Items/フォルダを開いてください。右クリックから「Create/Item/Weapon」を選択し、インスタンスを作成します。作成したファイルにArrowと名付け、インスペクター上で値を適当に設定します。この時UniqueIdが他のアイテムを被らないようにし注意してください。


最後にこのアイテムをItemUtilityに追加します。



最後に、何もできないアイテムです。これは物語の進行上必要なアイテムを想定しています。例えばNPCからお使いを頼まれた場合、運んでいる荷物などを手放すことはできません。さらに使用することもでいないため、インターフェースは一切実装しません。これらを踏まえ、Importantスクリプトには次のように記述してください。
Important.cs
1namespace FlMr_Inventory.Demo {
2
3    [CreateAssetMenu(menuName ="Item/Important",fileName ="Important")]
4    public class Important : ItemBase
5    {
6
7    }
8}
特に何もアクションを起こさず、スロットに鎮座しているだけなので、ItemBaseクラスの内容に追加することはありません。

ではこのクラスのインスタンスを作成します。Unityに戻って「Inventory/Demo/Items/」を開いてください。右クリックから「Create/Item/Important」を選択してインスタンスを生成します。ファイル名を「Key」に変更し、インスペクター上で適当に設定します。


最後にこのアイテムをItemUtilityに追加します。


他にも換金アイテムはICashable(必要であればIDeletable)を実装すると実現できます。もしその他のふるまいをするアイテム(設置アイテム、常時効果を発揮するアイテム、etc)を作りたい場合は、追加でインターフェースを作成することで実現できると思います。
以上のように、新しくアイテムのスクリプトを追加する際はItemBaseクラスを継承し、お好みでいくつかのインターフェースを実装します。そしてそのクラスのインスタンスをUnity上で生成します。ItemBaseクラスそのもののインスタンスを生成することはありません。そのためItemBaseクラスを抽象クラスにしましょう。
ItemBase.cs
1namespace FlMr_Inventory
2{
3    public abstract class ItemBase : ScriptableObject
4    {
5        ...
6    }
7}
これに伴い[CreateAssetMenu(menuName = "Item/ItemTemp", fileName = "itemTemp")]の記述も削除しています。抽象クラスのインスタンスを作成することは出来ないためです。

では動作確認のためInventoryTestクラスのStart()メソッドを次のように上書きしてください。
InventoryTest.cs
1void Start()
2{
3    bag.AddItem(1, 2);
4    bag.AddItem(2, 2);
5    bag.AddItem(3, 2);
6}

変更を保存し、Unityに戻り実行してください。3つそれぞれのアイテムをクリックしたとき、ログとして正しい情報が出力されるでしょうか。
(アイテムにインターフェースを実装したことの影響はまだ現れないことに注意してください)




以上でアイテム作成に関しては終了です。



では次回、インターフェースによる分類をもとに表示するメニューを変更します。

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