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

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

Подробнее..

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

02.01.2021 18:15:24 | Автор: admin

Наш век запомнится мне, как время стартапов. От школьника-активиста, до жителя Кремниевой долины - все делают своих единорогов. Как поняли - так и делают. Я тоже не прошла мимо тренда на безграничные возможности самореализации, и запустила школу программирования для детей Step to Science. Время жизни этого проекта было наполнено открытиями, борьбы со страхами, маленькими и большими победами, ошибками и много чем еще. Весной 2020 года команда столкнулась с сами знаете с чем, и, немного порефлексировав, я приняла решение закрыть проект.

За годы работы школы, был найден оптимальный алгоритм обучения ребят занимательной науке разработки игр на Unity 3D. Мы перебрали много тем, которые смогли захватить интерес школьников от 10 до 16 лет, опробовали десятки способов передачи информации, тренировки навыков и оценки заложенных знаний. Эта кропотливая работа принесла нам отличные результаты, в виде комплексной программы по созданию компьютерных игр на Unity 3D, и учебных успехов, которые достигали наши ребята!

Этими результатами и успехами мне хочется поделиться с широкой аудиторией. Если вы ведете кружок программирования или робототехники, а может, вы родитель, ищущий ответ на вопрос "чем увлечь подростка?" - я предлагаю вам воспользоваться материалами школы абсолютно безвозмездно, то есть, даром!

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

Spoiler

Справка для моих коллег - педагогов:

Unity3D это популярный движок разработки 3D-игр и интерактивных приложений для различных платформ и устройств.ПО обладает понятным и удобным интерфейсом;большим количеством мощных вспомогательных средств, связанных с проектированием и дизайном;современным уровнем трехмерной графики и спецэффектов.

Образовательные цели:

Знакомство с работой движка и языком программирования C#;

Освоение на практике основных принципов и технологий создания современных трехмерных компьютерных игр;

Формирование навыков моделирования объектов, создания персонажей и анимации, навыков работы с текстурами и освещением, проектированием ландшафта, работы со звуком, спецэффектами;

Образовательные задачи:

Создать портфолио проектов ученика;

Получить практические навыки профессионального разработчика игровых приложений;

Прохождение полного цикла знакомства с профессией game development.

Количество детей в группе: 7-10 человек.

Возраст детей: 10-16 лет.

Форма и режим занятий: занятия проходят в практической форме, 2 раза в неделю по 2 академических часа с 10 минутным перерывом.

Минимальные технические требования к ПК: OS Windows 7 SP1+, 8, 10, только 64-разрядные версии; ЦП поддержка набора инструкций SSE2; видеокарта с поддержкой DX10 (версия шейдеров 4.0).

Общая продолжительность программы: 2 модуля по 48 часов каждый.

Знакомство с Unity 3D мы начнем с легкой в разработке и веселой игры "Spaceship". Этот проект мы всегда делали на первом пробном занятии, т.к. благодаря простоте, ученик быстро получает желаемый результат, укрепляется в вере в свои силы, получает массу положительных эмоций и в глазах зажигается тот самый, желаемый родителями, интерес к новым знаниям!

Итак, поехали!

Spaceship

В данном занятии рассмотрено создание простого космического симулятора.


Занятие является пробным и рассчитано 1.5 - 2 часа времени.


Версия Unity 3D от 5.5 и выше. На компьютеры учеников должен быть скопирован файл Spaceship_template (если ссылка сломалась - пишите в Telegram @Evgeniya_Koroleva).

В ходе выполнения будут затронуты основы работы с редактором, моделями, скриптами, физикой и звуком.

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

Откроем юнити и создадим новый 3D проект, нажав на кнопку New. Дадим проекту имя и нажмём Create project.

Откроется окно редактора. На данном этапе стоит рассказать ученикам о назначении основных вкладок и окон. В рамках данного занятия, рассматриваем только самые основные, представленные на скриншоте.

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

Чтобы выйти из режима игры, ещё раз нажмём на Play.

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

Заменим стандартный скайбокс на скайбокс Млечный путь. Для этого необходимо вызвать окно освещения.

В данном окне надо найти поле Skybox Material и нажать на кнопку выбора материала.

В появившемся окне выбрать двойным кликом материал MilkyWay.

Окно Lighting можно закрыть.


Чтобы небо отображалось в окне Scene редактора, должна быть нажата кнопка показа неба и атмосферных эффектов (выделено красным) , а в выпадающем списке должна стоять галочка напротив пункта Skybox (выделено синим).

Космическому симулятору необходимы точки, смотря на которые мы бы понимали, что объект двигается. Для этого проекта такой точкой является космический корабль SF_Corvette-F3.
Найдём его в ресурсах и выложим на сцену.

Скорее всего, камера окажется внутри корабля и в окне Game корабль будет виден наизнанку. Выберем инструмент Move tool (выделен красным) и выдвинем камеру наверх.

На данном этапе стоит рассказать о том, как перемещаться по карте в режиме редактирования, а именно в окне сцены зажимаем правую кнопку мыши и летаем, нажимая WASD. Для большей скорости перемещения удерживаем Shift.


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

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

Чтобы камера лучше видела игрока, можно развернуть её с помощью инструмента Rotate tool.

Настало время добавить кораблю способность двигаться. На данном этапе стоит рассказать ученикам о том, что всё поведение объектов полностью зависит от компонентов, которые находятся на этом объекте. Допустим, объект SpaceFighter содержит компонент Transform, который задаёт объекту его положение в пространстве. Чтобы корабль мог летать, необходимо добавить ему компонент физики. Выделим SpaceFighter и нажмём в панели настроек кнопку Add Component, в появившемся выпадающем списке выберем Physics->Rigidbody.

Если сейчас мы нажмём на Play, то мы увидим, что корабль падает.

Чтобы этого не происходило, нам необходимо отключить гравитацию. Для этого достаточно у компонента Rigidbody убрать галочку Use Gravity.

В самой нижней строке мы видим сообщение об ошибке. Это сообщение говорит нам о том, что требуется сделать пересчёт физической модели столкновения для объекта. Для этого откроем объект SpaceFighter, нажав на стрелочку рядом с ним и найдём объект Default. По умолчанию на данном объекте находится компонент Mesh Collider. Выставим на компоненте галочку Convex и увидим реальную физическую модель столкновений для данного объекта.

На следующем этапе стоит рассказать, что у объектов есть их видимая часть, а есть физическая модель столкновений, и далеко не всегда эти вещи имеют одинаковую форму.

Теперь заставим наш объект двигаться. Для этого в объект SpaceFighter, рядом с Rigidbody, добавим ещё один компонент Constant Force (выделено красным).

Данный компонент добавляет силу, которая будет двигать наш корабль. Чтобы объект полетел вперёд, выставим ему в отмеченном синим цветом поле значение в районе 100.

Нажав на Play, мы можем видеть, как корабль улетает вдаль.

Теперь, прикрепим камеру к игроку. Для этого достаточно объект Main Camera перетянуть мышью на объект SpaceFighter.

Должно быть так

Теперь камера находится внутри нашего космического корабля и поэтому двигается вместе с ним.

Далее, необходимо добавить управление нашему игроку. Для этого в папке CustomResources/Scripts найдём скрипт Player_Battleship и перекинем его на игрока.

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

Если мы попытаемся зайти в режим игры, то можем заметить, что корабль управляется слишком резко и слишком быстро набирает скорость. Это вызвано тем, что компонент Rigidbody, обеспечивающий физику, настроен по стандарту на массу в один килограмм. Добавим массу нашему космическому кораблю, а заодно немного поменяем значения замедления, чтобы корабль был более управляемым.

Теперь наш корабль, напротив, стал довольно медленно поворачиваться. Настроим управление корабля:

Теперь можно заметить ещё одну странность корабль игрока проходит сквозь корабль SF_Corvette-F3. Чтобы этого избежать, необходимо на корабль SF_Corvette-F3 добавить компонент Mesh Collider.


Обратите внимание, что галочку Convex ставить нет необходимости. Данная опция нужна лишь для движущихся объектов, а этот корабль неподвижен.

Осталось немного добавить фоновую музыку и звуки полёта.


Возьмём звук \Absolute Space & Sci-Fi Vol.1 - Sample Pack - Voltz Supreme\Preperation - 70 bpm\Variations\Preperation - No Snare or Vox.mp3 из папки проекта и просто перекинем на любой объект, допустим, на космический корабль SF_Corvette-F3.

В этом случае автоматически добавится компонент AudioSource, в котором можно будет настроить звук, например, убавить громкость (выделено синим). Рекомендуемый уровень громкости 0.3.

Чтобы добавить звук полёта, возьмем звук Standard Assets\Vehicles\Aircraft\Audio\FlightWind.wav
и перетянем его на игрока. В настройках поставим галочку Loop (выделено синим) это закольцует звук и он будет проигрываться постоянно. Скрипт Player_Battleship автоматически найдёт этот звук и будет управлять его громкостью в зависимости от скорости игрока.

Если учащиеся справилась с заданием раньше срока, можно добавить немного динамики нашему проекту. Для этого закинем на объект Main Camera скрипт Player_Camera и настроим в соответствии со скриншотом.

Также можно добавить следы на краях крыла: внутри объекта SpaceFighter есть две точки trail и trail_1.
Добавим на них компонент Trail Renderer и протестируем - должны получиться яркие розовые линии.

