第4章 ScriptableObject - エディター拡張入門

第4章 ScriptableObject

4.1 ScriptableObject とは

ScriptableObject は独自のアセットを作成するための仕組みです。また、Unity のシリアライズ機構が扱う形式とも言えます。

Unity には独自のシリアライズ機構を持っており、すべてのオブジェクト(UnityEngine.Object)は、そのシリアライズ機構を通してデータのシリアライズ/デシリアライズを行い、ファイルと Unity エディター間でやり取りをしています。シリアライズ機構については 第5章「SerializedObject について」 を参照してください。

Unity 内部のアセット(マテリアルやアニメーションクリップ等)はすべて UnityEngine.Object の派生クラスです。独自のアセットを作成するために、UnityEngine.Object の派生クラスを作成したいですが、ユーザー側では UnityEngine.Object の派生クラスを作成するのは禁止されています。ユーザーが Unity のシリアライズ機構を利用した、独自のアセットを作成するには ScriptableObject を扱う必要があります。

4.2 ScriptableObject は Unity エディターの要

ScriptableObject は Unity エディターのいたる所で使われています。例えば、シーンビューやゲームビューなどの エディターウィンドウ は、ScriptableObject の派生クラスから生成されており、また、インスペクターに GUI を表示するための Editor オブジェクトも ScriptableObject の派生クラスから生成されています。Unity エディターは ScriptableObject で作成されているといっても過言ではありません。

アセンブリブラウザーで見ると ScriptableObject が継承されているのが分かる

図4.1: アセンブリブラウザーで見ると ScriptableObject が継承されているのが分かる

4.3 ScriptableObject を作成する

ScriptableObject を作成するには、まず ScriptableObject クラスを継承したクラスを作成する必要があります。この時、クラス名とアセット名は同じでなければいけません。MonoBehaviour と同じ制限です。

using UnityEngine;

public class ExampleAsset : ScriptableObject
{

}

インスタンス化

ScriptableObject を生成するには ScriptableObject.CreateInstance で生成します。new を使用してインスタンス化はしてはいけません。理由は MonoBehaviour と同じで、Unity のシリアライズ機構経由でオブジェクトを作成する必要があるからです。

using UnityEngine;
using UnityEditor;

public class ExampleAsset : ScriptableObject
{
    [MenuItem ("Example/Create ExampleAsset Instance")]
    static void CreateExampleAssetInstance ()
    {
        var exampleAsset = CreateInstance<ExampleAsset> ();
    }
}

アセットとして保存

次にインスタンス化したオブジェクトをアセットとして保存します。アセットの作成は AssetDatabase.CreateAsset を使って作成することが可能です。

アセットの拡張子は、必ず .asset でなくてはいけません。他の拡張子にしてしまうと、Unity が ScriptableObject 派生のアセットとして認識しません。

[MenuItem ("Example/Create ExampleAsset")]
static void CreateExampleAsset ()
{
    var exampleAsset = CreateInstance<ExampleAsset> ();

    AssetDatabase.CreateAsset (exampleAsset, "Assets/Editor/ExampleAsset.asset");
    AssetDatabase.Refresh ();
}

また、CreateAssetMenu 属性を使うことで簡単にアセットを作成できます。

using UnityEngine;
using UnityEditor;

[CreateAssetMenu(menuName = "Example/Create ExampleAsset Instance")]
public class ExampleAsset : ScriptableObject
{
}

CreateAssetMenu を使用した場合は 「Assets/Create」配下にメニューが作成されます。

スクリプトからアセットの ScriptableObject をロードする

読み込む方法も簡単で AssetDatabase.LoadAssetAtPath を使って読み込みます。

[MenuItem ("Example/Load ExampleAsset")]
static void LoadExampleAsset ()
{
    var exampleAsset =
        AssetDatabase.LoadAssetAtPath<ExampleAsset>
                               ("Assets/Editor/ExampleAsset.asset");
}

インスペクターにプロパティーを表示する

MonoBehaviour と同じで、フィールドに SerializeField を付けるだけで表示されるようになります。また PropertyDrawer も適用されます。

using UnityEngine;
using UnityEditor;

public class ExampleAsset : ScriptableObject
{
    [SerializeField]
    string str;

    [SerializeField, Range (0, 10)]
    int number;

    [MenuItem ("Example/Create ExampleAsset Instance")]
    static void CreateExampleAssetInstance ()
    {
        var exampleAsset = CreateInstance<ExampleAsset> ();

        AssetDatabase.CreateAsset (exampleAsset, "Assets/Editor/ExampleAsset.asset");
        AssetDatabase.Refresh ();
    }
}

4.4 ScriptableObject の親子関係

まずは、「親の ScriptableObject」と、その親が変数として持つ「子の ScriptableObject」をイメージしてください。

