01-05 Basic Character

Location: Assets/Plugins/Animancer/Examples/01 Basics/05 Basic Character

Recommended After: Basic Movement and Transitions

Learning Outcomes: in this example you will learn:

How to combine simple behaviours into more complex ones.

This example combines the Basic Movement and Transitions examples on a single character to demonstate how you can avoid several common problems you might encounter when implementing more complex behaviours.

Summary

  • Implementing a character with multiple different animations is notably more complex than just the one or two seen in earlier examples because you need to consider how they can transition into and interrupt each other.
  • Animancer plays animations when you tell it to and only when you tell it to, so if the wrong animation is playing then most likely your scripts are telling it to play the wrong animation.
  • Enums are an effective way to keep track of the states things are in for relatively simple scenarios, but for more complex situations it's often better to use a proper Finite State Machine system.
  • The Characters example re-implements the same logic as this example using Animancer's Finite State Machine system.

Overview

The code structure looks similar to the Transitions example, but now it has three animations:

Methods

This example has two methods we want to call which are first copied directly from the previous examples, but then will be modified to suit the needs of this example.

UpdateMovement is a copy of the Update method from the BasicMovementAnimations script in the Basic Movement example.

private void UpdateMovement()
{
    float forward = ExampleInput.WASD.y;
    if (forward > 0)
    {
        _Animancer.Play(_Move);
    }
    else
    {
        _Animancer.Play(_Idle);
    }
}

UpdateAction is a copy of the Update method from the PlayTransitionOnClick script in the Transitions example.

private void UpdateAction()
{
    if (ExampleInput.LeftMouseUp)
    {
        _Animancer.Play(_Action);
    }
}

Logical States

Animancer doesn't impose any limits on when you can play any given animation, it simply plays whatever you tell it to. This means your scripts are responsible for determining how your animations can interrupt each other.

In this case, we have 3 animations and we want to implement the following rules:

  • Idle can play anything.
  • Walk can play anything.
  • Action can only play Action (to allow Rapid Fire).

Since Idle and Walk have the same rules, we effectively want two logical states: "Acting" and "Not Acting". These logical states are entirely separate from the animation states that Animancer uses internally for each animation (AnimancerStates). A logical state can have any number of animation states associated with it:

  • Acting has one animation (Action).
  • Not Acting has two animations (Idle and Walk).
  • A logical state could even have no associated animations. For example, an effect that freezes the character could simply call _Animancer.Playable.PauseGraph(); to pause the character in their current pose instead of playing a new animation.

State Enum

One of the simplest ways to implement logical states is to use an Enumeration Type (commonly called an enum). An enum needs a name like any other Type in C# so since the class is called BasicCharacterAnimations we could call the enum BasicCharacterAnimationState. But that's rather long and would be annoying to repeat all over the place so instead we can make use of the C# feature Nested Types to declare the enum inside the class, which means that it can simply be called State.

public sealed class BasicCharacterAnimations : MonoBehaviour
{
    private enum State
    {
        NotActing,
        Acting,
    }

If we made it a public enum then other scripts would be able to use it via BasicCharacterAnimations.State, but since this one script is the only one it will be used in we just make it private.

With the enum declared, we also need a field to store the current State that the class is in.

