Unity3D Tutorial: Controlling Multiple Animators and ScriptableObject Messaging

Hi to all old readers and welcome to new ones!
This tutorial is about how to control multiple animators at the same time and on how to use Scriptable Objects as messengers.

Remember that those tutorials expects you to have some kind of programming experience, and they reflect my personal experience and may not agree with everyone.

What we gonna learn?

A friend asked a question somewhere about how to sync multiple animators, and I thought it was an interesting question, enough to make a tutorial out of it.

While multiple ways to achieve the result passed through my head, I’ve decided on showing one that I deem to be reusable and interesting to learn!

What are ScriptableObjects?

Unity has a lot to say about it, but I’m gonna quote the interesting parts:

Just like MonoBehaviours, ScriptableObjects derive from the base Unity Object but, unlike MonoBehaviours, you can not attach a ScriptableObject to a GameObject. Instead, you save them as Assets in your Project.

When using the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development. Data that you save from Editor Tools to ScriptableObjects as an asset is written to disk and is therefore persistent between sessions.

The important bits are that SOs (I’m gonna call them like this sometimes!) are saved as Assets, you cannot attach them to a GameObject.
While in the Editor any changes made to SO are permanent, during runtime or not.
Objects in the scene can reference SOs and interact with them normally.

So I’ve decided to make use of them as a way to deal with messaging objects, you gonna see some similarity with events during this tutorial.

So what we making?

I’m glad you asked! We doing a dance show!!

This was the first thing that came to my mind when thinking of “syncing multiple animators”~

Let’s do it!

First, this tutorial depends a lot on unity-only structures like animators, UI and scene objects, and those might be hard to explain with only text and images.
As such I recommend downloading the project at the link provided at the end of this tutorial before starting.

This is how our scene gonna look like at the end:

Each stick figure is called a Dancer, and is structured like this:

The Animator Controller is done like this:

Where each Dance_0# is an animation of a dance, and Dance# are trigger parameters.

Every dance State is linked like this in the Idle to Dance direction:

This mean that the animation can exit at any time (no Exit Time), so at any point the conditions are fulfilled it will start the change.
And no transition duration means that the change is instantly, the next animation will start at the next frame without blending.

And linked like this on the way back (Dance to Idle direction):

An Exit Time of 1 means that once the animation is finished (1 = 100%) it will start the transition as long as the conditions are fulfilled, since we don’t have conditions it will always change once the animation is finished.
Meaning that once they finish a dance they will go back to being idle.

This setup is repeated to every animation, only changing the Conditions to Dance2 and Dance3 respectively.

The UI is much more complicated and would increase the length of this tutorial to such amounts, if explained, that I’ve decided to not do so.
If you want to know how it works just download the project, it will just be referred as buttons that calls some methods.

Onto the ScriptableObject!

For the SO we gonna create the following script:

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu]
public class DancingAnimatorSO : ScriptableObject {

    // Here we create a hashset (a collection of unique items) of animators that we will use internally to trigger all animations.
    private HashSet<Animator> _animators = new HashSet<Animator>();

    // This will add an animator to the list of animators that we gonna control.
    public void RegisterAnimator(Animator animator) {
        _animators.Add(animator);
    }

    // This will remove an animator from our control.
    public void UnregisterAnimator(Animator animator) {
        _animators.Remove(animator);
    }

    // Just adding a bunch of methods to deal with the animation needs, you can add whatever you might need here!

    public void SetInteger(string paramName, int value) {
        foreach (Animator anim in _animators) {
            anim.SetInteger(paramName, value);
        }
    }

    public void SetBool(string paramName, bool value) {
        foreach (Animator anim in _animators) {
            anim.SetBool(paramName, value);
        }
    }

    public void SetFloat(string paramName, float value) {
        foreach (Animator anim in _animators) {
            anim.SetFloat(paramName, value);
        }
    }

    public void SetTrigger(string paramName) {
        foreach (Animator anim in _animators) {
            anim.SetTrigger(paramName);
        }
    }

    // Used to demo the system:
    public void ShowTime() {
        SetTrigger("Dance1");
        SetTrigger("Dance2");
        SetTrigger("Dance3");
    }
}

Here we declare a DancingAnimatorSO that inherits from ScriptableObject, officially creating a new SO script!
But wait, what is that [CreateAssetMenu] I see?

Well, that is an attribute you put on your SOs classes to allow them to be manually added as Assets into your project.
More directly, when you right click on your project view you will see it added to the list of things you can Create:

Without that, even after creating the script you wouldn’t be able to manually create an asset of DancingAnimatorSO type, making all our efforts null.

Not covered in this tutorial but nice to know: You can create SOs by scripting, with ScriptableObject.CreateInstance() and other options~

