Как обновить все сцены Unity-проекта в один клик

от автора

Танюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутину
Танюшка — автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутину

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;             }         }     } }

Использовать данные функции можно так:

// изменение настроек отображения 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));

Аналогично, может пригодиться расширение для класса 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);

То результат работы в движке будет таким:

Результат работы AddPersistentListener
Результат работы AddPersistentListener

Добавление новых объектов и изменение иерархии на сцене

Для тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:

/// <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_EDITOR using 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, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д.

Спасибо за внимание и до новых встреч!

ссылка на оригинал статьи https://habr.com/ru/post/559958/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *