Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Введение
Начинающий разработчик часто задается вопросом: а как сделана та или иная вещь в игре? Даже на первый взгляд простые вещи такие как дверь вызывают затруднения, поэтому сегодня разберем то, как можно создать дверь в 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/