Найти тему

Unity3d: Как сделать автоматизированную дверь с полного нуля

Оглавление

Введение

Начинающий разработчик часто задается вопросом: а как сделана та или иная вещь в игре? Даже на первый взгляд простые вещи такие как дверь вызывают затруднения, поэтому сегодня разберем то, как можно создать дверь в Unity с полного нуля.

Процесс создания двери в Unity

С чего начать?

Для начала предлагаю определить задачи по результатам которых на выходе получим готовый продукт. И так давайте подумаем что нам нужно:

  • Модель двери.
  • Скрипт позволяющий управлять дверью.
  • Механизмы для взаимодействия с дверью.

Модель двери

Модель двери должна состоять как минимум из двух частей - это неподвижная часть дверная коробка и подвижная часть дверное полотно. Также можно добавить третий элемент - это дверная ручка.

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

-2

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

-3

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

-4

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

-5

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

-6

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

-7

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

-8

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

-9

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

-10

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

-11

Скрипт для управления дверью

Для управлению дверью есть два варианта. Первый создать анимацию открытия/закрытия и подключить ее в скрипте, но тогда мы теряем гибкость в кастомизации управления дверью. Второй вариант описать вращение с помощью кода и тогда у нас будет возможность гибко настраивать дверь (скорость вращения, кривая вращения, вращения из разного положения и т.п.). Пойдем по второму пути. Вращать дверное полотно будем по оси 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 наше дверное полотно.

-12

Механизм управления дверью

Модель двери мы сделали, скрипт управления написали, остается вопрос а как теперь взаимодействовать с этой дверью? Для этого создадим простой триггер, который позволит управлять дверью когда персонаж подходит к двери.

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.

-13

Теперь когда в зону коллайдера переместиться объект с тегом “Player” то дверь автоматически откроется, а если объект покинет зону коллайдера то дверь автоматически закроется.

Заключение

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

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,
}
}

Присоединяйтесь к моим соц сетям: