Monday, July 4, 2011

XNA Game Design: Spritesheets


At this point, 2D game development should feel like an arts and crafts project (and if it doesn't, it's because you have no imagination and should probably pursue a career as an accountant you sad, boring person). You have a 2D canvas with cartesian coordinates to draw images across. You just have to lay out the rules of how they should be rendered.


By making a spritesheet class, we are specifying an image that contains multiple graphics, but should be cut up and only have one specific "tile" of the spritesheet drawn at a time. Videogame spritesheets are good for holding a single image with multiple animations in one specific file. It's more economical to hold more images in one solid file rather than to have multiple files each holding a single image, especially if the images in the file are all related (like corresponding frames to a character animation).

It should be noted not all videogame sprites operate the same way. Sometimes the media is limited on internal data storage, and it becomes more efficient to just specify the exact values of where to "cut" out each sprite in the code. The method I am suggested makes for very neat code, but would not pass for quality development on older forms of data storage. Thankfully, we're developing a game for windows in the year 2011, so we've already thrown most professional programming habits out of the moving vehicle. In it's stead, we will be left with very clean looking code.



Consider the following; the walking animation of Mega Man is composed of ten frames. Each frame is (overall) a different total width and height. By specifying a maximum size, we may ensure that whatever other sized animation frames fit into the source rectangle, but all that surrounding area is moot data. Truthfully, it's not a bad tradeoff. But if we were concerned with data storage, just specifying the coordinates of each next frame wouldn't hurt (it would just suck and would require tables of rectangle data. Not fun).

The idea behind our animated spritesheet is as follows:

  • The sprite will take in two additional parameters; an int describing the number of columns, and an int describing the number of rows.
  • The sprite will have six additional properties; two private ints describing the maximum number of columns and rows, two private ints describing the source rectangle width and height, a public int describing the current x index, and a public int describing the current y index.
  • Before drawing, it will perform a check to determine whether the x and y indices are within acceptable bounds.
So first off, just what do we mean by an x and y index?

These refer to the x and y coordinates of our sprite that gets cut out. Think of it like a two-dimensional array; the x index refers to the horizontal column that is cut out, and the y index refers to the vertical row that is extracted.

This shows an example set of x and y indices that represent the cutouts of each image.

Alright, so now we just need our code.


class Sprite_Set : ASprite
{
    private int Frame_width, Frame_height;
    private int X_Frame_Max, Y_Frame_Max;
    public int X_index = 0; 
    public int Y_index = 0;

    //All optional arguments must be listed last.
    public Sprite_Set(string path, int columns, int rows,
    Vector2? v = null) : base(path, v)
    {
        //Just trying to avoid a DivideByZeroException
        if (columns < 1 || rows < 1)
        {
            throw new Exception("Sprite_Set " + this + 
            " has too few rows or columns");
        }
        X_Frame_Max = columns;
        Y_Frame_Max = rows;
        Frame_width = image.Width / columns;
        Frame_height = image.Height / rows;
        }

        //This overwrites the source rectangle, which
        //we use to designate the coordinates and size of 
        //the image we want to cut out.
        public new Rectangle src_rect
        {
            get
            {
            return new Rectangle(X_index * Frame_width,
            Y_index * Frame_height, Frame_width, Frame_height);
            }
        }

        //This Update method ensures we are not asking for
        //indices that don't exist.
        public void Update()
        {
            if ((X_index >= X_Frame_Max) || 
            (Y_index >= Y_Frame_Max))
            {
                throw new Exception("Index out of bounds: 
                X (" + X_index + " / " + X_Frame_Max + "), 
                Y (" + Y_index + " / " + Y_Frame_Max + ")");
            }
        }

        //Our good old fashioned Draw Method
        public new void Draw(SpriteBatch s_Batch)
        {

            Update();
            base.Draw(s_Batch);
        }
}

It's a bit more code than we're used to, but it works rather well. With this, we can specify spritesheets of equidistant rectangular tiles, and draw only one tile at a time. While this makes it  perfect for things such as sprite animations, it's not the best for drawing entire maps from select tiles (as each drawn tile has a reference to the entire Texture2D spritesheet).

The more game related things we begin to focus on, the more broad these topics tend to become, and the more things will begin to depend on how you want you're game designed. I'm sure some reader can think of a dozen improvements to my sprite_set class. But in terms of a general skeleton, I like what this provides.


Homework: create two methods that increment the x and y indices respectfully. If the indices reach their maximum, it sets the respective index value to 0. These will be useful for creating simple animation update calls (Sprite_Set.Increment_Y();)


Yesterdays Homework Solution: You'll need a private float value and public integer value with get and set accesors to add opacity to your sprites. In the class variables, add:
private float _Opacity = 1.0f; //full opacity
and our public integer:
public int Opacity

{
    get
    {
        return (int)(_Opacity * 255);
    }
    set
    {
        if (!(value >= 0) || (value > 255)) return;
        else _Opacity = (float) (value/255);
    }
}

Finally, the Draw method just needs to be changed to premultiply the alpha values.
s_Batch.Draw(image, pos, src_rect, blend_color * _Opacity, angle, origin, scale, effect, z);