依存関係に基づいてスクリプト実行順設定を自動化する


Unityでは基本的にスクリプトの処理順番は不定なので、スクリプト間に依存関係があるときはAwakeやStart、UpdateやLateUpdateの違いで乗り切る。最後の手段としてScript Execution Orderを使う。依存関係に敏感な実装にしない。

というのがよくあるスタイルと思う。しかし処理順が問題でUpdateからLateUpdateにコードを移すとき、誤魔化しを感じることがあった。A <-- B <-- C のように2段の依存を見つけたときは特に。

そこで依存関係は明確にして、処理順は適切にコントロールする。という発想に切り替えてそれを補助するユーティリティを作ってみた。

DependsOnAttribute.cs
using System;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DependsOnAttribute : Attribute {
    public Type dependee;
    public DependsOnAttribute(Type dependee) {
        this.dependee = dependee;
    }
}
Editor/ExecutionOrderCoordinator.cs
using UnityEditor;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;

[InitializeOnLoad]
public class ExecutionOrderCoordinator {
    static ExecutionOrderCoordinator() {
        EditorApplication.update += DoRefreshExecutionOrders;
    }

    static void DoRefreshExecutionOrders() {
        EditorApplication.update -= DoRefreshExecutionOrders;
        if (!EditorApplication.isPlaying) {
            RefreshExecutionOrders();
        }
    }

    static void RefreshExecutionOrders() {
        var graph = GetDependencyGraph();
        List<Type> sorted = TSort(graph.dependencies);

        for (int i = 0; i < sorted.Count; ++i) {
            var type = sorted[i];
            var script = graph.monoScripts[type];
            var order = i - sorted.Count;

            if (MonoImporter.GetExecutionOrder(script) != order) {
                MonoImporter.SetExecutionOrder(script, order);
            }
        }
    }

    class DependencyGraph {
        public Dictionary<Type, MonoScript> monoScripts = new Dictionary<Type, MonoScript>();
        public Dictionary<Type, HashSet<Type>> dependencies = new Dictionary<Type, HashSet<Type>>();
    }

    static DependencyGraph GetDependencyGraph() {
        var graph = new DependencyGraph();
        foreach (var script in MonoImporter.GetAllRuntimeMonoScripts()) {
            var klass = script.GetClass();
            if (klass != null) {
                graph.monoScripts[klass] = script;

                var attrs = Attribute.GetCustomAttributes(klass, typeof(DependsOnAttribute));
                foreach (DependsOnAttribute attr in attrs) {
                    HashSet<Type> set;
                    if (!graph.dependencies.TryGetValue(klass, out set)) {
                        set = graph.dependencies[klass] = new HashSet<Type>();
                    }
                    set.Add(attr.dependee);

                    if (!graph.dependencies.ContainsKey(attr.dependee)) {
                        graph.dependencies[attr.dependee] = new HashSet<Type>();
                    }
                }
            }
        }
        return graph;
    }

    // The AWK Programming Language(1988) SECTION 7.3 TOPOLOGICAL SORTING
    // http://d.hatena.ne.jp/tociyuki/20120919
    // Color definition: white=0, gray=1, black=2
    static List<Type> Depends(Type target, Dictionary<Type, HashSet<Type>> dependencies, Dictionary<Type, int> marked, List<Type> sorted) {
        if (marked.ContainsKey(target)) {
            return null;
        }
        marked[target] = 1;
        foreach (var m in dependencies[target].OrderBy(x => x.FullName)) {
            if (!marked.ContainsKey(m)) {
                Depends(m, dependencies, marked, sorted);
            } else if (marked[m] == 1) {
                throw new InvalidOperationException(string.Format("nodes {0} and {1} are in a cycle", m, target));
            }
        }
        sorted.Add(target);
        marked[target] = 2;
        return sorted;
    }

    static List<Type> TSort(Dictionary<Type, HashSet<Type>> dependencies) {
        var marked = new Dictionary<Type, int>();
        var sorted = new List<Type>();
        foreach (var target in dependencies.Keys.OrderBy(x => x.FullName)) {
            Depends(target, dependencies, marked, sorted);
        }
        return sorted;
    }
}

Wikipediaから図を拝借。

上図のような依存関係があるとして、

C7.cs
[DependsOn(typeof(C8))]
[DependsOn(typeof(C11))]
public class C7 : MonoBehaviour { }
C8.cs
[DependsOn(typeof(C9))]
public class C8 : MonoBehaviour { }

このように依存関係を属性で定義していく。

スクリプトがビルドされると上図のようにScript Execution Orderが-1から負方向に向かって自動設定される。
このユーティリティを使うときは、手動で設定された順序と競合しないように、値を調整しておく必要がある(UIStretchなど)。