ゲーム開発
Unity
UnrealEngine
C++
Blender
Houdini
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • Houdini
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20220730_01

      【Unity】ScriptableObjectの活用方法についてまとめる

      UnityScriptableObject
      2022-07-30

      マイケル
      マイケル
      みなさんこんにちは!
      マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜
      マイケル
      マイケル
      今日は、Unity開発で使用すると便利な
      ScriptableObjectについて使い方を紹介しようと思うよ!
      エレキベア
      エレキベア
      名前は聞いたことあるクマが何なのクマ?
      マイケル
      マイケル
      ScriptableObjectは基本的には共有データの管理をするのに使用されるよ!
      特徴としては下記のようなものがあります。
      • スクリプトインスタンスから独立している
      • 共有データを保存できメモリの節約になる
      • Inspecter上でのデータ編集が可能
      • エディターセッション中にデータの保存が可能

      ScriptableObject – Unityマニュアル

      ScriptableObject を使用してゲームを構築する 3 つの方法

      エレキベア
      エレキベア
      なるほどクマ
      データ管理に適したオブジェクトなのクマね
      マイケル
      マイケル
      文字だけだと分かりにくいと思うから、
      実際にコードを書いて触ってみよう!

      ScriptableObjectの活用方法

      マイケル
      マイケル
      今回は
      ・データの管理
      ・シーン間のデータ共有
      ・イベント管理
      の3つの使用方法について解説します。
      なおUnityのバージョンは下記を使用していて、
      サンプルはGitHubにもあげていますので、こちらもご自由にご参照ください!


      [使用したUnityバージョン]
      2021.3.1f1

      [GitHub]
      GitHub – masarito617/unity-scriptable-object-sample

      エレキベア
      エレキベア
      楽しみクマ〜〜〜

      データの管理

      マイケル
      マイケル
      まずは最も基本的なデータ管理の方法について見ていきます!
      データを表示する
      マイケル
      マイケル
      下記のようなシーンを用意し、ゲーム実行時に
      ScriptableObjectからデータを設定する処理を作ってみましょう。
      ↑データ内容を表示する
      マイケル
      マイケル
      ScriptableObjectは下記のように継承して定義することができます。
      using UnityEngine;
      
      [CreateAssetMenu(fileName = "MonsterData", menuName = "SampleGame/MonsterData")]
      public class MonsterData : ScriptableObject
      {
          /// <summary>
          /// 名前
          /// </summary>
          public string monsterName;
      
          /// <summary>
          /// HP
          /// </summary>
          public int hp;
      
          /// <summary>
          /// 死亡しているか?
          /// </summary>
          public bool isDead;
      }
      
      ↑ScriptableObjectを継承して作成
      エレキベア
      エレキベア
      ScriptableObjectを継承して作成するのクマね
      マイケル
      マイケル
      CreateAssetMenuは、Assetsフォルダ内で右クリックした際に開く
      メニューに表示する名称になります。
      ↑アセットとして作成できるようになっている
      マイケル
      マイケル
      アセットを作成して、今回は下記のように設定しました。
      ↑データの設定
      エレキベア
      エレキベア
      名前を使われたクマ・・・
      マイケル
      マイケル
      あとは下記のように管理クラスにアタッチして
      設定処理を記述すれば完了です!
      using UnityEngine;
      using UnityEngine.UI;
      
      public class ShowOnlySceneManager : MonoBehaviour
      {
          /// <summary>
          /// UI
          /// </summary>
          [SerializeField] private Text nameText;
          [SerializeField] private Text hpText;
          [SerializeField] private Text isDeadText;
      
          /// <summary>
          /// ScriptableObject
          /// </summary>
          [SerializeField] private MonsterData monsterData;
      
          private void Awake()
          {
              // ScriptableObjectからデータを設定
              nameText.text = monsterData.monsterName;
              hpText.text = monsterData.hp.ToString();
              isDeadText.text = monsterData.isDead.ToString();
          }
      }
      
      ↑UIへのデータ設定処理
      ↑実行するとデータが表示される
      エレキベア
      エレキベア
      簡単に表示できたクマ〜〜
      マイケル
      マイケル
      MonoBehaviourやJSONファイル等で定義するのと比べてどうなの?
      という方は、下記記事がまとまっていて分かりやすいのでご参照ください!

      ・他のデータ形式との比較
      【Unity】ScriptableObjectにマスタデータを持たせるメリットについて

      マイケル
      マイケル
      基本的にはサーバ通信はJSON、それ以外はScriptableObject
      を使用するのがよさそうですね!
      マイケル
      マイケル
      あとはInspecter上で編集できてゲーム実行中でも編集できるので、
      エンジニア以外の方と共同で作業する場合やパラメータを調整する際には重宝しそうですね。
      エレキベア
      エレキベア
      簡単に使えるし便利クマね
      複数レコードを管理する
      マイケル
      マイケル
      それでは下記のように複数データを表示する場合はどのようにすればよいでしょうか?
      ↑複数のデータを表示する場合
      マイケル
      マイケル
      先ほど作成したデータをInstantiateして複製して設定することも可能ではありますが、
      ScriptableObjectのメリットを活かすには下記のようにデータをリストとして定義した方がよさそうです。
      using System;
      using System.Collections.Generic;
      using UnityEngine;
      
      [CreateAssetMenu(fileName = "MonstersData", menuName = "SampleGame/MonstersData")]
      public class MonstersData : ScriptableObject
      {
          public List<MonsterData> dataList;
      
          [Serializable]
          public class MonsterData
          {
              /// <summary>
              /// 名前
              /// </summary>
              public string monsterName;
      
              /// <summary>
              /// HP
              /// </summary>
              public int hp;
      
              /// <summary>
              /// 死亡しているか?
              /// </summary>
              public bool isDead;
          }
      }
      
      ↑複数データを定義できるようにする
      マイケル
      マイケル
      Inspecter上では下記のように設定することができます。
      ↑データの定義
      エレキベア
      エレキベア
      マスタデータのように使えるクマね
      ↑問題なく表示できることを確認
      マイケル
      マイケル
      データの設定を効率化したい場合には、
      後ほど紹介するExcelファイルから設定する拡張機能について参照ください!
      エレキベア
      エレキベア
      データ入力をExcelに出来たら効率化出来そうクマね

      シーン間のデータ共有

      マイケル
      マイケル
      次はシーン間でデータ共有をする方法についてです。
      こちらは「スクリプトインスタンスから独立している」といった特徴を活かした方法になっています。
      マイケル
      マイケル
      今回は下記のようにデータを入力するシーンと、
      それを表示するシーンの2つを用意しました。
      これらのシーン間でデータを共有できるようにしてみましょう!
      ↑データ入力シーン
      ↑データ表示シーン
      エレキベア
      エレキベア
      よくある構造クマね
      マイケル
      マイケル
      ゲーム実行中に入力データをそのまま設定してしまうと実行終了後にも上書きした値が残ってしまうため、
      ・初期化用の変数
      ・ランタイムで使用する変数
      の2つを用意しておくことで解決することができます。
      using System;
      using UnityEngine;
      
      [CreateAssetMenu(fileName = "MonsterSceneData", menuName = "SampleGame/MonsterSceneData")]
      public class MonsterSceneData : ScriptableObject, ISerializationCallbackReceiver
      {
          /// <summary>
          /// 名前
          /// </summary>
          [SerializeField] private string initMonsterName;
          [NonSerialized] public string MonsterName;
      
          /// <summary>
          /// HP
          /// </summary>
          [SerializeField] private int initHp;
          [NonSerialized] public int Hp;
      
          /// <summary>
          /// 死亡しているか?
          /// </summary>
          [SerializeField] private bool initIsDead;
          [NonSerialized] public bool IsDead;
      
          public void OnBeforeSerialize() { }
          public void OnAfterDeserialize()
          {
              // ランタイムでの書き込み用に値をコピーする
              MonsterName = initMonsterName;
              Hp = initHp;
              IsDead = initIsDead;
          }
      }
      
      ↑初期化用の変数とランタイムで使用する変数を分ける
      マイケル
      マイケル
      ISerializationCallbackReceiverを継承してOnAfterDeserializeのタイミングで設定 することで、Awake処理の前に設定することができます。

      ※呼び出しタイミングに関しては下記記事が分かりやすいです。
      【Unity】ISerializationCallbackReceiverを使ってみたんだ

      エレキベア
      エレキベア
      Inspecter上でいじるのはあくまで初期化用の値、
      ということにするわけクマね
      マイケル
      マイケル
      あとは各シーンの管理クラスにそれぞれアタッチして使用するだけです!
      using UnityEngine;
      using UnityEngine.SceneManagement;
      using UnityEngine.UI;
      
      public class InputSceneManager : MonoBehaviour
      {
          /// <summary>
          /// UI
          /// </summary>
          [SerializeField] private InputField nameInputField;
          [SerializeField] private InputField hpInputField;
          [SerializeField] private Toggle isDeadToggle;
          [SerializeField] private Button nextButton;
      
          /// <summary>
          /// ScriptableObject
          /// </summary>
          [SerializeField] private MonsterSceneData monsterData;
      
          private void Awake()
          {
              // ScriptableObjectからデータを設定
              nameInputField.text = monsterData.MonsterName;
              hpInputField.text = monsterData.Hp.ToString();
              isDeadToggle.isOn = monsterData.IsDead;
      
              // ボタン押下処理
              nextButton.onClick.AddListener(() =>
              {
                  // 入力データをScriptableObjectに設定
                  monsterData.MonsterName = nameInputField.text;
                  monsterData.Hp = int.Parse(hpInputField.text);
                  monsterData.IsDead = isDeadToggle.isOn;
      
                  // シーン遷移
                  SceneManager.LoadScene("ShowScene");
              });
          }
      }
      
      ↑入力シーンの処理
      using UnityEngine;
      using UnityEngine.SceneManagement;
      using UnityEngine.UI;
      
      public class ShowSceneManager : MonoBehaviour
      {
          /// <summary>
          /// UI
          /// </summary>
          [SerializeField] private Text nameText;
          [SerializeField] private Text hpText;
          [SerializeField] private Text isDeadText;
          [SerializeField] private Button backButton;
      
          /// <summary>
          /// ScriptableObject
          /// </summary>
          [SerializeField] private MonsterSceneData monsterData;
      
          private void Awake()
          {
              // ScriptableObjectからデータを設定
              nameText.text = monsterData.MonsterName;
              hpText.text = monsterData.Hp.ToString();
              isDeadText.text = monsterData.IsDead.ToString();
      
              // ボタン押下処理
              backButton.onClick.AddListener(() =>
              {
                  // 元のシーンに戻る
                  SceneManager.LoadScene("InputScene");
              });
          }
      }
      
      ↑表示シーンの処理
      ↑入力したデータがそれぞれのシーン間で保持される
      エレキベア
      エレキベア
      いい感じに共有できているクマね
      マイケル
      マイケル
      一点注意点としては、ScriptableObjectを参照していないシーンに遷移するとデータが一度破棄されるようなので、ランタイムで使用する場合にはその点に注意しましょう。
      ↑参照していないシーンに遷移するとインスタンスが破棄される
      エレキベア
      エレキベア
      これはうっかりがありそうクマね・・・

      イベント管理

      マイケル
      マイケル
      最後にScriptableObjectを使用したイベント管理の方法について紹介します!
      これは公式でも紹介されていて、こんな方法もあるのかと面白かったので試してみました。

      ScriptableObject を使用してゲームを構築する 3 つの方法

      マイケル
      マイケル
      今回は、仲間のモンスターが死亡した通知を受けたらメッセージを出力する処理を実装してみます。
      エレキベア
      エレキベア
      かわいそうクマ・・・
      マイケル
      マイケル
      まずはScriptableObjectで作成したGameEventクラスと、
      イベント通知を受け取るGameEventListenerクラスを下記のように作成します。
      using System.Collections.Generic;
      using UnityEngine;
      
      [CreateAssetMenu(fileName = "GameEvent", menuName = "SampleGame/GameEvent")]
      public class GameEvent : ScriptableObject
      {
          private readonly List<GameEventListener> _listeners = new List<GameEventListener>();
      
          public void Raise()
          {
              foreach (var listener in _listeners)
              {
                  listener.OnEventRaised();
              }
          }
      
          public void RegisterListener(GameEventListener listener)
          {
              _listeners.Add(listener);
          }
      
          public void UnRegisterListener(GameEventListener listener)
          {
              _listeners.Remove(listener);
          }
      }
      
      ↑イベントクラス。Raiseを呼び出すとListenerに通知する
      using UnityEngine;
      using UnityEngine.Events;
      
      public class GameEventListener : MonoBehaviour
      {
          [SerializeField] private GameEvent gameEvent;
          [SerializeField] private UnityEvent unityEvent;
      
          private void OnEnable()
          {
              gameEvent.RegisterListener(this);
          }
      
          private void OnDisable()
          {
              gameEvent.UnRegisterListener(this);
          }
      
          public void OnEventRaised()
          {
              unityEvent.Invoke();
          }
      }
      
      ↑イベント通知を受け取る側。OnEventRaisedが呼ばれると設定した処理を実行する
      エレキベア
      エレキベア
      Listenerの方にはScriptableObjectで作成したイベントと、
      イベント通知で実行したい処理を設定しておくクマね
      マイケル
      マイケル
      今回は下記のようにMonsterクラスを作成して、
      ・死亡する処理
      ・仲間の死亡通知を受ける処理
      を定義しておきます。
      using UnityEngine;
      
      public class Monster : MonoBehaviour
      {
          [SerializeField] private string deadMessage;
          [SerializeField] private string shockMessage;
          [SerializeField] private GameEvent deadEvent;
      
          private bool _isDead = false;
      
          /// <summary>
          /// 死亡する
          /// </summary>
          public void Dead()
          {
              if (_isDead) return;
      
              // 雄叫びをあげながら死亡
              _isDead = true;
              Debug.Log($"<color=red>{gameObject.name}: {deadMessage}</color>");
      
              // 死亡イベントを発行する
              deadEvent.Raise();
          }
      
          /// <summary>
          /// 仲間の死亡通知を受ける
          /// </summary>
          public void NotifyFriendDead()
          {
              if (_isDead) return;
      
              // ショックを受ける
              Debug.Log($"<color=cyan>{gameObject.name}: {shockMessage}</color>");
          }
      }
      
      マイケル
      マイケル
      ScriptableObjectは下記のように死亡した際に呼ばれる
      EnemyDeadEventという名前で作成します。
      ↑死亡イベント
      マイケル
      マイケル
      作成したらこれらを各Monsterオブジェクトにアタッチして
      値を設定しましょう。
      ↑とりあえず3体作成
      ↑GameEventListenerとMonsterスクリプトをアタッチして値を設定
      マイケル
      マイケル
      分かりやすいように各モンスターの通知を受けた際の
      メッセージ内容は変えておきます。
      ↑Monster2の設定内容
      ↑Monster3の設定内容
      エレキベア
      エレキベア
      これで準備は完了クマね
      マイケル
      マイケル
      あとは下記のように各MonsterクラスのDead処理を呼び出すボタンを用意
      して実行してみましょう!
      ↑Deadボタンを用意
      using UnityEngine;
      using UnityEngine.UI;
      
      public class EventSceneManager : MonoBehaviour
      {
          [SerializeField] private Button deadMonster1Button;
          [SerializeField] private Button deadMonster2Button;
          [SerializeField] private Button deadMonster3Button;
          [SerializeField] private Monster monster1;
          [SerializeField] private Monster monster2;
          [SerializeField] private Monster monster3;
      
          private void Awake()
          {
              // ボタンに応じたモンスターを死亡させる
              deadMonster1Button.onClick.AddListener(() => monster1.Dead());
              deadMonster2Button.onClick.AddListener(() => monster2.Dead());
              deadMonster3Button.onClick.AddListener(() => monster3.Dead());
          }
      }
      
      マイケル
      マイケル
      こちらで順番に死亡させてみた結果は下記のようになります。
      ↑死亡メッセージとご臨終メッセージ
      エレキベア
      エレキベア
      うまく通知を受け取れてるクマね
      これは便利クマ〜〜〜

      +αの活用方法(リンク)

      マイケル
      マイケル
      この記事で解説するのは以上になりますが、
      その他にも様々な活用方法があったのでリンクのみ載せておきます!
      Excelからデータをインポートする

      【Unity】Excel Importer Maker、xlsxに対応 – テラシュールブログ

      マイケル
      マイケル
      こちらは途中でも少し紹介した、Excelで入力したデータを
      ScriptableObjectへ変換する拡張機能
      についての記事になります。
      本格的にデータを管理したい場合にはこのようなツールを使用するのがいいと思います!
      エレキベア
      エレキベア
      データ作成と管理はExcelがやりやすいクマからね〜〜
      FlyWeightパターンの適用(メモリの節約)

      Unityでよく使うデザインパターン – KAYAC engineers blog

      マイケル
      マイケル
      そしてこちらはカヤックさんが公開していただいている、
      FlyWeightパターンを使用して複数オブジェクトで同一のScriptableObjectデータを参照することで、メモリを節約する方法になります!
      エレキベア
      エレキベア
      なるほどクマ〜〜〜
      共通データはScriptableObjectとしてまとめるといいクマね

      おわりに

      マイケル
      マイケル
      というわけで今回はScriptableObjectの使い方についてでした!
      どうだったかな?
      エレキベア
      エレキベア
      データ管理だけかと思ったら
      他にもいろんな使い方ができて面白かったクマ〜〜
      マイケル
      マイケル
      すごく便利な機能なので、Unityを使用する場合には活用していきたいね!
      それでは今日はこの辺で!アデュー!!
      エレキベア
      エレキベア
      クマ〜〜〜

      【Unity】ScriptableObjectの活用方法についてまとめる 〜完〜


      UnityScriptableObject
      2022-07-30

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
      2024-04-14
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28
      【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜
      2024-03-28
      【Unity】サウンドミドルウェアに依存しない設計を考える【CRI ADX・Wwise】
      2024-03-27