Monday, April 4, 2016

Duplicating and ordering sprites in Unity

The ART OF SWORD lead programmer contributes some Unity code that solves an animation problem to the DEVGAME community and explains its utility.

I was working on Art of Sword with the animation frames the artist had sent when I ran into a familiar problem; Unity's sprite Animations and AnimatorControllers do not allow you to use duplicate sprites, and neither do they allow you to specify the order of sprites displayed in the animations. That is, if you want to use a sprite multiple times in an animation, you have to include it multiple times in your spritesheet. And you essentially have to have each animation lined up in the spritesheet in order - duplicate frames and all - to let Unity's Animation system use them.

This is extremely inconvenient, particularly with the large frames the artist made and the grid-style spritesheet I had assembled them into. Without any duplicate frames the texture is 2048x1024 and contains 42 frames, each of which is 283x170. Obviously, I needed to write my own animator script. I already had a horribly kludged one from when I was first working on Art of Sword that required each individual sprite to be in a different texture AND I had to manually drag and drop each texture into the Unity Inspector. I still don't know exactly what was going through my head when I made that.

So with the new frames from the artist, I set out to write a proper animator that could take a single texture and give me an easy-to-use list of frames without my having to do anything more than copy the texture into the Unity Hierarchy. I came up with this:


CODE

using UnityEngine;
using System.Collections;

public class CharacterAnimator : MonoBehaviour
{
    public Texture2D sourceImage;
    public int frameWidth;
    public int frameHeight;
    public int framesPerRow;
    public int framesPerColumn;
    [HideInInspector] public Sprite[] frames;
    private SpriteRenderer spriteRenderer;
    [HideInInspector] public bool animationIsPlaying; //used in art of sword to block the player from taking any action while the animation is running
    [HideInInspector] public bool isDeath; //used in art of sword to "freeze" at the end of the death animation
    [HideInInspector] public float frameSpeed;
    [HideInInspector] public string facing; //only used in art of sword
    public int idleRightFrameIndex; //only used in art of sword
    public int idleLeftFrameIndex; //only used in art of sword
    void Awake ()
    {
        spriteRenderer = GetComponent<SpriteRenderer> ();
    }
    public void InitFrames () //called from the main Player component - lists all frames in texture
    {
        frames = new Sprite[framesPerRow * framesPerColumn];
        for (int y = 0; y < framesPerColumn; y++) //loop through each column in the spritesheet grid
        {
            for (int x = 0; x < framesPerRow; x++) //loop through each row in the column
            {
                Rect spriteRect = new Rect (x * frameWidth, sourceImage.height - ((y + 1) * frameHeight), frameWidth, frameHeight); //using x, y, frameheight, and framewidth the coordinates and size of each sprite are calculated and saved
                frames[x + (y * framesPerRow)] = Sprite.Create (sourceImage, spriteRect, new Vector2 (0.5f, 0.5f)); //the saved rectangle is cut out of the texture as a sprite and saved in the frames[] array which the animator uses
            }
        }
    }
    public void Play (string animationToPlay)
    {
        animationIsPlaying = true;
        StartCoroutine (Animate (animationToPlay));
    }
    IEnumerator Animate (string animation) //the string defining the animation, in the form of frame0,frame1,frame2,...,lastframe
    {
        int[] animationFrameIndexes = new int[animation.Split ("," [0]).Length]; //splits the string on each comma
        for (int i = 0; i < animation.Split ("," [0]).Length; i++)
        {
            animationFrameIndexes [i] = int.Parse (animation.Split ("," [0]) [i]);
        }
        foreach (int index in animationFrameIndexes)
        {
            spriteRenderer.sprite = frames [index];
            yield return new WaitForSeconds (frameSpeed);
        }
        if (isDeath) //"freeze" momentarily when player killed
        {
            yield return new WaitForSeconds (1);
        }
        Idle ();
        yield break;
    }
    public void Idle () //used in art of sword to return to the ready-to-act state
    {
        isDeath = false;
        if (facing == "right")
        {
            spriteRenderer.sprite = frames [idleRightFrameIndex];
        }
        else if (facing == "left")
        {
            spriteRenderer.sprite = frames [idleLeftFrameIndex];
        }
        animationIsPlaying = false;
    }
}

END CODE

Here's what it looks like in the inspector (I hid frameSpeed because I assigned it automatically from the player component speed variable):


And the results in the game are as follows:
 



The cool thing is that this script can be used for anything involving characters moving through several frames. Making a new animation is a simple matter of adding a public string to whatever component you're writing, filling in the 0-indexed frame numbers, and calling CharacterAnimator.Play () from the component.

Hopefully some of you will find this useful for your development projects.

4 comments:

  1. That is, if you want to use a sprite multiple times in an animation, you have to include it multiple times in your spritesheet.

    I have not run into this whatsoever. You can use the same sprite multiple times in the same animation as well as in any order.

    This might be an issue of older Unity not having this functionality. Or, I'm completely misunderstanding the issue you had.

    For example, from Elveteka:

    Screenshot 1, notice it's using SpriteSheetHero9_1

    Screenshot 2, for comparison, using SpriteSheetHero9_2

    Screenshot 3, later in the same animation, using the same sprite, SpriteSheetHero9_1

    ReplyDelete
    Replies
    1. Indeed. I published a sprite based game on Steam last December and I was able to get the sprite animations to use duplicate sprites and sprite out of order without issue.

      That said, this does have one massive advantage over the default animation tools. With this you can substitute a different spritesheet and as long as the frames are in the same order and are the same size you won't need to make any other changes. So it would be quick and easy to create multiple characters that have the same types of moves but look a bit different.
      With the default animation tools if you want to create a new character with a similar set of animations you have create all of the animations from scratch, which can get seriously annoying.

      Delete
    2. True enough, on being able to just substitute a different spritesheet.

      It's not that much effort to copy-paste a prefab, go into the animations and drag-and-drop sprites, though. So really unless you're doing a *lot* of slightly different characters (at which point you may be better served approaching it differently) or doing a fair number of different characters with very numerous animations, it won't be that much of an improvement.

      Sometimes the simpler approach will be better, and this seems to be rather niche.

      Delete
  2. Very cool.

    Maybe drop future code chunks into GitLab Snippets or Gists.

    ReplyDelete