
Unity - это один из популярнейших игровых движков, который используется, как инди-разработчиками для создания игр и мобильных приложений, так и компаниями, чьи продукты используют технологии виртуальной и дополненной реальности.
На этом движке, не смотря на его недостатки, создано большое количество крупных проектов с кучей сцен, которые приходится модифицировать со временем по мере развития проекта. И чем больше этих сцен, тем дольше может быть процесс их обновления.
В этой статье мне хотелось бы поделиться своими наработками, которые помогают мне быстро добавлять новые компоненты на сцены, а также менять существующие.
Для этого мы с вами сегодня напишем простой инструмент, который позволит автоматизировать обновление сцен в ваших Unity-проектах и поможет вам сэкономить в совокупности несколько минут или даже часов ручной работы.
А далее вы уже сможете использовать и модифицировать приведённый в примере код под свои нужды.
Зачем нужен такой инструмент
Представьте, что у вас есть 3 уровня в вашем проекте. Вы добавили новые скрипты, изменили интерфейс, обновили используемые 3D-модели. Но только в рамках одной сцены.
В итоге, вы вручную переносите проделанные изменения на две другие сцены, чтобы сохранить порядок в проекте. Когда нужно обновить только 3 сцены - это довольно несложно.
Но что будет, когда их станет 10? 20? 50?
Уверена, вы сразу вспомните все ругательства, которые знаете. Да ещё и можете забыть обновить какой-то из компонентов. В итоге придётся снова проверять все сцены на наличие нужных вам обновлений. А это ваше время.
Как эту проблему решить?
На самом деле, довольно просто!
Можно поступить несколькими способами. Например, использовать метод OnValidate() в классах, которые уже присутствуют на сцене. Но для этого нужно запустить каждую сцену и сохранить её вручную. Более того, не весь функционал изменения сцен нам будет доступен через OnValidate(), поскольку данный метод есть только у объектов, наследованных от MonoBehaviour.
Нам такой вариант не подходит. Но знать о нём тоже полезно.
Поэтому мы пойдём другим путём и напишем скрипт, который автоматически обновит каждую сцену и выполнит любые необходимые изменения. Написав такой скрипт один раз, вы точно сэкономите себе уйму времени в будущем.
Чтобы это сделать, мы создадим новый класс в папке Editor:

Обращаю внимание, на то, что название некоторых папок в Unity играет роль. В данном случае, в папке "Editor" мы будем хранить все скрипты, которые помогают нам расширить базовый функционал редактора Unity и, например, создавать диалоговые окна.
Далее добавим необходимые пространства имён, а также укажем, что наследоваться будем от Editor Window (а не от MonoBehaviour, как происходит по умолчанию):
using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{ [MenuItem("Custom Tools/Scene Updater")] public static void ShowWindow() { GetWindow(typeof(SceneUpdater)); } private void OnGUI() { if (GUILayout.Button("Update scenes")) Debug.Log("Updating") }}
С помощью атрибута [MenuItem("Custom Tools/Scene Updater")] мы создадим элемент меню с заданной иерархией в самом движке. Таким образом мы будем вызывать диалоговое окно будущего инструмента:

Далее мы добавим кнопку, с помощью которой будем запускать наш дальнейший код:
using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{ [MenuItem("Custom Tools/Scene Updater")] public static void ShowWindow() { GetWindow(typeof(SceneUpdater)); } private void OnGUI() { if (GUILayout.Button("Update scenes")) Debug.Log("Updating") }}
А теперь напишем несколько полезных функций, которые лично я довольно часто использую.
Быстрое добавление компонентов к объектам
Для добавления компонентов к объектам с уникальными именами можно написать вот такую функцию:
/// <summary>/// Добавление компонента к объекту с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>/// <typeparam name="T"> тип компонента </typeparam>private void AddComponentToObject<T>(string objectName) where T : Component{ GameObject.Find(objectName)?.gameObject.AddComponent<T>();}
Использовать её можно вот так:
AddComponentToObject<BoxCollider>("Plane");AddComponentToObject<SampleClass>("EventSystem");
Это немного быстрее, чем каждый раз прописывать всё самостоятельно.
Быстрое удаление объектов по имени
Аналогично можно сделать и для удаления объектов:
/// <summary>/// Уничтожение объекта с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>private void DestroyObjectWithName(string objectName){ DestroyImmediate(GameObject.Find(objectName)?.gameObject);}
И использовать так:
DestroyObjectWithName("Sphere");
Перенос позиции, поворота и размера между объектами
Для компонентов Transform и RectTransform можно создать функции, с помощью которых будет происходить копирование локальной позиции, поворота и размера объекта (например, если нужно заменить старый объект новым или изменить настройки интерфейса):
/// <summary>/// Копирование позиции, поворота и размера с компонента Transform у одного объекта/// на такой же компонент другого объекта./// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo, bool copyPosition = true, bool copeRotation = true, bool copyScale = true){ var newTransform = objectToCopyFrom.GetComponent<Transform>(); var currentTransform = objectToPasteTo.GetComponent<Transform>(); if (copyPosition) currentTransform.localPosition = newTransform.localPosition; if (copeRotation) currentTransform.localRotation = newTransform.localRotation; if (copyScale) currentTransform.localScale = newTransform.localScale;} /// <summary>/// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта/// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)/// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo, bool copyPosition = true, bool copeRotation = true, bool copyScale = true){ var newTransform = objectToCopyFrom.GetComponent<RectTransform>(); var currentTransform = objectToPasteTo.GetComponent<RectTransform>(); if (copyPosition) currentTransform.localPosition = newTransform.localPosition; if (copeRotation) currentTransform.localRotation = newTransform.localRotation; if (copyScale) currentTransform.localScale = newTransform.localScale;}
Причём, благодаря тому, что есть переменные-условия, мы сможем контролировать, какие параметры мы хотим скопировать:
var plane = GameObject.Find("Plane");var cube = GameObject.Find("Cube");CopyTransformPositionRotationScale(plane, cube, copyScale:false);
Изменение UI-компонентов
Для работы с интерфейсом могут быть полезны функции, позволяющие быстро настроить Canvas, TextMeshPro и RectTransform:
/// <summary>/// Изменение отображения Canvas/// </summary>/// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>/// <param name="renderMode"> способ отображения </param>/// <param name="scaleMode"> способ изменения масштаба </param>private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode){ canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode; var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>(); canvasScaler.uiScaleMode = scaleMode; // выставление стандартного разрешения if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize) { canvasScaler.referenceResolution = new Vector2(720f, 1280f); canvasScaler.matchWidthOrHeight = 1f; }} /// <summary>/// Изменение настроек для TextMeshPro/// </summary>/// <param name="textMeshPro"> тестовый элемент </param>/// <param name="fontSizeMin"> минимальный размер шрифта </param>/// <param name="fontSizeMax"> максимальный размер шрифта </param>/// <param name="textAlignmentOption"> выравнивание текста </param>private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center){ // замена стандартного шрифта textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset)); textMeshPro.enableAutoSizing = true; textMeshPro.fontSizeMin = fontSizeMin; textMeshPro.fontSizeMax = fontSizeMax; textMeshPro.alignment = textAlignmentOption;}/// <summary>/// Изменение параметров RectTransform/// </summary>/// <param name="rectTransform"> изменяемый элемент </param>/// <param name="alignment"> выравнивание </param>/// <param name="position"> позиция в 3D-пространстве </param>/// <param name="size"> размер </param>private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size){ rectTransform.anchoredPosition3D = position; rectTransform.sizeDelta = size; rectTransform.SetAnchor(alignment);}
Замечу, что для RectTransform я использую расширение самого класса, найденное когда-то давно на форумах по Unity. С его помощью очень удобно настраивать Anchor и Pivot. Такие расширения рекомендуется складывать в папку Utils:

