CRM64Pro Tutorial 07

Tileset maps.

Create a tileset resource, render a tile map, and move the ship only through walkable cells.

  • Beginner
  • TileMgr
  • Tileset
  • Grid movement

Overview

This tutorial extends Tutorial 06 with a tileset resource. It converts Base/tiles.bmp into a Tile, stores it in Tutorial.cdc, and renders a 12×8 map.

The ship moves between grid cells only when the destination tile is walkable. Moon cells demonstrate rendering two transparent tile layers in one location.

Final result

CRM64Pro Tutorial 07 tileset map
Runtime controls

Cursor keys move the ship to the next walkable cell, 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.cdc contains the background, ship and cursor.
  • Included assets available under bin/Base/: tiles.bmp.
  • Write access to the platform output directory.

What you will learn

  • Create a Tile from an Image and transfer ownership.
  • Configure a color key before saving.
  • Split an image into 48×48 tiles with Tile::setOffset().
  • Save and load a tile resource through CDC.
  • Render a map from tile IDs.
  • Restrict movement to walkable cells.

Step by step

Step 1: Verify the shared archive

Tile::save() can create an archive, but this tutorial must extend the earlier resource chain. The check prevents an incomplete tileset-only archive.

// Tutorial 07 introduces one new resource. Earlier tutorials already added
// the background, ship 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 tileset resource

The source image uses magenta as transparency. assignImage() transfers ownership to the tile, and setOffset() divides the image into 48×48 regions starting at ID 1.

mLog.msg(LL_INFO, "  Load tileset image: %s ... ", RESOURCE_TILE_IMAGE);
Sint32 idTileImage = mC64.imageMgr().loadFromFile(RESOURCE_TILE_IMAGE, "tutorialTileImageFile");
Image* pTileImage = mC64.imageMgr().get(idTileImage);
if(idTileImage < 0 || pTileImage == nullptr)
{
    logTaskFailed(mLog, idTileImage);
    return false;
}
pTileImage->setColorKey(255, 0, 255);
logTaskOk(mLog);

mLog.msg(LL_INFO, "  Create tile from image ... ");
Sint32 idTileFile = mC64.tileMgr().create(RESOURCE_TILE_CDC_NAME);
Tile* pTileFile = mC64.tileMgr().get(idTileFile);
if(idTileFile < 0 || pTileFile == nullptr)
{
    logTaskFailed(mLog, idTileFile);
    mC64.imageMgr().close(idTileImage);
    return false;
}
if(pTileFile->assignImage(idTileImage, 1) < 0)
{
    logTaskFailed(mLog);
    mC64.tileMgr().close(idTileFile);
    mC64.imageMgr().close(idTileImage);
    return false;
}
if(!pTileFile->setOffset(1, iTileSize, iTileSize))
{
    logTaskFailed(mLog);
    mC64.tileMgr().close(idTileFile);
    return false;
}
logTaskOk(mLog);

Step 3: Save the tileset

Tile::save() writes the tile and its owned image. Closing the temporary tile releases both before loading a fresh runtime copy.

// Save appends or replaces the tileset resource in Tutorial.cdc.
mLog.msg(LL_INFO, "  Save tileset to CDC ... ");
if(pTileFile->save(OUTPUT_CDC, RESOURCE_TILE_CDC_NAME) < 0)
{
    logTaskFailed(mLog);
    mC64.tileMgr().close(idTileFile);
    return false;
}
logTaskOk(mLog);

// Close the temporary tile. The tutorial loads a fresh copy from CDC next.
mLog.msg(LL_INFO, "  Close temporary tile ... ");
mC64.tileMgr().close(idTileFile);
logTaskOk(mLog);

Step 4: Load the tileset from CDC

TileMgr loads the resource by its stable name and validates it before rendering begins.

mLog.msg(LL_INFO, "  Load tileset from CDC ... ");
state.idTileSet = mC64.tileMgr().load(OUTPUT_CDC, RESOURCE_TILE_CDC_NAME);
Tile* pTileSet = mC64.tileMgr().get(state.idTileSet);
if(state.idTileSet < 0 || pTileSet == nullptr)
{
    logTaskFailed(mLog, state.idTileSet);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);

Step 5: Define walkable cells

Only stars and moon tile IDs are walkable. Coordinates are checked before the fixed map array is accessed.

// Check whether a tile can be walked on.
static bool isWalkableTile(Uint8 iTile)
{
    return iTile == iTileStars || iTile == iTileMoon;
}

// Check whether the map cell can be walked on.
static bool isWalkableCell(Sint32 iCellX, Sint32 iCellY)
{
    if(iCellX < 0 || iCellX >= iMapW) return false;
    if(iCellY < 0 || iCellY >= iMapH) return false;
    return isWalkableTile(iMap[iCellY][iCellX]);
}

Step 6: Render the map

Each map value selects a tile region. Moon cells draw the stars tile first, then the transparent moon tile over it.

// Draw the tile map.
static void renderMap(Tile* pTileSet)
{
    if(!pTileSet) return;

    for(Sint32 iY = 0; iY < iMapH; ++iY)
    {
        for(Sint32 iX = 0; iX < iMapW; ++iX)
        {
            pTileSet->setPosition(static_cast<float>(iMapX + iX * iTileSize), static_cast<float>(iMapY + iY * iTileSize));
            if(iMap[iY][iX] == iTileMoon)
            {
                pTileSet->render(iTileStars);
            }
            pTileSet->render(iMap[iY][iX]);
        }
    }
}

Step 7: Move between map cells

Movement rejects blocked destinations, then centers the ship image in an accepted cell.

// Move the player when the destination cell is walkable.
static void tryMovePlayer(TutorialState& rState, Image* pPlayerImage, Sint32 iDX, Sint32 iDY, Log& mLog)
{
    const Sint32 iNextX = rState.iPlayerCellX + iDX;
    const Sint32 iNextY = rState.iPlayerCellY + iDY;

    if(!isWalkableCell(iNextX, iNextY))
    {
        mLog.msg(LL_INFO, "  Movement blocked at cell %d, %d\n", iNextX, iNextY);
        return;
    }

    rState.iPlayerCellX = iNextX;
    rState.iPlayerCellY = iNextY;
    placePlayerOnCell(rState, pPlayerImage);
    mLog.msg(LL_INFO, "  Ship moved to cell %d, %d\n", rState.iPlayerCellX, rState.iPlayerCellY);
}
else if(event.key.key == SDLK_LEFT)
{
    tryMovePlayer(state, pPlayerImage, -1, 0, mLog);
}
else if(event.key.key == SDLK_RIGHT)
{
    tryMovePlayer(state, pPlayerImage, 1, 0, mLog);
}
else if(event.key.key == SDLK_UP)
{
    tryMovePlayer(state, pPlayerImage, 0, -1, mLog);
}
else if(event.key.key == SDLK_DOWN)
{
    tryMovePlayer(state, pPlayerImage, 0, 1, mLog);
}

Step 8: Select the cursor and finish the loop

Select the existing cursor, then exit before the final state update when quitting.

mLog.msg(LL_INFO, "  Select cursor ... ");
if(mC64.cursorMgr().select(state.idCursor) < 0 || !mC64.cursorMgr().show())
{
    logTaskFailed(mLog);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);
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);

Step 9: Release owned resources first

Tiles and cursors can own images, so they close before ImageMgr and the CDC archives.

mLog.msg(LL_INFO, "  Close tiles ... ");
mC64.tileMgr().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_07_Tileset.cpp
  • Asset: Base/tiles.bmp
  • Input/output archive: Tutorial.cdc
  • Tileset resource: Tutorial_tiles
  • Log: Tutorial_07_Tileset.log

Previous tutorial

Draw primitives and apply image filters.

Go to Tutorial 06: GFX

Tutorial index

Back to tutorials

Next tutorial

Create sprites and animation states.

Go to Tutorial 08: Sprite