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

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

ScriptableObject – Unityマニュアル

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

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

ScriptableObjectの活用方法

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


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

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

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

データの管理

マイケル
マイケル
まずは最も基本的なデータ管理の方法について見ていきます!
データを表示する
マイケル
マイケル
下記のようなシーンを用意し、ゲーム実行時に
ScriptableObjectからデータを設定する処理を作ってみましょう。
ScreenShot 2022 07 29 23 09 43↑データ内容を表示する
マイケル
マイケル
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フォルダ内で右クリックした際に開く
メニューに表示する名称になります。
ScreenShot 2022 07 29 23 10 33↑アセットとして作成できるようになっている
マイケル
マイケル
アセットを作成して、今回は下記のように設定しました。
ScreenShot 2022 07 29 23 10 59↑データの設定
エレキベア
エレキベア
名前を使われたクマ・・・
マイケル
マイケル
あとは下記のように管理クラスにアタッチして
設定処理を記述すれば完了です!
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へのデータ設定処理
ScreenShot 2022 07 29 23 11 20↑実行するとデータが表示される
エレキベア
エレキベア
簡単に表示できたクマ〜〜
マイケル
マイケル
MonoBehaviourやJSONファイル等で定義するのと比べてどうなの?
という方は、下記記事がまとまっていて分かりやすいのでご参照ください!

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

マイケル
マイケル
基本的にはサーバ通信はJSON、それ以外はScriptableObject
を使用するのがよさそうですね!
マイケル
マイケル
あとはInspecter上で編集できてゲーム実行中でも編集できるので、
エンジニア以外の方と共同で作業する場合やパラメータを調整する際には重宝しそうですね。
エレキベア
エレキベア
簡単に使えるし便利クマね
複数レコードを管理する
マイケル
マイケル
それでは下記のように複数データを表示する場合はどのようにすればよいでしょうか?
ScreenShot 2022 07 29 23 17 10↑複数のデータを表示する場合
マイケル
マイケル
先ほど作成したデータを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上では下記のように設定することができます。
ScreenShot 2022 07 30 1 06 02↑データの定義
エレキベア
エレキベア
マスタデータのように使えるクマね
ScreenShot 2022 07 30 1 09 50↑問題なく表示できることを確認
マイケル
マイケル
データの設定を効率化したい場合には、
後ほど紹介するExcelファイルから設定する拡張機能について参照ください!
エレキベア
エレキベア
データ入力をExcelに出来たら効率化出来そうクマね

シーン間のデータ共有

マイケル
マイケル
次はシーン間でデータ共有をする方法についてです。
こちらは「スクリプトインスタンスから独立している」といった特徴を活かした方法になっています。
マイケル
マイケル
今回は下記のようにデータを入力するシーンと、
それを表示するシーンの2つを用意しました。
これらのシーン間でデータを共有できるようにしてみましょう!
ScreenShot 2022 07 29 23 42 51↑データ入力シーン
ScreenShot 2022 07 29 23 43 07↑データ表示シーン
エレキベア
エレキベア
よくある構造クマね
マイケル
マイケル
ゲーム実行中に入力データをそのまま設定してしまうと実行終了後にも上書きした値が残ってしまうため、
・初期化用の変数
・ランタイムで使用する変数
の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");
        });
    }
}
↑表示シーンの処理
01 scene parameter↑入力したデータがそれぞれのシーン間で保持される
エレキベア
エレキベア
いい感じに共有できているクマね
マイケル
マイケル
一点注意点としては、ScriptableObjectを参照していないシーンに遷移するとデータが一度破棄されるようなので、ランタイムで使用する場合にはその点に注意しましょう。
03 scene empty↑参照していないシーンに遷移するとインスタンスが破棄される
エレキベア
エレキベア
これはうっかりがありそうクマね・・・

イベント管理

マイケル
マイケル
最後に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という名前で作成します。
ScreenShot 2022 07 30 9 35 14↑死亡イベント
マイケル
マイケル
作成したらこれらを各Monsterオブジェクトにアタッチして
値を設定しましょう。
ScreenShot 2022 07 30 9 40 18↑とりあえず3体作成
ScreenShot 2022 07 30 0 58 45↑GameEventListenerとMonsterスクリプトをアタッチして値を設定
マイケル
マイケル
分かりやすいように各モンスターの通知を受けた際の
メッセージ内容は変えておきます。
ScreenShot 2022 07 30 0 58 56↑Monster2の設定内容
ScreenShot 2022 07 30 0 59 08↑Monster3の設定内容
エレキベア
エレキベア
これで準備は完了クマね
マイケル
マイケル
あとは下記のように各MonsterクラスのDead処理を呼び出すボタンを用意
して実行してみましょう!
ScreenShot 2022 07 30 0 59 16↑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());
    }
}
マイケル
マイケル
こちらで順番に死亡させてみた結果は下記のようになります。
02 scene event↑死亡メッセージとご臨終メッセージ
エレキベア
エレキベア
うまく通知を受け取れてるクマね
これは便利クマ〜〜〜

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

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

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

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

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

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

おわりに

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

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

コメント