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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

А вы используете у себя в проекте инструменты для автоматизации ручной работы?

  • 100,0%Да1
  • 0,0%Нет0
Источник: https://habr.com/ru/post/559958/


Интересные статьи

Интересные статьи

Любому QA известен такой метод минимизации тест-кейсов, как Pairwise Testing — попарное тестирование. Метод отличный, достаточно простой и проверенный множеством команд. Но что делать...
Изображение предоставлено автором. Основано на фотографии Патрика Томаса с Ансплэша Это перевод статьи Дэвида Клира о методе ведения заметок Zettelkasten, благодаря которому немец...
Привет, Хабр! Предлагаю Вашему вниманию перевод статьи Good and Bad Practices of Coding in Python автора Duomly. Python – высокоуровневый язык программирования, акцентирующий внимание на удобо...
Компании переполнили рынок товаров и услуг предложениями. Разнообразие наблюдается не только в офлайне, но и в интернете. Достаточно вбить в поисковик любой запрос, чтобы получить подтверждение насыще...
Я уже восемь лет работаю в Vim и постоянно открываю что-то новое. Принято считать это достоинством Vim. Как по мне, так это недостаток открытости: куча скрытых функций спрятаны слишком глубоко. ...