Blake O'Hare .com

Gamelight Tutorial: Part 5 - Sprite Sheet Magic

One of the more tedious parts of making 2D sprite-based games is dealing with sprite cropping and alignment. Luckily, there's a class that handles all that for you. It's the SpriteSheet class in the Gamelight.Graphics.SpriteSheet namespace.

I looked up Megaman Sprite Sheet in Google Images and quickly found the following image:
stolen_sprites.png

Typically the steps that follow go something like this:
  • Crop out each individual sprite.
  • Convert magenta to transparency (either in the image asset or tell the code to treat magenta as transparent)
  • Tediously align each image to each other so the resulting animation isn't jumpy.

The Gamelight SpriteSheet class does all this for you. Simply add your sprite sheet image AS IS to your csproj file.

Adding Sprites

During the initialization of your scene (or before), create a new SpriteSheet class. The SpriteSheet class basically contains groups which have a name and a direction they're associated with. In this example, we will create 4 different groups corresponding to walking and standing, both left and right. To add an image to a group, you simply call the AddImage method with a string name, and a pair of coordinates that indicate SOME RANDOM PIXEL in the sprite. The easiest way to find this coordinate is to open the sprite sheet in mspaint and hold the mouse over the sprite. The coordinates of the cursor will appear in the bottom left corner of the window in the status bar.

gamelight_part5_example.png

However, before you call AddImage, you must specify which direction the sprite is facing, using the SetInputDirection method. You only have to call this method once and all successive calls to AddImage will be associated with that direction until you call SetInputDirection again to change it.

SpriteSheet sheet = new SpriteSheet(new Image("stolen_sprites.png"), false);
sheet.SetInputDirection(Direction.Left);
sheet.AddImage("walking", 21, 14);
sheet.AddImage("walking", 38, 15);
sheet.AddImage("walking", 63, 13);
sheet.AddImage("walking", 38, 15);
sheet.AddImage("standing", 86, 10);

sheet.SetInputDirection(Direction.Right);
sheet.AddImage("walking", 139, 13);
sheet.AddImage("walking", 167, 13);
sheet.AddImage("walking", 189, 11);
sheet.AddImage("walking", 167, 13);
sheet.AddImage("standing", 116, 16);


The false being passed in to the SpriteSheet constructor is indicating that the stolen_sprites.png image does NOT have any transparency and that it should be auto-detected. The way the auto-detection algorithm works is it finds the most common color in the 5x5 square of pixels in each of the four corners of the sheet. The most common color is then converted to RGBA:(0, 0, 0, 0) across the whole sheet.

Aligning Sprites

When you add sprites, the images that are parsed out of the sprite sheet are tightly cropped around non-transparent pixels. But this is not ideal. For example, the third image from the left shows Megaman walking left and sticking his arm out a bit. This image, when shown in sequence with the other two walking sprites, will appear to "jump" to the right by 8 pixels or so because the sprites will be aligned by their left border.

But no fear. There are ways to automatically align your sprites in a way that doesn't make them wobble.

sheet.SetAlignment(
    "walking",
    HorizontalAlignment.Autodetect, VerticalAlignment.Bottom);

sheet.SetAlignment(
    "standing",
    HorizontalAlignment.Autodetect, VerticalAlignment.Bottom);


Magic.

If for some reason you are displeased with either alignment auto detection or simply one of the non-auto detection options, you can add an additional offset of any particular sprite that will be added to the sprite's offset. To do this, use the AddImage method that takes in 2 additional ints for x and y offset for that sprite.

Accessing Sprites

To finally get the Image objects corresponding to the sprite groups, simply call GetGroup with the group name and direction you're interested in. It will return an Image array.

Image[] walking_left = sheet.GetImages("walking"Direction.Left);

And now you're ready to blit these images onto your background. These images are still cropped tightly around non-transparent pixels, but they have x and y offsets set such that when you blit them all on the same coordinate, they'll appear the way they are intended.

Click here to see the final product


Here's the code for the GameSceneBase object. A target FPS of 30 was used.

using System;
using Gamelight.Scene;
using Gamelight.Graphics;
using Gamelight.Graphics.SpriteSheet;
using Gamelight.Input;

namespace SpriteTest
{
    public class DemoScene : GameSceneBase
    {
        private int x = 200;
        private int y = 150;
        private bool facingLeft = false;
        private bool walking = false;
        private int counter;

        private Image[] left_walking;
        private Image[] right_walking;
        private Image[] left_stand;
        private Image[] right_stand;

        protected override void Initialize()
        {
            SpriteSheet ss = new SpriteSheet(
                new Image("stolen_sprites.png"), false);

            ss.SetInputDirection(Direction.Left);
            ss.AddImage("walking", 21, 14);
            ss.AddImage("walking", 38, 15);
            ss.AddImage("walking", 63, 13);
            ss.AddImage("walking", 38, 15);
            ss.AddImage("standing", 86, 10);

            ss.SetInputDirection(Direction.Right);
            ss.AddImage("walking", 139, 13);
            ss.AddImage("walking", 167, 13);
            ss.AddImage("walking", 189, 11);
            ss.AddImage("walking", 167, 13);
            ss.AddImage("standing", 116, 16);

            ss.SetAlignment(
                "walking",
                HorizontalAlignment.Autodetect,
                VerticalAlignment.Bottom);
            ss.SetAlignment(
                "standing",
                HorizontalAlignment.Autodetect,
                VerticalAlignment.Bottom);

            this.left_walking = ss.GetImages("walking"Direction.Left);
            this.right_walking = ss.GetImages("walking"Direction.Right);
            this.left_stand = ss.GetImages("standing"Direction.Left);
            this.right_stand = ss.GetImages("standing"Direction.Right);
        }

        protected override void ProcessInput(InputEvent[] events)
        {
            int velocity = 3;
            if (InputManager.IsKeyPressed(Key.Left))
            {
                this.x -= velocity;
                this.facingLeft = true;
                this.walking = true;
            }
            else if (InputManager.IsKeyPressed(Key.Right))
            {
                this.x += velocity;
                this.facingLeft = false;
                this.walking = true;
            }
            else
            {
                this.walking = false;
            }
        }

        protected override void Update(int gameCounter)
        {
            this.counter = gameCounter;
        }

        private Image GetCurrentImage()
        {
            Image[] image_group = this.walking
                ? this.facingLeft
                    ? this.left_walking
                    : this.right_walking
                : this.facingLeft
                    ? this.left_stand
                    : this.right_stand;

            return image_group[(this.counter / 3) % image_group.Length];
        }

        protected override void Render(Image gameScreen)
        {
            gameScreen.Fill(Colors.White);
            gameScreen.Blit(this.GetCurrentImage(), this.x, this.y);
        }
    }
}