
Введение
Начинающий разработчик часто задается вопросом: а как сделана та или иная вещь в игре? Даже на первый взгляд простые вещи такие как дверь вызывают затруднения, поэтому сегодня разберем то, как можно создать дверь в Unity с полного нуля.
Процесс создания двери в Unity
С чего начать?
Для начала предлагаю определить задачи по результатам которых на выходе получим готовый продукт. И так давайте подумаем что нам нужно:
-
Модель двери.
-
Скрипт позволяющий управлять дверью.
-
Механизмы для взаимодействия с дверью.
Модель двери
Модель двери должна состоять как минимум из двух частей — это неподвижная часть дверная коробка и подвижная часть дверное полотно. Также можно добавить третий элемент — это дверная ручка.
Модель можно скачать из свободных источников и добавить в наш проект, либо создать самому. Думаю с первым вариантом не должно быть проблем, поэтому опишу второй вариант. Для создания модели предлагаю использовать саму Unity и инструмент ProBuilder, который можно установить через менеджер пакетов.

После установки пакета открываем окно ProBuilder через верхнее меню редактора Tools->Probuilder->… Далее выбираем инструмент Door.

И создаем дверной проход:

Далее создаем дверное полотно с помощью инструмента Cube:

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

Если у дверного полотна установить Rotation по оси Y то мы увидим как будет выглядеть открытая дверь:

С помощью все того же куба делаем ручку и помещаем ее в объект Leaf нашей иерархии объектов, чтобы она двигалась вместе с дверью:

Создадим материал и добавим текстуру двери. Далее наложим материал на наши детали.

Для привязки текстуры будем использовать инструмент UV Editor в том же ProBuilder. Привяжем развертку к каждой детали:

В результате модель двери выглядит вот так:

Скрипт для управления дверью
Для управлению дверью есть два варианта. Первый создать анимацию открытия/закрытия и подключить ее в скрипте, но тогда мы теряем гибкость в кастомизации управления дверью. Второй вариант описать вращение с помощью кода и тогда у нас будет возможность гибко настраивать дверь (скорость вращения, кривая вращения, вращения из разного положения и т.п.). Пойдем по второму пути. Вращать дверное полотно будем по оси Y. Для вращения будем использовать начальный и конечный угол в градусах. По умолчанию угол ноль градусов.
Для вращения двери напишем корутин. На вход он принимает угол положения начальной позиции и угол к которому нужно повернуть. Анимировать вращение будем с помощью анимационной кривой.
[SerializeField] private Transform _rotatingLeaf; [SerializeField] private AnimationCurve _animationCurve; [SerializeField] private float _duration = 1.0f; private Coroutine _rotateCoroutine; private IEnumerator Rotate(float start, float end) { for (float i = 0; i < 1; i += Time.deltaTime / _duration) { _rotatingLeaf.transform.rotation = Quaternion.Lerp( Quaternion.Euler(0, start, 0), Quaternion.Euler(0, end, 0), _animationCurve.Evaluate(i)); yield return null; } _rotatingLeaf.transform.rotation = Quaternion.Euler(0, end, 0); _rotateCoroutine = null; }
Угол открытия зададим через сериализованное поле.
[Range(-180, 180)] [SerializeField] private float _openAngle = 90.0f;
Чтобы получить текущий угол вращения полотна напишем метод:
private float GetCurrentAngle() { float currentAngle = Quaternion.Angle(Quaternion.identity, _rotatingLeaf.transform.rotation); currentAngle *= _openAngle > 0 ? 1 : -1; return currentAngle; }
Так как дверь может находиться в трех положения: закрыта, открыта, приоткрыта, то добавим перечисление:
private enum DoorState { Undefined, Open, Close, }
И метод определяющий текущее состояние двери по углу открытия:
private DoorState GetDoorState(float angle) { if (Mathf.Approximately(0, angle)) return DoorState.Close; if (Mathf.Approximately(_openAngle, angle)) return DoorState.Open; return DoorState.Undefined; }
Осталось дело за малым напишем методы открытия/закрытия:
public void Open() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Open) return; if (_rotateCoroutine != null) StopCoroutine(_rotateCoroutine); _rotateCoroutine = StartCoroutine(Rotate(currentAngle, _openAngle)); } public void Close() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Close) return; if (_rotateCoroutine != null) StopCoroutine(_rotateCoroutine); _rotateCoroutine = StartCoroutine(Rotate(currentAngle, 0)); }
Такая реализация позволяет управлять дверью даже в момент когда она находиться в промежуточном состоянии, то есть приоткрыта.
Чтобы дверью можно было управлять только из крайних положений (полностью открыта или полностью закрыта) добавим еще один метод:
public void Toggle() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Close) Open(); else if (GetDoorState(currentAngle) == DoorState.Open) Close(); }
Все, накидываем скрипт на дверь и задаем необходимые параметры, указав в Rotating Leaf наше дверное полотно.

