CRM64Pro Tutorial 08
Animated sprites.
Create directional animations from a sprite sheet, clone sprites, and render independent visual variants.
Overview
This tutorial extends Tutorial 07 with an animated Sprite resource. It turns Base/sprite2.png into four directional animations and stores the configured sprite in the shared Tutorial.cdc archive.
The main sprite follows the cursor keys. Three child sprites move automatically with different animation speeds, scaling, color modulation and alpha.
Final result
Cursor keys move the main sprite, I toggles interpolated positioning, D toggles the debug window, grave (`) toggles the console, any key or mouse click is logged, and Q or ESC exits.
Prerequisites
- CRM64Pro GDK installed and configured with a supported C++17 compiler.
- Tutorial package downloaded and fully extracted, preserving its folder structure.
- Run Tutorials 02 and 04 so
Tutorial.cdccontains the background and cursor. - Included assets available under
bin/Base/:sprite2.png. - Write access to the platform output directory.
What you will learn
- Create a
Spritefrom a sprite-sheetImage. - Split the sheet with
Sprite::setOffset(). - Map animation rows to directional states.
- Create child sprites that reuse parent animation data.
- Change animation speed, scale, color and alpha independently.
- Toggle interpolated sprite positioning.
Step by step
Step 1: Verify the shared archive
The sprite must extend the archive created by the earlier tutorials. Checking it first prevents an incomplete sprite-only archive.
// Tutorial 08 introduces one new resource. Earlier tutorials already added
// the background and cursor to the CDC.
mLog.msg(LL_INFO, " Find Tutorial 04 CDC: %s ... ", OUTPUT_CDC);
if(!mC64.tool().fileExists(OUTPUT_CDC))
{
logTaskFailed(mLog);
return false;
}
logTaskOk(mLog);
Step 2: Create the sprite resource
Load the sheet, create an empty Sprite, and transfer ownership of the Image to it.
mLog.msg(LL_INFO, " Load sprite sheet: %s ... ", RESOURCE_SPRITE_IMAGE);
Sint32 idSpriteImage = mC64.imageMgr().loadFromFile(RESOURCE_SPRITE_IMAGE, "tutorialSpriteImageFile");
Image* pSpriteImage = mC64.imageMgr().get(idSpriteImage);
if(idSpriteImage < 0 || pSpriteImage == nullptr)
{
logTaskFailed(mLog, idSpriteImage);
return false;
}
logTaskOk(mLog);
mLog.msg(LL_INFO, " Create sprite from image ... ");
Sint32 idSpriteFile = mC64.spriteMgr().create(RESOURCE_SPRITE_CDC_NAME);
Sprite* pSpriteFile = mC64.spriteMgr().get(idSpriteFile);
if(idSpriteFile < 0 || pSpriteFile == nullptr)
{
logTaskFailed(mLog, idSpriteFile);
mC64.imageMgr().close(idSpriteImage);
return false;
}
if(pSpriteFile->assignImage(idSpriteImage, 1) < 0)
{
logTaskFailed(mLog);
mC64.spriteMgr().close(idSpriteFile);
mC64.imageMgr().close(idSpriteImage);
return false;
}
Step 3: Define animations and frames
Each 32×32 row becomes an animation. The rows map to the four directional states, all frames use 75 milliseconds, and every animation has a centered hotspot.
// Configure the sprite animation and behaviour.
static bool configureSprite(Sprite* pSprite)
{
if(!pSprite || !pSprite->setOffset(iSpriteW, iSpriteH)) return false;
if(!pSprite->setAnimStateIndex(0, SPR_STATE_UP) ||
!pSprite->setAnimStateIndex(1, SPR_STATE_LEFT) ||
!pSprite->setAnimStateIndex(2, SPR_STATE_DOWN) ||
!pSprite->setAnimStateIndex(3, SPR_STATE_RIGHT)) return false;
for(Sint32 iAnim = 0; iAnim < 4; ++iAnim)
{
if(!pSprite->setFrameTimeIndex(iAnim, -1, 75) ||
!pSprite->setAnimHotSpotIndex(iAnim, PH_CENTER, PH_CENTER)) return false;
}
return pSprite->selectAnim(SPR_STATE_DOWN);
}
if(!configureSprite(pSpriteFile))
{
logTaskFailed(mLog);
mC64.spriteMgr().close(idSpriteFile);
return false;
}
logTaskOk(mLog);
Step 4: Save and reload the sprite
Save the configured sprite by resource name, close the temporary object, and load a fresh runtime instance from the archive.
// Save appends or replaces the sprite resource in Tutorial.cdc.
mLog.msg(LL_INFO, " Save sprite to CDC ... ");
if(pSpriteFile->save(OUTPUT_CDC, RESOURCE_SPRITE_CDC_NAME) < 0)
{
logTaskFailed(mLog);
mC64.spriteMgr().close(idSpriteFile);
return false;
}
logTaskOk(mLog);
// Close the temporary sprite. The tutorial loads a fresh copy from CDC next.
mLog.msg(LL_INFO, " Close temporary sprite ... ");
mC64.spriteMgr().close(idSpriteFile);
logTaskOk(mLog);
mLog.msg(LL_INFO, " Load main sprite from CDC ... ");
state.idMainSprite = mC64.spriteMgr().load(OUTPUT_CDC, RESOURCE_SPRITE_CDC_NAME);
Sprite* pMainSprite = mC64.spriteMgr().get(state.idMainSprite);
if(state.idMainSprite < 0 || pMainSprite == nullptr)
{
logTaskFailed(mLog, state.idMainSprite);
Main::terminate();
return -1;
}
pMainSprite->setPosition(state.fMainX, state.fMainY);
logTaskOk(mLog);
Step 5: Create visual variants
SpriteMgr::child() reuses the parent’s animation data and creates a child image with independent rendering attributes.
if(!createSpriteClone(mC64, mLog, state.idMainSprite, state.idScaledSprite, "scaled") ||
!createSpriteClone(mC64, mLog, state.idMainSprite, state.idTintedSprite, "color mod") ||
!createSpriteClone(mC64, mLog, state.idMainSprite, state.idGhostSprite, "alpha mod"))
{
Main::terminate();
return -1;
}
Sprite* pScaledSprite = mC64.spriteMgr().get(state.idScaledSprite);
Sprite* pTintedSprite = mC64.spriteMgr().get(state.idTintedSprite);
Sprite* pGhostSprite = mC64.spriteMgr().get(state.idGhostSprite);
if(pScaledSprite)
{
pScaledSprite->setPosition(state.fScaledX, state.fScaledY);
pScaledSprite->setSpeed(0.8f);
}
if(pTintedSprite)
{
pTintedSprite->setPosition(state.fTintedX, state.fTintedY);
pTintedSprite->setSpeed(1.2f);
Image* pTintedImage = mC64.imageMgr().get(pTintedSprite->getImage());
if(pTintedImage) pTintedImage->setColorMod(255, 120, 120);
}
if(pGhostSprite)
{
pGhostSprite->setPosition(state.fGhostX, state.fGhostY);
pGhostSprite->setSpeed(1.6f);
Image* pGhostImage = mC64.imageMgr().get(pGhostSprite->getImage());
if(pGhostImage) pGhostImage->setAlphaMod(150);
}
Step 6: Select direction animations
Movement selects the horizontal or vertical animation with the largest change. A stationary sprite keeps its current animation.
// Choose the sprite animation for its movement.
static void selectSpriteAnim(Sprite* pSprite, float fDX, float fDY)
{
if(!pSprite) return;
if(fDX == 0.0f && fDY == 0.0f) return;
if(fabsf(fDX) > fabsf(fDY))
{
pSprite->selectAnim(fDX < 0.0f ? SPR_STATE_LEFT : SPR_STATE_RIGHT);
}
else
{
pSprite->selectAnim(fDY < 0.0f ? SPR_STATE_UP : SPR_STATE_DOWN);
}
}
Step 7: Move with optional interpolation
The fixed logic step updates positions. Bounds account for centered hotspots, and the interpolation flag is passed directly to Sprite::setPosition().
const float fSpriteHalfW = static_cast<float>(iSpriteW) * 0.5f;
const float fSpriteHalfH = static_cast<float>(iSpriteH) * 0.5f;
rState.fMainX = clampFloat(rState.fMainX + fDX, fSpriteHalfW, static_cast<float>(iScreenW) - fSpriteHalfW);
rState.fMainY = clampFloat(rState.fMainY + fDY, fSpriteHalfH, static_cast<float>(iScreenH) - fSpriteHalfH);
if(pMainSprite)
{
pMainSprite->setPosition(rState.fMainX, rState.fMainY, rState.iSmoothInterpolation != 0);
selectSpriteAnim(pMainSprite, fDX, fDY);
}
moveAutoSprite(mC64.spriteMgr().get(rState.idScaledSprite), rState.fScaledX, rState.fScaledY, rState.fScaledVX, rState.fScaledVY, fDeltaTime, fSpriteHalfW * fScaledSpriteScale, fSpriteHalfH * fScaledSpriteScale, rState.iSmoothInterpolation != 0);
moveAutoSprite(mC64.spriteMgr().get(rState.idTintedSprite), rState.fTintedX, rState.fTintedY, rState.fTintedVX, rState.fTintedVY, fDeltaTime, fSpriteHalfW, fSpriteHalfH, rState.iSmoothInterpolation != 0);
moveAutoSprite(mC64.spriteMgr().get(rState.idGhostSprite), rState.fGhostX, rState.fGhostY, rState.fGhostVX, rState.fGhostVY, fDeltaTime, fSpriteHalfW, fSpriteHalfH, rState.iSmoothInterpolation != 0);
Step 8: Render the sprites
The scaled child uses Sprite::renderEx(); the remaining sprites use their own image modulation through Sprite::render().
Sprite* pScaledSprite = mC64.spriteMgr().get(rState.idScaledSprite);
Sprite* pTintedSprite = mC64.spriteMgr().get(rState.idTintedSprite);
Sprite* pGhostSprite = mC64.spriteMgr().get(rState.idGhostSprite);
Sprite* pMainSprite = mC64.spriteMgr().get(rState.idMainSprite);
if(pScaledSprite) pScaledSprite->renderEx(fScaledSpriteScale, fScaledSpriteScale);
if(pTintedSprite) pTintedSprite->render();
if(pGhostSprite) pGhostSprite->render();
if(pMainSprite) pMainSprite->render();
Step 9: Handle movement and interpolation input
Key-down and key-up events maintain movement flags. Pressing I toggles whether future positions are interpolated.
else if(event.key.key == SDLK_LEFT)
{
state.bMoveLeft = true;
}
else if(event.key.key == SDLK_RIGHT)
{
state.bMoveRight = true;
}
else if(event.key.key == SDLK_UP)
{
state.bMoveUp = true;
}
else if(event.key.key == SDLK_DOWN)
{
state.bMoveDown = true;
}
else if(event.key.key == SDLK_I)
{
state.iSmoothInterpolation = state.iSmoothInterpolation ? 0 : 1;
mLog.msg(LL_INFO, " Smooth interpolation: %s\n", state.iSmoothInterpolation ? "enabled" : "disabled");
}
else if(event.type == SDL_EVENT_KEY_UP)
{
if(event.key.key == SDLK_LEFT) state.bMoveLeft = false;
else if(event.key.key == SDLK_RIGHT) state.bMoveRight = false;
else if(event.key.key == SDLK_UP) state.bMoveUp = false;
else if(event.key.key == SDLK_DOWN) state.bMoveDown = false;
}
Step 10: Finish the loop cleanly
A quit request exits before another state update. The fixed logic delta comes from the configured logic rate.
if(!bRunning) break;
state.fMouseX = mC64.cursorMgr().getX();
state.fMouseY = mC64.cursorMgr().getY();
state.iCurrentRenderRate = static_cast<Sint32>(mC64.timer().getCurrentRFR());
state.iCurrentLogicRate = static_cast<Sint32>(mC64.timer().getCurrentLFR());
updateLogic(state, 1.0f / static_cast<float>(iLogicRate));
Step 11: Release dependent resources first
Sprites and cursors can own images, so they close before ImageMgr and the CDC archives.
mLog.msg(LL_INFO, " Close sprites ... ");
mC64.spriteMgr().close(0);
logTaskOk(mLog);
mLog.msg(LL_INFO, " Close cursors ... ");
mC64.cursorMgr().close(0);
logTaskOk(mLog);
mLog.msg(LL_INFO, " Close images ... ");
mC64.imageMgr().close(0);
logTaskOk(mLog);
mLog.msg(LL_INFO, " Close CDC archives ... ");
mC64.archiveMgr().close(0);
logTaskOk(mLog);
Main::terminate();
Complete source
Use the source file as the authoritative version of this tutorial.
- View Tutorial_08_Sprite.cpp
- Asset:
Base/sprite2.png - Input/output archive:
Tutorial.cdc - Sprite resource:
Tutorial_sprite - Log:
Tutorial_08_Sprite.log
Tutorial index