Чтобы это исправить, в компоненте Trail Renderer найдём массив Materials, раскроем его, найдём поле Element0 и нажмём на кнопку выбора- кружок с правой стороны поля.

Выберем белый материал, допустим, этот:

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

В конце можно добавить простой прицел, создав объект типа Image и настроив его:

В поле Sprite компонента Image нажмём на кружок выбора и выберем текстуру прицела.

Выберем прицел.

В компоненте Image нажмём на появившуюся там кнопку Set Native Size (выделено красным). Дополнительно, можно выбрать цвет прицела, нажав на поле Color (выделено синим).

В конце занятия можно выполнить команду Build And Run, которая соберёт проект в виде уже готовой игры для компьютера, который ребята могут забрать домой, чтобы показать друзьям.
При этом появится диалоговое окно, где необходимо будет выбрать расположение и имя игры.

После данного действия будут созданы файл *.exe, папка *_GameData и UnityPlayer.dll, где * - выбранное имя. Файл UnityPlayer.dll в зависимости от версии может и не создаваться. Для работы готовой игры на другом компьютере все сгенерированные в ходе данного действия файлы и папки должны находиться в одной директории.

На этом мы завершаем первое занятие-знакомство с Unity 3D.

Следующие проекты - в следующих постах на habr. До скорых встреч!


P.S.: за помощь в подготовке проектов, невероятный преподавательский талант и незабываемый черный юмор, спасибо Александру Борисову!

Подробнее..

Flappy Bird на Unity 3D

10.01.2021 16:05:45 | Автор: admin

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

Spoiler

Часть 1 вы можете найти здесь

Часть 2 вы можете найти здесь

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

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

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

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

Решением стало снизить количество рассматриваемых тем из школьной программы, и добавить в проекты элемент узнаваемости, соревновательности и веселья! Так появилось занятие по сборке аналога игры Flappy Bird.

Цель занятия: научиться работать с 2д-физикой и Canvas при использовании последнего на разных разрешениях целевой платформы.

Рассматриваем с учащимися темы:

- включение AudioSource посредством Event-системы UI;
- детектирование тапа по экрану посредством Event-системы UI - Спрайты;
- система анимаций в разрезе 2D-игр;
- коллайдеры в 2D и их редактирование;
- сборка проекта под Android.

Поехали!

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

Для начала научимся пользоваться спрайтами. В ресурсах игры лежит файл sprites.png, который импортирован как спрайт с пометкой Multiple, это позволяет посредством кнопки SpriteEditor разрезать его на несколько одиночных спрайтов.

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

Стоит рассказать ребятам об особенностях работы коллайдеров, накладываемых на спрайт. Например, на птице установлен Polygon Collider 2D, позволяющий точно подобрать форму коллайдера под спрайт без необходимости делать его выпуклым для симуляции физики, как в 3D.

Анимация спрайтов.

Следующим шагом будет создание движущихся труб. Трубы устроены следующим образом: пара труб с установленным между ними триггером со скриптом, убивающим игрока, расположены в одном объекте. Передвигать нужно именно этот объект. Записываем на него анимацию, выходим из режима записи и проверяем.

Стоит упомянуть, что, помимо положения спрайта, в окно можно закинуть и другой спрайт - при воспроизведении анимации он заменит собой предыдущий. Подобным образом делается анимированный огонь и прочие спрайтовые эффекты.

Для компиляции под Android важно не забыть про удобное управление. Для мобильных игр, в большинстве своём, оно должно осуществляться одним пальцем. Делается это довольно просто (решение не является оптимальным, но оно самое простое).

В Canvas создаём панель и делаем её абсолютно прозрачной.

Все элементы управления закидываем внутрь этой панели. Это обязательно. Если этого не сделать, панель и кнопки могут некорректно отвечать на нажатия.
На панель необходимо закинуть компонент EventTrigger, и создать новое поле OnPointerEnter. В это поле закидываем птицу и выбираем внутри метод Jump.

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

Компиляция проекта под андроид.
Зайдём в File->Preferences->External Tools. Внизу страницы находятся три поля - SDK, JDK и NDK. Рядом кнопки Browse и Download. Первая - для выставления нужной папки, в которую установлены sdk, jdk, ndk, вторая - перекидывает на страницу загрузки того или иного инструмента.

Сперва необходимо установить Android studio и необходимые пакеты инструментов и SDK. На момент написания этих строк, устанавливалось API уровня 16 и 25. Также для компиляции необходим JDK.
NDK для данного проекта устанавливать нет необходимости.

Далее необходимо зайти в File-> Build Settings , выбрать платформу Android и нажать Switch Platform.

Затем переходим в Edit -> Project Settings -> Player

В свитке Resolution and presentation выбираем расположение Landscape left, это заставляет экран всегда быть в одном положении и не поворачиваться, если включён автоповорот.

В свитке other settings необходимо выставить имя приложения в том же формате, что и стандартное. Тем не менее, изменить это поле обязательно, в противном случае компиляция не пройдёт успешно. Также необходимо выбрать минимальные и целевое API.

По желанию можно выставить иконку и компилировать. На выходе мы получим APK файл игры.
Если подключить телефон на андроиде в режиме отладки и нажать Build and Run, то игра автоматически установится на телефон и запустится.

Хочется напомнить, что представленные методические рекомендации к занятиям по Unity, разрабатывались для преподавателей информатики и программистов, работающих с ребятами.

Подробнее..

Гравитационная комната в Unity 3D

16.01.2021 12:10:38 | Автор: admin

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

Spoiler

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

  1. Spaceship

  2. Домино

  3. Flappy Bird

В первых публикациях я немного рассказывала про школу программирования, в рамках которой создавались проекты по Unity 3D. Знакомясь с нашими наработками, у многих возникали вопросы, которые нельзя оставить без внимания:

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

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

Во-вторых, как основатель проекта, я преследовала свою идею и мечту. Мне не хотелось учить ребят тому, что им нигде не пригодится, давать навыки, которые они не смогут перенести на другую деятельность. Поэтому, в основу проекта лег лозунг "Профессиональные инструменты доступные детям".

В-третьих, мы подобрали такую форму организации занятий, которая помогала всем чувствовать себя комфортно. И ребятам, кто с раннего возраста возится в компьютере, и ребятам, которые компьютер увидели впервые на нашем занятии (многие дети дома пользуются только телефоном/планшетом, и не имеют опыта работы с ПК).

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

Построение образовательных траекторий требует дружелюбности, бесконечной эмпатии, гибкости и вариативности. К нам приходили разные ученики, с разным опытом, с разной мотивацией. Из разрозненной группы подростков, нам надо было сделать сообщество. А еще, нам надо было создать такое пространство, куда бы ребята хотели сами придти, а не потому что "мама сказала - надо!". Поэтому занятия облегчены, но для тех, кому дается легко - ты просто "вынимаешь" элемент из проекта, добавляя в него больше самостоятельности. Для тех, кому самый облегченный вариант не по зубам - рядом преподаватель (группы не более 7 человек), или товарищ, который всегда придет на помощь.

- Почему такая простая графика?

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

Перейдем от вопросов к делу!

Гравитационная комната

На уроке учимся:

- Работать с вертекснои привязкои при перемещении объектов (см. функцию клавиши V в Unity);

- Работать с привязкои к мировому пространству через Configurable Joint;

- Знакомим с акселерометром и гравитацией в разрезе игровых движков;

- Повторяем работы с rigidbody и коллаидерами.

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

Создается новыи проект, импортируется приложенныи ассет, создается новая сцена. На сцене создается куб размером примерно 7х7х7, затем его размер, с помощью инструмента scale, меняют примерно до 7х0.3х7.

Данную фигуру размножают, поворачивают на 90 градусов и привязывают по углам с зажатои клавишеи V так, чтобы получился закрытыи куб. У стенки, которая находится с положительнои стороны оси Z, выключаем компонент MeshRenderer, в итоге, одна из стенок становится прозрачнои. Переименовываем ее в "Комната" и закидываем остальные стены, пол и потолок. Затем, размер всеи комнаты можно менять с помощью scale.

Из сферы и цилиндров создаем подобие лампочки, на сферу накладываем материал со стандартным шеидером и эмиссиеи. Внутри сферы создаем point light, а стандартныи directional light со сцены удаляем. Чтобы задать нашеи лампочке ось вращения, создаем пустои объект на самои верхушке провода лампочки, называем его "ось лампочки", закидываем в него все объекты, относящиеся к лампочке.

На ось накладываем Configurable joint, при этом, автоматически добавится rigidbody. Необходимо зафиксировать положение оси в мире, установив transform X, Y и Z в положение Locked.

Устанавливаем камеру так, чтобы она нормально покрывала всю комнату, при этом в Transform камеры будет установлен угол 180 градусов по Y.

Также, на сцену добавляются объекты, к которым применяется MeshCollider -> Convex и Rigidbody с массои в районе 4-5 кг.

Пишем скрипт, которыи в Update() содержит всего одну строку:

и закидываем в любои объект на сцене, допустим, в камеру. Для того, чтобы игра работала на слабых устроиствах, закидываем скрипт Resolution туда же. Этот скрипт принудительно занижает разрешение рендера до 800х450 на устроиствах с экранами большего размера.

Далее, компилируем на Android уже известным нам, из предыдущего урока, способом. Проверяем. Если все выполнено верно, лампочка будет отклоняться вниз по отношению к устроиству в любом положении, пока еи не мешает потолок, а все объекты падают в ту сторону, в которую наклонен телефон.

Подробнее..