Код данного расширения оставляю для вас в спойлере:
RectTransformExtension.cs
using UnityEngine;public enum AnchorPresets{ TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight, VertStretchLeft, VertStretchRight, VertStretchCenter, HorStretchTop, HorStretchMiddle, HorStretchBottom, StretchAll}public enum PivotPresets{ TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight,}/// <summary>/// Расширение возможностей работы с RectTransform/// </summary>public static class RectTransformExtension{ /// <summary> /// Изменение якоря /// </summary> /// <param name="source"> компонент, свойства которого требуется изменить </param> /// <param name="align"> способ выравнивания </param> /// <param name="offsetX"> смещение по оси X </param> /// <param name="offsetY"> смещение по оси Y </param> public static void SetAnchor(this RectTransform source, AnchorPresets align, int offsetX = 0, int offsetY = 0) { source.anchoredPosition = new Vector3(offsetX, offsetY, 0); switch (align) { case (AnchorPresets.TopLeft): { source.anchorMin = new Vector2(0, 1); source.anchorMax = new Vector2(0, 1); break; } case (AnchorPresets.TopCenter): { source.anchorMin = new Vector2(0.5f, 1); source.anchorMax = new Vector2(0.5f, 1); break; } case (AnchorPresets.TopRight): { source.anchorMin = new Vector2(1, 1); source.anchorMax = new Vector2(1, 1); break; } case (AnchorPresets.MiddleLeft): { source.anchorMin = new Vector2(0, 0.5f); source.anchorMax = new Vector2(0, 0.5f); break; } case (AnchorPresets.MiddleCenter): { source.anchorMin = new Vector2(0.5f, 0.5f); source.anchorMax = new Vector2(0.5f, 0.5f); break; } case (AnchorPresets.MiddleRight): { source.anchorMin = new Vector2(1, 0.5f); source.anchorMax = new Vector2(1, 0.5f); break; } case (AnchorPresets.BottomLeft): { source.anchorMin = new Vector2(0, 0); source.anchorMax = new Vector2(0, 0); break; } case (AnchorPresets.BottomCenter): { source.anchorMin = new Vector2(0.5f, 0); source.anchorMax = new Vector2(0.5f, 0); break; } case (AnchorPresets.BottomRight): { source.anchorMin = new Vector2(1, 0); source.anchorMax = new Vector2(1, 0); break; } case (AnchorPresets.HorStretchTop): { source.anchorMin = new Vector2(0, 1); source.anchorMax = new Vector2(1, 1); break; } case (AnchorPresets.HorStretchMiddle): { source.anchorMin = new Vector2(0, 0.5f); source.anchorMax = new Vector2(1, 0.5f); break; } case (AnchorPresets.HorStretchBottom): { source.anchorMin = new Vector2(0, 0); source.anchorMax = new Vector2(1, 0); break; } case (AnchorPresets.VertStretchLeft): { source.anchorMin = new Vector2(0, 0); source.anchorMax = new Vector2(0, 1); break; } case (AnchorPresets.VertStretchCenter): { source.anchorMin = new Vector2(0.5f, 0); source.anchorMax = new Vector2(0.5f, 1); break; } case (AnchorPresets.VertStretchRight): { source.anchorMin = new Vector2(1, 0); source.anchorMax = new Vector2(1, 1); break; } case (AnchorPresets.StretchAll): { source.anchorMin = new Vector2(0, 0); source.anchorMax = new Vector2(1, 1); break; } } } /// <summary> /// Изменение pivot /// </summary> /// <param name="source"> компонент, свойства которого требуется изменить </param> /// <param name="preset"> способ выравнивания </param> public static void SetPivot(this RectTransform source, PivotPresets preset) { switch (preset) { case (PivotPresets.TopLeft): { source.pivot = new Vector2(0, 1); break; } case (PivotPresets.TopCenter): { source.pivot = new Vector2(0.5f, 1); break; } case (PivotPresets.TopRight): { source.pivot = new Vector2(1, 1); break; } case (PivotPresets.MiddleLeft): { source.pivot = new Vector2(0, 0.5f); break; } case (PivotPresets.MiddleCenter): { source.pivot = new Vector2(0.5f, 0.5f); break; } case (PivotPresets.MiddleRight): { source.pivot = new Vector2(1, 0.5f); break; } case (PivotPresets.BottomLeft): { source.pivot = new Vector2(0, 0); break; } case (PivotPresets.BottomCenter): { source.pivot = new Vector2(0.5f, 0); break; } case (PivotPresets.BottomRight): { source.pivot = new Vector2(1, 0); break; } } }}
Использовать данные функции можно так:
// изменение настроек отображения Canvasvar canvas = GameObject.Find("Canvas");ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);// изменение настроек шрифтаvar tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);// изменение RectTransformChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));
Аналогично, может пригодиться расширение для класса Transform для поиска дочернего элемента (при наличии сложной иерархии):
TransformExtension.cs
using UnityEngine;/// <summary>/// Расширение возможностей работы с Transform/// </summary>public static class TransformExtension{ /// <summary> /// Рекурсивный поиск дочернего элемента с определённым именем /// </summary> /// <param name="parent"> родительский элемент </param> /// <param name="childName"> название искомого дочернего элемента </param> /// <returns> null - если элемент не найден, /// Transform элемента, если элемент найден /// </returns> public static Transform FindChildWithName(this Transform parent, string childName) { foreach (Transform child in parent) { if (child.name == childName) return child; var result = child.FindChildWithName(childName); if (result) return result; } return null; }}
Для тех, кому хочется иметь возможность видеть событие OnClick() на кнопке в инспекторе - может быть полезна вот такая функция:
/// <summary>/// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)/// </summary>/// <param name="uiButton"> кнопка </param>/// <param name="action"> требуемое действие </param>private static void AddPersistentListenerToButton(Button uiButton, UnityAction action){ try { // сработает, если уже есть пустое событие if (uiButton.onClick.GetPersistentTarget(0) == null) UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action); } catch (ArgumentException) { UnityEventTools.AddPersistentListener(uiButton.onClick, action); }}
То есть, если написать следующее:
// добавление события на кнопкуAddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);
То результат работы в движке будет таким:

