Amazonで商品を見るセール会場へ

Unityのエディタ拡張|一括リネーム・Undo・グループ化と共有方法

Unityのエディタ拡張は、選択した対象にどの変更を加えるかを決めてから使います。一括リネームは配布ツール、コンポーネント削除やグループ化は条件を絞ったサンプル、複数プロジェクトでの共有はUPMパッケージという順で紹介します。

APIの参照版はUnity 6.3 LTS(6000.3)、UIのOutlineはuGUIです。掲載した修正版はUnity Editorでのコンパイル・実行未確認です。PNG保存先の検証部分だけを.NETで確認しました。既存画像は旧版の操作資料で、今回のコードを動かした画像ではありません。配布ツールのUnity 6.3での動作も未確認です。

目次

目的から選ぶエディタ拡張

目的・症状選び方・確認先
名前を一括変更Mulligan Renamer。置換・接頭辞・接尾辞・連番をプレビューして対象を確認する。
uGUIのOutlineを削除下のUndo付きサンプル。選択オブジェクト自身だけ、子は対象外。TMPのMaterialの輪郭は別機能。
3DのMaterialを割り当て保存済みMaterialをRendererの先頭スロットへ。uGUIのMask/RectMask2Dとは別。
GameビューをPNGへ保存Play中にキャプチャを要求。ファイルとピクセル寸法を別途確認する。
Hierarchyをまとめる同じ親・同じSceneの通常Transformだけを新しいGroupの子にする。UIとPrefabは対象外。
Prefabの一覧・画像・参照個別のCSV・PNG・参照グラフの記事へ。Sceneの編集処理と混ぜない。

導入前に確認する対象・版・Undo

  1. 最初は複製した検証用Sceneに少数の通常GameObjectを作り、保存します。実プロジェクトでは変更前の状態をバージョン管理などへ残します。
  2. 下の4つの.csファイルは最初にAssets/Editorへ置きます。CgmAssignMaterialとCgmGroupSelectedはCgmRemoveOutline.cs内のCgmSceneSelectionに依存します。同名の旧クラスを残して重複させないでください。
  3. Unity 6.3とuGUIを使う想定です。Consoleのコンパイルエラーを解消し、Tools/CG Methodにメニューが出ることを確認します。古い版へ流用する場合は、特にUndo.SetSiblingIndexなどのAPIを当該版の公式資料で確認します。
  4. 削除・Material割当・グループ化はPlay前、Hierarchyで選んだ通常のSceneオブジェクトが対象です。Prefab Asset/Instance、Prefab Mode、Preview Sceneは拒否します。混在時は全体を止めます。
  5. 実行後に対象数と見た目を確認し、Ctrl+Z(macOSは通常Command+Z)で1操作を戻せるか試します。期待どおりならSceneを保存し、再度開いて状態を確認します。PNGファイルの書き出し自体はUndoでは戻りません。

選択対象はSelection.gameObjects、Assetの判定はIsPersistentを参照します。以下の例はPrefabのOverride編集に対応する汎用ツールではありません。

一括リネーム:接頭辞・接尾辞と連番

元記事では接頭辞・接尾辞を追加する拡張とSiblingIndexを名前へ付ける拡張を紹介していました。どちらも全文コードや現在の配布先を追跡できないため、旧画像を新しいツールの導入手順として扱いません。再現可能な配布先は後述のMulligan Renamerです。

次は旧接頭辞・接尾辞ツールの動作資料です。子も処理するか、選択した対象だけかはツールごとに異なります。

リネームの動作

次は旧連番ツールです。SiblingIndexは同じ親の子の位置であり、選択数に応じた0・1・2の連番とは一致しません。間に未選択の子があれば番号が飛び、別の親では同じ番号が出ます。階層の並び順を名前へ固定したい用途と、選択リストへ連番を付けたい用途を分けます。

連番追加の動作

Undo付きで選択したOutlineを削除する

CgmRemoveOutline.csを保存します。選択オブジェクト自身のUnityEngine.UI.Outlineをすべて削除します。子を再帰的に検索しません。Outlineがない対象は変更せず、削除数をConsoleへ出します。未定義のNewBehaviourScriptをそのまま参照する旧コードは使いません。

using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;

// Assets/Editor/CgmRemoveOutline.cs
public static class CgmSceneSelection
{
    public static GameObject[] Get()
    {
        if (EditorApplication.isPlayingOrWillChangePlaymode)
            throw new InvalidOperationException("Exit Play Mode first.");
        GameObject[] objects = Selection.gameObjects;
        if (objects.Length == 0)
            throw new InvalidOperationException("Select Scene GameObjects in Hierarchy.");
        var stage = PrefabStageUtility.GetCurrentPrefabStage();
        foreach (GameObject obj in objects)
        {
            if (EditorUtility.IsPersistent(obj) || !obj.scene.IsValid() || !obj.scene.isLoaded ||
                EditorSceneManager.IsPreviewSceneObject(obj) ||
                (stage != null && obj.scene == stage.scene) || PrefabUtility.IsPartOfAnyPrefab(obj))
                throw new InvalidOperationException("Only ordinary Scene objects are supported: " + obj.name);
        }
        return objects;
    }

    public static void Change(string name, Action action)
    {
        Undo.IncrementCurrentGroup();
        int group = Undo.GetCurrentGroup();
        Undo.SetCurrentGroupName(name);
        try { action(); Undo.CollapseUndoOperations(group); }
        catch { Undo.RevertAllDownToGroup(group); throw; }
    }
}

public static class CgmRemoveOutline
{
    [MenuItem("Tools/CG Method/Remove Selected UI Outlines")]
    private static void Remove()
    {
        try
        {
            GameObject[] objects = CgmSceneSelection.Get();
            int removed = 0;
            CgmSceneSelection.Change("Remove UI Outlines", () =>
            {
                foreach (GameObject obj in objects)
                    foreach (Outline outline in obj.GetComponents<Outline>())
                    { Undo.DestroyObjectImmediate(outline); removed++; }
            });
            Debug.Log("Removed UI Outline components: " + removed);
        }
        catch (Exception e) { Debug.LogException(e); }
    }
}
  1. 通常のSceneにuGUIオブジェクトを2つ作り、片方にOutlineを付けます。両方を選択してTools/CG Method/Remove Selected UI Outlinesを実行します。
  2. Outlineが外れ、Consoleの削除数が1になることを確認します。TMPの輪郭や別のコンポーネントが消えていないことも確認します。
  3. UndoでOutlineの設定ごと戻るか確認します。Prefab Instanceを選んだときは拒否されることも試します。

破棄はUndo.DestroyObjectImmediateで記録します。対象を即座に消せるDestroyImmediateだけでは、編集操作のUndoを提供できません。この例の削除範囲をプロジェクト全体へ拡大する処理はありません。

Material割当:3D RendererとuGUIのMaskを分ける

旧例はMonoBehaviour.Startで「Mask」タグのRendererへUnlit/ColorのMaterialを生成する実行時コードでした。Editor拡張でもuGUIのMask設定でもありません。ここではすでに保存されたMaterial Assetを、選択したRendererの先頭スロットへ割り当てるEditorWindowに置き換えます。Shaderをコードから推測せず、Built-in/URP/HDRPに合うMaterialを先にInspectorで作ってください。

using System;
using UnityEditor;
using UnityEngine;

// Also requires CgmSceneSelection from CgmRemoveOutline.cs.
public class CgmAssignMaterial : EditorWindow
{
    private Material material;

    [MenuItem("Tools/CG Method/Assign First Renderer Material")]
    private static void Open() { GetWindow<CgmAssignMaterial>("Assign Material"); }

