2019 2022

Thank you for taking part in the NotSlot journey!
We invite you to join us in our next chapter:

  • by nir,
  • Danielle Elias

Unity 2D Animation Part 3: Scripting

To unleash Unity’s 2D Animation package’s full potential, we can interact with it through scripts. In this aerticle we will learn how to swap character sprites programmatically, inject a custom sprite into an existing character, replace all sprites as a grouped skin, and finally create a mixer for composing various characters.

Download Unity Project & PSB File
Follow along! Download the character’s PSB and the result Unity project of this article.

In the previous articles, we learned how we could work manually with the 2D Animation package’s components. To briefly recap:

  • A Sprite Library Asset contains all sprites and their variants, organized into categories and accessible by labels.
  • Not to confuse, but there is also a Sprite Library component, which attaches the Sprite Library Asset to the character.
  • A Sprite Resolver allows us to switch sprites by selecting the active variant from the Library Asset.

Random Swap

In our first demo, we will start by randomly selecting a head sprite.

Swapper.cs
using System.Linq;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Experimental.U2D.Animation;
using UnityEngine.U2D;

public class Swapper : MonoBehaviour
{

  [SerializeField]
  private SpriteLibrary spriteLibrary = default;

  [SerializeField]
  private SpriteResolver targetResolver = default;

  [SerializeField]
  private string targetCategory = default;


  private SpriteLibraryAsset LibraryAsset => spriteLibrary.spriteLibraryAsset;


  public void SelectRandom ()
  {
    string[] labels =
      LibraryAsset.GetCategoryLabelNames(targetCategory).ToArray();
    int index = Random.Range(0, labels.Length);
    string label = labels[index];

    targetResolver.SetCategoryAndLabel(targetCategory, label);
  }

}

The random swap method is straight forward:
I query for the available labels given the head category name. I convert them into an array for direct access to the elements.
I then select a random index and use it to retrieve a label name for the label collection. Finally, I call SetCategoryAndLabel to set the chosen label as the active variant for the head category.

Sprite Swapper.

In the editor, I’ve attached the component to the character. I assigned it the character’s Sprite Library, the head’s Sprite Resolver, and filled in the ‘Head’ category name.
All that remains is to add a UI button and call the method on the character.

Custom Injection

We’ll continue this demo by injecting a custom head sprite, which we haven’t define a label for beforehand.
In the existing script, I added a new method InjectCustom:

Swapper.cs
public void InjectCustom (Sprite customSprite)
{
  const string customLabel = "customHead";
  spriteLibrary.AddOverride(customSprite, targetCategory, customLabel);
  targetResolver.SetCategoryAndLabel(targetCategory, customLabel);
}

I added a new button, which calls the new method we created, passing it a custom sprite. Now, when clicking the button, it should... yeah...

Unsuccessful head sprite swapping.

Here is the thing, you see, when we rigged our character in the previous article, we created meta-data of the bones and how they are bound to the Sprite. A Sprite consist of a reference to the texture and, alongside it, the rigging meta-data.
When we injected the custom Sprite, it had the texture reference but was missing the rigging meta-data.

In the script, before injecting the Sprite, we should duplicate the rigging meta-data. I retrieve an existing head Sprite from the Sprite Library using the active label.
I then get the rigging meta-data from the existing head, both the bones and the bind poses. And finally, set the custom head’s bones and bind poses.

Swapper.cs
public void InjectCustom (Sprite customSprite)
{
  // Duplicate bones and poses
  string referenceLabel = targetResolver.GetLabel();
  Sprite referenceHead =
    spriteLibrary.GetSprite(targetCategory, referenceLabel);
  SpriteBone[] bones = referenceHead.GetBones();
  NativeArray<Matrix4x4> poses = referenceHead.GetBindPoses();
  customSprite.SetBones(bones);
  customSprite.SetBindPoses(poses);

  // Inject new sprite
  const string customLabel = "customHead";
  spriteLibrary.AddOverride(customSprite, targetCategory, customLabel);
  targetResolver.SetCategoryAndLabel(targetCategory, customLabel);
}

We can now select a random head or inject a custom one. It’s worth noting that you can skip this rig duplication step by duplicating the bones manually; the previous article demos how to do it.

Grouped Skins

Before creating a character mixer, let’s look into replacing all the characters’ Sprites as a grouped skin.
The recommended approach for this is to replace the Library Asset as a whole. We first need to create multiple libraries, each consisting of the same categories but different sprites.

Character Skinner.

In a newly created MonoBehaviour, I added a using statement at the top of the file. It’s required for all the scripts in the project, allowing us to interact with the 2D Animation package components.

For those using Unity 2021 and later, the components are no longer under the experimental namespace:

