Русский
Русский
English
Статистика
Реклама

Unity туториал

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

30.05.2021 12:21:38 | Автор: admin
Танюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутинуТанюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутину

Unity - это один из популярнейших игровых движков, который используется, как инди-разработчиками для создания игр и мобильных приложений, так и компаниями, чьи продукты используют технологии виртуальной и дополненной реальности.

На этом движке, не смотря на его недостатки, создано большое количество крупных проектов с кучей сцен, которые приходится модифицировать со временем по мере развития проекта. И чем больше этих сцен, тем дольше может быть процесс их обновления.

В этой статье мне хотелось бы поделиться своими наработками, которые помогают мне быстро добавлять новые компоненты на сцены, а также менять существующие.

Для этого мы с вами сегодня напишем простой инструмент, который позволит автоматизировать обновление сцен в ваших Unity-проектах и поможет вам сэкономить в совокупности несколько минут или даже часов ручной работы.

А далее вы уже сможете использовать и модифицировать приведённый в примере код под свои нужды.

Зачем нужен такой инструмент

Представьте, что у вас есть 3 уровня в вашем проекте. Вы добавили новые скрипты, изменили интерфейс, обновили используемые 3D-модели. Но только в рамках одной сцены.

В итоге, вы вручную переносите проделанные изменения на две другие сцены, чтобы сохранить порядок в проекте. Когда нужно обновить только 3 сцены - это довольно несложно.

Но что будет, когда их станет 10? 20? 50?

Уверена, вы сразу вспомните все ругательства, которые знаете. Да ещё и можете забыть обновить какой-то из компонентов. В итоге придётся снова проверять все сцены на наличие нужных вам обновлений. А это ваше время.

Как эту проблему решить?

На самом деле, довольно просто!

Можно поступить несколькими способами. Например, использовать метод OnValidate() в классах, которые уже присутствуют на сцене. Но для этого нужно запустить каждую сцену и сохранить её вручную. Более того, не весь функционал изменения сцен нам будет доступен через OnValidate(), поскольку данный метод есть только у объектов, наследованных от MonoBehaviour.

Нам такой вариант не подходит. Но знать о нём тоже полезно.

Поэтому мы пойдём другим путём и напишем скрипт, который автоматически обновит каждую сцену и выполнит любые необходимые изменения. Написав такой скрипт один раз, вы точно сэкономите себе уйму времени в будущем.

Чтобы это сделать, мы создадим новый класс в папке Editor:

Пример возможной иерархии для расширений движкаПример возможной иерархии для расширений движка

Обращаю внимание, на то, что название некоторых папок в Unity играет роль. В данном случае, в папке "Editor" мы будем хранить все скрипты, которые помогают нам расширить базовый функционал редактора Unity и, например, создавать диалоговые окна.

Далее добавим необходимые пространства имён, а также укажем, что наследоваться будем от Editor Window (а не от MonoBehaviour, как происходит по умолчанию):

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

С помощью атрибута [MenuItem("Custom Tools/Scene Updater")] мы создадим элемент меню с заданной иерархией в самом движке. Таким образом мы будем вызывать диалоговое окно будущего инструмента:

Новый элемент меню, через который мы будем использовать наш инструментНовый элемент меню, через который мы будем использовать наш инструмент

Далее мы добавим кнопку, с помощью которой будем запускать наш дальнейший код:

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

А теперь напишем несколько полезных функций, которые лично я довольно часто использую.

Быстрое добавление компонентов к объектам

Для добавления компонентов к объектам с уникальными именами можно написать вот такую функцию:

/// <summary>/// Добавление компонента к объекту с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>/// <typeparam name="T"> тип компонента </typeparam>private void AddComponentToObject<T>(string objectName) where T : Component{    GameObject.Find(objectName)?.gameObject.AddComponent<T>();}

Использовать её можно вот так:

AddComponentToObject<BoxCollider>("Plane");AddComponentToObject<SampleClass>("EventSystem");

Это немного быстрее, чем каждый раз прописывать всё самостоятельно.

Быстрое удаление объектов по имени

Аналогично можно сделать и для удаления объектов:

/// <summary>/// Уничтожение объекта с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>private void DestroyObjectWithName(string objectName){    DestroyImmediate(GameObject.Find(objectName)?.gameObject);}

