[Habr, Программирование, Разработка игр, C#, Unity] Как обновить все сцены Unity-проекта в один клик

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
30-Май-2021 13:32


Танюшка - автор канала 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Добавление новых объектов и изменение иерархии на сценеДля тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:
/// <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, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д. Спасибо за внимание и до новых встреч!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_habr, #_programmirovanie (Программирование), #_razrabotka_igr (Разработка игр), #_c#, #_unity, #_razrabotka_igr (разработка игр), #_unity, #_unity3d, #_unity_tutorial (unity туториал), #_unityscript, #_unity3d_uroki (unity3d уроки), #_unity_uroki (unity уроки), #_avtomatizatsija (автоматизация), #_avtomatizatsija_rutiny (автоматизация рутины), #_gejmdev (геймдев), #_habr, #_programmirovanie (
Программирование
)
, #_razrabotka_igr (
Разработка игр
)
, #_c#, #_unity
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 17-Май 10:37
Часовой пояс: UTC + 5