    private void OnGUI()
    {
        EditorGUILayout.HelpBox("Select ordinary Scene objects. Only material slot 0 changes.", MessageType.Info);
        material = (Material)EditorGUILayout.ObjectField("Material Asset", material, typeof(Material), false);
        if (GUILayout.Button("Assign To Selected Renderers")) Assign();
    }

    private void Assign()
    {
        try
        {
            if (material == null || !EditorUtility.IsPersistent(material))
                throw new InvalidOperationException("Choose a saved Material asset.");
            GameObject[] objects = CgmSceneSelection.Get();
            foreach (GameObject obj in objects)
            {
                Renderer renderer = obj.GetComponent<Renderer>();
                if (renderer == null || renderer.sharedMaterials.Length == 0)
                    throw new InvalidOperationException("Renderer with a material slot is required: " + obj.name);
            }
            CgmSceneSelection.Change("Assign Material", () =>
            {
                foreach (GameObject obj in objects)
                {
                    Renderer renderer = obj.GetComponent<Renderer>();
                    Undo.RecordObject(renderer, "Assign Material");
                    Material[] slots = renderer.sharedMaterials;
                    slots[0] = material;
                    renderer.sharedMaterials = slots;
                }
            });
            Debug.Log("Updated Renderer slot 0: " + objects.Length);
        }
        catch (Exception e) { Debug.LogException(e); }
    }
}
  1. CgmAssignMaterial.csと共通ヘルパーをAssets/Editorに保存し、Tools/CG Method/Assign First Renderer Materialを開きます。
  2. Material Asset欄へ使うMaterialをドラッグします。その後Hierarchyで変更したい通常SceneのMeshRenderer等を選びます。
  3. Assign To Selected Renderersを押し、各RendererのMaterialsのElement 0だけが変わることを確認します。後続スロット・Material Assetの色などは編集しません。
  4. Undoで元の参照へ戻るか確認します。Rendererがない、スロットが0個、Prefabが混ざる場合は全体を止めます。ピンク表示ならRender PipelineとShaderの対応を先に確認します。

uGUIのMask/RectMask2DやImageを変更する用途にはこのコードを使いません。Materialを共有すると複数Rendererが同じAssetを参照するため、そのAssetの色を後で変えると他の使用箇所にも影響します。旧記事のタグ、Shader.Find、毎回new Materialする手順をここへ混ぜないでください。

GameビューをPNGへ保存する

CgmCaptureGameView.csはPlay中・非PauseのときだけScreenCaptureを呼びます。Unity内部のGameView型をReflectionで開く処理と、ファイル保存が終わったかのような即時ログを除去しました。これはGameビューのキャプチャ要求であり、特定Cameraだけの描画やPrefabの透過PNGではありません。

using System;
using System.IO;
using UnityEditor;
using UnityEngine;

public static class CgmScreenshotPath
{
    public static string Validate(string path)
    {
        if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path))
            throw new ArgumentException("An absolute PNG path is required.");
        string full = Path.GetFullPath(path);
        if (!string.Equals(Path.GetExtension(full), ".png", StringComparison.OrdinalIgnoreCase))
            throw new ArgumentException("Use the .png extension.");
        if (!Directory.Exists(Path.GetDirectoryName(full)))
            throw new DirectoryNotFoundException("Choose an existing output directory.");
        if (File.Exists(full) || Directory.Exists(full))
            throw new IOException("Choose a new output filename.");
        return full;
    }
}

