Unityの持ち物システムを4段階で作る ― クラス → List → 保存 → シングルトン
Unityでゲームを作っていて、「持ち物を管理するクラス」が1つのファイルに全部入っていて読みにくい、と感じたことはありませんか。シングルトン、PlayerPrefs での保存、内部クラス、LINQ などが一度に出てくると、どこが何の話なのかが分からなくなります。
この記事では、持ち物システムを新しい概念が1つずつ増える4段階に分けて作ります。前の記事「Unityで3Dバトルアクションを1本作って学べること」の5節で触れた「所持アイテムの保存」を、順番に作り直す内容です。
| 段階 | 新しく覚えること |
|---|---|
| 1 | クラスに、データと操作をまとめる |
| 2 | List、FirstOrDefault、足りないときの例外 |
| 3 | [Serializable]、JsonUtility、PlayerPrefs |
| 4 | static の Instance、private なコンストラクタ |
段階1: 種類と数だけを持つ小さなクラス
まずは、アイテム1種類ぶんの「種類」と「数」を持つクラスだけを作ります。static も保存も出てきません。
public enum ItemType
{
Wood,
Stone,
}
public class OwnedItem
{
public ItemType Type
{
get { return type; }
}
public int Number
{
get { return number; }
}
private ItemType type;
private int number;
public OwnedItem(ItemType type)
{
this.type = type;
}
public void Add(int number = 1)
{
this.number += number;
}
public void Use(int number = 1)
{
this.number -= number;
}
}
空の GameObject に次のスクリプトを付けて再生すると、Console に結果が出ます。
using UnityEngine;
public class InventoryDemo : MonoBehaviour
{
private void Start()
{
var wood = new OwnedItem(ItemType.Wood);
wood.Add();
Debug.Log($"{wood.Type}: {wood.Number}"); // Wood: 1
wood.Add(3); // 1個以外も試す
Debug.Log($"{wood.Type}: {wood.Number}"); // Wood: 4
wood.Use(2);
Debug.Log($"{wood.Type}: {wood.Number}"); // Wood: 2
}
}
つまずきポイント: 引数を受け取っているのに使っていない
Add(int number) の中で this.number += 1 と書いてしまうと、Add() だけ試したときは正しく動きます。ところが Add(3) と呼んでも、数は1しか増えません。引数を取るメソッドは、1個の場合以外もテストすると気付けます。上のデモの wood.Add(3) が、その確認です。
段階2: 持ち物のリスト
次に、複数種類のアイテムを持てる Inventory を作ります。OwnedItem は段階1のままです。普通のクラスなので、new で作ります。
using System;
using System.Collections.Generic;
using System.Linq;
public class Inventory
{
private List<OwnedItem> ownedItems = new List<OwnedItem>();
public OwnedItem[] OwnedItems
{
get { return ownedItems.ToArray(); }
}
public void Add(ItemType type, int number = 1)
{
var item = GetItem(type);
if (null == item)
{
item = new OwnedItem(type);
ownedItems.Add(item);
}
item.Add(number);
}
public void Use(ItemType type, int number = 1)
{
var item = GetItem(type);
if (null == item || item.Number < number)
{
throw new Exception("アイテムが足りません");
}
item.Use(number);
}
public OwnedItem GetItem(ItemType type)
{
// 条件に合う最初の1つ。なければ null
return ownedItems.FirstOrDefault(x => x.Type == type);
}
}
GetItem の FirstOrDefault は、リストから条件に合う最初の1つを探す LINQ のメソッドです。見つからないときは null が返るので、Add では「なければ作る」、Use では「なければ例外」と使い分けています。
var inventory = new Inventory();
inventory.Add(ItemType.Wood, 3);
inventory.Add(ItemType.Stone);
inventory.Add(ItemType.Wood, 2); // 同じ種類は数が増える
Debug.Log($"Wood: {inventory.GetItem(ItemType.Wood).Number}"); // 5
inventory.Use(ItemType.Wood, 4);
Debug.Log($"Wood: {inventory.GetItem(ItemType.Wood).Number}"); // 1
// 別のインベントリは別の持ち物になる
var other = new Inventory();
Debug.Log(null == other.GetItem(ItemType.Wood)); // True
最後の3行がポイントです。new で作ったインベントリは、それぞれ別の持ち物になります。この性質を、段階4で static と見比べます。
段階3: 保存と読み出し
ここで初めて、保存を足します。JsonUtility で文字列にして、PlayerPrefs に入れます。まず、保存したいクラスに [Serializable] を付け、保存したいフィールドに [SerializeField] を付けます。
using System;
using UnityEngine;
[Serializable]
public class OwnedItem
{
// Type と Number は段階1と同じ
[SerializeField] private ItemType type;
[SerializeField] private int number;
// 以下、コンストラクタ、Add、Use も段階1と同じ
}
Inventory には、Save と Load を足します。
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[Serializable]
public class Inventory
{
private const string PlayerPrefsKey = "INVENTORY_STAGE3";
[SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
public void Save()
{
var jsonString = JsonUtility.ToJson(this);
PlayerPrefs.SetString(PlayerPrefsKey, jsonString);
PlayerPrefs.Save();
}
public static Inventory Load()
{
if (!PlayerPrefs.HasKey(PlayerPrefsKey))
{
return new Inventory();
}
return JsonUtility.FromJson<Inventory>(PlayerPrefs.GetString(PlayerPrefsKey));
}
// OwnedItems、Add、Use、GetItem は段階2と同じ
}
Load() は static ですが、まだシングルトンではありません。読んだものを、自分で持ち回ります。
private void Start()
{
var inventory = Inventory.Load();
inventory.Add(ItemType.Wood, 2);
Debug.Log($"Wood: {inventory.GetItem(ItemType.Wood).Number}");
inventory.Save();
}
再生するたびに、木が2個ずつ増えます。前回の保存を読み込むためです。最初からやり直したいときは、PlayerPrefs.DeleteAll(); を1回だけ動かします。
つまずきポイント: [SerializeField] を付け忘れて、保存されない
JsonUtility は、public なフィールドか、[SerializeField] が付いたフィールドだけを文字列にします。private のまま付け忘れると、エラーは出ずに、中身が空のまま保存されます。保存した文字列を、Debug.Log で一度見てみると原因が分かります。
段階4: シングルトンにする
最後に、どこからでも同じ持ち物に届くようにします。変えるのは、Instance と、private なコンストラクタだけです。段階3の Load() が、Instance の中で呼ばれます。
[Serializable]
public class Inventory
{
private const string PlayerPrefsKey = "INVENTORY_STAGE4";
public static Inventory Instance
{
get
{
if (null == _instance)
{
_instance = Load();
}
return _instance;
}
}
private static Inventory _instance;
[SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
// 外から new できないようにする(Instance だけが入口になる)
private Inventory()
{
}
private static Inventory Load()
{
if (!PlayerPrefs.HasKey(PlayerPrefsKey))
{
return new Inventory();
}
return JsonUtility.FromJson<Inventory>(PlayerPrefs.GetString(PlayerPrefsKey));
}
// Save、Add、Use、GetItem は段階3と同じ
}
拾う側と、表示する側が、別のスクリプトになっていても、同じ持ち物を見られます。
// 拾う側
public class InventoryDemo : MonoBehaviour
{
private void Start()
{
Inventory.Instance.Add(ItemType.Wood, 2);
Inventory.Instance.Save();
}
}
using UnityEngine;
using UnityEngine.InputSystem;
// 表示する側(UIの代わり)。スペースキーで、いまの木の数を出す
public class InventoryReader : MonoBehaviour
{
private void Update()
{
if (!Keyboard.current.spaceKey.wasPressedThisFrame) return;
var wood = Inventory.Instance.GetItem(ItemType.Wood);
Debug.Log(null == wood ? "Wood: 0" : $"Wood: {wood.Number}");
}
}
Inventory を引数で受け取らなくても、Instance からどこでも届きます。これが、シングルトンの便利なところです。
シングルトンの良いところと、難しくなるところ
- 良いところ: 参照を渡し回さなくてよいので、小さなゲームでは書きやすい。
- 難しくなるところ:
- どこからでも書き換えられるので、バグの場所を追いにくい。
- 段階2のように、新しい持ち物を作ってテストできない。
- 2人のプレイヤーが、別々の持ち物を持つ作りには、そのままでは向かない。
「小さなゲームなら十分」ですが、大きくなってきたら、段階2〜3の形(インスタンスを持ち回る)に戻す選択肢もある、と覚えておくと役立ちます。
まとめ
| 段階 | 追加したもの | 確認できること |
|---|---|---|
| 1 | OwnedItem |
Add(3) で3個増える |
| 2 | Inventory、List、FirstOrDefault |
別のインベントリは別の持ち物 |
| 3 | [Serializable]、JsonUtility、PlayerPrefs |
再生し直しても数が残る |
| 4 | static の Instance |
別のスクリプトからも同じ持ち物に届く |
1つのクラスに全部入ったコードに出会ったら、下から1つずつ、段階に分けて読むと理解しやすくなります。次のステップとして、持ち物にアイテムごとの上限を付ける、種類ごとの説明や画像を足す、保存をクラウドに載せる、といった拡張もおすすめです。










ディスカッション
コメント一覧
まだ、コメントがありません