Sprites and 2D

  CU_MDX_Sprite2D.zip (122.4 KiB, 4,238 hits)


  CUnitFramework.zip (101.1 KiB, 20,406 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

While most modern games these days are in 3D, there are a lot of great games that just sport 2D graphics (Monkey Island, anyone?). In this tutorial we will learn how to create_ and use 2D elements in our programs. The sprites that we will use in this tutorial are from the game, Chrono Trigger: a cat and Crono himself. You can get tons of game sprites from sites like Game Sprite Archives and RPG Icons, or you can just create_ your own art. The program we will create_ with these sprites is simple: Crono will run around, drink beer, and throw cats around the screen. What more could you ask for?

While we could accomplish 2D graphics by rendering a polygon facing the camera with a texture applied to it (or my Quad class mentioned in the Bitmap Font tutorial), DirectX provides a class that makes working with 2D graphics a lot easier. DirectX supports the use of 2D graphics via the Sprite class. I mentioned Sprites briefly in the text tutorial as a way to optimize text rendering calls, but I didn’t really explain what a Sprite really is. A Sprite is basically a polygon with a texture applied to it. However, the Sprite class itself provides us with a lot of calculations built in to itself. For example, when we render a Sprite, the polygon/texture will automatically face the camera, so the graphic will appear 2 dimensional.

A single Sprite object can be used to render many different textures, so we really only need to create_ one Sprite instance to render all of our graphics. To help work with Sprites, I wrote a class, SpriteBase, that can be used to render a Sprite.

namespace CUnit
{
/// Sprite wrapper
public class SpriteBase : WorldTransform
{
protected Vector3 m_pivot;

///

Default constructor
public SpriteBase() Toggle
{
// Empty

}

///

Draws the sprite using the Sprite.Transform property.
/// Sprite to draw with.
/// Texture to draw.
/// Rectangle that specifies which portion of the texture to draw.
public void Draw( Sprite sprite, Texture texture, Rectangle srcRect, Color color ) Toggle
{
sprite.Transform = Transform;
sprite.Draw( texture, srcRect, m_pivot, Vector3.Empty, color );

}

///

Flips the sprite horizontally.
public void FlipHorizontal() Toggle
{
XScale = -XScale;

}

///

Flips the sprite vertically.
public void FlipVertical() Toggle
{
YScale = -YScale;

}

///

Gets and sets the sprite’s pivot point.
public Vector3 Pivot Toggle
{
get { return m_pivot; }
set { m_pivot = value; }

}

}

}

To render with a Sprite, we call Sprite.Draw. Sprite.Draw renders the texture with the transformation matrix contained in the Sprite.Transform property. Since I’ve already written a handy dandy WorldTransform class that encapsulates this matrix, I base the SpriteBase class off of the Sprite.Draw method. Another useful calculation of the Sprite class is that the values entered into the X and Y translation values of the tranformation matrix are automatically converted into screen coordinates. So (0, 0, 0) would put the Sprite in the top-left corner of the viewport. The pivot parameter of the Sprite.Draw method specifies the point on the texture that the texture will rotate around. By default this value is located in the top-left corner of the texture. It’s often useful to have this pivot point located at the center of the texture so I made the pivot point accessible through SpriteBase.Pivot. The position vector is set to Vector3.Empty because the Sprite’s position is calculated with the transformation matrix. A position vector specified here would create_ an offset from the position specified by the transformation matrix. The Rectangle parameter specifies a rectangular portion of the texture that we want to render. This rectangle is defined in pixel coordinates rather than texture coordates. So if we wanted to render the upper left quarter of a texture that is 256×256, the rectangle would be defined as ( 0, 0, 128, 128) rather than ( 0, 0, 0.5, 0.5).

The SpriteBase class also lets you flip a texture horizontally and vertically. This is useful in situations where you have a texture of a character facing in one direction. Rather than creating another texture of him facing the other direction, you can simply flip the texture, which saves both time, energy, and memory.

namespace CUnit
{
/// An animated sprite.
public class SpriteAnimated : SpriteBase
{
protected int m_cellHeight;
protected int m_cellWidth;
protected int m_currentFrame;
protected int m_numColumns;
protected int m_startCell;
protected int m_endCell;
protected float m_lastFrameUpdate;
protected float m_secondsPerFrame;

///

Default Constructor
public SpriteAnimated() Toggle
{
// Empty

}

///

Constructor
/// Height of one cell
/// Width of one cell
/// Time to display a frame
/// Number of columns in the texture
/// Cell to start initial animation
/// Cell to end initial animation
public SpriteAnimated( int cellHeight, int cellWidth, float secondsPerFrame, int numColumns, int startCell, int endCell ) Toggle
{
m_cellHeight = cellHeight;
m_cellWidth = cellWidth;
m_numColumns = numColumns;
m_secondsPerFrame = secondsPerFrame;
m_currentFrame = startCell;
m_startCell = startCell;
m_endCell = endCell;

}

///

Advances the frame of animation.
/// Time elapsed since last frame.
public void Update( float elapsedTime ) Toggle
{
m_lastFrameUpdate += elapsedTime;
// Update frame based on time elapsed
if ( m_lastFrameUpdate >= m_secondsPerFrame )
{
// Loop frame if on last frame.
m_currentFrame = (m_currentFrame == m_endCell) ? m_startCell : m_currentFrame + 1;
m_lastFrameUpdate -= m_secondsPerFrame;

}

}

///

Draws the sprite.
/// Sprite to draw with
/// Texture to draw
public void Draw( Sprite sprite, Texture texture ) Toggle
{
int row = m_currentFrame / m_numColumns;
int column = m_currentFrame % m_numColumns;
Rectangle rect = new Rectangle( column * m_cellWidth, row * m_cellHeight, m_cellWidth, m_cellHeight );
base.Draw( sprite, texture, rect, Color.White );

}

///

Gets and sets the starting animation cell.
public int StartCell Toggle
{
get { return m_startCell; }
set { m_startCell = value; m_currentFrame = m_startCell; }

}

///

Gets and sets the ending animation cell.
public int EndCell Toggle
{
get { return m_endCell; }
set { m_endCell = value; }

}

///

Gets and sets the time each frame is displayed.
public float SecondsPerFrame Toggle
{
get { return m_secondsPerFrame; }
set { m_secondsPerFrame = value; }

}

}

}

While SpriteBase is useful for drawing static sprites, SpriteAnimated is used to draw animated sprites. Animated sprites are accomplished through the use of several images of an object or character drawn in an animated sequence as shown below. To render an animated sprite, we simply have to adjust the Rectangle parameter of the Sprite.Draw method to only render the portion of the image that we want displayed. By changing this rectangle to cover different portions of the texture we can create_ an animated sprite. To use an animated sprite, we need to know a few values: the height and width of each animation cell, the number of columns in the texture, the number of seconds to display each frame of animation, a starting animation frame, and an ending animation frame. These values are used in determine which frame of animation to display and when to display it.

Crono

Crono

By storing both a starting frame and an ending frame, we can store multiple animations in a single texture. You can see in the texture above that the first six frames are of Crono running and the last two frames are of Crono drinking like the alcoholic that he is. The drinking animation will be used as the idle animation. The current animation frame increments until it reaches the end frame and then it loops back around to the start frame to repeat the animation.

The SpriteAnimated.Update method is used to update_ the current frame of animation. The class is time based so you can set an animation speed, which will determine how long each frame of the animation is displayed. In the source code, you will see that the running animation plays faster than the idle animation, which is accomplished through the use of the m_secondsPerFrame variable.

namespace CUnit
{
///
/// Our Crono sprite. Simply has a state so we can set an idle or running animation.
///

public class Crono : SpriteAnimated
{
public enum State{ Idle, Running };
private State m_state;

///

Constructor
/// Height of one cell
/// Width of one cell
/// Time to display a frame
/// Number of columns in the texture
/// Cell to start animation
/// Cell to end animation
public Crono( int cellHeight, int cellWidth, float secondsPerFrame, int numColumns, int startCell, int endCell ) Toggle
{
m_cellHeight = cellHeight;
m_cellWidth = cellWidth;
m_numColumns = numColumns;
m_secondsPerFrame = secondsPerFrame;
m_currentFrame = startCell;
m_startCell = startCell;
m_endCell = endCell;
m_state = State.Idle;

}

///

Gets and sets Crono’s state
public State AnimationState Toggle
{
get { return m_state; }
set { m_state = value; }

}

}

}

The Crono class is just an example of how to extend the SpriteAnimated class. Since Crono will have 2 sets of animations, we need a way to remember his current animation state in order to play the corresponding animation. This is accomplished with the State enum.

namespace CUnit
{
/// Flying cat of death.
public class Cat : SpriteBase
{
private Vector3 m_velocity;
private Vector3 m_startPosition;
private float m_distance = 0.0f;

///

Updates the position with the velocity.
/// Time since last frame
public void Update( float elapsedTime ) Toggle
{
Position += m_velocity * elapsedTime;
m_distance = (Position – m_startPosition).Length();

}

///

Gets and sets the velocity
public Vector3 Velocity Toggle
{
get { return m_velocity; }
set { m_velocity = value; }

}

///

Gets the distance traveled from the starting position.
public float Distance Toggle
{
get { return m_distance; }

}

///

Gets and sets the starting position.
public Vector3 StartPosition Toggle
{
get { return m_startPosition; }
set { m_startPosition = value; }

}

}

}

The Flying Cat of Death class is another example of how to extend from the Sprite classes. Since Crono will be firing cats with his cool powers, the cat sprites need a velocity vector so we can update_ their positions. The distance variable is used to destroy the cat after it has flown a certain distance.

/// Renders the current frame.
/// The Direct3D device
/// Time elapsed since last frame
public override void OnRenderFrame( Device device, float elapsedTime )
{
device.Clear( ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0f, 0 );
device.BeginScene();

// Clip…

// Render cats and Crono
m_sprite.Begin( SpriteFlags.AlphaBlend );
foreach ( Cat c in m_cats )
{

c.Draw( m_sprite, m_texCat, new Rectangle( 0,0, 32, 32), Color.White );

}
m_crono.Draw( m_sprite, m_tex );
m_sprite.End();

// Clip…

device.EndScene();
device.Present();

}

Rendering a Sprite takes place between a Sprite.Begin call and a Sprite.End call. When we call Sprite.Begin, there are various SpriteFlags that we can specify. The flag that I specify, SpriteFlags.AlphaBlend, renders a texture using the texture’s alpha channel. If you look at the texture above, you’ll notice that it is on a big white rectangle. If we didn’t specify the AlphaBlend flag, we would see Crono enclosed in this ugly white rectangle. Since I set the alpha value of the background to 0 in Photoshop and I set the AlphaBlend flag, the white background is not rendered. If you want to use alpha channels in your textures, you have to use a format that supports them like .tga, .bmp, or .dds.

2D graphics are a quick and easy way to create_ fun programs like Crono throwing cats around the screen…

Crazy Crono