😈

【Unity】コンパイル結果に応じて歓声か罵声を浴びせる拡張機能

に公開

結論、何を作ったのか

  • コンパイル結果に応じて以下のようなウィンドウを出す


エラーがあれば鞭を与える


エラーがなければ飴を与える

何故作ったのか

  • コンパイル(ドメインリロード)終了を検知して何かするプログラムを作ってみたかった

このエディタ拡張を構成するプログラム

CompileWatcher.cs

  • コンパイルパイプラインを監視
    • コンパイル開始時にエラーカウントをリセット
    • 各アセンブリのコンパイル完了でエラー数を集計
  • コンパイル終了後に結果を判定
    • エラーなし:成功フラグを SessionState に保存
    • エラーあり:乱数でウホまたはアホを選び、結果表示ウィンドウを呼び出し
  • 成功フラグが有効なら成功用ウィンドウを呼び出し
実際のプログラムはこちら
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.Compilation;
using UnityEngine;

[InitializeOnLoad]
public static class CompileWatcher
{
    private static int errorCount;
    private static readonly System.Random rng = new(); //static で同シードによる偏りを防ぐ

    static CompileWatcher()
    {
        CompilationPipeline.compilationStarted += _ => errorCount = 0;
        CompilationPipeline.assemblyCompilationFinished += (path, msgs) =>
        {
            foreach (var m in msgs) if (m.type == CompilerMessageType.Error) errorCount++;
        };
        CompilationPipeline.compilationFinished += obj =>
        {
            //エラー数を SessionState に保存
            if (errorCount == 0)
            {
                SessionState.SetInt("CompileSucceeded", 0);
            }
            else
            {
                string msg = (rng.NextDouble() < 0.3) ? "ウホ" : "アホ";
                CompileResultWindow.Show(msg, Color.red, 1200, 700, 300);
            }
        };
    }

    [DidReloadScripts]
    private static void OnScriptsReloaded()
    {
        var isSucceeded = SessionState.GetInt("CompileSucceeded", 0) == 0;
        if (isSucceeded) CompileResultWindow.Show("なかなかやる", Color.green, 1200, 700, 150);
    }
}

CompileResultWindow.cs

  • ウィンドウを生成し、渡されたメッセージを中央に表示(アニメーション有)
  • Show()
    • ウィンドウインスタンス作成、メッセージ, 色, フォントサイズ設定
    • 画面中央へ固定サイズで配置
  • Animate() 文字拡大アニメーション, 一定時間後に自動でウィンドウ削除
  • OnDisable() アニメーション更新解除
実際のプログラムはこちら
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class CompileResultWindow : EditorWindow
{
    private const float DURATION = 3.0f;

    private Label  messageLabel;
    private string message;
    private Color  textColor;
    private int    fontSize;
    private double startTime;

    public void CreateGUI()
    {
        var root = rootVisualElement;
        root.style.flexDirection  = FlexDirection.Column;
        root.style.alignItems     = Align.Center;
        root.style.justifyContent = Justify.Center;

        messageLabel = new Label(message);
        messageLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
        messageLabel.style.color          = textColor;
        messageLabel.style.fontSize       = fontSize;
        messageLabel.style.opacity        = 1.0f;
        root.Add(messageLabel);

        EditorApplication.update += Animate;
    }

    /// <summary>
    /// ウィンドウを画面中央に表示
    /// </summary>
    public static void Show(string msg, Color col, float width, float height, int fontSize)
    {
        //インスタンス作成
        var w = CreateInstance<CompileResultWindow>();
        w.message      = msg;
        w.textColor    = col;
        w.fontSize     = fontSize;
        w.startTime    = EditorApplication.timeSinceStartup;
        w.titleContent = new GUIContent(""); //タイトルバー非表示

        //画面中央に配置, サイズ固定
        float sw = Display.displays[0].systemWidth;
        float sh = Display.displays[0].systemHeight;
        w.position = new Rect((sw - width) * 0.5f, (sh - height) * 0.5f, width, height);
        w.minSize  = new Vector2(width, height);
        w.maxSize  = new Vector2(width, height);

        w.ShowUtility();
        w.Focus();
    }

    void Animate()
    {
        float elapsed = (float)(EditorApplication.timeSinceStartup - startTime);
        float t       = Mathf.Clamp01(elapsed / DURATION);
        float scale   = 1f + 0.5f * t;

        //拡大アニメーション
        messageLabel.style.scale = new Scale(new Vector2(scale, scale));
        if (elapsed > DURATION + 0.5f)
        {
            EditorApplication.update -= Animate;
            Close();
        }
    }

    void OnDisable() { EditorApplication.update -= Animate; }
}

処理の流れ

実装のポイント

staticSystem.Random 型メンバ変数

CompileWatcher.cs
private static readonly System.Random rng = new(); //static で同シードによる偏りを防ぐ
  • 短時間に new を複数呼び出すと同じシードになる可能性アリ
  • 共有の rng を使い回してシード重複を根本から防ぐ

歓声ウィンドウ(なかなかやる)だけ別の関数にしている理由

CompileWatcher.cs
static CompileWatcher()
{
    (前略)
    CompilationPipeline.compilationFinished += obj =>
    {
        //エラー数を SessionState に保存
        if (errorCount == 0)
        {
            SessionState.SetInt("CompileSucceeded", 0);
        }
        else
        {
            string msg = (rng.NextDouble() < 0.3) ? "ウホ" : "アホ";
            CompileResultWindow.Show(msg, Color.red, 1200, 700, 300);
        }
    };
}

[DidReloadScripts]
private static void OnScriptsReloaded()
{
    var isSucceeded = SessionState.GetInt("CompileSucceeded", 0) == 0;
    if (isSucceeded) CompileResultWindow.Show("なかなかやる", Color.green, 1200, 700, 150);
}
  • Unity はエラーなしでアセンブリをビルドすると、ドメインリロード実行する
  • EditorApplication.delayCall はドメイン内キュー
  • delayCall に登録したコールバックは、現ドメインが生きている間だけ実行されるため、
    再読み込み中にリセットされて消失する

改善点

  • ハードコーディングしている部分が多い
    • ちょっと適当に作りすぎましたかね…

余談


一定確率でアホの代わりにウホが出るようになっています

参考

Discussion