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);
То результат работы в движке будет таким:
Добавление новых объектов и изменение иерархии на сцене
Для тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:
/// <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