[Habr, Программирование, Разработка игр, C#, Unity] Как обновить все сцены Unity-проекта в один клик
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Танюшка - автор канала 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
===========
Похожие новости:
- [Интернет-маркетинг, Контент-маркетинг, Email-маркетинг] Что такое автоматизация маркетинга и как она влияет на развитие бизнеса
- [Разработка веб-сайтов, Python, JavaScript, Программирование] Как создавать предметы генеративного искусства с помощью L-систем на языке Python (перевод)
- [Программирование, C++] Как компилятор C++ находит правельную функцию (перевод)
- [Программирование, Учебный процесс в IT, Читальный зал] Что не так с современным преподаванием информатики
- [Исследования и прогнозы в IT, IT-компании] Кто такие citizen developers и как они двигают вперед цифровую трансформацию: туториал по созданию робота
- [Программирование, CRM-системы, Будущее здесь, Интервью, IT-компании] Интервью с СЕО FitBase: будущее за автоматизацией
- [Программирование, .NET, ASP, C#] 15 простых советов по оптимизации производительности ASP.NET (перевод)
- [Умный дом, DIY или Сделай сам] Визуализация голосового помощника Алисы с эффектом голограммы
- [Программирование, C++] Худшие места в C++ для написания кода
- [Help Desk Software, CRM-системы, Service Desk, Управление проектами, Облачные сервисы] KPI сервиса с выездными сотрудниками: какие цели ставить и как достигать?
Теги для поиска: #_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
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Танюшка - автор канала 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") } } Новый элемент меню, через который мы будем использовать наш инструментДалее мы добавим кнопку, с помощью которой будем запускать наш дальнейший код: 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");
/// <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); /// <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); } Пример возможной иерархии для расширений стандартных классов Код данного расширения оставляю для вас в спойлере: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)); 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; } } /// <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}"); } } #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 =========== Похожие новости:
Программирование ), #_razrabotka_igr ( Разработка игр ), #_c#, #_unity |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:56
Часовой пояс: UTC + 5