    private State _CurrentState;

Method Changes

UpdateMovement and UpdateAction are still basically the same as in the Methods section above, but now we want them to set the _CurrentState.

UpdateMovement always sets the state to State.NotActing because it always plays one animation or the other.

private void UpdateMovement()
{
    _CurrentState = State.NotActing;

    float forward = ExampleInput.WASD.y;
    if (forward > 0)
    {
        _Animancer.Play(_Move);
    }
    else
    {
        _Animancer.Play(_Idle);
    }
}

And UpdateAction only sets State.Acting when the mouse is clicked.

private void UpdateAction()
{
    if (ExampleInput.LeftMouseUp)
    {
        _CurrentState = State.Acting;
        _Animancer.Play(_Action);
    }
}

Initialization

The startup code needs to change a bit now.

PlayTransitionOnClick (Old) BasicCharacterAnimations (New)

Previously, the OnActionEnd method would play the _Idle animation:

private void Awake()
{
    _Action.Events.OnEnd = OnActionEnd;
}

private void OnActionEnd()
{
    _Animancer.Play(_Idle);
}

But now, we need to set the _CurrentState and can just call UpdateMovement to choose between _Idle or _Move:

private void Awake()
{
    _Action.Events.OnEnd = OnActionEnd;
}

private void OnActionEnd()
{
    _CurrentState = State.NotActing;
    UpdateMovement();
}

Update

The last thing we need to do is write the Update method to call UpdateMovement and/or UpdateAction depending on the _CurrentState.

private void Update()
{
    if (_CurrentState == State.NotActing)
    {
        UpdateMovement();
        UpdateAction();
    }
    else
    {
        UpdateAction();
    }
}

That would do what we want, but if we had a few more states to check it would become a repetitive series of If Statements which is generally considered a bad practice because Switch Statements can do the same job more cleanly.

private void Update()
{
    switch (_CurrentState)
    {
        case State.NotActing:
            UpdateMovement();
            UpdateAction();
            break;

        case State.Acting:
            UpdateAction();
            break;
    }
}

Ordering

One last thing to note is that the order in which State.NotActing calls those methods is important.

If you were to put UpdateAction before UpdateMovement like this:

        case State.NotActing:
            UpdateAction();
            UpdateMovement();
            break;

When the user Left Clicks, the following would happen:

  1. Unity calls Update.
  2. Update reaches case State.NotActing.
  3. Update calls UpdateAction.
  4. UpdateAction checks ExampleInput.LeftMouseUp which is true so it plays the _Action animation and sets State.Acting.
  5. Update is still running and still inside case State.NotActing so now it calls UpdateMovement.
  6. UpdateMovement plays either _Move or _Idle and sets State.NotActing.

The problem here is that even though State.Acting was set properly in step #4 (along with its animation), it gets immediately set back to State.NotActing in step #5 so you will never actually get to see the character perform the action. That is why we call UpdateMovement before UpdateAction.

Conclusion

Here's what the full script looks like.

using Animancer;
using UnityEngine;

public sealed class BasicCharacterAnimations : MonoBehaviour
{
    [SerializeField] private AnimancerComponent _Animancer;
    [SerializeField] private ClipTransition _Idle;
    [SerializeField] private ClipTransition _Move;
    [SerializeField] private ClipTransition _Action;

    private enum State
    {
        NotActing,
        Acting,
    }

    private State _CurrentState;

    private void Awake()
    {
        _Action.Events.OnEnd = OnActionEnd;
    }
    
    private void OnActionEnd()
    {
        _CurrentState = State.NotActing;
        UpdateMovement();
    }

    private void Update()
    {
        switch (_CurrentState)
        {
            case State.NotActing:
                UpdateMovement();
                UpdateAction();
                break;

            case State.Acting:
                UpdateAction();
                break;
        }
    }

    private void UpdateMovement()
    {
        _CurrentState = State.NotActing;

        float forward = ExampleInput.WASD.y;
        if (forward > 0)
        {
            _Animancer.Play(_Move);
        }
        else
        {
            _Animancer.Play(_Idle);
        }
    }

    private void UpdateAction()
    {
        if (ExampleInput.LeftMouseUp)
        {
            _CurrentState = State.Acting;
            _Animancer.Play(_Action);
        }
    }
}

If you enter Play Mode, you have a character that can:

  • Stand still by default.
  • Walk when you hold W.
  • Shoot as fast as you can Left Click.

What Next?

That's the last of the Basics examples. Hopefully, by now you have some idea of how you could start using Animancer in your own projects, but the Help page lists various options for contacting the developer if you have any questions or suggestions. There are also many more examples covering the more advanced features of Animancer and ways you can use it. In particular, there are a few that relate to the topics covered in this example:

Example Topic
Characters Re-implement the same logic as this example using Animancer's Finite State Machine system. Relatively simple behaviours can be easily implemented in a single script like we did in this example. But in more complex situations, spending some extra time early on to properly structure your code will often save a lot of time later on when you need to expand it, change it, and debug issues.
Locomotion More detail about movement animations and actually moving characters around the scene.
Events More detail about End Events and other events you can use to run code at specific times during an animation.
Layers Playing multiple separate animations simultaneously on different body parts, such as to allow the character to walk and shoot at the same time even though we don't have a specific "Walking And Shooting" animation.