【Unity】ShaderのプロパティのID変数を自動で作るEditor拡張を作ってみた

# はじめに
UnityでShaderを作成し、マテリアルに値を渡す時のこのようなコードを書くと思います
```C#
public static readonly int MAIN_TEX_ID = Shader.PropertyToID("_MainTex");
```
プロパティが2、3個なら良いのですがたくさんあった場合に変数宣言したり、`"_MainTex"`を間違えないように打つのがめんどくさいので変数の宣言を自動で生成して丸々クリップボードに保存したり、クラスとして保存できるEditor拡張を作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/5bb195b4-8fab-c524-2024-683e5ecbcb29.png)
ProjectウィンドウでShaderを右クリックして`ShaderPropertyClass`をクリックする専用ウィンドウが出ます。
ここで保存したいプロパティを選び、クリップボードに保存するか、クラスにできます。

# ソースコード
https://github.com/ayaha401/ShaderPropertyClassGenerator 

# 解説
## 選択したものがShaderか判断する
右クリックしたデータがShaderだった時だけ`ShaderPropertyClass`というウィンドウを出現できるようにしたいので工夫します。

```C#
/// <summary>
/// 選択したものがShaderか調べる
/// </summary>
[MenuItem("Assets/Create/ShaderPropertyClass", true)]
private static bool CheckSelectObjectIsShader()
{
    return Selection.activeObject is Shader;
}
```
`MenuItem`は引数の`isValidateFunction`をTrueにすることでメニューを呼び出せるかの判定用メソッドを作ることができます。[ドキュメント](https://docs.unity3d.com/ja/2020.3/ScriptReference/MenuItem-ctor.html)
このメソッドは同じitemNameを持つメニュー関数を呼び出す前に呼び出されます。なので
```C#
[MenuItem("Assets/Create/ShaderPropertyClass", priority = 83)]
private static void ShaderPropertyClass()
{
}
```
これを呼び出す前に判定を行うことができます。
判定は
```C#
return Selection.activeObject is Shader;
```
これです。選択したオブジェクトがShaderかを判断できます。

## Shaderの項目の近くにMenuを置きたい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/98c42ac0-020a-71fc-ae0b-28ea64483658.png)
使いやすさを考えてこのようにShaderの項目の近くに配置したいです。
```C#
[MenuItem("Assets/Create/ShaderPropertyClass", priority = 83)]
```
`priority`の値でMenuのどこに作成するのか決められます。
ここの値はこちらの方法でUnityのすべてのMenuのpriorityの値を確認し、いい感じの値にします。

https://qiita.com/ayaha401/items/4ecbe72d4f7941b643e9

## Shaderのプロパティを取得する
Shaderに含まれているプロパティの個数や、名前を取得する方法です。
どちらも取得できるメソッドがUnityから用意されているので簡単です。
まずはプロパティの個数です。こちらは
```C#
int propertyCount = ShaderUtil.GetPropertyCount(shader);
```
これで取得可能です。
続いてプロパティ名は
```C#
string propertyName = ShaderUtil.GetPropertyName(_shader, i);
```
i番目の名前を取得可能です。

## ペーストするだけで使えるように組み立てる
```C#
public static readonly int MAIN_TEX_ID = Shader.PropertyToID("_MainTex");
```
これにできるように取得したデータを加工します。
`static readonly`なので大体`const`なので変数名を大文字に置き換えます。
```C#
/// <summary>
/// constの命名規則に合わせた命名を作成する
/// </summary>
/// <param name="input">Property名</param>
private string ConvertToConstantName(string input)
{
    input = input.TrimStart('_');
    string result = Regex.Replace(input, "(?<=.)([A-Z])", "_$1");
    return result.ToUpper();
}
```
プロパティ名の最初の`_`は不必要なので削除し、大文字と小文字の間に`_`を差し込み、全部大文字変換します。
これで`_MainTex`が`MAIN_TEX`になります。

```C#
string displayText = $"{_indent}public static readonly int {ConvertToConstantName(propertyName)}_ID = Shader.PropertyToID(\"{propertyName}\");";
```
あとはStringで組み立てます。

クリップボードに保存する方法はこちらです。
```C#
GUIUtility.systemCopyBuffer = _resultText;
```

## クラスを作成する
変数は上の方法で作成されているので他の部分をStringで頑張って組み立てます。
```C#
string className = _className;
StringBuilder classBuilder = new StringBuilder();
classBuilder.AppendLine("using UnityEngine;");
classBuilder.AppendLine("public static class " + className);
classBuilder.AppendLine("{");
classBuilder.AppendLine(_resultText);
classBuilder.AppendLine("}");
```
`StringBuilder`使うとStringで文字を合成するより早く合成できるので使います。

クラスを保存するPathは選択しているShaderと同じ場所に置きます。
```C#
string selectPath = AssetDatabase.GetAssetPath(_shader);
_savePath = Path.GetDirectoryName(selectPath);

string classPath = Path.Combine(_savePath, className + ".cs");
File.WriteAllText(classPath, classBuilder.ToString());
AssetDatabase.Refresh();
```
選択したShaderからPathを取得し、そこからディレクトリだけにしたものにクラス名をつっくけて保存します。

# おわりに
こちらのEditor拡張を使うことでめんどくさかったプロパティIDの宣言が爆速で終わるようになりました。
必要なプロパティだけチェックボックスで選択できるので途中でプロパティが増えても適切にコピペできます。