What the is a HashSet<Animator> you ask? It’s a surprise tool that will help us later~
Okay sorry for the joke, it’s just a collection of unique objects.
I’m using it because since there is no sense for having the same Animator being controlled multiple times, a HashSet allows me to only have one of each Animator added, no matter how many times I try to add repeated ones.
If I was using a List or Array, I would have to manually check if it was already added every time.

The RegisterAnimator and UnregisterAnimator methods just add or remove Animators from the collection.

After that we have some methods used to control the Animators. As this was our objective in the first place.

Lastly, ShowTime is a method only used to show how this system works, it’s not anything important.

For the curious ones: Because of the way we set the Animator Controller, setting all three triggers at the same time will make them do the each of the dance in sequence of 1, 2 and 3.

Onward to next script!

We gonna take a look at PlayAnimComponent:

using UnityEngine;

public class PlayAnimComponent : MonoBehaviour {

    // Here we can have a reference set from the inspector to the ScriptableObject that is gonna sync all animations.
    public DancingAnimatorSO dancingAnimatorSO;

    // Having a reference to the animator to link it, also to control it individually if we need to.
    private Animator _animator;

    private void Awake() {
        _animator = GetComponent<Animator>();
    }

    // Here we give control of our animator, also we reset it so it can dance synced to the others.
    public void Register() {
        dancingAnimatorSO.RegisterAnimator(_animator);

        _animator.Play("Idle", -1, 0f);
        _animator.ResetTrigger("Dance1");
        _animator.ResetTrigger("Dance2");
        _animator.ResetTrigger("Dance3");
    }

    // Assuming direct control!
    public void Unregister() {
        dancingAnimatorSO.UnregisterAnimator(_animator);
    }

    // Common animator parameters setters
    public void SetInteger(string paramName, int value) {
        _animator.SetInteger(paramName, value);
    }

    public void SetBool(string paramName, bool value) {
        _animator.SetBool(paramName, value);
    }

    public void SetFloat(string paramName, float value) {
        _animator.SetFloat(paramName, value);
    }

    public void SetTrigger(string paramName) {
        _animator.SetTrigger(paramName);
    }
}

First it sets a public field for a DancingAnimatorSO, allowing us to set our SO from the Inspector.

Secondly I’ve decided to add a reference to the Animator too, you could also have two scripts in such a way that:
YourFirstComponent is a normal component that just allows you to play your Animations through your Animator.
YourSyncComponent could inherit from YourFirstComponent, allowing you to sync your Animator through the ScriptableObject.

Next we have the Register and Unregister methods, the important thing here is that on the Register method we are resetting the Animator in such a way that at the next frame it could start syncing with what our SO wants.
Of course as this is a simple example, the method to reset is simple as well, in more complex scenarios this could prove to be quite a problem and also require you to set this Animator current state to the others.

And a few of our well known parameter controlling methods.

And last but not least!

Our Dancer script:

using UnityEngine;

public class Dancer : MonoBehaviour {

    // Having our controller component allows us to play our animations on command.
    private PlayAnimComponent _playAnimator;

    private void Awake() {
        _playAnimator = GetComponent<PlayAnimComponent>();
    }

    // Used to show the dances with the button.
    public void PlayRandomDance() {
        switch (Random.Range(0, 3)) {
            case 0:
                _playAnimator.SetTrigger("Dance1");
                break;
            case 1:
                _playAnimator.SetTrigger("Dance2");
                break;
            case 2:
                _playAnimator.SetTrigger("Dance3");
                break;
        }
    }
}

But in this case is not actually important at all, sorry Dancer!

It only contains one method and it is used to play a random dancing animation. This is called by the buttons bellow each dancer.

Back to our UI

Each Dance! button calls the Dancer.PlayRandomDance() method.

Each Register button calls the PlayAnimComponent.Register() method, while also hiding itself, and revealing the button underneath called Unregister, that button calls the PlayAnimComponent.Unregister() method, hides itself and reveals the Register button.

The Start the Show! button calls the DancingAnimatorSO.ShowTime() method.

And all this together allows us to achieve what the first gif showed!

What are other applications for this?

The Unity blog has showed us some like giving the player’s health to a scriptableobject, and allowing any other object get it’s value from there.
This also allows the scriptableobject to raise events such as applying a post processing effect of black and white, or playing more dangerous music or sounds!

Full Project Download:

You can download the project I used to test here.

Thank you for reading my tutorial!

I hope this tutorial was of help to you, and if possible a donation of 1$ may be of great help on aiding the development of future tutorials:

 

https://www.patreon.com/TinyBirdGames

Thank you very much!

Liked it? Take a second to support TinyBird Games on Patreon!

Comments 2

  • Great Tut! The title lured me in, but I was looking to control, 3Danimators that are on different game objects. Not multiple animations. Imagine this > Carnival type game there is an animated arching target. The target has a trigger that sets of particle effects. There is also an key animated door that also needs to be played when the target trigger is entered. Can you show me how to modify your tutorial to reference the animator on an other game object. Scriptable Objects sounds like the perfect solution.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.