Tutorial 03 - Slicers and animation

In the third tutorial, we'll look at how to use the Slicer, which is provided to assist automatic generation of sprites from spritesheets. We'll apply a basic, frame based animation to the resulting sprites and throw in a little mouse control.

#include <XRhodes/xrhodes.hpp> const int SCR_W = 640; const int SCR_H = 480; const Float UPDATE_DELAY = 1.0f / 60; const Uint UP_DIR = 2; // 5 for OS X debug struct SpriteObject { void Render(const XR::Sprite *pSpr, Float xScale, Float yScale) const { XR::Auto::PushMatrix __p; XR::Translate(position); ::glScalef(xScale, yScale, 1.0f); pSpr->Draw(); } XR::Vector2 position; }; bool ProcessEvents(SDL_Event &ev) { while(::SDL_PollEvent(&ev) != 0) { if(ev.type == SDL_QUIT) { return false; } } return true; } int main(int argc, char *argv[]) { // initialization XR::Core core(argv[0], UP_DIR, UPDATE_DELAY); XR::GFX::Init(SCR_W, SCR_H); ::glEnable(GL_BLEND); String texName(XR::pCore->GetPath() + "03_spritesheet.png"); GLuint hTexture(XR::Texture2D::Generate(texName)); if(hTexture == XR::INVALID_GL_ID) { XR::pCore->GetBuffer() << "Couldn't generate texture from " << texName << std::endl; exit(2); } XR::Slicer slicer; XR::Slicer::Unit unit; XR::Slicer::Unit::Cut cut; unit.widthPcnt = .25f; unit.heightPcnt = .25f; cut.left = .0f; cut.top = .0f; cut.xRepeats = 3; cut.yRepeats = 3; unit.Add(cut); slicer.Add(unit); XR::SpriteVector sprites; slicer.Process(hTexture, sprites); SpriteObject obj; int index(0); Float frameDelay(1.0f / 12); Float tFrame(frameDelay); bool leftPressed(false); bool rightPressed(false); // game loop SDL_Event ev; Float tDelta; Float tUpdate(.0f); Float tRender(.0f); Float tCumulative(.0f); XR::Clock::Query(); while(ProcessEvents(ev)) { tDelta = XR::Clock::Query(); tUpdate -= tDelta; if(tUpdate <= .0f) { XR::QueryKeys(); XR::QueryMouse(); leftPressed = XR::GetMouseState().buttons & SDL_BUTTON_LMASK; rightPressed = XR::GetMouseState().buttons & SDL_BUTTON_RMASK; obj.position.at[X] = XR::GetMouseState().position.x; obj.position.at[Y] = SCR_H - XR::GetMouseState().position.y; tFrame -= core.GetUpdateDelay(); if(tFrame <= .0f) { tFrame += frameDelay; ++index; if(index >= sprites.size()) { index = 0; } } tUpdate += core.GetUpdateDelay(); } tRender -= tDelta; if(tRender <= .0f) { XR::GFX::Clear(); Float scale(5.0f); Float xScale(leftPressed ? -scale: scale); Float yScale(rightPressed ? -scale: scale); obj.Render(sprites[index], xScale, yScale); XR::GFX::Present(); tRender += core.GetUpdateDelay(); } } std::for_each(sprites.begin(), sprites.end(), XR::PtrDeleter<XR::Sprite>()); XR::Texture2D::Delete(hTexture); return 0; }
Let's start with our SpriteObject class:
struct SpriteObject { void Render(const XR::Sprite *pSpr, Float xScale, Float yScale) const { XR::Auto::PushMatrix __p; XR::Translate(position); ::glScalef(xScale, yScale, 1.0f); pSpr->Draw(); } XR::Vector2 position; };
Stores a position, and renders given Sprite at given scaling along the x and y axes.
String texName(XR::pCore->GetPath() + "03_spritesheet.png"); GLuint hTexture(XR::Texture2D::Generate(texName)); if(hTexture == XR::INVALID_GL_ID) //...

We start off by loading a texture. This should be familiar from the second tutorial. You can use the following bitmap image:

fail

XR::Slicer slicer; XR::Slicer::Unit unit; XR::Slicer::Unit::Cut cut;

We then declare our Slicer. Slicer is used to Cut adjacent Units from a texture and store them in a XR::SpriteVector. The Slicer::Unit struct defines the size of the sprites to be cut, as a percentage of the source texture (refer to the OpenGL documentation or the second tutorial to see why). Slicer::Unit::Cuts define the position the cut is made (top left texture coordinate of the first sprite to be cut), and the number of times the Cut should be repeated to the right and down.

This means that if you want only one cut, then set Cut's xRepeats and yRepeats members to 0. If you don't want a cut to be made, don't define one.;)

unit.widthPcnt = .25f; unit.heightPcnt = .25f; cut.left = .0f; cut.top = .0f; cut.xRepeats = 3; cut.yRepeats = 3; unit.Add(cut); slicer.Add(unit);

Here we set the parameters of the Unit and the Cut.

As you see, you'll add your Cut to the corresponding unit: multiple cuts of the same unit can be made. Then, you'll add the Unit to the Slicer you want to carry out the slicing.

XR::SpriteVector sprites; slicer.Process(hTexture, sprites);

SpriteVector is just a typedef for std::vector<Sprite*>. This is where we'll have the Slicer put our Sprites. Note that Slicer is not responsible for these Sprites and won't keep track of them. Cleaning them up when we're finished is our job.

SpriteObject obj; int index(0); Float frameDelay(1.0f / 12); Float tFrame(frameDelay); bool leftPressed(false); bool rightPressed(false);

Variables for our object, its animation and the mouse control. We'll see these in details shortly.

XR::QueryMouse(); leftPressed = XR::GetMouseState().buttons & SDL_BUTTON_LMASK; rightPressed = XR::GetMouseState().buttons & SDL_BUTTON_RMASK; obj.position.at[X] = XR::GetMouseState().position.x; obj.position.at[Y] = SCR_H - XR::GetMouseState().position.y;

The update code. We'll query the mouse first (otherwise the information returned by XR::GetMouseState() will be invalid), then extract the buttons' state info from it.

XRhodes uses SDL for input, and the way we find out about the state of the buttons is applying bitmasks to them with bitwise AND.

The next peculiarity is converting the mouse position to object position. In OpenGL's coordinate system, y goes up and 0 is the bottom of the screen (before transformations). SDL's y goes down and 0 is the top of the screen. We therefore take SCR_H and deduct the y mouse coordinate from it so our object will render where it should.

tFrame -= core.GetUpdateDelay(); if(tFrame <= .0f) { tFrame += frameDelay; ++index; if(index >= sprites.size()) { index = 0; } }

Animation code. tFrame is the timer we use for advancing the frames of the animation. As it is updated at every update, we decrement it by the update delay. When it reached .0, we increment the frame index and add the frame delay to the frame timer. If the frame index went over the number of sprites, we set it back to 0. This is a very simplified approach but it works for now.

Float scale(5.0f); Float xScale(leftPressed ? -scale: scale); Float yScale(rightPressed ? -scale: scale); obj.Render(sprites[index], xScale, yScale);

We apply an arbitrary scaling to our sprite. If the left mouse was pressed we'll flip the sprite along the x axis. If the right mouse was pressed, we'll flip the sprite along the y axis.

When we have all the information we need, we render the SpriteObject.

std::for_each(sprites.begin(), sprites.end(), XR::PtrDeleter<XR::Sprite>()); sprites.clear(); XR::Texture2D::Delete(hTexture);

XR::PtrDeleter<> is a functor template that deletes and sets pointers of a given type, in a sequence, to 0. With std::for_each() we pass it everything in sprites.

Exercises:

  1. Modify the Slicer's definition doubling unit.widthPcnt. Observe the result. Consult stdout.txt (located in your executable's folder).
  2. Modify the Slicer definition so that the Sprites are created in a top to bottom, left to right order.
  3. Modify the animation so that it plays backward when the left mouse button is held.
  4. Study the documentation of Slicer and modify the code so that it reads the definition from an XRSF file.

Back to Resources.