見出し画像

【Unity備忘録】UnityEditor.ScriptableSingleton

参考資料

ScriptableSingletonの問題点

本来、ScriptableObjectのインスタンスはアセット化により永続化・管理される。
一方、ScriptableSingletonはFilePathAttributeを利用して独自の永続化が行われる。
これによってインスタンスを作成するのにわざわざアセット作成の手順を踏む必要がなくなり、より簡便にスクリプト上で扱うことが出来る。

反面、アセット化されないことによって発生する問題がある。
エディタ上でインスタンスのためのUIがなくなることである。
アセットがないのだからエディタ上で選択することが出来ない。
エディタ拡張のためのマネージャークラスであることを考えると、UIは各自で実装せよということだろうが、継承クラスごとにいちいちUIを作成するのは手間がかかる。

幸い、ScriptableSingletonはScriptableObjectを継承している。
ScriptableSingletonの継承クラスを選択可能な形で列挙出来れば、後はデフォルトのエディタ上の機能を利用することが出来る。
これなら比較的容易に実装出来るだろう。

実装イメージ

完成図
簡単なクラス図

ソースと解説

ScriptableSingletonManagerSettings

ScriptableSingletonManagerの設定クラス。

ScriptableSingletonの継承クラスを走査するためにリフレクションを用いる。
AppDomain.CurrentDomain.GetAssemblies()でAssemblyを列挙してもいいが、出来るだけメタ情報に触れずに走査すべきAssemblyを除外したい。

Unityのプロジェクトで管理されるAssemblyはUnityEditor.Compilation.CompilationPipeline.GetAssemblies()で列挙することが出来る。
これらの名前を正規表現でフィルターする。

編集と保存のためにScriptableSingletonManagerと分離する。

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[IgnoreInspectableSingleton] //列挙除外のためのカスタム属性(後述)
[FilePath("Singleton/ScriptableSingletonManagerSettings.txt", FilePathAttribute.Location.ProjectFolder)]
public class ScriptableSingletonManagerSettings : ScriptableSingleton<ScriptableSingletonManagerSettings>
{
    public List<string> AssemblyPatterns = new List<string>();
    public List<string> IgnoreAssemblyPatterns = new List<string>();

    /*
    ScriptableSingletonのhideFlagsは作成時にHideFlags.HideAndDontSaveが代入される
    これはHideFlags.NotEditableが含まれるため、編集可能にするためにはフラグを下さなければいけない
    */
    private void OnEnable()
    {
        hideFlags &= ~HideFlags.NotEditable;
    }
}

参考:Unity Community: ScriptableSingleton disabled SerializedProperty in Editor Window

ScriptableSingletonManager

ScriptableSingletonWindowのロジックを実装するクラス。

  1. SingletonsはバインドのためにList<Object>で定義する(ScriptableSingletonの参照は永続化しないようなので保存しても無意味)

  2. ReloadはSingletonsを更新する

  3. Saveはリフレクションによって継承クラスのSaveを呼び出す
    UnityEditor.Selectionを経由するためValidateも用意する

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Collections.Immutable;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Compilation;
using Assembly = System.Reflection.Assembly;

[System.AttributeUsage(System.AttributeTargets.Class)]
public class IgnoreInspectableSingletonAttribute : System.Attribute
{

}

[IgnoreInspectableSingleton]
public class ScriptableSingletonManager : ScriptableSingleton<ScriptableSingletonManager>
{
    // ScriptableSingleton<T>のTypeを動的に取得するユーティリティ関数
    private System.Type GetSingletonType(System.Type type) => typeof(ScriptableSingleton<>).MakeGenericType(type);

    private IEnumerable<Object> GetSingletons() => 

        // ScriptableSingletonはエディタ用なのでAssembliesType.Editorを指定する
        CompilationPipeline.GetAssemblies(AssembliesType.Editor).
        // UnityEditor.Compilation.Assembly => string
        Select(x => x.name).

        // ScriptableSingletonManagerSettingsの正規表現でフィルターする
        Where(x => ScriptableSingletonManagerSettings.instance.AssemblyPatterns.Any(y => Regex.IsMatch(x, y)) && 
            !ScriptableSingletonManagerSettings.instance.IgnoreAssemblyPatterns.Any(y => Regex.IsMatch(x, y))).
        // string => System.Reflection.Assembly > Types[]
        Select(x => Assembly.Load(x).GetTypes()).
        SelectMany(x => x).

        /* 
        ScriptableSingletonの継承クラスをフィルターする
        ScriptableSingleton<T>はScriptableObjectを型制約しているので事前チェック
        このタイミングでIgnoreInspectableSingletonAttributeによる除外を行う
        */
        Where(x => x.IsSubclassOf(typeof(ScriptableObject)) &&
            !x.IsAbstract &&
            !System.Attribute.IsDefined(x, typeof(IgnoreInspectableSingletonAttribute))).
        // typeof(継承クラス) => typeof(ScriptableSingleton<継承クラス>)
        Select(x => GetSingletonType(x)).
        // 継承クラスであることを確認する
        Where(x => x.GetGenericArguments()[0].IsSubclassOf(x)).
        // typeof(ScriptableSingleton<継承クラス>) => UnityEngine.Object
        Select(x => x.GetProperty("instance", BindingFlags.Public | BindingFlags.Static).GetValue(null) as Object).
        /*
        独自の拡張メソッド
        以下と等価
        Select(x =>
        {
            x.hideFlags &= ~HideFlags.NotEditable
            return x;
        })
        */
        Callback(x => x.hideFlags &= ~HideFlags.NotEditable);

    // 常に先頭にScriptableSingletonManagerSettingsを表示させる
    // IgnoreInspectableSingletonAttributeで除外されるため二重表示はない
    private IEnumerable<Object> GetFullSingletons() => Enumerable.Repeat(ScriptableSingletonManagerSettings.instance, 1).Concat(GetSingletons());

    /*
    独自のPropertyDrawer
    以下と等価

    [CustomPropertyDrawer(typeof(MyAttribute))]
    public class MyPropertyDrawer : PropertyDrawer
    {
        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            var propertyField = new PropertyField(property);
            propertyField.SetEnabled(false);
            return propertyField;
        }
    }
    */
    [MultipleDraw(typeof(PropertyFieldProcessor), typeof(ReadonlyProcessor))]
    public List<Object> Singletons = new List<Object>();

    /*
    SaveのValidateのためにSystem.Collections.Immutable.ImmutableHashSetを用いる
    System.Collections.Generic.HashSet<Object>に置き換えても可
    */
    private ImmutableHashSet<Object> GetSavableSingletonSet() => Singletons.
        Where(x => System.Attribute.IsDefined(x.GetType(), typeof(FilePathAttribute))).
        ToImmutableHashSet();

    private ImmutableHashSet<Object> _savableSingletonSet;
    public ImmutableHashSet<Object> SavableSingletonsSet => _savableSingletonSet ??= GetSavableSingletonSet();

    public void Reload()
    {
        Singletons = GetFullSingletons().ToList();
        _savableSingletonSet = GetSavableSingletonSet();
    }
    public void Save()
    {
        var selected = Selection.activeObject;
        GetSingletonType(selected.GetType()).GetMethod("Save", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(selected, new object[] { true });
    }
    
    public bool ValidateSave() => SavableSingletonsSet.Contains(Selection.activeObject);

    private void Awake()
    {
        Reload();
    }
    private void OnEnable()
    {
        hideFlags &= ~HideFlags.NotEditable;
    }
}

ScriptableSingletonWindow

UI Toolkitを用いたEditorWIndowの継承クラス。

  1. ListViewにScriptableSingletonManagerをバインドすることでSingletonsが列挙される

  2. ListView.selectionChangedでUnityEditor.SelectionにSingletonsを代入することで選択、インスペクターウィンドウへの表示を行う

  3. OnSelectionChangeでValidateを行うため、プロジェクトウィンドウなどで別のものが選択されてもSaveボタンの状態が更新される

using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class ScriptableSingletonWindow : EditorWindow
{
    public VisualTreeAsset Tree;
    private System.Action _validate;

    [MenuItem("Window/MyWindow/ScriptableSingleton")]
    public static void ShowScriptableSingleton()
    {
        ScriptableSingletonWindow wnd = GetWindow<ScriptableSingletonWindow>();
        wnd.titleContent = new GUIContent("ScriptableSingleton");
    }

    public void CreateGUI()
    {
        var root = rootVisualElement;
        var tree = Tree.Instantiate();
        var manager = ScriptableSingletonManager.instance;
        var bindManager = new SerializedObject(manager);

        var listView = tree.Q<ListView>("singletons");

        var reload = tree.Q<ToolbarButton>("reload");
        var save = tree.Q<ToolbarButton>("save");

        listView.Bind(bindManager);
        listView.SetEnabled(true);
        listView.selectionChanged += x =>
        {
            Selection.objects = x.Select(x => (x as SerializedProperty).objectReferenceValue).ToArray();
        };

        reload.clicked += manager.Reload;
        save.clicked += manager.Save;

        _validate = () =>
        {
            var saveResult = manager.ValidateSave();
            save.SetEnabled(saveResult);
        };
        _validate();
        
        root.Add(tree);
    }
    private void OnSelectionChange()
    {
        _validate?.Invoke();
    }
}

Treeに割り当てられるScriptableSingletonWindow.uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
    <uie:Toolbar>
        <uie:ToolbarButton text="Reload" display-tooltip-when-elided="true" name="reload" />
        <uie:ToolbarSpacer style="flex-grow: 1;" />
        <uie:ToolbarButton text="Save" display-tooltip-when-elided="true" name="save" />
    </uie:Toolbar>
    <ui:ListView focusable="true" virtualization-method="DynamicHeight" show-foldout-header="true" show-bound-collection-size="false" header-title="Singletons" binding-path="Singletons" name="singletons" />
</ui:UXML>


この記事が気に入ったらサポートをしてみませんか?