02-03 Doors

Location: Assets/Plugins/Animancer/Examples/02 Fine Control/03 Doors

Recommended After: Quick Play

Learning Outcomes: in this example you will learn:

How to implement doors that can open and close.

How to implement a generalised system for interacting with objects.

How to play animations in Edit Mode.

This example demonstrates how you can use a single animation and some simple scripting to create a system for doors that can be opened and closed, can start open, closed, or anywhere inbetween, and can show the starting state in Edit Mode.

Pro-Only Features are used in this example: modifying AnimancerState.Speed and AnimancerState.NormalizedTime. Animancer Lite allows you to try out these features in the Unity Editor, but they're not available in runtime builds unless you purchase Animancer Pro.

The video below shows several doors:

  • They start in different states: closed (left), 60% open (middle), and fully open (right).
  • Clicking on a door toggles its state.
  • Since the middle door starts closer to fully open than closed, the first interaction closes it.
  • On the last door we also see that it can be interrupted at any time to change direction.

Summary

  • To play an animation backwards, set its Speed to -1.
  • To pause all animations and save processing time, call animancerComponent.Playable.PauseGraph();.
  • Interfaces allow you to decouple systems which helps with writing flexible and bug free code.
  • You can easily play animations in Edit Mode.

Overview

There are 3 scripts in this example to keep the animation control code separate from the user input code:

IInteractable is a simple Interface which forms the basis for a simple decoupled interaction system:

public interface IInteractable
{
    void Interact();
}

ClickToInteract allows the user to interact with any IInteractable by clicking on it:

using UnityEngine;

public sealed class ClickToInteract : MonoBehaviour
{
    private void Update()
    {
        if (!ExampleInput.LeftMouseUp)
            return;

        var ray = Camera.main.ScreenPointToRay(ExampleInput.MousePosition);

        if (Physics.Raycast(ray, out var raycastHit))
        {
            var interactable = raycastHit.collider.GetComponentInParent<IInteractable>();
            if (interactable != null)
                interactable.Interact();
        }
    }
}

Door manages the actual animations and implements IInteractable to be controlled by the above system:

using Animancer;
using UnityEngine;

[SelectionBase]
public sealed class Door : MonoBehaviour, IInteractable
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip _Open;

    [SerializeField, Range(0, 1)]
    private float _Openness;

    private void Awake()
    {
        var state = _Animancer.Play(_Open);
        state.NormalizedTime = _Openness;

        _Animancer.Evaluate();
        _Animancer.Playable.PauseGraph();

        state.Events.OnEnd = _Animancer.Playable.PauseGraph;
    }

    public void Interact()
    {
        var state = _Animancer.States[_Open];

        if (_Openness < 0.5f)
        {
            state.Speed = 1;
            _Openness = 1;
        }
        else
        {
            state.Speed = -1;
            _Openness = 0;
        }

        _Animancer.Playable.UnpauseGraph();
    }

#if UNITY_EDITOR
    private void OnValidate()
    {
        if (_Open != null)
            AnimancerUtilities.EditModeSampleAnimation(_Open, this, _Openness * _Open.length);
    }
#endif
}

The code structure is fairly straightforward:

Interaction System

We could have just hard coded the interaction system in the Door script or controlled it using UI Buttons, but in the interest of encouraging good practices rather than simply getting the job done quickly this example uses a more modular approach.

The ClickToInteract component is a somewhat hacky substitute for whatever mechanism you might use to trigger interactions in a real game. It has a fairly simple Update method which waits for the user to Left Click then uses a raycast to determine what they clicked on and tries to find a component that implements the IInteractable Interface attached to that object or any of its parents. If it finds something, it calls the Interact method on that component, whatever it may be.

This allows the interaction system to be decoupled from the actual interactable objects. It has no idea what kinds of things can be interacted with or what they will do, it just tells them when it's interacting with them. We could add more different types of interactable objects without touching the interaction system or we could change the interaction system without touching the interactable objects. For example, First Person Shooter games wouldn't want Left Click to trigger interactions, they commonly use E instead (since it's near WASD on a QWERTY keyboard) and instead of using an infinite length raycast they commonly limit interactions to a short distance in front of the player. But Doors do not need to know about either of those changes, they just open and close when something tells them to.

We could test this system by writing a very simple script that implements the IInteractable interface to log a message and putting it on a cube in the scene (or any object with a Collider):

using UnityEngine;

namespace Animancer.Examples.FineControl
{
    public sealed class ClickMe : MonoBehaviour, IInteractable
    {
        public void Interact()
        {
            Debug.Log("You interacted with " + name);
        }
    }
}

The Namespace is necessary because the IInteractable Interface is located in that namespace. Alternatively, you could put using Animancer.Examples.FineControl; at the top of the script, or specify the full name of Animancer.Examples.FineControl.IInteractable when refering to it.

A more complex interaction system might give the Interact method a parameter for details about the triggering object. For example, in addition to the animation it uses on itself, a Door could have a Humanoid-OpenDoor animation which it plays on the character who interacts with it. A Lever might do something similar to allow people to pull it. And so on, all without the character needing to hold interaction animations for every possible object. Or the responsibility could be reversed: still give the Door or Lever its corresponding humanoid animation, but simply expose it as an InteractionAnimation property in the IInteractable Interface so that when a character triggers the interaction they can also check if there's an animation they need to play.

Basic Door

The OpenDoor animation rotates the door from closed to open and we do not have a second animation to close it again. So to create a system that can both open and close we just need to manipulate the animation's Speed to play forwards or backwards.

First we need a new script to implement the IInteractable interface and reference the AnimancerComponent and the AnimationClip:

using Animancer;
using UnityEngine;

namespace Animancer.Examples.FineControl
{
    public sealed class DoorTutorial : MonoBehaviour, IInteractable
    {
        [SerializeField] private AnimancerComponent _Animancer;
        [SerializeField] private AnimationClip _Open;
    }
}

As with the simple ClickMe example above, the Namespace is necessary because the IInteractable Interface is located in that namespace. Alternatively, you could put using Animancer.Examples.FineControl; at the top of the script, or specify the full name of Animancer.Examples.FineControl.IInteractable when refering to it.

That will give us a compiler error because the IInteractable Interface requires the implementer to have an Interact method. So let us add a Field to keep track of whether it is open or not and implement the Interact method to toggle the state and play the animation with the appropriate speed:

private bool _IsOpen;

public void Interact()
{
    // Toggle the state.
    _IsOpen = !_IsOpen;

    // Make sure the animation is playing.
    var state = _Animancer.Play(_Open);

    // If the door is opening, play forwards.
    if (_IsOpen)
        state.Speed = 1;
    else// Otherwise play backwards.
        state.Speed = -1;
}

One last thing that method needs to do is pause the graph when the animation finishes. Otherwise the animation Time would continue increasing after it ends and when we reverse the Speed we would have to wait for the Time to go all the way back down to the end of the animation before anything happens. And since we pause the graph we are responsible for unpausing it as well. That can be done very easily using an End Event.

public void Interact()
{
    ...

    // Unpause now.
    _Animancer.Playable.UnpauseGraph();

    // Pause when done.
    state.Events.OnEnd = _Animancer.Playable.PauseGraph;
}

The full script now looks like this:

using Animancer;
using UnityEngine;

public sealed class DoorTutorial : MonoBehaviour, IInteractable
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip _Open;

    private bool _IsOpen;

    public void Interact()
    {
        _IsOpen = !_IsOpen;

        var state = _Animancer.Play(_Open);

        if (_IsOpen)
            state.Speed = 1;
        else
            state.Speed = -1;

        _Animancer.Playable.UnpauseGraph();

        state.Events.OnEnd = _Animancer.Playable.PauseGraph;
    }
}

That gives us a door which starts closed and can be clicked on to toggle between open and closed.

Openness

What we've got so far is quite functional and would get the job done if we were happy with all doors starting closed when the scene loads, but we can still improve upon that. We could make the _IsOpen field serialized to be able to start some doors open, but we can do even better than that. Instead, let's make it so that doors can start anywhere between as well.

First we want a float with a value between 0 and 1. Instead of a regular float field where you can type in any value, the Range attribute tells Unity to draw it using a slider that can only go between the specified values.

Code Inspector
[SerializeField, Range(0, 1)]
private float _Openness;

Note that the [Range] attribute only affects the Inspector. It will not prevent your code from setting the value to anything you want.

Then we need to apply that value on startup:

private void Awake()
{
    // Play the animation immediately.
    var state = _Animancer.Play(_Open);

    // Set our new field as the NormalizedTime.
    // This is why the 0 to 1 range is important.
    state.NormalizedTime = _Openness;

    // We do not actually want the animation to play though,
    // we just want to apply its value at the time we just set.
    _Animancer.Evaluate();
    _Animancer.Playable.PauseGraph();

    // We still need to pause it whenever it finishes.
    state.Events.OnEnd = _Animancer.Playable.PauseGraph;
}

All the AnimancerState.Events would normally be cleared whenever we play a new animation, but since there is only one animation we just leave it playing and pause/unpause the graph instead.

Finally, we need to make a few adjustments to the Interact method:

public void Interact()
{
    // Get the state we created in Awake.
    // Or we could have stored it in a field when we made it.
    var state = _Animancer.States[_Open];

    // If nearly closed, play the animation forwards.
    if (_Openness < 0.5f)
    {
        state.Speed = 1;
        _Openness = 1;
    }
    else// Otherwise play it backwards.
    {
        state.Speed = -1;
        _Openness = 0;
    }

    // And make sure the graph is playing as mentioned above.
    // The state is already playing since we never stopped it.
    _Animancer.Playable.UnpauseGraph();
}

Note how we immediately set the _Openness to 0 or 1. We are using it as the destination value rather than an indication of how open the door currently is (we could use state.NormalizedTime if we wanted that). This means that interrupting the animation will always toggle it to go the other way like our original script rather than choosing which way to go based on how open the door currently is (except for the first interaction after startup).

The full script now looks like this:

using Animancer;
using UnityEngine;

public sealed class DoorTutorial : MonoBehaviour, IInteractable
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private AnimationClip _Open;

    [SerializeField, Range(0, 1)]
    private float _Openness;

    private void Awake()
    {
        var state = _Animancer.Play(_Open);
        state.NormalizedTime = _Openness;
        _Animancer.Evaluate();
        _Animancer.Playable.PauseGraph();

        state.Events.OnEnd = _Animancer.Playable.PauseGraph;
    }

    public void Interact()
    {
        var state = _Animancer.States[_Open];

        if (_Openness < 0.5f)
        {
            state.Speed = 1;
            _Openness = 1;
        }
        else
        {
            state.Speed = -1;
            _Openness = 0;
        }

        _Animancer.Playable.UnpauseGraph();
    }
}

Now we can use the slider to choose how open the door is when the scene loads.

Edit Mode

That works great at runtime, but it would be annoying to have to go into Play Mode every time we want to see exactly how open a door will be. Fortunately, Animancer allows you to easily play animations in Edit Mode by using the OnValidate MonoBehaviour Message which Unity will call in Edit Mode whenever an instance of this script is loaded or a value is changed in the Inspector:

#if UNITY_EDITOR
private void OnValidate()
{
    if (_Open != null)
        AnimancerUtilities.EditModeSampleAnimation(_Open, this, _Openness * _Open.length);
}
#endif

Now we have a door that can start anywhere between open and closed and will show that state in Edit Mode as well.