// Assets/Editor/CgmCaptureGameView.cs
public static class CgmCaptureGameView
{
    [MenuItem("Tools/CG Method/Request Game View PNG")]
    private static void Capture()
    {
        if (!EditorApplication.isPlaying || EditorApplication.isPaused)
        { Debug.LogWarning("Run Play Mode, unpause, and render the Game view first."); return; }
        string path = EditorUtility.SaveFilePanel("New Game View PNG", "",
            DateTime.Now.ToString("yyyyMMdd-HHmmss-fff"), "png");
        if (string.IsNullOrEmpty(path)) return;
        try
        {
            path = CgmScreenshotPath.Validate(path);
            ScreenCapture.CaptureScreenshot(path, 1);
            Debug.Log("PNG capture requested. Check the file after rendering: " + path);
        }
        catch (Exception e) { Debug.LogException(e); }
    }
}
  1. Play Modeへ入り、Gameビューに必要なフレームを表示します。出力サイズを固定する場合は、先にGameビューの解像度設定を確認します。
  2. Tools/CG Method/Request Game View PNGで既存フォルダー内の新しい.png名を選びます。キャンセル時は何もしません。既存ファイル/ディレクトリと同名は拒否します。
  3. Consoleの「requested」は要求したという意味です。描画が進んだ後に保存先でファイルの存在、PNGとして開けること、幅・高さを確認します。Editorをすぐ終了しないでください。
  4. デザインカンプと比べる場合は、両方の実ピクセル寸法を確認します。superSize=1だけで表示上の等倍や特定のピクセル寸法を保証しません。必要なフレームを厳密に指定するなら描画終了後に呼ぶ別の設計で検証します。

公式仕様はScreenCapture.CaptureScreenshotです。PNGの既存ファイルはAPI自体が上書きし得るため、例では呼び出し前に既存パスを拒否します。ただし確認から書込までの競合を排他的に防ぐAPIではありません。要求後の書込失敗は出力とConsoleで確認します。

次の保存ダイアログと結果画像は旧版の資料です。修正版の解像度・保存成功・現在のUIを検証した画像ではありません。

スクリーンショットの保存先
スクリーンショットの結果

同じ親のオブジェクトをUndo付きでグループ化する

CgmGroupSelected.csは同じ親・同じSceneの通常TransformをHierarchy順にまとめます。祖先と子の混在、別の親、別Scene、RectTransform、Prefabを対象にしません。親の祖先に拡大縮小がある場合も、この例の対象外です。旧例のCtrl+Gを無条件に割り当てず、メニューから実行します。

using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

// Also requires CgmSceneSelection from CgmRemoveOutline.cs.
public static class CgmGroupSelected
{
    [MenuItem("Tools/CG Method/Group Selected Siblings")]
    private static void Group()
    {
        try
        {
            GameObject[] objects = CgmSceneSelection.Get();
            Transform[] selected = objects.Select(o => o.transform).OrderBy(t => t.GetSiblingIndex()).ToArray();
            Transform parent = selected[0].parent;
            Scene scene = selected[0].gameObject.scene;
            foreach (Transform t in selected)
            {
                if (t is RectTransform || t.parent != parent || t.gameObject.scene != scene)
                    throw new InvalidOperationException("Select non-UI siblings in the same Scene.");
            }
            for (Transform t = parent; t != null; t = t.parent)
                if (t.localScale != Vector3.one)
                    throw new InvalidOperationException("Scaled ancestors are outside this example's scope.");
            int firstIndex = selected[0].GetSiblingIndex();
            CgmSceneSelection.Change("Group Selected Siblings", () =>
            {
                GameObject group = new GameObject("Group");
                SceneManager.MoveGameObjectToScene(group, scene);
                Undo.RegisterCreatedObjectUndo(group, "Group Selected Siblings");
                Undo.SetTransformParent(group.transform, parent, "Group Selected Siblings");
                Undo.RecordObject(group.transform, "Group Selected Siblings");
                group.transform.localPosition = Vector3.zero;
                group.transform.localRotation = Quaternion.identity;
                group.transform.localScale = Vector3.one;
                Undo.SetSiblingIndex(group.transform, firstIndex, "Group Selected Siblings");
                for (int i = 0; i < selected.Length; i++)
                {
                    Undo.SetTransformParent(selected[i], group.transform, "Group Selected Siblings");
                    Undo.SetSiblingIndex(selected[i], i, "Group Selected Siblings");
                }
                Selection.activeGameObject = group;
            });
        }
        catch (Exception e) { Debug.LogException(e); }
    }
}
  1. 空の通常Sceneで同じ親の下にCube A/B/Cを作り、AとCを選びます。Tools/CG Method/Group Selected Siblingsを実行します。
  2. 同じSceneの元の親の下にGroupができ、選択した子がA→Cの順で入ることを確認します。Groupは先頭の選択位置へ入り、未選択のBは元の親の下に残ります。
  3. 見た目とWorld Transformを比較し、Undoで親・SiblingIndex・Groupの生成が戻ることを確認します。スクリプトが名前・階層パスで検索する場合は、その参照への影響も確認します。

