Thank you for taking part in the NotSlot journey!
We invite you to join us in our next chapter:
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.
In the previous articles, we learned how we could work manually with the 2D Animation package’s components. To briefly recap:
In our first demo, we will start by randomly selecting a head sprite.
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.
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.
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
:
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...
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.
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.
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.
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...
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.
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
:
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
.
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:
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.
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!