下記が、そのイメージをコードに流し込んだものです。

親の ScriptableObject

using UnityEngine;

public class ParentScriptableObject : ScriptableObject
{
    [SerializeField]
    ChildScriptableObject child;
}

子の ScriptableObject

using UnityEngine;

public class ChildScriptableObject : ScriptableObject
{
  //何もないとインスペクターが寂しいので変数追加
  [SerializeField]
  string str;

  public ChildScriptableObject ()
  {
    //初期アセット名を設定
    name = "New ChildScriptableObject";
  }
}

次に、ParentScriptableObject をアセットとして保存します。引数にある child もインスタンス化した状態にしてみましょう。

子の ScriptableObject

using UnityEngine;
using UnityEditor;

public class ParentScriptableObject : ScriptableObject
{
  const string PATH = "Assets/Editor/New ParentScriptableObject.asset";

  [SerializeField]
  ChildScriptableObject child;

  [MenuItem ("Assets/Create ScriptableObject")]
  static void CreateScriptableObject ()
  {
    //親をインスタンス化
    var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();

    //子をインスタンス化
    parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

    //親をアセットとして保存
    AssetDatabase.CreateAsset (parent, PATH);

    //インポート処理を走らせて最新の状態にする
    AssetDatabase.ImportAsset (PATH);
  }
}

ParentScriptableObject をアセットとして保存した後、インスペクターを見てみると、図4.2 のように child プロパティーが Type mismatch になります。

child プロパティーが <code class="inline-code tt">Type mismatch</code> となっている

図4.2: child プロパティーが Type mismatch となっている

試しに、Type mismatch の部分をダブルクリックしてみると、ChildScriptableObject の情報がインスペクターに表示され、問題なく正しい挙動をしているように見えます。

ChildScriptableObject のプロパティーを操作することが出来る

図4.3: ChildScriptableObject のプロパティーを操作することが出来る

UnityEngine.Object をアセットとして扱うにはディスクに保存しなければいけない

Type mismatch 状態の child を持つ ParentScriptableObject を作成したら、このまま Unity を再起動してみましょう。再度、ParentScriptableObject のインスペクターを見ると child の部分が None(null) になっているのがわかります。

アセットにする前に child のインスタンスを作成したのに null となっている

図4.4: アセットにする前に child のインスタンスを作成したのに null となっている

このは理由は ScriptableObject の基底クラスである UnityEngine.Object をシリアライズデータとして扱うには、ディスク上にアセットとして保存しなければいけません。Type mismatch 状態は、インスタンスは存在するが、ディスク上にアセットとして存在しない状態を指しています。つまり、そのインスタンスがなんらかの状況(Unity 再起動など)で破棄されてしまうとデータにアクセスができなくなります。

ScriptableObject はすべてアセットとして保存すること

Type mismatch の状況を回避するのはとても簡単です。ScriptableObject をすべてアセットとして保存して、その参照をシリアライズ可能なフィールドに持たせることで解決します。

テクスチャなどのアセットのように普段行うようなドラッグ&ドロップの操作で参照を持たせることが出来る

図4.5: テクスチャなどのアセットのように普段行うようなドラッグ&ドロップの操作で参照を持たせることが出来る

ただし、今回のように「親」と「子」の関係がある状態で、それぞれ独立したアセットを作成してしまうのは管理面で見ても得策ではありません。子の数が増えたり、リストを扱うことになった時にその分アセットを作成するのは非常にファイル管理が面倒になってきます。

そこで サブアセット という機能を使って親子関係であるアセットを1つにまとめ上げることができます。

サブアセット

親となるメインアセットにアセット情報を付加することで UnityEngine.Object がサブアセットとして扱われます。このサブアセットの例で一番わかりやすいのがモデルアセットです。

モデルアセットの中には、メッシュやアニメーションなどのアセットが含まれています。これらは普段、独立したアセットとして存在しなければいけませんが、サブアセットとして扱うことで、メッシュやアニメーションクリップのアセットを、メインのアセット情報の中に包括してディスク上に保存することなく使用することができます。

モデルアセットの中にはメッシュやアバター、アニメーションなどが含まれる

図4.6: モデルアセットの中にはメッシュやアバター、アニメーションなどが含まれる

ScriptableObject もサブアセットの機能を使うことで、ディスク上に余計なアセットを増やすことなく親子関係の ScriptableObject を構築することができます。

AssetDatabase.AddObjectToAsset

UnityEngine.Object をサブアセットとして登録するには、メインとなるアセットにオブジェクトを追加します。

子の ScriptableObject

using UnityEngine;
using UnityEditor;

public class ParentScriptableObject : ScriptableObject
{
  const string PATH = "Assets/Editor/New ParentScriptableObject.asset";

