Unityにインポートされたアセットの名前がルールに沿っているか判定する

[PONOS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ponos)の18日目の記事です。

昨日は@nisei275さんの[RustのゲームエンジンBevyに触れてみる - Qiita]
(https://qiita.com/nisei275/items/7daf8542f7a4ab4b0fb8)でした。

# はじめに

私達のチームではUnityエディタ上の視認性向上のため、**アセットの名前をパスカルケースで命名する**というルールを定めています。

しかし、アセットの命名は人間の判断で行われるため、時には不適切な名付けをしてしまうこともあります。
（キャメルケースにしてしまったり、名前に空白を含めてしまったり…）
プルリクエストの際に名前が適切かをチェックすることも可能なのですが、できたら他の人に指摘される前に自身で間違いに気づいて修正しておきたいものです。

そこで今回は、作業者がこのルールを守りやすくするための環境をエディタ拡張で実現できないか考えてみました。

確認環境は`Unity 2020.3.4f1`です。

# 作成する拡張機能を考える

作業者が一番楽なのは、「アセット名が適切でない場合はプログラムで自動的に名前を適切なものに変更する」のような機能ですが、機械的に名前を変更すると名前の意味が変わってしまう恐れがあります。

なので、作業者自身がリネームする手間はありますが、今回は**「アセット名が適切でない場合はエラーログを出力する」**という機能を実装して、誤った名前をつけていることに気づきやすい環境を目指してみます。
アセット名を判定するタイミングは、アセットがUnityにインポートされたタイミング（リネームも含む）が良いでしょう。

# インポートされたアセットの情報を取得する

Unityにインポートされたアセットの情報は、`AssetPostprocessor`クラスを利用することで取得できます。
[Unity - Scripting API: AssetPostprocessor]
(https://docs.unity3d.com/2020.3/Documentation/ScriptReference/AssetPostprocessor.html)

`AssetPostprocessor`には「テクスチャのインポート後に処理する」「プレハブのインポート後に処理する」など目的に応じた複数のメッセージが用意されていますが、今回はその中から`OnPostprocessAllAssets`を採用します。
`OnPostprocessAllAssets`はすべてのアセットのインポート処理が終了したときに呼ばれるメッセージです。
[Unity - Scripting API: AssetPostprocessor.OnPostprocessAllAssets(string[],string[],string[],string[])](
https://docs.unity3d.com/2020.3/Documentation/ScriptReference/AssetPostprocessor.OnPostprocessAllAssets.html)

使用するには`AssetPostprocessor`クラスを継承したクラスを用意し、`OnPostprocessAllAssets()`メソッドを実装します。

```cs
public class AssetNameValidator : AssetPostprocessor
{
    static void OnPostprocessAllAssets(
        string[] importedAssets, 
        string[] deletedAssets, 
        string[] movedAssets, 
        string[] movedFromAssetPaths)
    {
        // ここでアセットのパスを判定する。    　　　　　　　
    }
}
```

あとは、このメソッドに渡されるパスの判定処理を実装すれば、適切でないアセット名に対してエラーログを出力する機能が実装できます。

# コード全文

説明は以上です。コードの全文は以下のようになります。

```cs
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;

/// <summary>
/// インポートされたアセットの名前が適切かチェックするためのクラス。
/// AssetPostprocessorを継承したこのクラスでアセットのインポート後のイベントを受け取ります。
/// </summary>
public class AssetNameValidator : AssetPostprocessor
{
    /// <summary>
    /// すべてのアセットのインポート処理が終了したときに実行されます。
    /// </summary>
    /// <param name="importedAssets">インポートされたアセットのパスの配列。</param>
    /// <param name="deletedAssets">削除されたアセットのパスの配列。</param>
    /// <param name="movedAssets">移動したアセットのパスの配列。</param>
    /// <param name="movedFromAssetPaths">移動したアセットの移動前のパスの配列。</param>
    static void OnPostprocessAllAssets(
        string[] importedAssets, 
        string[] deletedAssets, 
        string[] movedAssets, 
        string[] movedFromAssetPaths)
    {
        // 判定対象のアセットのパスのコレクションを作成する。
        // 同じアセットのパスが複数回含まれる可能性があるため、重複を避けるためHashSetを使用する。
        // 削除されたアセットや、移動前のアセットのパスは判定不要なので除外する。
        HashSet<string> assetPaths = new HashSet<string>();
        assetPaths.UnionWith(importedAssets);
        assetPaths.UnionWith(movedAssets);

        foreach (var assetPath in assetPaths)
        {
            var assetName = Path.GetFileNameWithoutExtension(assetPath);
            if (!IsValidAssetName(assetName))
            {
                Debug.LogError($"\"{assetPath}\"の名前は適切ではありません。");
            }
        }
    }

    /// <summary>
    /// アセットの名前が適切か判定します。
    /// 今回はパスカルケース（っぽい）名前になっているかどうかの判定を実装しています。
    /// ・最初の文字が大文字
    /// ・ハイフンやアンダースコアで文字列が連結されていない
    /// ・名前に空白が含まれていない
    /// </summary>
    /// <param name="assetName">判定対象のアセットの名前。</param>
    /// <returns>適切な場合、true。</returns>
    static bool IsValidAssetName(string assetName)
    {
        // 空白、ハイフン、アンダースコアが含まれているのはNG。
        if (0 <= assetName.IndexOfAny(new char[] { ' ', '-', '_' }))
        {
            return false;
        }

        // 名前の最初が大文字でないとNG。
        if (char.IsLower(assetName[0]))
        {
            return false;
        }

        return true;
    }
}
```

不適切な名前のアセットがインポートされると以下のようにエラーログが出力されます。

![assetname_validation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/5b78c53d-669c-49ef-cd44-8a233ed6e1f3.png)
（分かりづらいのですが、`GoodMaterial`は名前が適切なため、エラーログが出ていません）

# まとめ

ルールって作るのは簡単ですが、守るのは大変ですよね。
作業者がルールを守りやすい環境を作ることも意識していきたいものです。

明日は@aibausoundさんです！