親変更はUndo.SetTransformParent、並び順はUndo.SetSiblingIndex、生成はUndo.RegisterCreatedObjectUndoへ記録します。Layout Group配下のUIをまとめるとレイアウトの計算対象が変わるため、この例をUI向けの汎用グループ化としません。

コンポーネントの使用箇所を検索する

削除する前に対象を調べたいときは、Hierarchyの標準検索でコンポーネント型を絞れるか先に確認します。Scene内の検索と、Project内の全Prefabを開く検索は範囲が違います。非Activeの対象や複数Sceneを含むかも確認してください。

次は旧検索ウィンドウの資料です。元記事のSearch For/Find Nextは配布元の記事を参照する旧拡張で、今回そのコードを導入・実行していません。新しい修正版4ファイルにこのウィンドウは含まれません。

コンポーネント検索ウィンドウ

配布ツール:Mulligan Renamerの導入と確認

Mulligan Renamerの作者READMEには接頭辞・接尾辞、置換、文字の削除、開始値と書式を指定する連番、Asset/GameObjectの入力が説明されています。導入方法とWindow/Red Blueメニューの記載は取得時点で確認しました。価格・対応版は配布先で再確認してください。

  1. READMEのUPM用Git URLを確認し、Package ManagerでGit URLから追加します。必要ならGitを導入します。既存manifest.jsonのdependenciesをすべて置き換えず、現在のパッケージを保ちます。
  2. Window/Red Blueからツールを開き、少数のGameObjectを入力します。接頭辞・接尾辞を設定し、変更前後の名前と入力リストを確認してから実行します。
  3. 連番は開始値・書式・処理順を確認します。たとえば開始0、2桁で3件なら00/01/02が目的の結果かを照合します。HierarchyのSiblingIndexそのものと同じとは仮定しません。
  4. 最初はScene内の対象で変更・Undo・保存後の再読込を試します。Assetの改名はファイルの参照や文字列パスにも関わるため、別に少数のAssetで確認します。Git導入や競合パッケージのエラーがあればConsoleとREADMEへ戻ります。

README記載のGit URL:https://github.com/redbluegames/unity-mulligan-renamer.git?path=/Assets/RedBlueGames/MulliganRenamer。固定したコミットや版をチームでそろえる場合は、実際に導入確認した版を記録してください。この改稿では外部パッケージをUnityへインストールしていません。

Mulligan Renamerの画面

次は2016年の日本語リネーマー紹介の旧画像です。日本語UIが必要な場合の候補ですが、画像だけを根拠に現在の導入やUnity 6.3対応を保証しません。

日本語リネーマー

複数プロジェクトで共有する:Editor限定UPMパッケージ

Unity本体のインストールフォルダーを書き換える旧DLL/ivy.xml方式の出典はUnity 5.3.5f1の2016年の記事です。現在の導入手順としては使いません。まずAssets/Editorで確認し、共有する段階でカスタムUPMパッケージへまとめます。

  1. プロジェクトのAssetsの外にcom.cgmethod.editor-toolsフォルダーを作り、直下へpackage.json、Editorフォルダーへ4つの.csとCGMethod.EditorTools.asmdefを置きます。Assets/Editor側の同じクラスを同時に残さないでください。
  2. 以下のpackage.jsonは、このサンプルがuGUIのOutlineを参照するためcom.unity.ugui 2.0.0を依存にしています。対象プロジェクトの導入済み版・互換性をPackage Managerで確認し、版を変える場合はコンパイルから再確認します。
  3. asmdefはEditorのみを対象にし、Unity.uguiを参照します。独自asmdefへ移す場合も共通ヘルパーを同じEditorアセンブリに入れます。
  4. Package Managerのローカルパッケージ追加から、このpackage.jsonを選びます。公式のローカルパッケージ導入を参照してください。相対/絶対参照の保存先がほかのPCでも解決できるか確認します。
  5. 2つ目の検証用プロジェクトでもメニュー・Undoを確認し、PlayerビルドへEditorコードが混ざらないか確認します。チーム共有には同じ版を取得できるGit等を使い、変更前の版へ戻せるよう記録します。

package.json(パッケージ直下):

{
  "name": "com.cgmethod.editor-tools",
  "version": "0.1.0",
  "displayName": "CG Method Editor Tools",
  "unity": "6000.3",
  "description": "Scene selection examples; Unity Editor runtime validation pending.",
  "dependencies": {
    "com.unity.ugui": "2.0.0"
  }
}

CGMethod.EditorTools.asmdef(Editorフォルダー内):

{
  "name": "CGMethod.EditorTools",
  "references": ["Unity.ugui"],
  "includePlatforms": ["Editor"],
  "autoReferenced": true
}

次の2画像は旧DLL生成・旧共通化の資料です。新しいUPMの導入確認画面ではありません。Unityインストール先へDLLやivy.xmlをコピーする手順の代わりに使わないでください。

DLL生成の準備
共通化の確認

動かないときの切り分けと確認範囲

目的・症状選び方・確認先
メニューが出ないConsoleの最初のコンパイルエラー、Editor配置、重複クラス、uGUI参照を確認。Assets/EditorとUPMの二重配置を避ける。
選択を拒否されたPlay中、ProjectのAsset、Prefab Mode/Instance、別親・別Scene・UIを目的別の対象条件と照合。対応していない対象を無視して一部だけ変更しない。
Undoが期待と違う少数の通常Sceneで1操作→Undoを試す。独自Edit Modeコールバックや階層パス依存を確認。Prefabへ対象条件を緩めるだけで流用しない。
PNGがない・寸法が違うPlay/非Pause/描画、保存先・権限・Console、Gameビュー設定と実ファイル寸法を確認。requestedログを保存完了と判断しない。
共有先でコンパイルエラーUnity版・uGUI版・Editor限定asmdef・参照の重複を確認。プロジェクトごとの依存を確認してから共有する。

改稿時に実行したのは、掲載コードから抜き出したCgmScreenshotPathの.NETテストです。空・相対・拡張子違い・存在しない親・既存ファイル・同名ディレクトリの6条件を拒否し、日本語/空白/&を含む新しい絶対.PNGパスを受け入れることを確認しました。Unityでのメニュー・Scene編集・Undo・PNG書込・UPM導入・Playerビルドは未確認です。上の少数対象の期待値を、使用する環境で照合してください。

Prefab・UI制作の関連ガイド

Prefabの内部構造CSV、フォルダーツリーCSV、正面PNG、Nested/Variant参照グラフは用途別の記事です。UI制作の専用拡張は下の関連カードから確認できます。

エディタ拡張の記事は、ほかにもあります。

元記事のコード保管先は旧GitHubコードです。リポジトリの旧例と今回の4ファイル・2設定ファイルは同じ版ではありません。実行するときは本記事の全文とファイル名をそろえてください。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Unityのおすすめ書籍

開発PCの選び方

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次