Добавление новых объектов и изменение иерархии на сцене
Для тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:
/// <summary>/// Изменение слоя объекта по названию слоя/// </summary>/// <param name="gameObject"> объект </param>/// <param name="layerName"> название слоя </param>private void ChangeObjectLayer(GameObject gameObject, string layerName){ gameObject.layer = LayerMask.NameToLayer(layerName);}/// <summary>/// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии/// </summary>/// <param name="prefabPath"> путь к префабу </param>/// <param name="parentGameObject"> родительский объект </param>/// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0){ if (parentGameObject) { var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform); // изменение порядка в иерархии сцены внутри родительского элемента newGameObject.transform.SetSiblingIndex(hierarchyIndex); } else Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));}
Таким образом, при выполнении следующего кода:
// изменение тэга и слоя объектаvar cube = GameObject.Find("Cube");cube.tag = "Player";ChangeObjectLayer(cube, "MainLayer"); // создание нового объекта на сцене и добавление его в иерархию к существующемуInstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);
Элемент встанет не в конец иерархии, а на заданное место:

Цикл обновления сцен
И наконец, самое главное - функция, с помощью которой происходит вся дальнейшая автоматизация открывания-изменения-сохранения сцен, добавленных в File ->Build Settings:
/// <summary>/// Запускает цикл обновления сцен в Build Settings/// </summary>/// <param name="onSceneLoaded"> действие при открытии сцены </param>private void RunSceneUpdateCycle(UnityAction onSceneLoaded){ // получение путей к сценам для дальнейшего открытия var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList(); foreach (var scene in scenes) { // открытие сцены EditorSceneManager.OpenScene(scene); // пометка для сохранения, что на сцене были произведены изменения EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); // проведение изменений onSceneLoaded?.Invoke(); // сохранение EditorApplication.SaveScene(); Debug.Log($"UPDATED {scene}"); }}
А теперь соединим всё вместе, чтобы запускать цикл обновления сцен по клику на кнопку:
Полный код SceneUpdater.cs
#if UNITY_EDITORusing System;using UnityEditor.Events;using TMPro;using UnityEngine.UI;using System.Collections.Generic;using UnityEngine.SceneManagement;using UnityEditor;using UnityEditor.SceneManagement;using System.Linq;using UnityEngine;using UnityEngine.Events;/// <summary>/// Класс для обновления сцен, включённых в список BuildSettings (активные и неактивные)/// </summary>public class SceneUpdater : EditorWindow{ [MenuItem("Custom Tools/Scene Updater")] public static void ShowWindow() { GetWindow(typeof(SceneUpdater)); } private void OnGUI() { // пример использования if (GUILayout.Button("Update scenes")) RunSceneUpdateCycle((() => { // изменение тэга и слоя объекта var cube = GameObject.Find("Cube"); cube.tag = "Player"; ChangeObjectLayer(cube, "MainLayer"); // добавление компонента к объекту с уникальным названием AddComponentToObject<BoxCollider>("Plane"); // удаление объекта с уникальным названием DestroyObjectWithName("Sphere"); // создание нового объекта на сцене и добавление его в иерархию к существующему InstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1); // изменение настроек отображения Canvas var canvas = GameObject.Find("Canvas"); ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize); // изменение настроек шрифта var tmp = canvas.GetComponentInChildren<TextMeshProUGUI>(); ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight); // изменение RectTransform ChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f)); // добавление события на кнопку AddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp); // копирование настроек компонента CopyTransformPositionRotationScale(GameObject.Find("Plane"), cube, copyScale:false); })); } /// <summary> /// Запускает цикл обновления сцен в Build Settings /// </summary> /// <param name="onSceneLoaded"> действие при открытии сцены </param> private void RunSceneUpdateCycle(UnityAction onSceneLoaded) { // получение путей к сценам для дальнейшего открытия var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList(); foreach (var scene in scenes) { // открытие сцены EditorSceneManager.OpenScene(scene); // пометка для сохранения, что на сцене были произведены изменения EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); // проведение изменений onSceneLoaded?.Invoke(); // сохранение EditorApplication.SaveScene(); Debug.Log($"UPDATED {scene}"); } } /// <summary> /// Добавление обработчика события на кнопку (чтобы было видно в инспекторе) /// </summary> /// <param name="uiButton"> кнопка </param> /// <param name="action"> требуемое действие </param> private static void AddPersistentListenerToButton(Button uiButton, UnityAction action) { try { // сработает, если уже есть пустое событие if (uiButton.onClick.GetPersistentTarget(0) == null) UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action); } catch (ArgumentException) { UnityEventTools.AddPersistentListener(uiButton.onClick, action); } } /// <summary> /// Изменение параметров RectTransform /// </summary> /// <param name="rectTransform"> изменяемый элемент </param> /// <param name="alignment"> выравнивание </param> /// <param name="position"> позиция в 3D-пространстве </param> /// <param name="size"> размер </param> private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size) { rectTransform.anchoredPosition3D = position; rectTransform.sizeDelta = size; rectTransform.SetAnchor(alignment); } /// <summary> /// Изменение настроек для TextMeshPro /// </summary> /// <param name="textMeshPro"> тестовый элемент </param> /// <param name="fontSizeMin"> минимальный размер шрифта </param> /// <param name="fontSizeMax"> максимальный размер шрифта </param> /// <param name="textAlignmentOption"> выравнивание текста </param> private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center) { // замена стандартного шрифта textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset)); textMeshPro.enableAutoSizing = true; textMeshPro.fontSizeMin = fontSizeMin; textMeshPro.fontSizeMax = fontSizeMax; textMeshPro.alignment = textAlignmentOption; } /// <summary> /// Изменение отображения Canvas /// </summary> /// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param> /// <param name="renderMode"> способ отображения </param> /// <param name="scaleMode"> способ изменения масштаба </param> private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode) { canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode; var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>(); canvasScaler.uiScaleMode = scaleMode; // выставление стандартного разрешения if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize) { canvasScaler.referenceResolution = new Vector2(720f, 1280f); canvasScaler.matchWidthOrHeight = 1f; } } /// <summary> /// Получение всех верхних дочерних элементов /// </summary> /// <param name="parentGameObject"> родительский элемент </param> /// <returns> список дочерних элементов </returns> private static List<GameObject> GetAllChildren(GameObject parentGameObject) { var children = new List<GameObject>(); for (int i = 0; i< parentGameObject.transform.childCount; i++) children.Add(parentGameObject.transform.GetChild(i).gameObject); return children; } /// <summary> /// Копирование позиции, поворота и размера с компонента Transform у одного объекта /// на такой же компонент другого объекта. /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты /// </summary> /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param> /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param> /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param> /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param> /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param> private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo, bool copyPosition = true, bool copyRotation = true, bool copyScale = true) { var newTransform = objectToCopyFrom.GetComponent<Transform>(); var currentTransform = objectToPasteTo.GetComponent<Transform>(); if (copyPosition) currentTransform.localPosition = newTransform.localPosition; if (copyRotation) currentTransform.localRotation = newTransform.localRotation; if (copyScale) currentTransform.localScale = newTransform.localScale; } /// <summary> /// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта /// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta) /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты /// </summary> /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param> /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param> /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param> /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param> /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param> private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo, bool copyPosition = true, bool copyRotation = true, bool copyScale = true) { var newTransform = objectToCopyFrom.GetComponent<RectTransform>(); var currentTransform = objectToPasteTo.GetComponent<RectTransform>(); if (copyPosition) currentTransform.localPosition = newTransform.localPosition; if (copyRotation) currentTransform.localRotation = newTransform.localRotation; if (copyScale) currentTransform.localScale = newTransform.localScale; } /// <summary> /// Уничтожение объекта с уникальным названием /// </summary> /// <param name="objectName"> название объекта </param> private void DestroyObjectWithName(string objectName) { DestroyImmediate(GameObject.Find(objectName)?.gameObject); } /// <summary> /// Добавление компонента к объекту с уникальным названием /// </summary> /// <param name="objectName"> название объекта </param> /// <typeparam name="T"> тип компонента </typeparam> private void AddComponentToObject<T>(string objectName) where T : Component { GameObject.Find(objectName)?.gameObject.AddComponent<T>(); } /// <summary> /// Изменение слоя объекта по названию слоя /// </summary> /// <param name="gameObject"> объект </param> /// <param name="layerName"> название слоя </param> private void ChangeObjectLayer(GameObject gameObject, string layerName) { gameObject.layer = LayerMask.NameToLayer(layerName); } /// <summary> /// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии /// </summary> /// <param name="prefabPath"> путь к префабу </param> /// <param name="parentGameObject"> родительский объект </param> /// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param> private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0) { if (parentGameObject) { var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform); // изменение порядка в иерархии сцены внутри родительского элемента newGameObject.transform.SetSiblingIndex(hierarchyIndex); } else Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject))); }}#endif
Таким образом, всего за один клик все сцены нашего проекта автоматически обновятся, а мы тем временем сможем пойти выпить кофе и просто понаслаждаться проделанной работой:

Заключение
Имея в своём распоряжении такой инструмент, вы сможете делать всё, что вам угодно за считанные клики: сериализовать поля, менять иерархию на сценах, настраивать Fuse/IClone/DAZ и других персонажей, а также менять Build Pipeline, но об этом как-нибудь в другой раз.
Главное, не забывайте использовать систему контроля версий и проверять запуск ваших модификаций сперва на одной сцене (т.е. без использования RunSceneUpdateCycle).
Запустить тестовый проект и получить полный код можно на моём GitHub.
Кстати, тех, кто планирует строить карьеру в IT, ябуду рада видеть на своёмYouTube-канале IT DIVA. Там вы сможете найти видео по тому, как оформлять GitHub, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д.
Спасибо за внимание и до новых встреч!