// Unity 2020
using UnityEngine.Experimental.U2D.Animation;

// Unity 2021+
using UnityEngine.U2D.Animation;

I added a Sprite Library inspector field. And also created the only method we need for this script, containing a Library Asset parameter.
All I do in this method is passing the new Library Asset to the Sprite Library component. Yep, that’s all...

Skinner.cs
using UnityEngine;
using UnityEngine.Experimental.U2D.Animation;

public class Skinner : MonoBehaviour
{

  [SerializeField]
  private SpriteLibrary spriteLibrary = default;


  public void Replace (SpriteLibraryAsset libraryAsset)
  {
    spriteLibrary.spriteLibraryAsset = libraryAsset;
  }

}

While this workflow is mostly editor work, it’s great for downloading asset bundles with new skins over the network reusing existing animated characters.

Character Mixer

In our final demo, we will create a mixer to mix-and-match sprites to create various characters.
We need a way to store all the available categories, so I created the serialized struct MixerCategory:

MixerCategory.cs
using System;
using UnityEngine.Experimental.U2D.Animation;

[Serializable]
public struct MixerCategory
{

  public string name;

  public string displayTitle;

  public SpriteResolver resolver;

}

For the dropdown UI, I created another MonoBehaviour consisting of two inspector fields: UI Text and UI Dropdown. We will use this script to control a Mixer Selector prefab.
It has a single method that accepts two parameters: the MixerCategory serialized struct we previously created and an array of labels.
I start by setting the UI label to the title provided by the MixerCategory struct.
Next up, I create an empty list of UI Dropdown Option Data. For each label passed to the method, I make a new Dropdown Option Data and add it to the list.
I then assign the list to the UI Dropdown’s options property.
Last, I hook up a listener for when the dropdown value changes. In such a case, we will receive the index of the selected item. I can then use the index to retrieve a label from the labels array. The label is then assigned as the active variant of the Sprite Resolver using SetCategoryAndLabel.

MixerSelector.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MixerSelector : MonoBehaviour
{

  [SerializeField]
  private Text titleLabel;

  [SerializeField]
  private Dropdown dropdown;


  public void Init (MixerCategory category, string[] labels)
  {
    // Set title
    titleLabel.text = category.displayTitle;

    // Populate dropdown
    List<Dropdown.OptionData> spriteLabels = new List<Dropdown.OptionData>();
    foreach ( string label in labels )
    {
      Dropdown.OptionData data = new Dropdown.OptionData(label);
      spriteLabels.Add(data);
    }

    dropdown.options = spriteLabels;

    // Handle change
    dropdown.onValueChanged.AddListener(optionIndex =>
    {
      string label = labels[optionIndex];
      category.resolver.SetCategoryAndLabel(category.name, label);
    }); 
  }

}

I created a UI Canvas prefab and attached the MixerSelector component we just created.
I added a label and dropdown UI elements to the prefab and assigned them to the component.

Hang on there; we have one last script to create:

Mixer.cs
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.U2D.Animation;

public class Mixer : MonoBehaviour
{

  [SerializeField]
  private SpriteLibrary spriteLibrary = default;

  [SerializeField]
  private MixerSelector selectorTemplate = default;

  [SerializeField]
  private MixerCategory[] categories = default;


  private SpriteLibraryAsset LibraryAsset => spriteLibrary.spriteLibraryAsset;


  private void Start ()
  {
    foreach ( MixerCategory category in categories )
      AddUISelector(category);
  }


  private void AddUISelector (MixerCategory category)
  {
    string[] labels = LibraryAsset.GetCategoryLabelNames(category.name)
                                  .ToArray();
    MixerSelector item = Instantiate(selectorTemplate, transform);
    item.Init(category, labels);
  }

}

The last MonoBehaviour is the Mixer that manages this whole operation. It has three inspector fields: a Sprite Library, a MixerSelecter script we just created, and an array of MixerCategory structs we made before.
I also create a method that accepts a MixerCategory for which I will create a MixerSelector instance.
I start by retrieving an array of labels for the category; I get them by calling GetCategoryLabelNames on the Library Asset that I access through the Sprite Library.
I then use the prefab reference to instantiate a new MixerSelector as a child of this script’s transform.
I call the Init method we created and pass it the category struct and the options labels.

Character Mixer.

Finally, when the MonoBehaviour starts, I iterate the categories and create a selector for each of them.

To hook it all up, I created a UI Panel and attached to it the Mixer component. I assigned the character’s Sprite Library and the MixerSelector prefab we have created. I also filled in all the MixerCategory data for this character, including the category names, display titles, and the Sprite Resolvers from the character’s objects.

We can now create any sprite combination for our character!

© All rights reserved.