CRM64Pro Tutorial 04

Custom cursor.

Add a cursor and its image to the Tutorial 02 CDC archive, load it by name and use it as the application pointer.

  • Beginner
  • CursorMgr
  • Image ownership
  • CDC resources

Overview

This tutorial extends Tutorial 02 with a custom mouse cursor. Tutorial 02 creates Tutorial.cdc with the background and ship images. Tutorial 04 opens that archive, adds a cursor and its owned image, then loads every runtime resource from the CDC.

The render callback, fixed-rate movement, debug window and console remain unchanged so the new code stays focused on Cursor and CursorMgr.

Final result

CRM64Pro Tutorial 04 final result
Runtime controls

Cursor keys move the ship, 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 Tutorial 02 first to create Tutorial.cdc with Tutorial_bg and Tutorial_ship.
  • Included assets available under bin/Base/: tutorial_cursor.png.
  • Write access to the platform output directory.

What you will learn

  • How to add a cursor resource to an existing CDC archive.
  • How to create a Cursor and assign an Image to it.
  • How to transfer image ownership to a cursor.
  • How to load cursor and image resources from a CDC archive.
  • How to set the hotspot, select the cursor and make it visible.

Step by step

Step 1: Verify the Tutorial 02 archive

Tutorial 04 adds a resource instead of creating a new archive. Checking the prerequisite first prevents an incomplete cursor-only CDC from being created when Tutorial 02 has not been run.

// Tutorial 04 introduces one new resource. Tutorial 02 already created
// the CDC with the background and ship.
mLog.msg(LL_INFO, "  Find Tutorial 02 CDC: %s ... ", OUTPUT_CDC);
if(!mC64.tool().fileExists(OUTPUT_CDC))
{
    logTaskFailed(mLog);
    return false;
}
logTaskOk(mLog);

Step 2: Load the cursor image

The cursor starts as a normal PNG loaded through ImageMgr. Both the returned ID and borrowed image pointer are checked before creating the cursor.

mLog.msg(LL_INFO, "  Load cursor image: %s ... ", RESOURCE_CURSOR_IMAGE);
Sint32 idCursorImage = mC64.imageMgr().loadFromFile(RESOURCE_CURSOR_IMAGE, "tutorialCursorImageFile");
Image* pCursorImage = mC64.imageMgr().get(idCursorImage);
if(idCursorImage < 0 || pCursorImage == nullptr)
{
    logTaskFailed(mLog, idCursorImage);
    return false;
}
logTaskOk(mLog);

Step 3: Create the cursor and assign its image

CursorMgr::create() returns an empty cursor. Passing 1 to assignImage() transfers ownership of the loaded image to that cursor. Closing the cursor will therefore also close its owned image.

mLog.msg(LL_INFO, "  Create cursor from image ... ");
Sint32 idCursorFile = mC64.cursorMgr().create(RESOURCE_CURSOR_CDC_NAME);
Cursor* pCursorFile = mC64.cursorMgr().get(idCursorFile);
if(idCursorFile < 0 || pCursorFile == nullptr)
{
    logTaskFailed(mLog, idCursorFile);
    mC64.imageMgr().close(idCursorImage);
    return false;
}
if(pCursorFile->assignImage(idCursorImage, 1) < 0)
{
    logTaskFailed(mLog);
    mC64.cursorMgr().close(idCursorFile);
    mC64.imageMgr().close(idCursorImage);
    return false;
}
logTaskOk(mLog);

Step 4: Save the cursor to the CDC

Cursor::save() stores both the cursor block and its associated image block. An existing resource with the same name is replaced, so the tutorial can be run again against the same Tutorial 02 archive.

// Save appends or replaces the cursor resource in Tutorial.cdc.
mLog.msg(LL_INFO, "  Save cursor to CDC ... ");
if(pCursorFile->save(OUTPUT_CDC, RESOURCE_CURSOR_CDC_NAME) < 0)
{
    logTaskFailed(mLog);
    mC64.cursorMgr().close(idCursorFile);
    return false;
}
logTaskOk(mLog);

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

Step 5: Load the cursor from the CDC

The background and ship keep the names created by Tutorial 02. CursorMgr loads the new cursor by its resource name and also restores its associated image.

mLog.msg(LL_INFO, "  Load cursor from CDC ... ");
state.idCursor = mC64.cursorMgr().load(OUTPUT_CDC, RESOURCE_CURSOR_CDC_NAME);
Cursor* pCursor = mC64.cursorMgr().get(state.idCursor);
if(state.idCursor < 0 || pCursor == nullptr)
{
    logTaskFailed(mLog, state.idCursor);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);

Step 6: Configure and show the custom cursor

PH_CENTER places the cursor hotspot at the center of its image. CursorMgr then builds and selects the cursor before making it visible. Each operation is checked so setup stops cleanly if any step fails.

// Center hotspot is a good default for a game-style pointer.
mLog.msg(LL_INFO, "  Set cursor hotspot and select cursor ... ");
if(!pCursor->setHotSpot(PH_CENTER, PH_CENTER) ||
   mC64.cursorMgr().select(state.idCursor) < 0 ||
   !mC64.cursorMgr().show())
{
    logTaskFailed(mLog);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);

Step 7: Let CursorMgr draw the pointer

The render callback still draws only the background and ship. CursorMgr draws the selected mouse cursor automatically, so no cursor rendering code is added to renderFrame().

// Draw the current frame. CursorMgr draws the selected cursor automatically.
static void renderFrame(Image* pBgImage, Image* pPlayerImage, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();

    if(pPlayerImage)
    {
        SDL_FRect rDst = {
            rState.fPlayerX,
            rState.fPlayerY,
            static_cast<float>(pPlayerImage->getWidth()),
            static_cast<float>(pPlayerImage->getHeight())
        };
        pPlayerImage->render(0, nullptr, &rDst);
    }
}

Step 8: Update logic and release resources

A quit request exits before the next fixed logic update. Cleanup closes cursors before images because each custom cursor may own an image.

if(!bRunning) break;
state.fMouseX = mC64.cursorMgr().getX();
state.fMouseY = mC64.cursorMgr().getY();
updateLogic(state, pPlayerImage, 1.0f / static_cast<float>(iLogicRate));

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.

Previous tutorial

Save and reload screen configuration.

Go to Tutorial 03: Configuration

Tutorial index

Back to tutorials

Next tutorial

Create bitmap fonts and explore built-in fonts.

Go to Tutorial 05: Font