Unity本格入門:いきのこバトルで学ぶ所持品データとオーディオ
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)
対象読者:第5回(UI)を読み、所持アイテムの保存と効果音の鳴らし方をコードレベルで追いたい方
OwnedItemsData.csとAudioManager.csを中心に、JSON セーブ・シングルトン・Resources読み込みを整理します。
前提:第1回:タイトル・セーブデータ の SaveData(SavableSingletonBase)とは別実装ですが、JsonUtility と PlayerPrefs の考え方は共通です。第5回:UI とメニュー の ItemsDialog・ItemButton と合わせて読むと流れがつながります。
記事の目次
- この記事で扱う範囲
- コードを全部見てみよう
- クラス関係(この記事で登場する範囲)
- UMLで整理する(シーケンス)
- ポイント解説(①〜⑧)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ①
OwnedItemsDataのInstanceと private コンストラクタ - ② JSON と固定キーで
PlayerPrefsに保存 - ③
Add/Use/GetItemの流れ - ④
OwnedItemと[Serializable] - ⑤
AudioManagerとDontDestroyOnLoad - ⑥ 二重インスタンスの防止
- ⑦
Resources.LoadAllで SE を辞書化 - ⑧
PlayとItemButtonからの呼び出し
この記事で扱う範囲
- 全文掲載:
OwnedItemsData.cs、AudioManager.cs - 参照:
ItemButtonのきのこ使用時(AudioManager.Instance.Play("eat")など)