Платформер от первого лица на Unity3D

17.01.2021 12:19:01 | Автор: admin

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

Spoiler

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

  1. Spaceship

  2. Домино

  3. Flappy Bird

  4. Гравитационная комната

Продолжаем учиться - играя! Это занятие небольшое, но очень важное для дальнейшего движения по образовательной траектории. Мы познакомимся с системой анимации, контейнерами, и попробуем различные анимационные комбинации.

Все материалы к занятию вы найдете здесь.

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

Создадим новый проект, импортируем в него стандартные ассеты Characters и Prototyping. Из префабов Prototyping создадим подобие игрового уровня.

Создадим платформу и вызовем окно анимации.

Нажмем на Create и назовем нашу анимацию.

Переидем в режим записи и создадим новое правило для платформы - Transform->position.

Это необходимо, чтобы проставить первыи и последнии кеифреим.

Поставим курсор куда-нибудь посередине и подвигаем платформу.

Автоматически создался еще один кеифреим. Если запустить проект, можно заметить, что платформа двигается.

Чтобы игрок мог на нее запрыгивать и передвигаться вместе с неи, на платформе нужно создать коллаидер-триггер и назначить скрипт Platform.

Переключившись в режим Curves, немного изменим характер ее движения.

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

Создадим еще одну платформу, но вложим ее в пустои объект и выровняем в нули. Создадим новую анимацию.

Теперь, перемещая родительскии объект, можно перемещать платформу вместе с анимациеи.

Также, стоит коснуться работы аниматора. Наидем AnimatorController, выделим его и откроем окно Animator.
Создадим еще одну анимацию и настроим переходы.
На этом этапе стоит объяснить принципы работы аниматора и возможность создавать условные переходы и переходы между анимациями с блендингом.

Теперь платформа воспроизводит первую анимацию, затем, 2 раза вторую, и так - по кругу. Обязательно расскажите ребятам, о возможности менять скорость анимации в аниматоре.

В заключении урока, стоит объяснить, что система анимации - обширная тема, и мы затронули ее лишь поверхностно. Стоит дать ученикам возможность самостоятельнои работы, попутно отвечая на все возникающие вопросы.

Подробнее..

Деревья (плагин SpeedTree) на Unity 3D

24.01.2021 00:17:21 | Автор: admin

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

Spoiler

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

  1. Spaceship

  2. Домино

  3. Flappy Bird

  4. Гравитационная комната

  5. Платформер

В Unity, в asset store, можно найти множество готовых объектов для будущей игры, но иногда, хочется сделать что-то своими руками. Для зданий, и других простых архитектурных объектов, мы часто использовали SketchUp, для "живых" анимированных объектов - Blender. На этом уроке, мы сами "вырастим" дерево в Unity!

Небольшое отступление для тех, кто следит за публикациями, но не решается попробовать включить в свои уроки новые инструменты. Попробуйте сделать первый шаг с 3D. Программ по 3D моделированию очень много, среди них есть настолько простые, что методом "тыка" можно освоить их за пару вечеров. Посмотрите обязательно:

  • Tinkercad - составление фигур из геометрических форм;

  • SketchUp - для более детально проработанных архитектурных объектов;

  • ZBrush - можно создавать сложные, крутые модели, а можно ограничиться "лепкой" колобка, что тоже очень занятно!

Но вернемся к теме занятия, и рассмотрим, как создать дерево с помощью плагина SpeedTree, включенного в дистрибутив Unity.

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

Создадим новыи проект, в нем создадим терреин.

Добавим на него объект 3D Object Tree

Добавим дереву ветки, создав блок веток. Увеличим их количество примерно до 20 (интересующие параметры выделены синим). В каждом блоке изменяем параметр Frequency (цветом на скриншоте не выделен).

Изменим их расположение, поменяв кривые распределения, длины, и поменяем ползунок стремления ветвеи к свету.

Далее, добавим к этим ветвям еще несколько веток.

Не забудем про листву.

Следующий шаг - выставление материалов.

Для деревьев важно выставлять материалы с помощью шеидера Nature. Импортируем стандартныи ассет Environment, он содержит готовые материалы. Материалы коры нужно назначить всем веткам. Если материал не подходит, Unity предложит использовать корректныи материал.

Жмем Apply и "переконвертирования" материала. Будьте осторожны, Unity часто вылетает на этом этапе.

Итак, дерево готово! Его можно разместить на терреине благодаря Mass Place Trees.

При должном усердии, можно получить вот такой лес:

Подробнее..

Синтезатор на 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;    }}

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

Готово!

Подробнее..

Игровые механики на уроке геометрии или векторы на Unity 3D

20.04.2021 16:13:49 | Автор: admin

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

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

