【Unity】スクリプトの自動コンパイルを抑制＆ショートカットキーでコンパイルする【エディタ拡張】

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unity、たまに意図的にコンパイルそのものを止めたくなる時があるんだけど、何か良い手無いかな...。(主に複数のフォルダ整理やADF整理など)<br>ちなみに自分は速攻でコンパイルエラーになるコードを仕込むことで、完了時間を短くした状態で整理すると言う力技で対処したことならある。</p>&mdash; mao (@TEST_H_) <a href="https://twitter.com/TEST_H_/status/1291302666820128769?ref_src=twsrc%5Etfw">August 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# はじめに
新規スクリプトの追加や移動でいちいちコンパイルが走ると開発のイテレーション速度が落ちるので

* Unityの自動コンパイルを抑制
* ショートカットキー or メニューから実行することで明示的にコンパイルする

という挙動を実現するエディタ拡張を書いてみた。

ちなみにこれまで無駄なコンパイルが走ってほしくないときは自分も@mao_さんと同じやり方でやっていた。

<font color="red">2021/1/2 更新</font>

* スクリプトの変更が検知されない不具合修正
* 再生後に `Tools/Compile Locker/Compile` でコンパイルが走らない不具合修正

<font color="red">更新ここまで</font>

# 動作環境
* macOS Catalina（10.15.6）
* Unity2019.4.16

にて動作確認。

# ソースコード
``` CompileLocker.cs
#if UNITY_EDITOR
using System;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;

[InitializeOnLoad]
public class CompileLocker
{
    private const string MenuItemPath = "Tools/Compile Locker/";

    public class CompileLockerData : ScriptableSingleton<CompileLockerData>
    {
        public bool isInitialized;
        public bool isLocking;
    }

    static CompileLocker()
    {
        Debug.Log("Assemblies reloaded.");

        Lock();

        if (isInitialized) return;
        isInitialized = true;

        Debug.LogWarning($"Disabled auto compiling. For manual compiling, execute \"{MenuItemPath}Compile\".");
    }

    private static bool isInitialized
    {
        get => CompileLockerData.instance.isInitialized;
        set => CompileLockerData.instance.isInitialized = value;
    }

    private static bool isLocking
    {
        get => CompileLockerData.instance.isLocking;
        set => CompileLockerData.instance.isLocking = value;
    }

    private static void Lock()
    {
        if (isLocking) return;

        EditorApplication.LockReloadAssemblies();
        isLocking = true;
    }

    private static void Unlock()
    {
        if (isLocking == false) return;

        EditorApplication.UnlockReloadAssemblies();
        isLocking = false;
    }

    private static Task _compileTask = null;

    [MenuItem(MenuItemPath + "Compile %r", false)]
    private static void ReloadAssemblies()
    {
        if (_compileTask != null) return;

        _compileTask = CompileTask().ContinueWith(_ => _compileTask = null);
    }

    private static async Task CompileTask()
    {
        AssetDatabase.Refresh();
        Unlock();

        if (EditorApplication.isCompiling)
        {
            Debug.Log("Compiling...");

            // 連打防止
            while (EditorApplication.isCompiling)
            {
                await Task.Delay(TimeSpan.FromSeconds(0.5f));
            }
        }
        else
        {
            Debug.Log("Nothing to compile.");
            Lock();
        }
    }

    [MenuItem(MenuItemPath + "Force Compile %#r")]
    private static void ForceCompile()
    {
        AssetDatabase.Refresh();
        Unlock();
        Debug.Log("Compiling...");
        CompilationPipeline.RequestScriptCompilation();
        // NOTE: 必ずアセンブリのリロードが走るのでstaticコンストラクタでロックされる
    }
}
#endif
```

# 挙動
* プロジェクトに入れるだけでUnityの自動コンパイルを抑制する
* `⌘ + R` もしくは `Tools/Compile Locker/Compile`
  * →必要に応じて通常のコンパイルを走らせる
* `⌘ + shift + R` もしくは `Tools/Compile Locker/Force Compile`
  * →プロジェクト内のスクリプトをすべて強制的にコンパイルする

Windosの場合は `⌘` を `Ctrl` に読み替えてください。

## ショートカットキーが被る
* `⌘ + R` ： `Assets/Reimport` と被る
* `⌘ + shift + R` ：Extenject（Zenject）の `Validate Then Run` と被る

ので、適当に変更したほうがいいです。「被ったら両方実行」とかできないんだろうか。

# ハマったポイント
## コンパイルが走らない１
```.cs
EditorApplication.UnlockReloadAssemblies();
EditorApplication.LockReloadAssemblies();
```

みたいなコードだとコンパイルが走らないので `EditorApplicaion.delayCall` を使って再ロックをちょっと遅らせたり `EditorApplication.update` を１回待ったりしたけどうまくいかなかった。

## コンパイルが走らない２
~~`CompileChecker.cs` を編集した場合は `⌘ + R` でコンパイルしようとしてもなぜかコンパイルが走らず、 `CompileChecker.cs` を先に `Reimport` してから `⌘ + R` する必要がある。謎~~
→ `AssetDatabase.Refresh()` で解決しました。

# 注意
当然再生中もコンパイルが走らなくなります。あと自動コンパイルが走らない挙動はUnityがおかしくなったと勘違いされると思うので、チーム開発ではこのスクリプトを `.gitignore` に登録するのを忘れずに。

# 所感
Assembly Definition（ `.asmdef` ）ファイルと合わせて使うとさらに無駄なコンパイルを減らせそう。

...記事を書き終わってから「Rider上でファイル操作すればいいのでは？🤔」と思った。

# 参考
* [【小技】Unityのコンパイルを無効化する【Unity】](https://raspberly.hateblo.jp/entry/InvalidateUnityCompile)
  * 単純に自動コンパイルをオンオフできるやつ
* [decoc/CompileLocker.cs](https://gist.github.com/decoc/bde047ac7ad8c9bfce7eb408f2712424)（Gist）
  * 同上
* [Force Assembly Recompile](https://forum.unity.com/threads/force-assembly-recompile.651175/#post-5922650)（Unityフォーラム）
* [【Unity】エディタ拡張で async / await を使用する](https://baba-s.hatenablog.com/entry/2020/03/26/090000)（コガネブログ）