説明(学習のヒント):JsonUtility と PlayerPrefs で所持データを永続化し、Resources から読み込んだ SE を AudioManager から鳴らすイメージです。DontDestroyOnLoad による常駐も記事で整理します。
クラス関係(この記事で登場する範囲)
説明(学習のヒント):*-- は「所持品リストが OwnedItemsData の一部」という合成。AudioManager はシーン上の MonoBehaviour として存在します。
UMLで整理する(シーケンス)
きのこ使用時に、所持の更新・SE 再生・セーブがどう繋がるかを表します。
説明(学習のヒント):ボタン1回で「データ更新 → SE → 保存」の3つが続くので、順番を頭に入れるとデバッグしやすいです。
コードを全部見てみよう
OwnedItemsData.cs(全文)
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class OwnedItemsData
{
private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";
public static OwnedItemsData Instance
{
get
{
if (null == _instance)
{
_instance = PlayerPrefs.HasKey(PlayerPrefsKey)
? JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey))
: new OwnedItemsData();
}
return _instance;
}
}
private static OwnedItemsData _instance;
public OwnedItem[] OwnedItems => ownedItems.ToArray();
[SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
private OwnedItemsData()
{
}
public void Save()
{
var jsonString = JsonUtility.ToJson(this);
PlayerPrefs.SetString(PlayerPrefsKey, jsonString);
PlayerPrefs.Save();
}
public void Add(Item.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(Item.ItemType type, int number = 1)
{
var item = GetItem(type);
if (null == item || item.Number < number)
{
throw new Exception("アイテムが足りません");
}
item.Use(number);
}
public OwnedItem GetItem(Item.ItemType type)
{
return ownedItems.FirstOrDefault(x => x.Type == type);
}
[Serializable]
public class OwnedItem
{
public Item.ItemType Type => type;
public int Number => number;
[SerializeField] private Item.ItemType type;
[SerializeField] private int number;
public OwnedItem(Item.ItemType type)
{
this.type = type;
}
public void Add(int addNumber = 1)
{
number += addNumber;
}
public void Use(int useNumber = 1)
{
number -= useNumber;
}
}
}AudioManager.cs(全文)
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
public class AudioManager : MonoBehaviour
{
[SerializeField] private AudioSource audioSource;
private readonly Dictionary<string, AudioClip> _clips = new Dictionary<string, AudioClip>();
public static AudioManager Instance { get; private set; }
private void Awake()
{
if (null != Instance)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
Instance = this;
var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
_clips.Add(clip.name, clip);
}
}
public void Play(string clipName)
{
if (!_clips.ContainsKey(clipName))
{
throw new Exception("Sound " + clipName + " is not defined");
}
audioSource.clip = _clips[clipName];
audioSource.Play();
}
}ポイント①:OwnedItemsData の Instance と private コンストラクタ
new を外部から抑制するためコンストラクタを private にし、初回アクセス時に PlayerPrefs から復元するか 空のリストで生成します。第1回の SaveData と同様、ゲーム全体でひとつの所持リストとして扱えます。
ポイント②:JSON と固定キーで PlayerPrefs に保存
SaveData はクラス名ハッシュでキーを分けましたが、こちらは OWNED_ITEMS_DATA 固定文字列です。設計の好みの違いとして覚えておけば十分です。
ポイント③:Add / Use / GetItem の流れ
GetItemで種類を検索、FirstOrDefaultで無ければnullAddは無ければOwnedItemをListに追加してから個数加算Useは不足時に例外(ゲームロジック側で呼び出しを制御)
ポイント④:OwnedItem と [Serializable]
JsonUtility は フィールドをシリアライズするため、[SerializeField] 付きの private フィールドと public プロパティの組み合わせで、保存とカプセル化の両立を図っています。
ポイント⑤:AudioManager と DontDestroyOnLoad
DontDestroyOnLoad(gameObject);タイトルやメインをまたいでも AudioManager の GameObject を破棄しないため、BGM/SE を連続して鳴らしやすくします。プレハブをシーンに1つ置く想定です。
ポイント⑥:二重インスタンスの防止
if (null != Instance)
{
Destroy(gameObject);
return;
}誤ってシーンに2つ置いた場合、後から来た方を破棄し、最初の Instance を維持します。SingletonMonoBehaviourInSceneBase は例外を投げる方式でしたが、DontDestroyOnLoad 系ではこのパターンもよく使われます。
ポイント⑦:Resources.LoadAll で SE を辞書化
var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
_clips.Add(clip.name, clip);
}Assets/Resources/2D_SE/ 以下のクリップを起動時に一括ロードし、**ファイル名(clip.name)**で Dictionary から引きます。大規模プロジェクトでは Addressables 等への移行も検討されますが、少数の SE をまとめる用途ではこの形でも扱いやすいです。
ポイント⑧:Play と ItemButton からの呼び出し
ItemButton できのこを食べると AudioManager.Instance.Play("eat") が走り、MainSceneController.Instance.EatItem() で満腹を回復し、OwnedItemsData.Instance.Save() で保存、という一連の連携になります(第3回・第5回と接続)。
コードの流れを整理しよう
説明(学習のヒント):下の3本は「きのこを使うとき」の流れ、上は「AudioManager が起動するとき」の流れです。Resources 読み込みとボタン操作は別のタイミングです。
自分でカスタマイズしてみよう!
挑戦①:SE ファイル名と Play の引数
Resources/2D_SE のファイル名を変えると、clip.name が変わるので、Play("eat") などの文字列と一致させる必要があります。
挑戦②:PlayerPrefs の削除でリセット
開発中は PlayerPrefs.DeleteKey("OWNED_ITEMS_DATA") で所持を初期化できます(エディターの PlayerPrefs 削除機能でも可)。
まとめ
OwnedItemsDataは private コンストラクタ + Instance で所持リストを一元管理し、JSON + PlayerPrefs で永続化するAudioManagerはDontDestroyOnLoadでシーン間に残し、Resources で SE を辞書に載せてPlay(string)する- UI(第5回)・プレイヤー(第3回)から
Instance経由で参照される、という横断的なサービスとして位置づけられる
いきのこバトルについては、第1〜6回で主要な読み筋をカバーしました。別の題材の記事を増やす場合は 目次 の「今後の題材」を参照してください。
最終更新:2026年4月