И использовать так:

DestroyObjectWithName("Sphere");

Перенос позиции, поворота и размера между объектами

Для компонентов Transform и RectTransform можно создать функции, с помощью которых будет происходить копирование локальной позиции, поворота и размера объекта (например, если нужно заменить старый объект новым или изменить настройки интерфейса):

/// <summary>/// Копирование позиции, поворота и размера с компонента Transform у одного объекта/// на такой же компонент другого объекта./// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,     bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<Transform>();    var currentTransform = objectToPasteTo.GetComponent<Transform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}    /// <summary>/// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта/// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)/// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,    bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<RectTransform>();    var currentTransform = objectToPasteTo.GetComponent<RectTransform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}

Причём, благодаря тому, что есть переменные-условия, мы сможем контролировать, какие параметры мы хотим скопировать:

var plane = GameObject.Find("Plane");var cube = GameObject.Find("Cube");CopyTransformPositionRotationScale(plane, cube, copyScale:false);

Изменение UI-компонентов

Для работы с интерфейсом могут быть полезны функции, позволяющие быстро настроить Canvas, TextMeshPro и RectTransform:

/// <summary>/// Изменение отображения Canvas/// </summary>/// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>/// <param name="renderMode"> способ отображения </param>/// <param name="scaleMode"> способ изменения масштаба </param>private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode){    canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode;    var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>();    canvasScaler.uiScaleMode = scaleMode;    // выставление стандартного разрешения    if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)    {        canvasScaler.referenceResolution = new Vector2(720f, 1280f);        canvasScaler.matchWidthOrHeight = 1f;    }} /// <summary>/// Изменение настроек для TextMeshPro/// </summary>/// <param name="textMeshPro"> тестовый элемент </param>/// <param name="fontSizeMin"> минимальный размер шрифта </param>/// <param name="fontSizeMax"> максимальный размер шрифта </param>/// <param name="textAlignmentOption"> выравнивание текста </param>private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center){    // замена стандартного шрифта    textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset));    textMeshPro.enableAutoSizing = true;    textMeshPro.fontSizeMin = fontSizeMin;    textMeshPro.fontSizeMax = fontSizeMax;    textMeshPro.alignment = textAlignmentOption;}/// <summary>/// Изменение параметров RectTransform/// </summary>/// <param name="rectTransform"> изменяемый элемент </param>/// <param name="alignment"> выравнивание </param>/// <param name="position"> позиция в 3D-пространстве </param>/// <param name="size"> размер </param>private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size){    rectTransform.anchoredPosition3D = position;    rectTransform.sizeDelta = size;    rectTransform.SetAnchor(alignment);}

Замечу, что для RectTransform я использую расширение самого класса, найденное когда-то давно на форумах по Unity. С его помощью очень удобно настраивать Anchor и Pivot. Такие расширения рекомендуется складывать в папку Utils:

Пример возможной иерархии для расширений стандартных классов Пример возможной иерархии для расширений стандартных классов

Код данного расширения оставляю для вас в спойлере:

RectTransformExtension.cs
using UnityEngine;public enum AnchorPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,    VertStretchLeft,    VertStretchRight,    VertStretchCenter,    HorStretchTop,    HorStretchMiddle,    HorStretchBottom,    StretchAll}public enum PivotPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,}/// <summary>/// Расширение возможностей работы с RectTransform/// </summary>public static class RectTransformExtension{    /// <summary>    /// Изменение якоря    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="align"> способ выравнивания </param>    /// <param name="offsetX"> смещение по оси X </param>    /// <param name="offsetY"> смещение по оси Y </param>    public static void SetAnchor(this RectTransform source, AnchorPresets align, int offsetX = 0, int offsetY = 0)    {        source.anchoredPosition = new Vector3(offsetX, offsetY, 0);        switch (align)        {            case (AnchorPresets.TopLeft):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.TopCenter):            {                source.anchorMin = new Vector2(0.5f, 1);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.TopRight):            {                source.anchorMin = new Vector2(1, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.MiddleLeft):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(0, 0.5f);                break;            }            case (AnchorPresets.MiddleCenter):            {                source.anchorMin = new Vector2(0.5f, 0.5f);                source.anchorMax = new Vector2(0.5f, 0.5f);                break;            }            case (AnchorPresets.MiddleRight):            {                source.anchorMin = new Vector2(1, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.BottomLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 0);                break;            }            case (AnchorPresets.BottomCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 0);                break;            }            case (AnchorPresets.BottomRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.HorStretchTop):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.HorStretchMiddle):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.HorStretchBottom):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.VertStretchLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.VertStretchCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.VertStretchRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.StretchAll):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 1);                break;            }        }    }    /// <summary>    /// Изменение pivot    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="preset"> способ выравнивания </param>    public static void SetPivot(this RectTransform source, PivotPresets preset)    {        switch (preset)        {            case (PivotPresets.TopLeft):            {                source.pivot = new Vector2(0, 1);                break;            }            case (PivotPresets.TopCenter):            {                source.pivot = new Vector2(0.5f, 1);                break;            }            case (PivotPresets.TopRight):            {                source.pivot = new Vector2(1, 1);                break;            }            case (PivotPresets.MiddleLeft):            {                source.pivot = new Vector2(0, 0.5f);                break;            }            case (PivotPresets.MiddleCenter):            {                source.pivot = new Vector2(0.5f, 0.5f);                break;            }            case (PivotPresets.MiddleRight):            {                source.pivot = new Vector2(1, 0.5f);                break;            }            case (PivotPresets.BottomLeft):            {                source.pivot = new Vector2(0, 0);                break;            }            case (PivotPresets.BottomCenter):            {                source.pivot = new Vector2(0.5f, 0);                break;            }            case (PivotPresets.BottomRight):            {                source.pivot = new Vector2(1, 0);                break;            }        }    }}

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

// изменение настроек отображения Canvasvar canvas = GameObject.Find("Canvas");ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);// изменение настроек шрифтаvar tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);// изменение RectTransformChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));

Аналогично, может пригодиться расширение для класса Transform для поиска дочернего элемента (при наличии сложной иерархии):

TransformExtension.cs
using UnityEngine;/// <summary>/// Расширение возможностей работы с Transform/// </summary>public static class TransformExtension{    /// <summary>    /// Рекурсивный поиск дочернего элемента с определённым именем    /// </summary>    /// <param name="parent"> родительский элемент </param>    /// <param name="childName"> название искомого дочернего элемента </param>    /// <returns> null - если элемент не найден,    ///           Transform элемента, если элемент найден    /// </returns>    public static Transform FindChildWithName(this Transform parent, string childName)    {        foreach (Transform child in parent)        {            if (child.name == childName)                return child;            var result = child.FindChildWithName(childName);            if (result)                return result;        }        return null;    }}

Для тех, кому хочется иметь возможность видеть событие OnClick() на кнопке в инспекторе - может быть полезна вот такая функция:

/// <summary>/// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)/// </summary>/// <param name="uiButton"> кнопка </param>/// <param name="action"> требуемое действие </param>private static void AddPersistentListenerToButton(Button uiButton, UnityAction action){    try    {        // сработает, если уже есть пустое событие        if (uiButton.onClick.GetPersistentTarget(0) == null)            UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action);    }    catch (ArgumentException)    {        UnityEventTools.AddPersistentListener(uiButton.onClick, action);    }}

То есть, если написать следующее:

// добавление события на кнопкуAddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);

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

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

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

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

/// <summary>/// Изменение слоя объекта по названию слоя/// </summary>/// <param name="gameObject"> объект </param>/// <param name="layerName"> название слоя </param>private void ChangeObjectLayer(GameObject gameObject, string layerName){    gameObject.layer = LayerMask.NameToLayer(layerName);}/// <summary>/// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии/// </summary>/// <param name="prefabPath"> путь к префабу </param>/// <param name="parentGameObject"> родительский объект </param>/// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0){    if (parentGameObject)    {        var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform);                    // изменение порядка в иерархии сцены внутри родительского элемента        newGameObject.transform.SetSiblingIndex(hierarchyIndex);    }    else        Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));}

Таким образом, при выполнении следующего кода:

// изменение тэга и слоя объектаvar cube = GameObject.Find("Cube");cube.tag = "Player";ChangeObjectLayer(cube, "MainLayer");               // создание нового объекта на сцене и добавление его в иерархию к существующемуInstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);

Элемент встанет не в конец иерархии, а на заданное место:

Цикл обновления сцен

