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 Button
s, 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 Door
s 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 |
---|---|
Note that the |
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.