特定の画像をシーン、プレハブ上から探すEditor拡張

## はじめに
Unityでシーンやプレハブ全体から特定の画像を探したい時、手動や検索は膨大な時間がかかってしまいます。
自分は四角いImageにスプライトを割り当て忘れたため、後々割り当てていないものを探すのが大変になってしまいました。
Editorの拡張を使うとマクロのようにボタンひとつで自動で探し出すことができます。

## コード
早速ですが全体のコードです。
```ReplaceToImageEditor.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;

public class ReplaceToImageEditor : Editor
{
    [MenuItem("Tools/Replace to image")]
    
    static void ApplySettings()
    {
        var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
        var images = new List<Image>();

        if (prefabStage == null)
        {
            images.AddRange(GameObject.FindObjectsOfType<Image>(true));
            Debug.Log("Scene Mode");
        }
        else
        {
            var rootObject = prefabStage.prefabContentsRoot;
            images.AddRange(rootObject.GetComponentsInChildren<Image>(true));
            Debug.Log("Prefab Mode");
        }

        var squareSprite = AssetDatabase.LoadAssetAtPath<Sprite>("Assets/***/square.png");
        if (squareSprite == null)
        {
            Debug.LogError("Failed to load square sprite");
            return;
        }
        
        var validSpriteNames = new List<string> { "しかく", "四角形" }; // ここのリストに追加した名前の画像を置換します
        
        foreach (var obj in images)
        {
            if (img != null)
            {
                if (img.sprite == null)
                {
                    img.sprite = squareSprite;
                    img.type = Image.Type.Sliced;
                    EditorUtility.SetDirty(img);
                    Debug.Log($"Change sprite : square is set to {img.name}.", img);
                }
                else if (img.sprite.name == "squareSprite" && img.type == Image.Type.Simple)
                {
                    img.type = Image.Type.Sliced;
                    Debug.Log($"Change the ImageType of {obj.name} to Sliced", img);
                    EditorUtility.SetDirty(img);
                }
                else if (validSpriteNames.Contains(img.sprite.name))
                {
                    img.sprite = squareSprite;
                    img.type = Image.Type.Sliced;
                    EditorUtility.SetDirty(img);
                    Debug.Log($"Change sprite : square is set to {img.name}.", img);
                }
            }
        }
    }
}
```
細かくみていきます。

#### メニューを作る

```C#
[MenuItem("Tools/Replace to image")]
```
これをクラス内の頭に書くとメニューに表示されるようになります。
```/```で区切ると階層になります。
今回はツールの下に作成しました。

#### 関数の作成
まず関数を作成し、
該当するImageを入れるリストを宣言します。
```GetCurrentPrefabStage()```は開いているプレハブを取得してくれます。
なければnullになります。
```C#
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
var images = new List<Image>();
```

次のif文ではプレハブモードだった場合とそうでない場合（=シーン）で分岐しています。
シーンモードだった場合```GameObject.FindObjectsOfType<Image>(true)```でシーン上のImageを全て探し、リストに全て入れます。引数のtrueは非アクティブのオブジェクトも含めるかです。
プレハブモードだった場合もほぼ同じですが、最初に開いているプレハブのルートオブジェクトを取得し、子の中からImageを探しています。
```C#
if (prefabStage == null)
{
    images.AddRange(GameObject.FindObjectsOfType<Image>(true));
    Debug.Log("Scene Mode");
}
else
{
    var rootObject = prefabStage.prefabContentsRoot;
    images.AddRange(rootObject.GetComponentsInChildren<Image>(true));
    Debug.Log("Prefab Mode");
}
```

ここでは画像を取得し、nullチェックを行なっています。
基本的にスクリプトでアセットを取得するにはResourcesからしか読み込めませんが、Editor内でのみ、Resources外でも読み込むことができます。
以下の形で使います。
```AssetDatabase.LoadAssetAtPath<探したい型>("パス");```

```C#
var squareSprite = AssetDatabase.LoadAssetAtPath<Sprite>("Assets/***/square.png");
if (squareSprite == null)
{
    Debug.LogError("Failed to load square sprite");
    return;
}
```
このリストは類似した画像を探したいとき、リストに画像の名前を追加すると引っ掛かるようにするために作成しました。
``` C#
var validSpriteNames = new List<string> { "しかく", "四角形" };
```

最後、foreachでひとつひとつ取得できた画像を見ていきます。
1.スプライトが割り当てられていない場合
2.割り当てたいスプライトではあるがイメージタイプがSimpleになっている場合
3.先ほどの類似した画像に当てはまった場合
の3パターンで場合分けしています。
ここはやりたい内容によって調節してください。
1では画像が割り当てられていないので割り当てたい画像（squareSprite）を入れています。
Image TypeがSimpleだと小さい画像を引き伸ばして使った時に端がぼやけてしまいます。Slicedした画像を使うと引き伸ばされないためSlicedに変更しています。
```C#
EditorUtility.SetDirty(obj);
```
↑これを行わないと変更を保存できません。
保存まで行ってくれるメソッドもありますが、合っているかを確認して保存は手動にしたいため変更を出すようにこちらを使いました。

Logの第二引数にはGameObjectを入れることができます。
出てきたログをクリックすると該当のオブジェクトがヒエラルキーのどこにあるのか強調表示してくれます。
いちいち探す手間が省けるのでとても便利ですね。

2ではImage Typeを変更しています。

3も画像を変更し、Image Typeを変更しています。

```C#
foreach (var obj in images)
{
    if (obj != null)
    {
        if (obj.sprite == null)
        {
            obj.sprite = squareSprite;
            obj.type = Image.Type.Sliced;
            EditorUtility.SetDirty(obj);
            Debug.Log($"Change sprite : square is set to {obj.name}.", obj);
        }
        else if (obj.sprite.name == "squareSprite" && obj.type == Image.Type.Simple)
        {
            obj.type = Image.Type.Sliced;
            Debug.Log($"Change the ImageType of {obj.name} to Sliced", img);
            EditorUtility.SetDirty(obj);
        }
        else if (validSpriteNames.Contains(img.sprite.name))
        {
            obj.sprite = squareSprite;
            obj.type = Image.Type.Sliced;
            EditorUtility.SetDirty(obj);
            Debug.Log($"Change sprite : square is set to {img.name}.", obj);
        }
    }
}
```

## おわりに
今回初めてEditor拡張でツールを実装してみました。
今までゴリ押しで探していましたが一気に手間が省ける上、一つのスクリプトで作れてしまうので今後もマクロ感覚で作ってみようと思います！