Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Using Unity's New 2D Animation Package for Traditional Sprite Animation
Diving into Unity's new 2D Animation package and finding a lot to love for the creation sprite-based 2D games.
This post was originally posted to my personal blog
Introduction
The Unity package ecosystem can be a bit of a mess to navigate, so I wanted to highlight a package I dove into recently that I was surprised more people weren't talking about — the new 2D Animation package. The components and ideas introduced in the package significantly change the 2D animation workflow in a way that makes working with 2D in Unity a lot more reasonable, and in some ways, I believe, fundamentally change how to do 2D in Unity for the better.
Background
Note: This post is focused on flipbook-style sprite animation, but bone-based animation also benefits from this workflow
If you've done 2D animation in Unity before you'll be familiar with this workflow:
1) Import and slice a spritesheet
2) Select the sprites from that sheet that form an animation, drag them into the Scene window to create a new animation.
3) Rinse and repeat step 2 for each animation for that spritesheet.
4) If you're using animation controllers and this spritesheet shares the same states as others, you'll make a new override controller for this tilesheet specifically, and link up all these animations.
This means that every single new 2D object in your game needs unique animations and a unique controller, even if the sheets are the "same". This is because the Sprites are hard-referenced in the Animation, where the Animation essentially plays back a series of a direct references to a Sprite asset. So you can't share animations between objects, because the animations themselves point to specific sprites. You're essentially starting over the process from scratch for each object.
This is a giant pain. It means that if you have even just 5 tilesheets that have 5 animations each, you're looking at 25 additional assets in your project that need to be unique, and require lots of manual setting up, but that are fundamentally all doing the exact same thing: playing back sprites in tilesheet locations. Doing things this way makes sense when you consider that the animation system expects uniqueness-per-animation, which is often the case with 3D rigs. But for 2D, why isn't there just a way to play animations by reference to a tilesheet index? Why can't you just say "this animation plays tilesheet sprites 0-4" and share that across sprites?
SpriteLibrary/SpriteResolver
Well I have great news! You can now effectively do that! Unity's new 2D Animation package provides two major tools that more or less replace the functionality of the above workflow. The package sounds like it's meant primarily for bone-based animation (which I suspect is why more people aren't talking about it), but it has tools in it that work for generic 2D Animation as well.
Sprite Library
One of the core things the package introduces is the concept of a SpriteLibrary. As the name may suggest, this is a collection of sprites. It's almost directly a sort of C# dictionary, where sprites are indexed first by Category, then by a given sprite's Label (set by you in the library, not the generated sprite name that comes from slicing a tilesheet). Categories can be roughly whatever you want, and are used a way to group sprites that are used together or are semantically similar. You could have a category, as ther documentation suggests, that defines all the sprites used for a given character. Or, as we'll talk about more below, you could have a category that defines the frames of an animation, with each entry in a category being a specific frame.
What this allows you to do is address sprites not by direct reference anymore, but by a collection that can be generically referenced. The SpriteLibrary API, for example, allows you to do this:
public Sprite GetSprite(string category, string label)
Note here that neither the caller nor the function itself needs to know what's in the library. A version of the function:
var sprite = MySpriteLibrary.GetSprite("Character", "Face");
could be called on any library. Obviously if the library doesn't have a corresponding category and label you'll have a null sprite, but the point here is that SpriteLibrary has given us a reusable way to reference sprites. Contrast the above with the code with the standard ways to get a single Sprite from a tilesheet:
// From Resouces Folder
public Texture2D texture;
int spriteIndex = 0;
var sprite = Resources.LoadAll<Sprite>(texture.name)[spriteIndex];
//Via Atlas
public SpriteAtlas atlas;
var sprite = atlas.GetSprite("child_sprite_name");
// (Atlases are also un-ordered, so you can't just called GetSprites()[0])
Both of these (and many other options) require you to know the exact sprite you're looking for, either by its index or its name. SpriteLibraries give us ways to structure sprite data in a way that can be easily referenced, and, importantly, reusable. The same code path can be used for any libraries that share the same structure, whereas the above samples all need to be edited to account for the specifics of the tilesheet or atlas they are working with.
Sprite Resolver
Now where this gets interesting isn't just through the use of a SpriteLibrary. A sprite library would be worthless as an asset alone. The 2D Animation package also gives us access to another new component, the SpriteResolver.
A SpriteResolver, when placed on an object that also has a SpriteLibrary component and linked in Sprite Library, allows you to dynamically assign the given sprite of an attached SpriteRenderer component by changing the values on the resolver. These values map directly to the values you set on the linked in SpriteLibrary.
So, the following code:
GetComponent<SpriteResolver>().SetCategoryAndLabel("Character", "Face");
Tells the resolver to try and grab the corresponding sprite from the SpriteLibrary that it has access to. If this code succeeds, the sprite will update in the SpriteRenderer! Below is a screenshot of what would happen if you called the following code on this game object.
GetComponent<SpriteResolver>().SetCategoryAndLabel("Idle", "3");
In the library asset itself, you can see that the category Idle, at sprite label 3, points to the Scavengers_SpriteSheet_2 sprite we see linked above in the SpriteRenderer component.
It may be hard to grasp the philosophical change in this, but to support you in having the same revelation I had, Unity provides some pretty good samples in the 2D Animation Package (Download by clicking "Download Samples" From Window->Package Manager and selecting 2D Animation). What I just discussed is demonstrated in their SpriteSwap samples.
Putting It Together with Animations
So providing a nicer way to set single sprites on an object is great, and maybe worth the package alone, but the beauty of SpriteResolver isn't just that it exposes an API to set sprites this way, but that the Category and Label fields of the Resolver can be animated.
So if you take a generic way to address sprites, and add in a generic way to change those addresses.... you get a generic sprite animation system! This is huge. It's made adding new content to Cantata trivial. Here's my process:
I slice up the new spritesheet into sprites. I do this at runtime because Cantata is data-driven, but you could just as easily do this at edit time via the import settings.
I create a copy of Cantata's default "template" SpriteLibrary, and assign the sprite references in that library to my newly sliced sprites. The template library in Cantata has categories that define animations. So a Category may be "AttackNorth", and contains four sprites, with labels 0,1,2, and 3.
Because all the Cantata objects ("Interactables" in Cantata lingo) share the same spritesheet layout, and all of them have their animations in the same place, I always know that, say, that sprite on the tilesheet at index 13 always maps to MoveEast sprite 3. If you were importing arbitrarily arranged sheets, you'd need to do a little extra here to make sure the sprites go to the right places.
On the core "Interactable" prefab, I swap in the SpriteLibrary I just made with the prefab's value (the template).
That's it! The set animations on the animation controller all animate the same label values, so I can use the prefab animation controller as is, with no need to create a new controller, or even an override controller.
For new Interactables, I don't need to create new animations, new controllers, nothing. I only need a SpriteLibrary, which can be easily created and edited at edit-time or runtime. I wasn't even done adding in all the animations for Cantata, but using this system I was able to swap out ~60 animations (out of a total projected of ~700) for a single set of animations and a single controller.
I'm also thinking of creating a dynamic effects loading system that functions off a similar concept, and this has paved the way as well for me to add in new unit types and sizes that would have been harder in the old system. It's a serious workflow game-changer.
And importantly, it just works. Creating new assets in Cantata before wasn't exactly hard, but it was tedious and easy to misclick when creating animations and forgetting to link the proper animation to certain place or whatever. Now I never have to worry about it!
Building Animations
I want to take a small step back here as well and talk about how to actually animate the labels, because it's a bit more frustrating that it may seem. A big shoutout here to this blog post as well that seems to be the only other place on the internet that describes how to actually set these values, and who taught me how to do it.
Annoyingly, you can't just create an animation and easily address the Labels on the SpriteResolver by name. Instead they need to be a "LabelHash". This is a number that is effectively impossible to set manually.
This means to set up your animations, you need to create an animation, target the GameObject you want to animate, and select the SpriteResolver property in the animation editor and add in fields for LabelHash and CategoryHash (both or either, depending on what you want to animate). Your animator should look roughly like this (minus the keyframes):
To add in animations, you can't just set keyframes and the target label/category name. Instead you have to use their hash. Like I said above as well, these hashes cannot be set directly.
Instead, you need to record the animation yourself. Click the red button to start the animation recording system, and then move the animation playhead to the desired location and then change the category or click the desired sprite in the target SpriteResolver component to make a keyframe of those values at the playhead location. I know.
Additionally, and this is very important, you need to make sure the keyframes are set to "Broken", and "Both Tangents" set to "Constant". You can do this by right-clicking the keyframe. Because the hashes are discreet values, attempting to tween between them will result in invalid values. By using these settings you're effectively making sure the values are set through the duration of their playback.
After setting the values this way, you should be good to go. For Cantata I play these animations back with an animation controller, but you could just as easily just have an animator playing back a single set of animations. I'm also not sure how the label hashes are calculated, but it seems like, as long as the library you swap in for another one has the same names/categories for a given sprite reference, the hashes are interchangeable, i.e. you don't need to make new animations for every new SpriteLibrary.
Conclusion
Hopefully with this post you've got a bit of a handle on what the new 2D Animation project offers to people doing more "traditional" 2D animation inside of Unity. For me, this is now the de-facto way I'll handle 2D animation if I'm doing animation in Unity, as it allows me to cut out any redundant work and also allows me to create animations and controllers that are reusable!
If you have any questions, feel free to reach out to me via the comments or via @kkukshtel on Twitter. Thanks for reading!
Errata
If you're trying to work with animation controllers and playing back both old-style animations and label-based animations, there is a known bug where they will conflict and not playback properly. For now, try to not mix the two.
Read more about:
Featured BlogsAbout the Author
You May Also Like