И наконец, самое главное - функция, с помощью которой происходит вся дальнейшая автоматизация открывания-изменения-сохранения сцен, добавленных в File ->Build Settings:

/// <summary>/// Запускает цикл обновления сцен в Build Settings/// </summary>/// <param name="onSceneLoaded"> действие при открытии сцены </param>private void RunSceneUpdateCycle(UnityAction onSceneLoaded){    // получение путей к сценам для дальнейшего открытия    var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList();    foreach (var scene in scenes)    {        // открытие сцены        EditorSceneManager.OpenScene(scene);                    // пометка для сохранения, что на сцене были произведены изменения        EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());                    // проведение изменений        onSceneLoaded?.Invoke();                    // сохранение        EditorApplication.SaveScene();                    Debug.Log($"UPDATED {scene}");    }}

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

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

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

Подробнее..

Синтезатор на Unity 3D

09.04.2021 12:18:12 | Автор: admin

Учебные материалы для школы программирования. Часть13

Предыдущие уроки можно найти здесь:

При тематическом планировании уроков, мы сталкиваемся с интересной задачкой: в нашей группе учатся и мальчишки и девчонки. У них разные вкусы, разные любимые герои, жанры. И если все занятия будут на тему гонок или Silent Hill - ряды девочек на ваших занятиях, - поредеют. Эту ситуацию мы решаем двумя способами:

  • спрашиваем мнение ребят и проводим голосование на уроке. Да-да, только дети смогут сделать вашу программу интересной, поэтому их мнение действительно значимо. Не для галочки. Опрос и обсуждение важно проводить регулярно, ибо все группы разные, темы меняются от возраста к возрасту, меняются в зависимости от того, во что сейчас играют и что смотрят в кино;

  • чередуем и комбинируем. Мы стараемся чередовать "спокойные" и "агрессивные" проекты. Например, в этом месяце мы делаем лабиринт ужасов, но после его защиты - приступаем к созданию острова с лесом и домиком. При комбинировании, мы предлагаем всем ребятам уделить внимание визуальной составляющей (раскрасить, добавить элементов, составить композицию и пр.), и девочки с большим энтузиазмом берутся за такую задачу.

Сегодня мы сделаем проект из категории: спокойный, для девчат. Мальчишкам тоже хорошо заходит - особенно момент, когда в конце урока получается звуковая какофония =)

Рассмотрим следующие темы:

  • выставление вращения объектов в локальной системе координат посредством конверсии из углов Эйлера в кватернионы;

  • события объектов OnMouseEnter и OnMouseExit;

  • метод POW класса Mathf - возведение в степень;

  • парсинг float из имени объекта через системный метод Parse ;

  • стек постэффектов от Unity Technologies;

  • функция движка RequireComponent.

Особое внимание обратим на:

  • изменение скорости воспроизведения и высоты звука через Pitch;

  • использование аудиомикшера и постэффектов на мастер-канале микшера, в частности, реверберации и эмуляции комнаты.

Порядок выполнения

Создаётся новый проект, импортируется приложенный ассет, открывается сцена piano (в проекте заранее заготовлена сцена, ключевыми объектами которой являются клавиши, расположенные на сцене в "Пианино/Клавиши").
Клавиши пронумерованы в соответствии с полутонами, начиная с ноты "до" и заканчивается нотой "фа" следующей октавы, т.е. "до", "до диез", "ре", "ре диез", "ми", "фа" и т.д.

Клавиши расположены с начала октавы в той же последовательности, что и на реальном пианино.

Весь проект умещается в один скрипт. Полный листинг содержит пояснения:

using System.Collections;using System.Collections.Generic;using UnityEngine;using System;[RequireComponent(typeof(AudioSource))] // необходимо для того, чтобы скрипт требовал установленный аудиосорсpublic class Piano : MonoBehaviour {public KeyCode Key; // энумератор для выбора клавиши клавиатуры, на которую реагирует скриптAudioSource src; // Аудиосорс, приват-переменная     void Start () {src = GetComponent<AudioSource>(); // получаем аудиосорс        src.pitch = Mathf.Pow(1.059462f, float.Parse(name) - 1f); // высота звука равна 1.059462f в степени (имя_клавиши - 1).}    void Update () { // Для уменьшения отклика стоит использовать FixedUpdateif (Input.GetKeyDown(Key)) { // если нажали клавишу.playNote(); // играем        }        if (Input.GetKeyUp(Key)) { // если отпустили клавишу.stopNote(); // не играем        }}    private void OnMouseEnter() { // если мышь над коллайдером клавиши        playNote(); // играем}    private void OnMouseExit() { // если мышь вышла из коллайдера клавиши         stopNote(); // не играем}    private void playNote() { // играемtransform.localRotation = Quaternion.Euler(-3, 0, 0); // ставим локальный угол поворота на -3 градуса по Хsrc.Play(); // Включаем звук с начала}    private void stopNote(){ // не играемtransform.localRotation = Quaternion.Euler(0, 0, 0); // ставим локальный угол поворота на 0 градусов по всем осям    src.Stop(); // Останавливаем звук    }}

Особое внимание стоит уделить строке:

src.pitch = Mathf.Pow(1.059462f, float.Parse(name) - 1f);//высота звука равна 1.059462f в степени (имя_клавиши - 1).

Число1.059462высчитано математически и является простой заменой логарифмической функции, делящей одну октаву на 12 полутонов. Таким образом, каждый последующий полутон в 1.059462 раза выше предыдущего по частоте, что при количестве 12 полутонов даёт умножение частоты на 2 с ошибкой в 0.00003 Гц на октаву. С учётом того, что динамический диапазон нашего пианино не превышает полторы октавы, звук практически не искажается.

Тем не менее, звуки пианино плохо ложатся в микс, при отпускании клавиши отсутствует затухание, потому было принято решение немного "размазать" всё реверберацией с небольшой задержкой и эмуляцией большой комнаты, которая добавит "хвостов" затухания резко прерывающимся звукам.

Создана новая группа аудиомикшера, на мастер-канал которой установлена реверберация со следующими параметрами.

А всем аудиосорсам в качестве output установлен мастер-канал аудиомикшера.

Каждой клавише пианино необходимо вручную выставить необходимую клавишу клавиатуры. Так как мы имеем дело с энумератором, необходимо выбрать из представленных вариантов. Клавиши установлены в следующем порядке:

Далее, немного оформляем сцену и добавляем следующие эффекты:

  • SSAO - подчеркнёт тени между клавишами, добавит глубины картинке.

  • Bloom - высветлит светлые участки ещё сильнее, сделает картинку более приятной на глаз, интенсивность нужно выбрать довольно низкую.

  • Антиалиасинг, чтобы убрать пикселизацию.

  • Винетка, чтобы затенить края, выделив основной объект.

    Можно добавить SSR, для легкого отражения на вертикальных плоскостях, что немного улучшит картинку.

Готово!

Подробнее..

Регдоллы на Unity 3D

16.04.2021 18:11:03 | Автор: admin

Учебные материалы для школы программирования. Часть15

Предыдущие уроки можно найти здесь:

Регдоллы - физика тряпичных кукол, основная задача которых - реалистичное падение тел со скелетом. Регдоллы применяются везде - от шутеров (падающие враги) до гонок. Допустим, в Goat Simulator регдоллы являются важнои частью геимплея.

Goat SimulatorGoat Simulator

В данном занятии рассмотрены следующие аспекты:

  • работа со стандартным генератором регдоллов;

  • понимание скелета гуманоидных моделеи;

  • исправление неверно выставленных коллаидеров на Rigidbidy посредством дополнительных объектов в иерархии.

Порядок выполнения

Создадим новыи проект и импортируем в него приложенныи ассет.
Ассет содержит стандартную юнити-модель робота Каила, модель лестницы и демо-сцену (но вы можете выбрать любую другую "человекоподобную" модель, и самостоятельно разработать дизайн окружения).

Создадим новую сцену, установим на нее плеин или квад в качестве пола. На пол поставим лестницу, на лестницу - модель робота Каила.

Самое время рассказать о скелете: во время занятия на доске был нарисован один из типов стандартного скелета, с небольшими пояснениями для регдолла - какая кость куда крепится и как используется (лайфхак для педагогов: интегрируйте это занятие в цикл уроков о человеке и его строении).

Перед созданием регдолл-системы необходимо выставить модель в Т-позу. Поворачиваем 2 этих объекта в локальнои системе координат до нужного угла.

должно быть так:

Далее, нажимаем в окне объектов Create->Ragdoll и конфигурируем его следующим образом:

Жмем Create и упираемся в одну проблему. Как можно заметить, модель имеет неверные коллаидеры.

Исправим их размер вручную, кроме того коллайдера, которыи находится на правои (от нас) руке. С ним поступим иначе. Это капсула на руке, и еи нельзя напрямую выставить угол наклона, но можно скопировать этот компонент на пустои объект, разместить его в пивоте руки и повернуть локально. Капсулу на самои руке удалим.

После всех этих манипуляции, модель должна падать корректно, а именно, мягко и естественно.

Готово!

Подробнее..

Судно на воздушной подушке на Unity 3D

16.04.2021 18:11:03 | Автор: admin

Учебные материалы для школы программирования. Часть14

Предыдущие уроки можно найти здесь:

Сегодня мы настроены на отдых и развлечения! Поэтому, этот урок будет простой и "короткий". Мы не будем работать с графикой (но вас никто не ограничивает в праве усовершенствовать проект), уделим внимание управлению и работе с физикои, на примере создания судна на воздушнои подушке.

Порядок выполнения

Создадим новыи проект, импортируем в него приложенныи ассет. В данном ассете содержится модели, звук и простая сцена.

Первое, что нам необходимо сделать - это установить на сцену модели карты и СВП, затем создать материал с нулевым трением и назначить его юбке СВП

На само судно устанавливаем Rigidbody со следующими параметрами:

Обратите внимание, что на коллаидерах установлена галочка Convex, а Rigidbody не имеет галочку использования гравитации. Вместо нее используется ConstantForce с довольно большим значением, направленная вниз.

Как видите, на скриншоте уже наложен скрипт. Но до того, как приступать к нему, необходимо установить рулевые лопатки.
Также, на модель в точках установки рулевых лопаток, установлены пустые геим-обджекты с названием Gizmo, в них и уложены сами лопатки.

Скрипт конфигурируется согласно позапрошлому скриншоту. Полныи листинг скрипта выглядит таким образом:

using UnityEngine;using System.Collections; public class Howercraft: MonoBehaviour {    public Rigidbody HowercraftRigidbody; // риджитбади     public Transform CenterOfMass; // центр масс    public float power = 25000; // мощность вперёд/назад    public float torque = 25000; // мощность влево/вправо    float finAngle; // угол отклонения лопаток     float pitch; // питч для звука    public Transform[] Fins; // массив с лопатками    public AudioSource mainEngine; // звук основного двигателя       public AudioSource pushEngine; // звук турбин     // Use this for initialization     void Start() {        HowercraftRigidbody.centerOfMass = CenterOfMass.position - HowercraftRigidbody.position; // устанавливаем центр масс    }     // Update is called once per frame    void Update() {                float inpFB = Input.GetAxis("Vertical"); // ввод вперёд/назад        float inpLR = Input.GetAxis("Horizontal"); // и влево/вправо              Vector3 vely = new Vector3(HowercraftRigidbody.transform.forward.x, 0, HowercraftRigidbody.transform.for ward.z); // находим вектор приложения силы          float gain = Mathf.Clamp01(HowercraftRigidbody.transform.up.y); // если перевёрнуты, силы будут равны нулю             HowercraftRigidbody.AddForce(vely * power * inpFB * gain, ForceMode.Force); // добавляем линейные силы              HowercraftRigidbody.AddRelativeTorque(0, torque * inpLR * inpFB * gain, 0, ForceMode.Force); // и поворот              finAngle = Mathf.Lerp(finAngle, -45 * inpLR, Time.deltaTime / 0.2f); // угол лопаток            foreach(Transform Fin in Fins) {            Fin.localEulerAngles = new Vector3(0, finAngle, 0); // выставляем угол         }        mainEngine.pitch = 0.9f + HowercraftRigidbody.velocity.magnitude / 60f; //высота звука основного двигателя               pitch = Mathf.Lerp(pitch, Mathf.Abs(inpFB) * 1.3f, Time.deltaTime / 0.5f); // высчитываем высоту звука турбины               pushEngine.pitch = 1f + 2f * pitch;        pushEngine.volume = 0.3f + pitch / 3f;    }}

При этом скрипт лучше давать последовательно, сначала физическии движок, потом звуковои.

Готово!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru