Reusing 2D Sprite Animations in Unity

Posted On: 2020-10-12

By Mark

One of the conveniences of using Unity for 3D is the ability to share animation data across multiple characters, thereby reducing the amount of animation work required. When Unity implemented its 2D workflow, it was designed to allow developers to easily create animations by swapping sprites, but, unfortunately, didn't support any way to reuse those animations*. For me, this has become something of an inconvenience, as the timing of my animations drives a lot of the gameplay - from timing when to spawn projectiles to disabling and re-enabling control. For my project, sharing an ability (such as an attack) between multiple characters required sharing the animations - any other approach would have too much duplication to be viable.

Sprite Library

Fortunately, there is a simple, elegant solution available - provided one is willing to use experimental* features. The Sprite Library feature of the 2D animation package allows developers to define sprite swaps according to labels rather than individual sprites. This, in turn, makes it possible to define one animation using labels, and reuse it across many different characters, simply by swapping which sprite is associated with each label.

Reusing Animations

In order to animate the sprite, you need to record changes to the sprite resolver* rather than changes to the sprite itself. These changes will then be stored on the animation as changes to the category and/or label for the resolver, and, at runtime, the resolver will pick the corresponding sprite from the associated library.

Since the resolver picks the sprite at runtime, this means that changing the sprite library will change which sprites are being used. Putting that together with the animations, it becomes simple to share an animation across characters: create two different libraries (one for each character) that use a common set of labels and categories, but different sprites*. Once that's in place, the exact same animations can be used with either library, and the game engine will display the correct sprite. What's more, this approach scales well beyond two characters: any number of characters (or objects) can share animations, so long as you're consistent with the labels and categories.

Automating Libraries

For myself, I already had a pattern in place for how I organize individual animation frames' sprites on a per character basis. Thus, I quickly found myself trying to solve a simple automation challenge: generate sprite libraries per character, using labels and categories that match my existing pattern.

As it turns out, this was extremely simple to do. Sprite Library Assets are Scriptable Objects, so they are simple to create via scripts. The library then has a simple AddCategoryLabel method which can be used to add a sprite with a particular category and label. Beyond that, I simply had to traverse the file system to pick out the correct category and label based on my conventions*. For those that are curious to see the actual code, I've included it (collapsed) below.

#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

using UnityEngine.Experimental.U2D.Animation;

public static class SpriteLibraryAutomation
{
    [MenuItem("Tools/Make Sprite Library")]
    public static void SpriteLibraryStuff()
    {
        foreach(var item in AssetDatabaseUtilities.GetValidatedSelection("Sprite Library"))
        {
            Debug.Log($"Creating sprite library for {item.name}");

            var path = AssetDatabase.GetAssetPath(item);
            var subfolders = AssetDatabaseUtilities.GetLeafDirectories(path).ToList();
            if (subfolders.Count == 0)
            {
                Debug.LogWarning($"Unable to generate sprite libary for {item.name}: no sprites available.");
            }

            var libraryName = $"{item.name}.asset";
            var libraryPath = AssetDatabaseUtilities.AssetPathCombine(path, libraryName);
            var library = AssetDatabase.LoadAssetAtPath<SpriteLibraryAsset>(libraryPath);
            if (library == null)
            {
                //Make new one
                library = ScriptableObject.CreateInstance<SpriteLibraryAsset>();
                AssetDatabase.CreateAsset(library, libraryPath);
            }

            foreach(var folder in subfolders)
            {
                var label = folder.Split('/').Last();
                foreach (var sprite in AssetDatabaseUtilities.LoadAllInFolder<Sprite>(folder))
                {
                    var category = sprite.name.Split('.').First();
                    library.AddCategoryLabel(sprite, category, label);
                }

            }
        }
    }
}
#endif

//Some common automation helpers I normally have in another file:

#if UNITY_EDITOR
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public static class AssetDatabaseUtilities
{
    /// <summary>
    /// Recursively walks all directories in the specified path. Returns the asset path for any folders that do not contain any subfolders (a "leaf" on the tree.)
    /// </summary>
    public static IEnumerable<string> GetLeafDirectories(string path)
    {
        var subfolders = AssetDatabase.GetSubFolders(path);
        if (subfolders.Length > 0)
        {
            foreach (var subfolder in subfolders)
            {
                foreach (var result in GetLeafDirectories(subfolder))
                {
                    yield return result;
                }
            }
        }
        else
        {
            //This is a leaf node, return it here.
            yield return path;
        }
    }

    /// <summary>
    /// Yields a list of assets the user has selected in the editor. Items that are not assets will be omitted (and the user will see a warning for each item omitted).
    /// </summary>
    /// <param name="activityDescription">A description of the activity that requires the selection (used in error messages.)</param>
    public static IEnumerable<Object> GetValidatedSelection(string activityDescription)
    {
        if (Selection.objects.Length == 0)
        {
            Debug.LogWarning("Selection required before " + activityDescription);
            yield break;
        }
        foreach (Object o in Selection.objects)
        {
            if (!AssetDatabase.Contains(o))
            {
                Debug.LogWarning("Asset Database does not contain " + o.name);
                continue;
            }
            yield return o;
        }
    }

    /// <summary>
    /// Combines the provided paths together, according to the conventions used by unity's asset database.
    /// </summary>
    public static string AssetPathCombine(params string[] paths)
    {
        if (paths == null)
        {
            throw new System.ArgumentNullException("paths");
        }
        return string.Join("/", paths);
    }

    /// <summary>
    /// Loads and returns all assets of type T in the specified folder. 
    /// </summary>
    public static IEnumerable<T> LoadAllInFolder<T>(string folderPath)
        where T : UnityEngine.Object
    {
        return AssetDatabase.FindAssets($"t:{typeof(T).Name}", new string[] { folderPath }).Select(x => AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(x)));
    }
}
#endif

Rapid Iteration

The combination of having shared animations and using automation for nearly every step of my import workflow has allowed me to rapidly iterate on artwork, seeing it in-game almost immediately. This, in turn, has been a massive improvement for my productivity, as any character can have an existing ability added as soon as I've drawn it*. As I keep progressing with my work, I'm always on the lookout for things that can help reduce the amount of distraction from the underlying goal of any given task. By reusing animations, I have made a massive improvement in this regard - and it was surprisingly simple to implement. Hopefully, this post has helped you be able to reuse your animations too.