見出し画像

【Unity】シーンのテンプレートを作る【エディタ拡張】

こんにちは。
最近少しずつUnityを使い始めました。
しかしなかなかゲームという感じには至らず、それよりも仕事の癖というか何かを自動化しようとついそちらのほうを調べてしまう。

そういう訳で今回はUnityでゲームを制作する際に何度も使う(らしい)シーンの作成にテンプレートを用意することを考えてみました。

■やりたいこと

・シーンを作成する際に既定のGameObject追加する
・既定のC#Scriptファイル(CS)を生成する
・GameObjectにCSをアタッチする
・GameObjectを規定のPrefab化する

■制作した環境

・Win10
・Unity Version 2019.4.10f1

■ソースコード

// GenerateTemplateScene.cs

using System;
using System.IO;
using System.Collections.Generic;
using UnityEditor.SceneManagement;
using UnityEditor.Callbacks;

using UnityEditor;
using UnityEngine;
using System.Reflection;

namespace gyaricsontool.editorextension
{
   public class GenerateTemplateScene : EditorWindow
   {
       static int instanceID = 0;
       static string currentPath { get; set; } = "";     // アクティブインスタンスのパス
       static string currentName { get; set; } = "";     // アクティブインスタンスの名前(フォルダ名)


       [MenuItem("Assets/Create/テンプレートシーン", priority = 0)]     // コンテキストメニューから選択(Project
       private static void GenerateFromContextMenu()
       {
           createTemplatePackage();
       }


       /// <summary>
       /// 
       /// </summary>
       private static void createTemplatePackage()
       {
           // 使用する情報を取得
           instanceID = Selection.activeInstanceID;
           currentPath = AssetDatabase.GetAssetPath(instanceID);
           currentName = currentPath.Split('/')[currentPath.Split('/').Length-1];  // カレントのフォルダ名
           currentName = currentName.Replace(" ", "_");    // [ ]を[_]に置換(スクリプトファイル名などで使用するため)

           // シーンを作成
           //Debug.Log($"ID:{instanceID}/path:{currentPath}/Name:{currentName}");
           CreateScene(currentName);
           // フォルダを作成
           IEnumerable<string> folders = new List<string>{"Scripts", "Prefabs"};   // 作成したいフォルダ名群
           foreach (var name in folders)
           {
               AssetDatabase.CreateFolder(currentPath, name);
           }
           // スクリプトを作成
           GenerateScripts(currentName + "TempA", TemplateA);
           GenerateScripts(currentName + "TempB", TemplateB);

           // 機能実行を記憶(クリップボードを利用)
           GUIUtility.systemCopyBuffer = "OK@" + currentName;
       }

       /// <summary>
       /// 新しくシーンを作成する 
       /// </summary>
       /// <param name="baseName">作成するシーンの名前</param>
       private static void CreateScene(string baseName = "Template")
       {
           var sceneName = baseName + ".unity";
           var scenePath = GetCurrentPath() + "/" + sceneName;
           var path = AssetDatabase.GenerateUniqueAssetPath(scenePath);
           var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
           EditorSceneManager.SaveScene(scene, path);
       }

       /// <summary>
       /// UnityEditor上のProjectタブのカレントパスを取得する 
       /// </summary>
       /// <returns>フォルダのパス or string.Empty</returns>
       private static string GetCurrentPath()
       {
           var instanceID = Selection.activeInstanceID;    // 現在選択しているアイテムのインスタンスID
           var instancePath = AssetDatabase.GetAssetPath(instanceID);
           var currentPath = string.IsNullOrEmpty(instancePath) ? "Assets" : instancePath; // 対象インスタンスのパスが空の場合はAssetsをパスに使用する

           // フォルダを選択している場合はここでリターン
           if (Directory.Exists(currentPath)) return currentPath;

           // ファイルを選択している場合はそのファイルの場所を見つけてリターン
           if(File.Exists(instancePath))
           {
               var parent = Directory.GetParent(instancePath);
               var fullName = parent.FullName;
               var uniName = fullName.Replace("\\", "/");  // UnityEditor上の名前
               return FileUtil.GetProjectRelativePath(uniName);
           }

           // 迷子
           return string.Empty;
       }

       /// <summary>
       /// 
       /// </summary>
       /// <param name="scriptName">作成するスクリプトファイルの名前</param>
       /// <param name="scriptSource">作成するスクリプトのソース</param>
       private static void GenerateScripts(string scriptName, string scriptSource)
       {
           var filePath = currentPath + "/Scripts/" + scriptName + ".cs";
           var scriptPath = AssetDatabase.GenerateUniqueAssetPath(filePath);
           var script = scriptSource.Replace(@"#TEMPLATENAME#", scriptName);

           // スクリプトファイルに書き込み
           File.WriteAllText(scriptPath, script, System.Text.Encoding.UTF8);

           // エディタ更新
           AssetDatabase.Refresh();
       }

       /// <summary>
       /// 
       /// </summary>
       [DidReloadScripts][Obsolete]
       private static void AttachScriptsAndToPrefab()
       {
           // 本スクリプトでのシーン作成を行った直後かどうかをチェック(クリップボード利用)
           if (GUIUtility.systemCopyBuffer.Split('@')[0] != "OK") return;
           
           // クリップボードから情報を取得
           currentName = GUIUtility.systemCopyBuffer.Split('@')[1];

           // クリップボードクリア
           GUIUtility.systemCopyBuffer = "";

           // GameObjectを作成
           var objA = new GameObject(currentName + "TempA");
           var objB = new GameObject(currentName + "TempB");

           // Scriptをアタッチ
           objA.AddComponentExt(currentName + "TempA");
           objB.AddComponentExt("templateB." + currentName + "TempB");

           // GameObjectをPrefab化
           var prefabA = PrefabUtility.SaveAsPrefabAsset(objA, "Assets/Scenes/" + currentName + "/prefabs/" + objA.name + ".prefab");
           var prefabB = PrefabUtility.SaveAsPrefabAsset(objB, "Assets/Scenes/" + currentName + "/prefabs/" + objB.name + ".prefab");
           // GameObjectをPrefabに適用
           _ = PrefabUtility.ConnectGameObjectToPrefab(objA, prefabA);
           _ = PrefabUtility.ConnectGameObjectToPrefab(objB, prefabB);
           
           Debug.Log($"{currentName} Scene Created!");
       }

       // 作成するスクリプトファイル:TemplateA
       private static readonly string TemplateA = @"using UnityEditor;
using UnityEngine;

public class #TEMPLATENAME# : MonoBehaviour
{
   void Start()
   {
   }
   //void myfunction()
   //{
   //}
}

";
       // 作成するスクリプトファイル:TemplateB
       private static readonly string TemplateB = @"using UnityEditor;
using UnityEngine;

namespace templateB
{
   public class #TEMPLATENAME# : MonoBehaviour
   {
       void Start()
       {
       }
       //void myfunction()
       //{
       //}
   }
}
";


   }

}

/// <summary>
/// 
/// </summary>
public static class AddComponentExtension
{
   public static Component AddComponentExt(this GameObject obj, string scriptName)
   {
       Assembly asm = Assembly.Load("Assembly-CSharp");

       var type = asm?.GetType(scriptName) ?? null;

       if (type == null)
       {
           Debug.LogError($"Failed to ComponentType:{scriptName}");
           return null;
       }
       
       return obj.AddComponent(type);
   }

}

■解説

・実行方法
このスクリプトをUnityProjectのAssetsフォルダ以下の[Editor]フォルダ内に保存する。
プロジェクトウィンドウ内で右クリックをし、コンテキストメニューから
[Create]→[テンプレートシーン]を選択する(日本語化している場合は[作成]→[テンプレートシーン]。

画像1

表示位置は下記部分のpriorityの値を変更することで移動します。
※どうやらこの数値はUnity内のメニューすべてで共通?しているようなので、1や2にしても表示されている位置を思ったように調整できないもよう。

[MenuItem("Assets/Create/テンプレートシーン", priority = 0)]

・実行結果

画像2

Zombieというフォルダでテンプレートを作成した場合はこういう感じになる。
シーン内にGameObjectが配置され、それにはスクリプトがアタッチされている。

・動作

そのフォルダ内にそのフォルダ名を使ったテンプレートシーンとゲームオブジェクト、スクリプトが作成されます。
この部分が曲者で、最初は作成したScriptをそのままAddComponentすればよいと思ったが、どうやらそれは出来ないらしい。
詳しいことはまだよくわかってないが、Unityが追加したスクリプトのComponentTypeを参照できるためにはコンパイルが必要らしい。そしてコンパイルするとスクリプトで持っている変数の値が揮発してしまう。それを回避するためにテンプレート名などは一時的にクリップボードに退避している。
Unityはファイルが追加されたことで自動的にコンパイルが走るので、コンパイル後にさらにスクリプトを実行するようにしている。

[DidReloadScripts][Obsolete]
private static void AttachScriptsAndToPrefab()

[DidReloadScripts]は読んで字のごとく、スクリプトがリロードされたときに実行する属性を付与する。
つまり、AttachScriptsAndToPrefab() までが一連の処理ということになる。

・その他
テンプレートとなるスクリプトには、namespaceを使ったパターンとそうでないパターンを用意しているので改変する場合は参考にしてください。

■今後のこと

今回のスクリプトでは、変数情報をクリップボードに退避し利用しているが、これはテンポラリファイルかUnityのユーザー設定に保存したほうがいいかも知れない。
UnityやC#でのセオリーとしてはなにが一般的なのだろうか…。
テンプレートとなるスクリプトについてもソースコード内に文字列として保持しているが、これもテキストファイルとして用意するか、UIパネル上で自由に書き換えができたほうがいいかも知れない。
また、今回はスクリプトのみだが、テンプレートの種類を自由に選択できるようにするならば、fbxやmatのプレハブを指定できたほうが便利かも知れない。
ソースコードも、2種類のクラスを書いてしまっているのでソースファイルを分けたほうが正しいと思う。

あと、noteにソースを載せることがやはり見づらいのでQiitaを利用するか、Gitに公開するかを考えたほうがいい気もする。


■参考

Unity:未知のスクリプトをGameObject(カスタムエディター)に動的にアタッチする方法
この記事がなければ完成しなかった。
が、コンパイル後でないと参照できないということになかなか気づけず、何度もRefreshの呼び場所が悪いのかと四苦八苦した。

・【Unity】【エディタ拡張】スクリプトからスクリプトファイル(.cs)を生成する
C#初心者なので、この記事で文字列先頭の@の意味を知った。というかそういう記述方法を初めて見た。

この記事が気に入ったらサポートをしてみませんか?