ScriptableObject(スクリクタブルオブジェクト)

2022年10月22日

Unityでは、データの取り扱いに特別なクラスが用意されています。うまく活用することで、依存性の低いコードを記述することができます。

ScriptableObjectとは

特徴

次のような点がこれまで扱ってきたスクリプトとの違いです。
難しく感じるかもしれませんが、チュートリアルを通して効果を実感していきましょう。

  • 異なるタイプのデータを保持するために特別に設計された特殊なスクリプトです。
  • 従来のC#スクリプトとは異なり、GameObjectに直接アタッチする必要はなく、実際にアタッチすることもできません。
  • 他のスクリプトインスタンスに依存しないデータへのアクセスを可能にするクラスです。
  • スクリプトインスタンスから独立した大量の共有データを格納できます。
  • GameObject間でデータを共有するのに活用できます。
  • 実行中に変化する可能性のある大量のデータを処理するプロジェクトに最適です。
  • プログラムに精通していないデザイン担当やサウンド担当が、自由に置き換えることができるようにScriptableObjectにSpriteやPrefab、AudioClipを保存する場所として活用できます。

基本チュートリアル

チュートリアルを通して、検証してみましょう
チュートリアルでは、ゲーム中に登場する敵キャラの様々な情報(最大HPなど)を取得するためのデータの作成を想定しています。
メモリの使用量の削減、依存性の低いコードの作成を学習の目的としています。

プロジェクトの作成

プロジェクト名ScriptableObjectTestで3Dプロジェクトを作成します。

オブジェクトの作成

Cubeオブジェクトを1つ作成します。
名前はSlimeにします。

シーン名の変更

名前をGameSceneにします。

ScriptableObjectの作成

まず雛形となるC#スクリプトを作成し、次にScriptableObjectを作成する手順になります。

EnemyParam.cs(敵:エネミーのパラメータの意味)

新しくC#スクリプトを作成し、名前を EnemyParam とします。

Unityが生成作成したコードを次の手順で変更/追記します。

  • MonoBehavioursの継承をScriptableObjectの継承に置き換えます。
  • Start()メソッドとUpdate()メソッドを削除します。
  • 最大HPを表す数値として、int型の maxHp フィールドを宣言します。
using UnityEngine;

public class EnemyParam : ScriptableObject
{
    public int maxHp;
}
Scirptableオブジェクトでゲームオブジェクト型を扱いたい場合

Unityで使える様々な型をアウトレット接続で代入することができますが、Scene内(ヒエラルキー内)のオブジェクトはアウトレット接続できません。Prefabからオブジェクトを作成するサンプルのように、GameObject型で変数宣言し、Prefabをアウトレット接続(D&D)します。

Createのメニューを作成

この時点でScriptableObjectは使用可能ですが、この雛形(テンプレート)オブジェクトを何度も使用できるようにしたいと思います。
Unityの拡張エディタの機能を使って、Unityエディターのメニュー項目に組み込みましょう。

Publicクラス行のすぐうえにCreateAssetMenu属性(アトリビュート)を追加します。
メニュー名もここで指定することができます。

EnemyParam.cs(先程のコードにCreateAssetMenu属性を追加)

using UnityEngine;

[CreateAssetMenu(menuName = "EnemyParam")]
public class EnemyParam : ScriptableObject
{
    public int maxHp;
}

スクリプトを保存してUnityに戻ります。

ScriptableObjectアセットを作成

プロジェクトウィンドウで、CreateメニューEnenyParamというサブメニューが追加されていますので選択します。(右クリックのメニューからか、プロジェクトウィンドウの左上の「+メニュー」から選択できます)

今回は、スライム用のパラメータセットという意味で、名前を変更しておきましょう。
プロジェクトウィンドウに新しくできたファイルの名前を、New Enemy Paramから SlimeParam に変更します。
アイコンが次のように変化しているのを確認します。これがScriptableObject(スクリプタブル オブジェクト)のアイコンです。

ScriptableObjectに値を代入

プロジェクトウィンドウのSlimeParamを選択し、インスペクターで表示されているMaxHpに値を設定します。

ファイルの拡張子は、.assetになります。テキストファイルなのでメモ帳でも開くことができます。フォーマットは、yamlフォーマットです。これは、Unityのシーンファイル(.unity)と同じです。
注目して欲しいのは最後の行ですね。maxHpの値が保存されているのがわかります。

SlimeParam.asset

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 4a06560b0dfd34657ab0f23eceba8fe9, type: 3}
  m_Name: SlimeParam
  m_EditorClassIdentifier: 
  maxHp: 10

ScriptableObjectを別のスクリプトにアウトレット接続して使う

では、この作成したパラメータセットを使ってみることにしましょう。
そのために、新しくのスクリプトを作成します。
そして、Slimeオブジェクトにアタッチします。

SlimeStatus.cs

EnemyParam型のSlimeParamを宣言しています。

using UnityEngine;

public class SlimeStatus : MonoBehaviour
{
    [SerializeField]
    EnemyParam slimeParam;

    int slimeHp;

    void Start()
    {
        slimeHp = slimeParam.maxHp;
    }

    // あくまで、動作テスト用。実際のコードでは、他のクラスからアクセスするべき
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            slimeHp--;
            Debug.Log(slimeHp);
        }
    }
}

インスペクター でSlimeParamをアウトレット接続します