В этой статье, мы обратим свой взор в прошлое, и вспомним, с чего начиналась детская школа программирования Step to Science. Первоначальная идея проекта состояла в том, чтобы быть не просто кружком технического творчества, а стать для детей ответом на вопрос, "зачем учиться в школе?"

К чему нам физика, алгебра и геометрия, если мы не планируем проектировать космические корабли, если для счета у нас есть калькулятор в телефоне, расплачиваемся мы чаще картой, так что даже сдачу в уме считать не надо.
Я тоже в детстве вела такие рассуждения, и у родителей не было иных способов донести до меня истину, кроме фразы "нет слова не хочу, есть слово надо" и ремня, который без лишней полемики мотивировал садиться за уроки.

С возрастом, перейдя на другую сторону баррикад, я поняла что хочу ребятам объяснить, показать, доказать, что учиться в школе действительно важно! И игровой проект, который мы разберем сегодня - один из цикла занятий по изучению школьных предметов через игры на Unity 3D.

Кроссплатформенный движок Unity дает огромные возможности учителю: через увлекательный процесс создания игр мы изучаем законы физики, геометрии, делаем расчеты, проектируем окружение, используем сторителлинг, сценарные механики. И конечно-же программируем. Вариантов интеграций Unity в другие образовательные и предметные области - бесчисленное множество!

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

На примере создания 2D игры баскетбол, рассмотрим векторы (скорости, сил, локальнои и глобальнои систем координат). Разберем принципы представления систем координат и представления векторов. Также будет затронута работа с LineRenderer и многокамерность.

Поехали!

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

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

Конечно, необходимо выставить правильныи Order in layer у спраитов. Добавим мяч, применим к нему Circle collider и Rigidbody.

Внутри мяча должен находиться пустои объект с Audio Source, настроенным на воспроизведение звука удара.

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

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ball : MonoBehaviour {    public AudioSource hitSound;    public Rigidbody2D rig;    // Use this for initialization    void Start () {    }    // Update is called once per frame    void FixedUpdate() {    }    private void OnCollisionEnter2D(Collision2D other) {        if (other.relativeVelocity.magnitude > 1f) {            hitSound.Play();            hitSound.volume = Mathf.Clamp01(other.relativeVelocity.magnitude / 10);            rig.velocity *= 0.8f;        }    }}

В скрипте нет автопоиска Rigidbody, так что придется закинуть его руками. Если нажать на Play, наш мяч упадет, издавая звуки. Чтобы мяч отскакивал, создадим физическии материал и закинем его на коллаидер мяча.

Теперь подумаем о том, чтобы мяч показывал свое направление. Для этого создадим скрипт, которыи рисует стрелки: нам понадобятся два пустых объекта с LineRenderer, один в другом.

Создадим материал для стрелки:

И добавим скрипт, которыи будет выставлять вершины LineRenderer'ов, делая из них стрелки:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Arrow : MonoBehaviour {   public Vector3 showVector;    public LineRenderer lrenderer1;    public LineRenderer lrenderer2;    Transform myTransform;    // Use this for initialization    void Start () {        //lrenderer1 = GetComponent<LineRenderer>();        myTransform = transform;    }   // Update is called once per frame    void Update () {        showVector = new Vector3(showVector.x, showVector.y, 0f);        lrenderer1.SetPosition(0, myTransform.position);        lrenderer1.SetPosition(1, myTransform.position + showVector);          if (showVector.magnitude >= 2f) { // длинная стрелка            lrenderer2.SetPosition(0, myTransform.position + showVector - showVector.normalized);        } else {            lrenderer2.SetPosition(0, myTransform.position + showVector * 0.5f);        }        lrenderer2.SetPosition(1, myTransform.position + showVector);        if (showVector.magnitude < 0.1f) {            lrenderer1.enabled = lrenderer2.enabled = false;        } else {            lrenderer1.enabled = lrenderer2.enabled = true;        }    }}

Закинем скрипт на объект-родитель стрелки и настроим его.

Теперь надо написать скрипт, которыи будет вектор скорости передавать в наш скрипт "показывания" стрелки. Он очень простои:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class VectorVelocity : MonoBehaviour {    public Rigidbody2D rig;    public Arrow arrow;    // Use this for initialization    void Start () {     }    // Update is called once per frame    void Update () {        if (rig.bodyType == RigidbodyType2D.Dynamic) {            arrow.showVector =  rig.velocity / 5f;        }    }}

Закинем его на мяч, в скрипте укажем риджибади мяча и объект со скриптом стрелки.

Теперь вектор скорости показывается верно. Вектор скорости уменьшен в 15 раз, чтобы его было хорошо видно. А для того, чтобы было видно траекторию мяча - добавим ему Trail Renderer на любои привязанныи к мячу объект.

Теперь сделаем так, чтобы мяч можно было кидать. Для этого необходимо выставить ему тип Rigidbody как Kinematic и написать небольшои скрипт.

Листинг скрипта:

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.EventSystems;public class Spawner : MonoBehaviour {    public Rigidbody2D ball;    public TrailRenderer tr;    Quaternion oldRotation;    Vector3 oldPosition;    public bool readyToShoot = true;    // Use this for initialization    void Start () {        oldPosition = ball.transform.position;        oldRotation = ball.transform.rotation;    }    // Update is called once per frame    public void Respawn () {        ball.transform.position = oldPosition;        ball.transform.rotation = oldRotation;        ball.velocity = Vector3.zero;        ball.angularVelocity = 0;        ball.bodyType = RigidbodyType2D.Kinematic;        readyToShoot = true;        tr.Clear();    }    public void Shoot(Vector3 speed) {        if (!readyToShoot) {            return;        }        ball.bodyType = RigidbodyType2D.Dynamic;        ball.velocity = speed;        readyToShoot = false;    }}

Скрипт выкладываем на пустои объект в мире и устанавливаем ему наш мяч в качестве риджитбади и его треил.

Этот скрипт сам по себе ничего не делает. Чтобы он работал, необходимо организовать ввод. Создадим UI -> Panel на сцене, выставим панели нулевую альфу и установим на него скрипт TouchPanel.cs , приложенныи в проект.

Внутри панели должен лежать спраит со следующими параметрами (обратите внимание на привязку):

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

Для того, чтобы сделать включение/выключение стрелок, используется скрипт Toggle, которыи реализован через эвент-систему юнити. Его необходимо закинуть на кнопку и сконфигурировать следующим образом.

Готово!

P.S. Делитесь ссылкой на статью с коллегами, друзьями и любопытными учениками. Будет здорово, если вы попробуете провести один из уроков в своей школе или в кружке детского технического творчества, и напишите пару слов обратной связи о том, как прошел урок по Unity 3D. Успехов!

Подробнее..

Тир. Стрельба рейкастами на Unity 3D

06.05.2021 18:22:32 | Автор: admin

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

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

В этом проекте рассмотрим процесс работы:

- с рейкастами и векторами;
- с методами других пользовательских классов;
- с AudioSource и с Rigidbody через код;
- три основных составляющих выстрела, психологически действующих на игрока (звук, свет и свечение, анимация и след от выстрела);
- инстанцирование префабов.

Ключевыми темами проекта выступают именно рейкасты и векторы. Последним, необходимо уделять довольно много времени каждый день, и на простых примерах объяснять, как они устроены. Но если вам удалось быстро выполнить проект с учениками, то в этом уроке будет уместно рассмотреть mecanim-систему.

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

Создаём новый проект, импортируем приложенный ассет.
Помимо стандартных ресурсов пакет имеет сторонний плагин для рисовки декалей. Его работа в контексте данного урока не рассматривается.

Проект урока разбит на 2 части - тир и гранаты.

Тир

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

Внутри проекта есть скрипт DecalShooter, который создаёт декали, и в котором расположен весь код стрельбы, включая рейкаст. В нём будет вводиться код взаимодействия с мишенью.
Для начала, необходимо подготовить саму мишень. Ею служит цилиндр, который необходимо уменьшить по Y до состоянии платины, удалить CapsuleCollider и поставить MeshCollider с галочкой Convex. Дополнительно, на цилиндр устанавливается текстура мишени, внутри цилиндра создаётся point light, подсвечивающий мишень, и объект с AudioSource для воспроизведения звука, а на сам цилиндр устанавливается Rigidbody с обработкой коллизий типа Continius Dynamic и галочкой isKinematik. У AudioSource не забудьте убрать галочку PlayOnAwake и закинуть звук попадания в мишень.

Следующим шагом становится создание скрипта Target, который сразу же накладываем на нашу мишень.

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

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Target : MonoBehaviour {    public GameObject light;    public Rigidbody rig;    public AudioSource src;     bool enabled = true;     // Use this for initialization     void Start() {        rig = GetComponent<Rigidbody>();        src = GetComponent<AudioSource>();     }     // Update is called once per frame     void Update() {     }     public void shoot() {        if (!enabled) {           return;        }         rig.isKinematic = false;         light.SetActive(false);         src.Play();         enabled = false;    } }

Код мишени содержит публичный методshoot, который необходим для того, чтобы другие скрипты могли обращаться к нашей мишени. Следующий шаг - проверка из файла DecalShooter попали ли мы в мишень и вызов методаshoot. В рейкаст необходимо включить следующий участок кода:

   if (Input.GetKeyDown(KeyCode.Mouse0)) {            time = 0.3f;             ShootSource.Play();             anim.Play("fire");             Muzzleflash.SetActive(true);            // Сама стрельба             RaycastHit hitInfo;            Vector3 fwd = transform.TransformDirection(Vector3.forward);                         if (Physics.Raycast(transform.position, fwd, out hitInfo, 100f)) {                GameObject go = Instantiate(                    DecalPrefab,                    hitInfo.point,                     Quaternion.LookRotation(                        Vector3.Slerp(-hitInfo.normal, fwd, normalization)                     )                ) as GameObject;                go.GetComponent<DecalUpdater>().UpdateDecalTo(                    hitInfo.collider.gameObject,                     true                );                Vector3 explosionPos = hitInfo.point;                Target trg = hitInfo.collider.GetComponent<Target>();                                if (trg) {                    trg.shoot();                }                                Rigidbody rb = hitInfo.collider.GetComponent<Rigidbody>();                                if (rb != null) {                    rb.AddForceAtPosition(fwd * power, hitInfo.point, ForceMode.Impulse);                    Debug.Log("rb!");                }             }            // Сама стрельба         }

Данный код пытается получить компонент из объекта hitInfo и, если это удаётся, вызывает методshoot. Мишень падает, свет от мишени выключается, звук попадания воспроизводится. Далее, желательно дать группе свободное задание по кастомизации своего проекта. Как альтернативу, можно предложить изменить код таким образом, чтобы мишень меняла цвет при попадании. Делается это заменой в Target строк:

light.SetActive(false);

на

light.GetComponent<Light>().color = Color.red;

Таким образом, свет меняется и не удаляется.

Гранаты

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

Для начала, создадим другой скрипт - дальномер.

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI; public class Lenght :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     // Use this for initialization     void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hitInfo, 200)) {            rasstoyanie = hitInfo.distance;            Dalnost.text = rasstoyanie.ToString();        }    }}

Скрипт закинем в FPScontroller/FirstPersonCharacter. В Canvas создадим текст, закинем его в скрипт.
В этом скрипте реализован простейший рейкаст, и на его примере мы разбираем, как рейкаст передаёт информацию в структуру и как нам получать из структуры эту информацию.
При срабатывании рейкаста мы выводим дальность на экран.

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

using System.Collections;using System.Collections.Generic; using UnityEngine;using UnityEngine.UI; public class Length :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     public GameObject sharik;     // Use this for initialization    void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                 if(Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), outhitInfo, 200)) {            rasstoyanie = hitInfo.distance;             Dalnost.text = rasstoyanie.ToString ();            if(Input.GetKeyDown(KeyCode.Mouse1)) {                GameObject go = Instantiate(                    sharik,                     transform.position + Vector3.Normalize(hitInfo.point - transform.position),                     transform.rotation                );                Rigidbody rig = go.GetComponent<Rigidbody>();                rig.velocity = Vector3.Normalize(hitInfo.point - transform.position) * 10;            }         }    } }

Сразу же возникнет вопрос - зачем такое решение? К тому же, при направлении в небо бросок не произойдёт. Сделано всё это в учебных целях. Т.к. в рамках урока код пишется поэтапно, на данном участке объясняется, как найти вектор направления на объект по конечным и начальным координатам. Теперь дело за самой гранатой. Создадим скрипт Grenade.

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Grenade :  MonoBehaviour {    public Transform explosionPrefab;     void OnCollisionEnter(Collision collision) {        ContactPoint contact = collision.contacts[0];                // Rotate the object so that the y-axis faces along the normal of the surface        Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);        Vector3 pos = contact.point;         Instantiate(explosionPrefab, pos, rot);        Destroy(gameObject);    }}

Закидываем на сцену гранату, на меш Body ставим коллайдеру Convex, добавляем гранате RIgidbody и наш скрипт. Получившуюся гранату добавляем в префаб и удаляем со сцены.

Скрипт удаляет объект, на котором висит, но до этого инстантиирует объект, который мы выбираем в качестве эффекта взрыва.

Создадим эффект взрыва. В нём должен быть свет от взрыва, AudioSource с галочкой PlayOnAwake и звуком взрыва, Spital Blend на 90 процентов переведённый в 3д и увеличенный радиус распространения звука.
Для правильной отработки всех эффектов и разлёта Rigidbody нужно создать ещё один скрипт. Его мы назовём Explosion:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Explosion :  MonoBehaviour {    public float radius = 5.0f;    public float power = 10.0f;    public GameObject svet;    void Start() {        Destroy(svet, 0.1f);        Vector3 explosionPos = transform.position;        Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);        foreach(Collider hit in colliders) {            Rigidbodyrb = hit.GetComponent<Rigidbody>();            if (rb != null) {                rb.AddExplosionForce(power, explosionPos, radius, 3.0f);            }        }    }}

Его нужно закинуть на эффект взрыва и создать префаб.
Данный префаб и используется в скрипте гранаты для создания взрыва.

Готово!

Подробнее..

Категории

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

  • Имя: Макс
    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