  [SerializeField]
  ChildScriptableObject child;

  [MenuItem ("Assets/Create ScriptableObject")]
  static void CreateScriptableObject ()
  {
    //親をインスタンス化
    var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();

    //子をインスタンス化
    parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

    //親に child オブジェクトを追加
    AssetDatabase.AddObjectToAsset (parent.child, PATH);

    //親をアセットとして保存
    AssetDatabase.CreateAsset (parent, PATH);

    //インポート処理を走らせて最新の状態にする
    AssetDatabase.ImportAsset (PATH);
  }
}

図4.7 をみるとわかりますが、親である ParentScriptableObject が 2つあるように見えたり、実質データを持っているのは階層的にはサブアセットの ParentScriptableObject であったりと少し特殊な階層構造となっていることがわかります。

メインアセットにサブアセットが追加された状態

図4.7: メインアセットにサブアセットが追加された状態

この状態は、ユーザーが(サブアセットを作成したことによって)特殊なアセットを作成したと Unity が判断し、メインアセットを何にも属さない DefaultAsset として表示した状態です。

メインアセットとして扱いたいアセットがサブアセット側に移動してしまう状況はとても気持ちのいいものではありません。このように、ユーザーの手でサブアセットを作成することはできますが、これをモデルのように最大限活用することはいまだ想定されていません。

HideFlags.HideInHierarchy でサブアセットを隠す

サブアセット自体を隠すことによって、メインアセットのみが存在するような見た目を作成することができます。

[MenuItem ("Assets/Create ScriptableObject")]
static void CreateScriptableObject ()
{
  var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();
  parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

 //サブアセットとなる child を非表示する
  parent.child.hideFlags = HideFlags.HideInHierarchy;

  AssetDatabase.AddObjectToAsset (parent.child, PATH);

  AssetDatabase.CreateAsset (parent, PATH);
  AssetDatabase.ImportAsset (PATH);
}

このように、階層表示はなくなりましたが2つのアセットを1つにまとめて扱うことができるようになりました。

ParentScriptableObject のみ表示されているがサブアセットの ChildScriptableObject の参照が正しく行われている

図4.8: ParentScriptableObject のみ表示されているがサブアセットの ChildScriptableObject の参照が正しく行われている

このサブアセットを隠す方法は、AnimatorController でも行われています。確かめてみましょう。

[MenuItem ("Assets/Set to HideFlags.None")]
static void SetHideFlags ()
{
  //AnimatorController を選択した状態でメニューを実行
  var path = AssetDatabase.GetAssetPath (Selection.activeObject);

  //サブアセット含めすべて取得
  foreach (var item in AssetDatabase.LoadAllAssetsAtPath(path)) {
    //フラグをすべて None にして非表示設定を解除
    item.hideFlags = HideFlags.None;
  }
  //再インポートして最新にする
  AssetDatabase.ImportAsset (path);
}

上記のコードを実行すると、HideFlags が解除され、サブアセットが表示されます。

Layer や BrendTree などがサブアセットとして追加されていることが分かる

図4.9: Layer や BrendTree などがサブアセットとして追加されていることが分かる

メインアセットからサブアセットを削除する

サブアセットの削除方法は簡単です。Object.DestroyImmediate を使うことによってサブアセットを削除することができます。

[MenuItem ("Assets/Remove ChildScriptableObject")]
static void Remove ()
{
  var parent = AssetDatabase.LoadAssetAtPath<ParentScriptableObject> (PATH);

  //アセットの CarentScriptableObject を破棄
  Object.DestroyImmediate (parent.child, true);

  //破棄したら Missing 状態になるので null を代入
  parent.child = null;

  //再インポートして最新の情報に更新
  AssetDatabase.ImportAsset (PATH);
}

4.5 アイコンの変更

デフォルトのアイコンは [ScriptableObject] となっています。特に重要ではありませんがこのアイコンを変更する方法があります。

スクリプトにアイコンを設定

スクリプトアセットを選択してアイコンの部分を選択すると、アイコン変更パネルが表示されます。ここで「Other」ボタンをクリックして変更したいアイコンのテクスチャを選んでください。

わかりにくいがスクリプトと ScriptableObject にアイコンが設定された

図4.10: わかりにくいがスクリプトと ScriptableObject にアイコンが設定された

Gizmos にアイコンを置く

もう1つアイコンを変更する方法として、Gizmos フォルダーに[クラス名] Icon という名前でアイコン画像を置くと変更されるようになります。Gizmos フォルダーが Assets フォルダー直下のみという仕様で少し使いづらいかもしれませんが、同じアイコン画像が3つ並ばなくても良くなる点で、この方法も覚えておくと便利かもしれません。

図4.11:

第3章 データの保存 第5章 SerializedObject について