Slimeを選択し、インスペクターのSlime Statausスクリプトを見てみましょう。
Slime Paramにアウトレット接続で、値を代入できますね。
ここに、先ほど作ったSlimeParamをD&Dしてみましょう。
スクリプトの型を代入する方法と同じです。SlimeParamには、maxHpフィールドがありますよね。pubulic修飾子がついているので、アクセスできます。

実行結果

コンソールに次のように表示されると思います。(どのようなアクションで表示されるかはコードから考えてみてください)

まとめ

継承を使う場合、派生クラスで基本クラスのメンバーが使えるので、派生クラスのコード行が短くなります。しかし、作成されるインスタンスは、基本クラスのコードも含まれますのでメモリをその分消費してしまします。

多量にオブジェクトを作成する場合、この使用量がバカにできないことがあります。その点、ScriptableObjectであれば新たに多量のメモリを使用することはありません。
扱いも慣れればそれほど難しくないので、経験を通して活用できるようにしましょう。

応用チュートリアル

実際にサンプルを使って試してみましょう

サンプルコード

maxHp以外でも、次のように様々な宣言を使うことで多種多様な情報を保存することができます。
今回は、作成されたファイルを、Resourcesフォルダへ保存してみましょう。
フォルダは最初作られていないので、その場合は名前を間違えないように新規作成します。

EnemyParam.cs(応用のために情報を追加しています)

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "EnemyParam")]
public class EnemyParam : ScriptableObject
{
    public int maxHp;

    public bool isBoss;

    public enum Gender
    {
        男,
        女,
        フリー,
    }

    public Gender gender;

    public List<Item> items;

    [System.Serializable]
    public class Item
    {
        public string name;
        public int attackPower;
        public int difensivePower;
    }
}

SlimeStatus.cs(応用のため、テスト内容を更新しています)

Resourcesフォルダ(リソースフォルダ)から、動的にデータを読み出しています。
これは、このコードが実行されるまで、メモリの利用がなされないということです。

using UnityEngine;

public class SlimeStatus : MonoBehaviour
{
    private EnemyParam slimeParam;

    void Update()
    {
        //スライムのパラメータをResourcesからロードして、武器セットの1つ目の名前を知る」
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // Resourceフォルダ内のSlimeParamファイルをEnemyParam型で読み込む
            slimeParam = Resources.Load<EnemyParam>("SlimeParam");
            // アイテム1つ目の名前を代入
            string firstItemName = slimeParam.items[0].name;
            Debug.Log(firstItemName);
        }
    }
}

ScriptableObjectのインスペクター

データをサンプルにように入力してみました。

ファイルの拡張子は、.assetになります。テキストファイルなのでメモ帳でも開くことができます。フォーマットは、yamlフォーマットです。これは、Unityのシーンファイル(.unity)と同じです。
注目して欲しいのは最後の行ですね。maxHp、gender、itemsの各値が保存されているのがわかります。
name: “\u68CD\u68D2″は、日本語が、文字コードで表記されているところになります。

SlimeParam.asset

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 4a06560b0dfd34657ab0f23eceba8fe9, type: 3}
  m_Name: SlimeParam
  m_EditorClassIdentifier: 
  maxHp: 10
  gender: 0
  items:
  - name: "\u68CD\u68D2"
    attackPower: 1
    difensivePower: 1
  - name: "\u5C0F\u5200"
    attackPower: 2
    difensivePower: 2
  isBoss: 0

クラス図

EnemyParamスクリプトのリファクタリング(Gender列挙型とItemクラスを別スクリプトへ書き出した場合)

見やすくするため、Gender列挙型とItemクラスをEnemyParamクラス内に所有していますが、他のクラスで活用したい場合、これを外部にファイルに書き出してもいいです。(インテリセンスで、Gender列挙型とItemクラスのシグネチャ部分を右クリックすると外部ファイル書き出しができます)

EnemyParam.cs(Gender列挙型とItemクラスを別ファイルにした)

using UnityEngine;

public class SlimeStatus : MonoBehaviour
{
    private EnemyParam slimeParam;

    void Update()
    {
        //スライムのパラメータをResourcesからロードして、武器セットの1つ目の名前を知る」
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // Resourceフォルダ内のSlimeParamファイルをEnemyParam型で読み込む
            slimeParam = Resources.Load<EnemyParam>("SlimeParam");
            // アイテム1つ目の名前を代入
            string firstItemName = slimeParam.items[0].name;
            Debug.Log(firstItemName);
        }
    }
}

Gender.cs

public enum Gender
{
    男,
    女,
    フリー,
}

Item.cs

[System.Serializable]
public class Item
{
    public string name;
    public int attackPower;
    public int difensivePower;
}

クラス図

まとめ

応用のサンプルを通して、ScriptableObjectの汎用性が見えてきたでしょうか?
ここで、最初に記述されている、特徴についてもう一度確認してみましょう。理解が深まったのではないでしょうか

その他のCreateAssetMenuアトリビュート

今回、メニューに使うアトリビュートとして、menuNameを使いました。

[CreateAssetMenu(menuName = "EnemyParam")]

メニュー作成で表示される項目はmenuName以外にも次のようなカスタマイズが可能です。インスタンスを “xxx.asset" ファイルとしてプロジェクト内に簡単に作成と格納ができます。

fileName作成されるデフォルト(最初)のファイル名
menuNameAssets/Create メニューに表示される名前
orderAssets/Create メニュー内のメニューアイテムの位置

参考

Scriptable object 入門と活用例

データをロードする場合の注意

Unity

Posted by hidepon