Doors Are Why I Drink – Part 2: Our Approach

Layout

The Animator

Okay, so it’s not as bad as it looks, and even to us, it looks over-engineered. But each piece has a purpose. For example, these five states:

These five states have no attached animation clip, and are all set to not write defaults. The state leading from Entry is there to prevent an initial animation from overriding the door position/rotation on start. So, if the level designer leaves the door opened at start, it starts open. If it starts closed, it’s closed. Or, if left somewhere in-between, it keeps its position all the way up to the moment it needs to animate.

The empty states labeled “Pre-Opening” and “Pre-Closing” are there because transitions from “Any State” are not configurable, and we needed to provide a transition time for the transitions coming from where-ever. In fact, due to a limitation in Mecanim, we also had to create a Boolean parameter called “ZeroTimeState”. It’s just there, always set to true. The transitions coming from “Pre-Closing” and “Pre-Opening” have no exit time, and wait on ZeroTimeState to be true to transition out of the state, leading to those states lasting 0 seconds. This can’t be done any other way (without scripting the transition outside of Mecanim with Animator.CrossFade) because inexplicably, states without animation clips have a fixed length of 1 second. We could increase the speed of the lack of animation clip (whatever that means) in the state’s settings, but the speed would have to be infinity in order for the state to take 0 seconds. And of course, the states can’t just be removed because then the transition time between “Any State” and “Opening”/”Closing” would then cease to be configurable.

Meanwhile, the “Fully-Opened” and “Fully-Closed” states are there to signal that the animations for opening or closing a door were completed. StateMachineBehaviour scripts attached to the “Opening” and “Closing” fire events and change the state of the door by setting the parameters “IsOpen”, “IsClosed”, and “IsFinishedMoving” whenever the animator leaves one of those two states. If “Opening” and “Closing” just stopped at the end and didn’t transition over into empty states, the animator would never trigger the StateMachineBehaviour’s OnStateExit callbacks. Additionally, a more subtle reason for having the empty states is that our StateMachineBehaviours override OnStateUpdate to do some work. If the doors just stopped moving at the end of “Opening” and “Closing” and never fell through to empty states, the StateMachineBehaviours would keep executing their update loops on every frame, despite there being no more work to be done. Falling through to an empty state ensures that we stop triggering update loops in the StateMachineBehaviours.

Our Opener.cs script, which provides a way for other scripts to open/close the door programmatically without needing to know the animator controller’s parameters, reads the “IsOpen”, “IsClosed”, and “IsFinishedMoving” parameters to determine what state the door is currently in. Why couldn’t we just add animation events to our animation clips that call Opener.cs directly? Well, that would work, but it would make the whole system considerably less modular. With our approach, any door, including a door on a safe or on an oven, could just drop two animation clips for opening and closing into an Animation Override Controller and the entire system would just work. If we used animation events, each time a new animation clip is made for a door, the animator would have to remember that they have to place two specifically named animation events or else the door mechanism stops working in a subtle way (the Opener script would not be able to properly categorize the door’s internal state, which doesn’t immediately become obvious until another script asks for the door’s state and is told the wrong thing).

The “Open” and “Close” triggers are pretty simple. They just trigger the transitions from “Any State” to their respective state chains. “Toggle” is a convenience trigger for either opening or closing the door, depending on which action was performed last. The second the door enters the “Opening” state, our StateMachineBehaviour attached to that state sets the parameter “IsOpen” to true and “IsClosed” and “IsFinishedMoving” to false. Once the “Opening” state finishes, “IsFinishedMoving” is set to true. This allows us to use “IsOpen” and “IsClosed” as conditions in transitions from “Any State”. For instance, if the “Toggle” trigger is set and “IsOpen” is true, we should close the door, etc. This explains the triple arrows you see in our animator controller:

Each transition from “Any State” to one of the state chains is actually several possible transitions. You can either follow a transition because “Open” or “Close” triggers were set, or because a “Toggle” trigger was set. Additionally, if the door is locked and you either try to “Open” it or “Toggle” it while it was last closed, it plays the “Locked” animation, if one is set.

It should be noted here that our “Locked” state defaults to having an empty animation clip. Animation Override Controllers are designed to override animations, not states. So if the “Locked” state had no animation clip assigned, it would simply not appear in an Animation Override Controller. This is nice for states like “Pre-Opening”, which are internal states that shouldn’t be exposed in Animation Override Controllers, but not so nice for “Locked”, which needs to possibly have an animation clip assignable to it, but shouldn’t be required. To make it appear as an overridable, we opened the Animation window and recorded an empty animation clip with no animated properties and inserted it into the “Locked” state. Because the clip doesn’t animate any properties, doors that don’t have any special “attempt to open locked door” animation will just not animate if they are locked and opening is attempted.

In our next post, we’ll go over the remainder of the parameters in our Animator Controller, how interruption works, and why it is necessary and vital in any game with free first-person player movement.