Механизм управления дверью
Модель двери мы сделали, скрипт управления написали, остается вопрос а как теперь взаимодействовать с этой дверью? Для этого создадим простой триггер, который позволит управлять дверью когда персонаж подходит к двери.
public class DoorTrigger : MonoBehaviour { [SerializeField] private Door _door; private void OnTriggerEnter(Collider other) { if (other.tag == "Player") { _door.Open(); } } private void OnTriggerExit(Collider other) { if (other.tag == "Player") { _door.Close(); } } }
Создаем пустой объект назовем его также DoorTrigger, добавляем на него этот скрипт и добавим компонент BoxCollider. Коллайдеру задаем размеры и устанавливаем флаг isTrigger.

Теперь когда в зону коллайдера переместиться объект с тегом “Player” то дверь автоматически откроется, а если объект покинет зону коллайдера то дверь автоматически закроется.
Заключение
Вот такой краткий гайд получился. Мы с полного нуля создали дверь, начали с модели и закончили автоматизаций. В заключении привожу полный код скрипта + бонусом добавил возможность задавать звуки открытия/закрытия, пользуйтесь на здоровье)
Hidden text
public class DoorController : MonoBehaviour { [Header("Target object")] [Tooltip("Automatically uses an object from LeafRoot")] [SerializeField] private Transform _rotatingLeaf; [Header("Main")] [SerializeField] private DoorState _state = DoorState.Close; [SerializeField] private AnimationCurve _animationCurve; [SerializeField] private float _duration = 1.0f; [Range(-180, 180)] [SerializeField] private float _openAngle = 90.0f; [Header("Audio")] [SerializeField] private AudioClip _openingClip; [SerializeField] private AudioClip _closingClip; [Header("Optional")] [SerializeField] private AudioSource _audioSource; private Coroutine _rotateCoroutine; private void Awake() { AssignLeaf(); if (!_audioSource) _audioSource = gameObject.AddComponent<AudioSource>(); } public void Toggle() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Close) Open(); else if (GetDoorState(currentAngle) == DoorState.Open) Close(); } public void Open() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Open) return; if (_rotateCoroutine != null) StopCoroutine(_rotateCoroutine); PlaySound(_closingClip); _rotateCoroutine = StartCoroutine(Rotate(currentAngle, _openAngle)); } public void Close() { var currentAngle = GetCurrentAngle(); if (GetDoorState(currentAngle) == DoorState.Close) return; if (_rotateCoroutine != null) StopCoroutine(_rotateCoroutine); PlaySound(_openingClip); _rotateCoroutine = StartCoroutine(Rotate(currentAngle, 0)); } private void OnValidate() { AssignLeaf(); switch (_state) { case DoorState.Open: _rotatingLeaf.transform.rotation = Quaternion.Euler(0, _openAngle, 0); break; case DoorState.Close: _rotatingLeaf.transform.rotation = Quaternion.identity; break; } } private IEnumerator Rotate(float start, float end) { for (float i = 0; i < 1; i += Time.deltaTime / _duration) { _rotatingLeaf.transform.rotation = Quaternion.Lerp( Quaternion.Euler(0, start, 0), Quaternion.Euler(0, end, 0), _animationCurve.Evaluate(i)); yield return null; } _rotatingLeaf.transform.rotation = Quaternion.Euler(0, end, 0); _rotateCoroutine = null; } private float GetCurrentAngle() { float currentAngle = Quaternion.Angle(Quaternion.identity, _rotatingLeaf.transform.rotation); currentAngle *= _openAngle > 0 ? 1 : -1; return currentAngle; } private void AssignLeaf() { if (!_rotatingLeaf) _rotatingLeaf = transform; } private void PlaySound(AudioClip clip) { _audioSource.clip = clip; _audioSource.Play(); } private DoorState GetDoorState(float currentAngle) { if (Mathf.Approximately(0, currentAngle)) return DoorState.Close; if (Mathf.Approximately(_openAngle, currentAngle)) return DoorState.Open; return DoorState.Undefined; } private enum DoorState { Undefined, Open, Close, } }
Присоединяйтесь к моим соц сетям:
YouTube: https://www.youtube.com/channel/UC8Pm1hZfQMKE8nfSdYqKugg
VK: https://vk.com/stupenkovanton
GitHub: https://github.com/stupenkov
Linkedin: https://www.linkedin.com/in/stupenkov/
ссылка на оригинал статьи https://habr.com/ru/post/686294/
Добавить комментарий