How to make a door from scratch

Introduction

A novice developer often asks the question: how is this or that thing made in the game? Even at first glance, simple things like a door cause difficulties, so today we will analyze how you can create a door in Unity from scratch.

The process of creating a door in Unity

Where to begin?

To begin with, I propose to define tasks based on the results of which we will get the finished product at the output. And so let’s think about what we need:

door model

The model of the door must consist of at least two parts – this is the fixed part of the door frame and the movable part of the door leaf. You can also add a third element – this is a door handle.

The model can be downloaded from free sources and added to our project, or created by yourself. I think there should be no problems with the first option, so I will describe the second option. To create a model, I suggest using Unity itself and the ProBuilder tool, which can be installed through the package manager.

After installing the package, open the ProBuilder window through the top menu of the editor Tools->Probuilder->… Next, select the Door tool.

And create a doorway

Next, create a door leaf using the Cube tool.

Select one of the vertices and set the anchor point. It is necessary because the door leaf will rotate relative to it.

If the door leaf is set to Rotation along the Y axis, then we will see how the open door will look

Using the same cube, we make a handle and place it in the Leaf object of our object hierarchy so that it moves with the door

Let’s create a material and add a texture to the door. Next, we will apply the material to our parts.

To bind the texture, we will use the UV Editor tool in the same ProBuilder. Associate a sweep to each detail

As a result, the door model looks like this

Script for door control

There are two options for door control. The first is to create an opening/closing animation and connect it in the script, but then we lose flexibility in customizing the door control. The second option is to describe the rotation using a code, and then we will be able to flexibly adjust the door (rotation speed, rotation curve, rotation from different positions, etc.). Let’s take the second path. We will rotate the door leaf along the Y axis. For rotation, we will use the start and end angles in degrees. The default angle is zero degrees.

Let’s write a coroutine to rotate the door. As input, it takes the position angle of the initial position and the angle to which you want to turn. We will animate the rotation using an animation curve.

[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;
}

Let’s set the opening angle through a serialized field.

[Range(-180, 180)] [SerializeField] private float _openAngle = 90.0f;

To get the current canvas rotation angle, write the following method:

private float GetCurrentAngle()
{
   float currentAngle = Quaternion.Angle(Quaternion.identity, _rotatingLeaf.transform.rotation);
   currentAngle *= _openAngle > 0 ? 1 : -1;
   return currentAngle;
}

Since the door can be in three positions: closed, open, ajar, we add an enumeration:

private enum DoorState
{
   Undefined,
   Open,
   Close,
}

And a method that determines the current state of the door by the opening angle:

private DoorState GetDoorState(float angle)
{
   if (Mathf.Approximately(0, angle))
       return DoorState.Close;

   if (Mathf.Approximately(_openAngle, angle))
       return DoorState.Open;

   return DoorState.Undefined;
}

The only thing left is to write the opening/closing methods:

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));
}

This implementation allows you to control the door even when it is in an intermediate state, that is, ajar.

In order for the door to be controlled only from the extreme positions (fully open or fully closed), let’s add one more method:

public void Toggle()
{
   var currentAngle = GetCurrentAngle();
   if (GetDoorState(currentAngle) == DoorState.Close)
       Open();
   else if (GetDoorState(currentAngle) == DoorState.Open)
       Close();
}

That’s it, we put the script on the door and set the necessary parameters by specifying our door leaf in the Rotating Leaf.

Door control mechanism

We made the door model, wrote the control script, the question remains how to interact with this door now? To do this, we will create a simple trigger that will allow us to control the door when the character approaches the door.


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();
       }
   }
}

Create an empty object, also call it DoorTrigger, add this script to it and add the BoxCollider component. We set the dimensions of the collider and set the isTrigger flag.

Now, when an object with the “Player” tag moves into the collider zone, the door will automatically open, and if the object leaves the collider zone, the door will automatically close.

Conclusion

Here’s a short guide. We created a door from scratch, started with a model and finished with automation. In conclusion, I give the full code of the script + added the ability to set opening / closing sounds as a bonus, use it to your health)

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

Join my social networks:

YouTube: https://www.youtube.com/channel/UC8Pm1hZfQMKE8nfSdYqKugg

VK: https://vk.com/stupenkovanton

GitHub: https://github.com/stupenkov

LinkedIn: https://www.linkedin.com/in/stupenkov/

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *