[Unity] プロジェクト単位でカスタムテンプレートを適用する

前回の記事でUnityのスクリプトテンプレートを書き換える方法を紹介しました。

しかし、上記にはいくつかの問題点が存在します。

 ・インストールされたUnity全体に影響が及ぶ
 ・共同開発では各々のPCで設定する必要あり
 ・1台のPCで異なるテンプレートを併用することが出来ない

テンプレートの変更はインストールされたUnity配下のファイルを弄ることになるので当然といえば当然です。

そこで、上記の問題を払拭するためにUnityプロジェクト個別にテンプレートを適用する方法を考えてみました。

■実現方針
Unityプロジェクトそれぞれに対してテンプレートを適用するので、Assetsディレクトリ配下にテンプレートファイルを置いて、これをスクリプト作成時に用いるようにします。
Unityにはもともとそのような機能は無いため、エディタ拡張機能を用いて実現する形となります。

スクリプト作成時に読み出し処理を行うためには、以下のようにAssetPostprocessorの派生クラスを定義したスクリプトをAssets配下に置いておきます。

using UnityEngine;
using UnityEditor;
using System.Collections;

public class TemplateGenerator : AssetPostprocessor {
    private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromPath) {
        // ここにテンプレート書き換え処理を記述
    }
}

上記のOnPostprocessAllAssets()の第1引数importedAssetsより新規作成されたスクリプトファイルへのパスを取得できます。
ここで厄介なのが、importedAssetsには外部からインポートされたファイルも含むことです。
また、OnPostprocessAllAssets()の呼び出されるタイミングがインポート終了後というのも曲者です。

このままでは新規作成されたのか外部からインポートされたのか判断できません。
外部からインポートされたファイルを書き換えてしまうと大変なので、何とかして新規作成かどうかの判定を行います。

新規作成されたスクリプトはUnityのテンプレートファイルから作成されるため、こちらと中身が一致していれば新規作成かどうかの判定ができます。
ファイル内容比較を新規作成/インポートのたびに行うため、非常に非効率な方法です。
他に思いつく方法が無いので今回はこれで行きます。

テンプレートの読み書きは.NET FrameworkのファイルIOで実現できます。

■導入手順
1.以下パスのディレクトリを丸ごとコピーし、UnityプロジェクトのAssets配下にドラッグします。

Windows
 (Unityのインストールパス)EditorDataResourcesScriptTemplates
Mac
 (Unityのインストールパス)ContentsResourcesScriptTemplates

unity-pj-template

2.以下スクリプトをAssets配下の任意の場所に置きます。

TemplateGenerator.cs

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Globalization;
using System;

public class TemplateGenerator : AssetPostprocessor {
    // 拡張子とテンプレートファイルとの紐付情報
    private static Dictionary<string, string> templateFiles =
    new Dictionary<string, string>() {
        { ".js", "80-Javascript-NewBehaviourScript.js.txt" },
        { ".cs", "81-C# Script-NewBehaviourScript.cs.txt" },
        { ".boo", "82-Boo Script-NewBehaviourScript.boo.txt" },
        { ".shader", "83-Shader-NewShader.shader.txt" },
        { ".compute", "84-Compute Shader-NewComputeShader.compute.txt" },
    };

    // テンプレートファイルの格納ディレクトリパス
    private const string TEMPLATE_DIR = "ScriptTemplates";

    // テンプレート内のシンボル置換定義
    private delegate string Replace(string path);
    private static Dictionary<string, Replace> replaceDef =
    new Dictionary<string, Replace>() {
        { "NAME", (path) => { return Regex.Replace(path, "(.*/)|(\..*$)", ""); } },
        { "SCRIPTNAME", (path) => { return Regex.Replace(path, "(.*/)|(\..*$)|(\s)", ""); } },
        { "SCRIPTNAME_LOWER", (path) => {
            var ti = CultureInfo.CurrentCulture.TextInfo;

            path = Regex.Replace(path, "(.*/)|(\..*$)|(\s)", "");

            if ( Regex.Match(path, "^[A-Z]").Success ) {
                // 先頭が大文字なら小文字に変換
                path = ti.ToLower(path[0]) + path.Substring(1);
            } else if ( Regex.Match(path, "^[a-z]").Success ) {
                // 先頭が小文字なら大文字に変換して「my」を先頭に付加
                path = "my" + ti.ToUpper(path[0]) + path.Substring(1);
            } else {
                // アルファベット以外ならそのまま「my」を先頭に付加
                path = "my" + path;
            }

            return path;
        } },
        // ★必要に応じて置換するシンボルを追加してください
        { "AUTHOR", (path) => { return ""; } },
        { "DATE", (path) => { return DateTime.Now.ToString("yyyy.MM.dd"); } },
    };

    //-------------------------------------------------------------------------
    // テンプレートファイルからファイルを生成する
    //-------------------------------------------------------------------------
    private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromPath) {
        var templateDir = Path.Combine(Application.dataPath, TEMPLATE_DIR);
        var unityTemplateDir = Path.Combine(EditorApplication.applicationContentsPath, "Resources");
        unityTemplateDir = Path.Combine(unityTemplateDir, "ScriptTemplates");

        Debug.Log("unityTemplateDir = " + unityTemplateDir);

        foreach ( var path in importedAssets ) {
            // 拡張子チェック
            string templatePath;
            if ( !templateFiles.TryGetValue(Path.GetExtension(path), out templatePath) ) {
                continue;
            }

            // インポートされたスクリプトとUnity側のテンプレートとの比較

            // Unityテンプレートファイル読み込み
            var unityTemplateData = File.ReadAllText(Path.Combine(unityTemplateDir, templatePath));
            unityTemplateData = ReplaceSymbol(unityTemplateData, path);

            // インポートしたスクリプトファイル読み込み
            var importedData = File.ReadAllText(path);

            // 比較
            if ( string.Compare(unityTemplateData, importedData) != 0 ) {
                // 異なっていたら新規作成でない
                continue;
            }

            // プロジェクト内テンプレートファイル読み込み
            var templateData = File.ReadAllText(Path.Combine(templateDir, templatePath));
            templateData = ReplaceSymbol(templateData, path);

            // プロジェクト内テンプレートファイルのデータで上書き
            var sr = new StreamWriter(path);
            sr.Write(templateData);
            sr.Close();

            // 強制的にインポート
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
        }
    }

    //-------------------------------------------------------------------------
    // #~#のシンボル置換
    //-------------------------------------------------------------------------
    private static string ReplaceSymbol(string text, string path) {
        foreach ( var def in replaceDef ) {
            text = Regex.Replace(text, "#" + def.Key + "#", def.Value(path), RegexOptions.Singleline);
        }

        return text;
    }
}

3.TEMPLATE_DIRに1.のScriptTemplatesディレクトリへのパスを指定します。
Assets直下に置いた場合は変更する必要がありません。

これでAssets配下のScriptTemplatesのテンプレートが適用されるようになります。

必要なら50行目付近に置換したいシンボルを追加してください。
例としてスクリプトの制作者名と作成した日付を定義しています。

以上で導入手順は終わりです。

かなり乱暴な手法となってしまいましたが、プロジェクト個別にテンプレートを設定する方法が無かったため紹介させていただきました。