UnityのTutorial Frameworkを自作のEditor拡張で使ってみた

「[Applibot Advent Calendar 2023](https://qiita.com/advent-calendar/2023/applibot)」１日目の記事になります。
[去年の記事](https://qiita.com/taroshun_v2/items/149cc988d3c4ff379e32)と同様に今年もトップバッターです！よろしくお願いします！

# はじめに
Unity開発が大規模になるにつれて、開発独自のEditor拡張が増えていきます。
そのため、新しく入る人の学習コストや教える側のコストもどんどん増えていきます。
（ドキュメント化していた場合、さらにメンテナンスコストがかかってしまいます）
もし、そのEditor拡張のチュートリアルを実装すればこれらのコストはかなり軽減できます。

今回はUnityの[Turtorial Framework](https://blog.unity.com/ja/engine-platform/in-editor-tutorials-to-try-in-2021-1)の機能を使って、
独自で作成したEditor拡張のチュートリアルを作りたいと思います。

# 作ったもの
チュートリアルの対象となる独自のEditor拡張を紹介します。
スクリプト名を指定し、その名前のScriptとScriptableObjectファイルを作成する単純な機能になります。
Scriptの中のコードは、元となるテンプレートテキストから生成しています。

<img width=500 alt="EditorTutorial.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/d14ebefb-36c9-026d-87e4-3e072020e1f3.gif">

※アップロードできる容量の問題で画質荒めです。

# 目次
以下の流れで解説していきます。

[実装環境](#実装環境)
[1.Packageをインストール](#1packageをインストール)
[2.Editor拡張作成](#2editor拡張作成)
[3.チュートリアルのテンプレート作成](#3チュートリアルのテンプレート作成)
[4.独自のEditor用に作成してカスタマイズ](#4独自のeditor用に作成してカスタマイズ)


# 実装環境
**バージョン**
2021.3.16f1
**OS**
macOS

# 1.Packageをインストール
Package Managerより、Tutorial Authoring ToolsとTutorial Frameworkそれぞれインストールします。

![スクリーンショット 2023-11-26 22.24.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/22e27ff0-c888-d2ef-078e-802a5e6f042d.png)

# 2.Editor拡張作成
独自で作成したEditor拡張は、
指定した名前のScriptableObjectを継承したクラスで実装しているScriptを生成し、その後ScriptableObjectファイルを生成します。
モンスターのデータ作成を作る時に使うかなと思い、名前はMonsterCreatorにしています。
ここは、主にEditor拡張の作り方なので、興味がない方は[3.チュートリアルのテンプレート作成](#3チュートリアルのテンプレート作成)までスキップしてください。

以下がEditorWindowのScriptになります。
``` MonsterCreatorWindow.cs
using System.IO;
using MonsterAssetCreator;
using UnityEditor;
using UnityEngine;

public class MonsterCreatorWindow : EditorWindow
{
    /// <summary>
    /// 設定
    /// </summary>
    public MonsterCreatorSetting _setting;

    /// <summary>
    /// テンプレートで使うクラス名の置き換え文字列
    /// </summary>
    private static readonly string _CLASS_NAME_VALUE = "#CLASS_NAME#";
        
    [MenuItem("Custom/MonsterCreator")]
    public static void ShowWindow()
    {
        GetWindow<MonsterCreatorWindow>("モンスターのスクリプト作成");
    }

    private void OnGUI()
    {
        _setting = EditorGUILayout.ObjectField("設定", _setting, typeof(MonsterCreatorSetting), true) as MonsterCreatorSetting;

        if (_setting == null)
        {
            return;
        }
        _setting.ScriptName = EditorGUILayout.TextField("スクリプト名", _setting.ScriptName);
        _setting.ScriptTemplate = EditorGUILayout.ObjectField("テンプレート", _setting.ScriptTemplate, typeof(TextAsset), true) as TextAsset;
        _setting.GeneratePath = EditorGUILayout.ObjectField("生成パス", _setting.GeneratePath, typeof(DefaultAsset), true) as DefaultAsset;
        if (GUILayout.Button("①スクリプトの生成"))
        {
            _GenerateScript();
            // GUIを更新
            EditorUtility.SetDirty(this);
        }
    
        bool isExistScript = File.Exists(_setting.GetScriptPath());
        bool isExistFile = File.Exists(_setting.GetScriptableObjectFilePath());

        // 指定したパスにScriptがない場合はGUIのボタンを無効化する
        GUI.enabled = isExistScript;
    
        if (GUILayout.Button("②ScriptableObjectの生成"))
        {
            _GenerateScriptableObject();
        }
        
        GUI.enabled = true;
        // なんとなく状況を表示
        GUILayout.Label(isExistScript?"[o]スクリプト生成済":"[x]スクリプト未生成");
        GUILayout.Label(isExistFile?"[o]ScriptableObjectファイル生成済":"[x]ScriptableObjectファイル未生成");
    }

    /// <summary>
    /// テンプレートのスクリプトの生成
    /// </summary>
    private void _GenerateScript()
    {
        // テンプレートのスクリプトを読み込む
        string templateScript = _setting.ScriptTemplate.text;
        // テンプレートのスクリプトを置き換える
        templateScript = templateScript.Replace(_CLASS_NAME_VALUE, _setting.ScriptName);
        string generateScriptPath = _setting.GetScriptPath();
        // ファイルを生成する
        File.WriteAllText(generateScriptPath, templateScript);
        // 生成したファイルをインポートする
        AssetDatabase.ImportAsset(generateScriptPath);
    }

    /// <summary>
    /// ScriptableObjectのAssetの生成
    /// </summary>
    private void _GenerateScriptableObject()
    {
        // ScriptableObjectのインスタンスを生成する
        ScriptableObject instance = CreateInstance(_setting.ScriptName);
        string generateScriptableObjectPath = _setting.GetScriptableObjectFilePath();
        // ScriptableObjectのインスタンスを保存する
        AssetDatabase.CreateAsset(instance, generateScriptableObjectPath);
        // 生成したファイルをインポートする
        AssetDatabase.ImportAsset(generateScriptableObjectPath);
    }
}
```
EditorWindowを閉じるたびに参照が外れてしまうので、設定用のScriptableObjectも作成しました。
SelectGeneratePathメソッドについては、TutorialFrameworkの機能で外部から参照する機能で実装しています。

``` MonsterCreatorSetting.cs
using UnityEditor;
using UnityEngine;

namespace MonsterAssetCreator
{
    [CreateAssetMenu(menuName = "MonsterAssetCreator/MonsterCreatorSetting")]
    public class MonsterCreatorSetting: ScriptableObject
    {
        /// <summary>
        /// 生成するディレクトリ
        /// </summary>
        public DefaultAsset GeneratePath;

        /// <summary>
        /// スクリプトのテンプレート
        /// </summary>
        public TextAsset ScriptTemplate;
        
        /// <summary>
        /// 作成するスクリプト名
        /// </summary>
        public string ScriptName;
        
        /// <summary>
        /// 生成するScriptableObjectファイルのパス
        /// </summary>
        public string GetScriptableObjectFilePath()
        {
            return $"{AssetDatabase.GetAssetPath(GeneratePath)}/{ScriptName}.asset";
        }
        
        /// <summary>
        /// 生成するScriptのパス
        /// </summary>
        public string GetScriptPath()
        {
            return $"{AssetDatabase.GetAssetPath(GeneratePath)}/{ScriptName}.cs";
        }
        
        /// <summary>
        /// 生成したScriptableObject
        /// ※チュートリアルのScriptableObjectから呼ぶ
        /// </summary>
        public void SelectGeneratePath()
        {
            EditorGUIUtility.PingObject(AssetDatabase.LoadMainAssetAtPath(GetScriptableObjectFilePath()));
        }
    }
}
```
以下は、ScripableObjectのスクリプトを作成するためのテンプレートテキストになります。
かなり単純な内容ですが、ここはみなさんが複製したい処理を入れるなどを行ってください
``` ScriptableObjectTemplate.txt
using UnityEngine;

public class #CLASS_NAME# : ScriptableObject
{
}
```

EditorWindow開くと以下のようになります。
（メニューバーのCustom > MonsterCreatorで開けます）
設定ファイルを設定していないので何も出ない状態です。
まず、設定ファイルを作りましょう。

<img width=500 alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/b12c6581-c907-c30d-ca56-12e8dd3b994f.png">

設定ファイルは作成したい場合は、ProjectWindowで右クリックで以下の操作で作成してください。
Create　> MonsterAssetCreator > MonsterCreatorSetting
次に、ディレクトリの作成を行い、下のようなファイルの構成にしました。

<img width=500 alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/43030638-07ed-fb4b-875b-b0b35dc0cff2.png">

先ほど作成した設定ファイルを参照すると一覧が出ます。
※スクリプトの生成後、コンパイルを走らせてからScriptableObjectの生成ボタンを押してください
<img width=500 alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/e3850590-e3cd-8338-4483-342ae112b164.png">
テンプレートテキストとディレクトリの参照をつけることで準備完了です。
名前を指定して、スクリプトの生成と、ScriptableObjectの生成ボタンをぽちぽちすれば生成されるものになります。

# 3.チュートリアルのテンプレート作成
チュートリアル機能で使用するファイルは、ScriptableObjectファイルになりますになります。
そのチュートリアルのScriptableObjectファイルを作成する用のTutorialsのFolderを作成します。
そのFolder内で
Create > Tutorials > Ready-to-Use TutorialProject
をクリック

![スクリーンショット 2023-11-26 22.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/a9de9cf7-2f96-cf1e-d78d-8fafaf32b48d.png)

チュートリアルに必要なScriptableObjectファイルのテンプレートを作成されます。

![スクリーンショット 2023-11-26 22.27.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/4cd4b8c4-fa2f-46ad-ec4f-6330c57069a7.png)

この状態でTutorialを再生することができます。
メニューバーからTutorials > ShowTutorialsを押してWindowを開きます。

![スクリーンショット 2023-11-26 23.35.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/aae7a3f0-3061-c6ae-e957-5df859d2f739.png)

これだけでも動くので触ってみてください！

<img width=500 alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/c28cb738-3be0-af6b-2245-213cc80ec80b.png">

# 4.独自のEditor用に作成してカスタマイズ

チュートリアルのテンプレートから、自分のEditor拡張Windowを対象にチュートリアルのScriptableObjectファイルを書き換えていきます。
チュートリアルは以下の流れで作っていきます。
[0.トップで概要説明](#0トップで概要説明)
[1.EditorWindowを開く](#１editorwindowを開く)
[2.設定ファイルの選択](#2設定ファイルの選択)
[3.スクリプト名記入](#3スクリプト名記入)
[4.Scriptの生成ボタンを押す](#4scriptの生成ボタンを押す)
[5.ScriptableObjectファイルの生成ボタンを押す](#5scriptableobjectファイルの生成ボタンを押す)
[6.生成したオブジェクトを選択](#6生成したオブジェクトを選択)

設定方法について細かく説明していくと大変なので、修正したパラメータをピックアップして説明していきます。

### 0.トップで概要説明
TutorialWindowで概要を書きます。TutorialContainerを書き換えます。
テンプレートで作った方はTutorialsファイルがそれに当たります。
Sectionsに追加することでTutorialWindowの一覧に表示されるようになります。
今後は、このSectionsにいろんなチュートリアルを追加していくことになると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/71f87795-00db-30a4-2f5d-7f822d535926.png)

実行画面こんな感じです。
（チェックついているのは気にしなくて大丈夫です）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/032abcd2-1cb7-65c9-3469-379a0c9caee5.png)


### １.EditorWindowを開く
ここではWindowを開くように誘導します。
CompletionCriteriaの設定した条件を満たすことで次に進むことができるようになります。
テンプレートで作った方は[5-StartPage]のファイルを編集すれば良いと思います。
MonsterCreatorWindowを開けば進むように設定するため、Criteria内のEditorWindowTypeをMonsterCreatorWindowに設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/86a3a33d-5aec-ff68-75dc-5618b60f6452.png)

Windowを開くことで画像のようなチェックマークがつき、Nextボタンが押せるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/fcf661d4-3caf-0f23-094f-4cf4b967f2c3.png)

### 2.設定ファイルの選択
前述と同様にTutorialPageファイルを編集します。
少し表示する要素が違うので、新しくTutorialPageを作成します。
以下の画像のように、Create > Tutorials > Tutorial Page > Page with Narrativeでページを作成してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/4d38bc2a-99b9-3093-471d-5dd2799fcf10.png)

ここではEnable Maskingにチェックを入れ、その中の設定でTutorialWindowとMonsterCreatorWindowを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/596e6e5f-672e-d838-a9d9-76265c2e5689.png)

実行する2つのWindowにフォーカスするようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/72ecdcb1-768a-430b-fd04-2eeb5a64df0d.png)

### 3.スクリプト名記入
スクリプト名を記入する欄にフォーカスするように設定します。
Selector ModeをGui Style Name、GUI Style Nameは「textfield」にしています。
※複数textfieldが存在するEditor WIndowの場合は、最初と最後、または全部しか選べないので注意

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/15bebb44-1346-f17f-ade0-f49749e420d1.png)
入力欄がフォーカスされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/6e972f48-05fb-37ef-26f3-6cb099d56d42.png)

### 4.Scriptの生成ボタンを押す
ボタンにフォーカスするように設定します。
ほぼ前述と同じ設定で、SelectorModeをGuiContentにして、Textを「①スクリプトの生成」にします。
スクリプト内で指定した名前と同じにする必要があります。
```MonsterCreatorWindow.cs
GUILayout.Button("①スクリプトの生成"))// <-ここの名前と同じ
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/5ce0ab70-f017-2c18-1f78-17467dd43801.png)
実行するとボタンにフォーカスされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/1883926e-2352-b1a5-23b3-4cbe26f4a590.png)

### 5.ScriptableObjectファイルの生成ボタンを押す
前述と同様の設定にして、Textに「②ScriptableObjectの生成」を記入します。
実行すると2つ目のボタンにフォーカスされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/75948292-4ca9-6817-5635-d12b17e20631.png)

### 6.生成したオブジェクトを選択

TutorialsWindowとProjectWindowにフォーカスするように設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/4f6b91f7-f56e-8174-e6d0-55e2b6d55db8.png)
生成したファイルを選択する処理をCustomCallbacksに登録します。
以下のように設定ファイルを参照して、生成したファイルのパスを選択するメソッドを呼ぶようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/9f1dcdb5-511b-43d4-8f67-48ea2b8ea674.png)

実行するとProject Windowにフォーカスしつつ、生成したファイルまで移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/fd0a3d43-7e39-4786-3426-70667e4a47bf.png)

最後に、チュートリアルの流れを設定します。
画像のようにTutorialファイル（テンプレートだと[New Tutorial]ファイル）の設定を変更します。
WindowLayoutのリストに入れた順番でチュートリアルを実行できます。
これでチュートリアルの流れは完成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/78622b23-037d-22ea-a1ad-13df8b4f8acf.png)

# 完成
細かいところは省きましたが、Editor拡張のチュートリアルを実行することができます。
<img width=500 alt="EditorTutorial.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/875049/d14ebefb-36c9-026d-87e4-3e072020e1f3.gif">

# おわりに
今回はEditor拡張のチュートリアル機能について実装しました。
調査期間の都合上、チュートリアルのCallback処理や完了チェックなどの実装はできてないです。
今後、いろんなものを作りながら色々学んでいこうと思います。

ここまで読んで**ドキュメント書けばそんなの実装しなくても良くない？** と思われる方も多いと思います。

しかし、ドキュメント化しても、Unity開発が進むにつれてEditor拡張はさらに改修され、
**作成した当時のドキュメントの内容と異なった状態になってしまい、修正コストがかかってしまいます。**
さらに、作った人が開発しなくなった場合は、**最悪使われなくなってしまいます。** 

チュートリアル機能を作ること自体コストはかかりますが、新しく入る人に対する教えるコストを減らすことや、使用方法を思い出すために利用することもできます。
開発期間が長くなればなるほど、この機能の恩恵が得られると思います。

以上、「[Applibot Advent Calendar 2023](https://qiita.com/advent-calendar/2023/applibot)」 1日目の記事でした！
明日は、@FirstSS-Subさんの記事です！よろしくお願いします！

最後まで読んでいただきありがとうございました。