CRM64Pro GDK v0.80.0
A free cross-platform game development kit built on top of SDL 3.0
Loading...
Searching...
No Matches
Tutorial 16.3: Scene Platform

Overview

This tutorial loads a forest side-scroller, creates a platform player from TMX object data, applies acceleration, gravity and jumping, and resolves movement against a tile layer. A dead-zone camera follows the player, while a Scene trigger opens a house prompt.

Final result

CRM64Pro Tutorial 16.3 Scene Platform

Runtime controls

  • Left / Right: Accelerate and move the player.
  • Space: Jump while grounded.
  • S: Enable or disable smooth scrolling.
  • P / R: Pause or resume the Scene.
  • D: Show or hide object bounds.
  • F1: Show or hide trigger zones.
  • F2: Show or hide the tile grid.
  • F3: Show or hide the camera dead zone.
  • Q or ESC: Exit the tutorial.

Object bounds and trigger zones use Scene::setDebugObjectOverlay() with ::SDOT_STANDARD. The tile grid and camera dead zone remain independent Scene and layer overlays configured through Scene::setDebugOverlay().

Prerequisites

  • CRM64Pro GDK installed and configured with a supported C++17 compiler.
  • Tutorial package downloaded and fully extracted, preserving its folder structure.
  • Complete the earlier Scene tutorials or understand TMX layers and SceneObject callbacks.
  • Keep bin/Base/tiled/forest/map.tmx and its referenced resources available.
  • Keep the Tutorial_bg resource from Tutorial 02 in Tutorial.cdc.

What you will learn

  • Load a fixed-layout TMX Scene through an object factory.
  • Implement acceleration, gravity and grounded jumping.
  • Sweep an AABB against a Scene tile layer.
  • Define which tile cells are solid.
  • Configure a clamped dead-zone camera.
  • Attach a delayed trigger to a Scene object.
  • Pause the Scene around a modal interaction.

Walkthrough

1. Keep runtime state together

The player update and trigger callbacks share the required Scene objects, velocity, grounded state and UI toggles.

struct TutorialState
{
Scene* pScene = nullptr;
Sint32 idBgImage = -1;
bool bJumpQueued = false;
SceneObject* pPlayer = nullptr;
SceneLayerTile* pGroundLayer = nullptr;
float fVelocityX = 0.0f;
float fVelocityY = 0.0f;
bool bOnGround = false;
bool bSmooth = true;
bool bDebugAABB = false;
bool bDebugTriggers = false;
bool bDebugTileGrid = false;
bool bDebugCameraDeadZone = false;
bool bHousePromptLock = false;
bool bHouseTriggerInside = false;
};

2. Create the platform player from TMX

The factory creates PlatformPlayer for TMX objects typed hero or player. The object's map dimensions are kept unless they are missing.

class TutorialObjectFactory : public ISceneObjectFactory
{
public:
SceneObject* create(const string& rType) override
{
if(rType == "hero" || rType == "player") return new(std::nothrow) PlatformPlayer();
return nullptr;
}
};
void initialize() override
{
if(getWidth() <= 0.0f || getHeight() <= 0.0f) setSize(96.0f, 128.0f);
}

3. Accelerate toward the requested speed

The input axis selects a target velocity. Ground and air use different acceleration, so direction changes feel responsive without removing air control.

const bool bLeft = mC64.getKeyState(SDLK_LEFT);
const bool bRight = mC64.getKeyState(SDLK_RIGHT);
const Sint32 iMoveAxis = (bRight ? 1 : 0) - (bLeft ? 1 : 0);
const float fDT = rContext.fDeltaTime;
const float fTargetVX = static_cast<float>(iMoveAxis) * fPlayerSpeed;
const float fAccel = g_pState->bOnGround ? fGroundAccel : fAirAccel;
g_pState->fVelocityX = approach(g_pState->fVelocityX, fTargetVX, fAccel * fDT);

4. Apply jumping and gravity

A queued jump is consumed only while grounded. Gravity is applied every logic tick and clamped to the configured maximum fall speed.

if(g_pState->bJumpQueued && g_pState->bOnGround)
{
g_pState->fVelocityY = fJumpVelocity;
g_pState->bOnGround = false;
}
g_pState->bJumpQueued = false;
g_pState->fVelocityY += fGravity * fDT;
g_pState->fVelocityY = clampFloat(g_pState->fVelocityY, -2000.0f, fMaxFallSpeed);

5. Sweep against the platform layer

Physics sweeps the player's AABB through the intended displacement and returns the resolved rectangle plus horizontal, vertical and ground contacts.

const float fWidth = getWidth();
const float fHeight = getHeight();
const SDL_FRect rectStart = { getX(), getY(), fWidth, fHeight };
SDL_FRect rectResolved = rectStart;
bool bHitX = false;
bool bHitY = false;
bool bGroundedSweep = false;
const bool bSweepOK = mC64.physics().sweepAABBOnLayerTile(
rectStart,
g_pState->fVelocityX * fDT,
g_pState->fVelocityY * fDT,
g_pState->pGroundLayer,
&rectResolved,
&bHitX,
&bHitY,
&bGroundedSweep,
&PlatformPlayer::solidTileForTutorial,
nullptr
);
if(!bSweepOK) return;
if(bHitX) g_pState->fVelocityX = 0.0f;
if(bHitY) g_pState->fVelocityY = 0.0f;
bool bGroundedProbe = false;
if(!bGroundedSweep && fabsf(g_pState->fVelocityY) <= 0.001f)
{
bGroundedProbe = mC64.physics().isGroundedOnLayerTile(
rectResolved,
g_pState->pGroundLayer,
fMaxStepDown,
&PlatformPlayer::solidTileForTutorial,
nullptr
);
}
g_pState->bOnGround = (bGroundedSweep || bGroundedProbe);
setPosition(rectResolved.x, rectResolved.y);

6. Decide which cells are solid

Empty cells are passable. The player may move above the map, but the horizontal and lower map boundaries remain solid.

static bool solidTileForTutorial(const SceneLayerTile* pTileLayer, Sint32 iCellX, Sint32 iCellY, Uint32 iCellValue, void*)
{
if(!pTileLayer) return false;
const Sint32 iMapW = pTileLayer->getWidth();
const Sint32 iMapH = pTileLayer->getHeight();
if(iCellX < 0 || iCellX >= iMapW) return true;
if(iCellY >= iMapH) return true;
if(iCellY < 0) return false;
return iCellValue != 0;
}

7. Validate the fixed layer roles

The tutorial expects the collision grid at layer 3 and gameplay objects at layer 5. It stops cleanly if the map no longer matches that contract.

if(g_pState->pScene->getLayerType(iGroundLayer) != SLT_TILE)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Layer %d must be a tile layer (platforms).\n", iGroundLayer);
closeTutorial();
return -1;
}
g_pState->pGroundLayer = g_pState->pScene->accessLayerTile(iGroundLayer);
if(!g_pState->pGroundLayer)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Missing required tile layer for platforms. ground=%d\n", iGroundLayer);
closeTutorial();
return -1;
}
if(g_pState->pGroundLayer->getCellWidth() <= 0 || g_pState->pGroundLayer->getCellHeight() <= 0)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Invalid tile size in layer %d.\n", iGroundLayer);
closeTutorial();
return -1;
}

8. Require the custom player

Finding an object by name is not sufficient: it must also be PlatformPlayer so its physics update will run.

SceneLayerObject* pPlayerLayer = g_pState->pScene->accessLayerObject(iPlayerLayer);
g_pState->pPlayer = findPlayerObject(pPlayerLayer);
SceneObject* pHouse = findObjectByType(pPlayerLayer, "house");
if(!g_pState->pPlayer || !pPlayerLayer)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Missing player object layer. player=%d\n", iPlayerLayer);
closeTutorial();
return -1;
}
if(dynamic_cast<PlatformPlayer*>(g_pState->pPlayer) == nullptr)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Player object must use type 'hero' or 'player' in TMX.\n");
closeTutorial();
return -1;
}

9. Add the house trigger

The trigger is local to the house, watches the player layer and emits STAY after the player remains inside for 500 milliseconds.

SceneObjectTrigger mHouseTrigger;
mHouseTrigger.sName = "house_entry";
mHouseTrigger.ShapeLocal.eType = SST_RECTANGLE;
mHouseTrigger.ShapeLocal.calculateAABB(fHouseCheckX, fHouseCheckY, fHouseCheckW, fHouseCheckH);
mHouseTrigger.iTargetLayer = iPlayerLayer;
mHouseTrigger.iStayDelayMs = 500;
mHouseTrigger.addIncludeType("player");
mHouseTrigger.addIncludeType("hero");
if(!pHouse->addTrigger(mHouseTrigger))
{
mC64.logMgr().get()->msg(LL_WARNING, "Failed to add house trigger zone.\n");
}
pHouse->setOnTriggerEvent(onHouseTrigger);

10. Configure the dead-zone camera

The camera starts near the player, follows its center with damping, remains inside map bounds and moves only when the target leaves the dead zone.

SceneCameraParams mCam;
// Dead-zone rectangle size (centered on screen anchor): camera moves only when target leaves this area.
mCam.rDeadZone = { 0.0f, 0.0f, fCameraDeadZoneX * 2.0f, fCameraDeadZoneY * 2.0f };
// Follow damping in 1/seconds. Higher values follow faster.
mCam.fDamping = 12.0f;
// Clamp camera to map/layer bounds.
mCam.bClampToBounds = true;
// Follow point on player: X and Y center
mCam.ptTargetAnchor = { 0.5f, 0.5f };
// Screen focus point where target anchor should appear: exact viewport center.
mCam.ptScreenAnchor = { 0.5f, 0.5f };
if(!g_pState->pScene->setCameraPosition(iGroundLayer, fStartCamX, fStartCamY, true) ||
!g_pState->pScene->setCameraTarget(iGroundLayer, g_pState->pPlayer) ||
!g_pState->pScene->setCameraParams(iGroundLayer, mCam) ||
!g_pState->pScene->setCameraMode(iGroundLayer, SCM_DEADZONE))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene camera.\n");
closeTutorial();
return -1;
}

11. Pause around the house prompt

STAY arms the prompt. Choosing No moves the player away; the lock prevents the dialog from reopening until the player exits the trigger.

if(ev.eType == STET_STAY)
{
g_pState->bHouseTriggerInside = true;
}
else if(ev.eType == STET_EXIT)
{
g_pState->bHouseTriggerInside = false;
g_pState->bHousePromptLock = false;
}
// Freeze engine/layers before opening a modal dialog.
g_pState->pScene->pause();
const bool bExit = (Main::instance().tool().messageBox("House Found", "House found, do you want to exit?", MBB_YES | MBB_NO, MBT_QUESTION) == MBB_YES);
if(!bExit)
{
g_pState->pPlayer->setPosition(g_pState->pPlayer->getX() - 24.0f, g_pState->pPlayer->getY());
}
g_pState->pScene->resume();
return bExit;

12. Update and release resources

Scene::update() runs player physics, trigger processing and camera logic. The shared cleanup function also handles partially completed setup.

g_pState->pScene->update();
if(handleHousePrompt()) bDone = true;
static void closeTutorial()
{
Main& mC64 = Main::instance();
mC64.sceneMgr().close(0);
mC64.imageMgr().close(0);
Main::terminate();
}

Complete source

The tutorial source is authoritative. The following listing is included directly from validation/source/Tutorial_16_Scene_Platform.cpp.

/* ----------------------------------------------------------------------
CRM64Pro GDK - Roberto Prieto
Copyright (C) 2013-2026 MegaStorm Systems
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
------------------------------------------------------------------------
Tutorial 16 Scene Platform
------------------------------------------------------------------------
Overview:
This tutorial loads a side-scroller map, moves a player with platform
physics, checks tile collisions, follows the player with a camera, and
reacts to a house trigger.
Key concepts:
- Load a TMX map with fixed layer roles.
- Create the player through a Scene object factory.
- Move a SceneObject with acceleration, gravity and jumping.
- Check tile collision against a platform layer.
- Use a Scene camera dead zone.
- Handle SceneObject trigger callbacks.
- Pause and resume the Scene around a modal message.
Layer layout:
- Loads: Base/tiled/forest/map.tmx
- Layer 1: Image background.
- Layer 2: Image background.
- Layer 3: Platform collision tile layer.
- Layer 4: Decorative tile layer.
- Layer 5: Object layer (player + house trigger).
Controls:
- Left/Right: Move player.
- Space: Jump.
- S: Toggle smooth scroll.
- P / R: Pause or resume the Scene.
- D: Toggle object AABB debug rendering.
- F1: Toggle trigger zone debug rendering.
- F2: Toggle tile grid debug rendering.
- F3: Toggle camera dead-zone debug rendering.
- Q / ESC: Quit.
Loop order:
- Poll events and capture input edges/toggles.
- Run one fixed logic tick via Scene::update().
- Let Scene update the camera.
- Show the house prompt after the player stays in the trigger.
- Render from callback using Scene::render().
------------------------------------------------------------------------ */
#include "CRM64Pro.h"
#include "RunParameter.h"
using namespace CRM64Pro;
// Paths used by the tutorial.
#define BASEDIR "Base/"
#ifdef CRM64PRO_PLATFORM_WINDOWS
#define OUTPUTDIR "Win64/"
#elif defined(CRM64PRO_PLATFORM_LINUX)
#define OUTPUTDIR "Linux/"
#elif defined(CRM64PRO_PLATFORM_MACOS)
#define OUTPUTDIR "macOS/"
#endif
#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"
static const char* szMapFile = BASEDIR"tiled/forest/map.tmx";
// Screen and viewport size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const Sint32 iViewW = 600;
static const Sint32 iViewH = 250;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = 0;
// Logic timing.
static const Sint32 iLogicRate = 20;
static const float fPlayerSpeed = 240.0f;
static const float fGroundAccel = 1800.0f;
static const float fAirAccel = 1000.0f;
static const float fGravity = 1400.0f;
static const float fJumpVelocity = -560.0f;
static const float fMaxFallSpeed = 1200.0f;
static const float fMaxStepDown = 8.0f;
static const float fCameraDeadZoneX = 20.0f;
static const float fCameraDeadZoneY = 20.0f;
static const float fHouseCheckX = 24.0f;
static const float fHouseCheckY = 132.0f;
static const float fHouseCheckW = 36.0f;
static const float fHouseCheckH = 33.0f;
static const Sint32 iGroundLayer = 3;
static const Sint32 iPlayerLayer = 5;
struct TutorialState
{
Scene* pScene = nullptr;
Sint32 idBgImage = -1;
bool bJumpQueued = false;
SceneObject* pPlayer = nullptr;
SceneLayerTile* pGroundLayer = nullptr;
float fVelocityX = 0.0f;
float fVelocityY = 0.0f;
bool bOnGround = false;
bool bSmooth = true;
bool bDebugAABB = false;
bool bDebugTriggers = false;
bool bDebugTileGrid = false;
bool bDebugCameraDeadZone = false;
bool bHousePromptLock = false;
bool bHouseTriggerInside = false;
};
static TutorialState* g_pState = nullptr;
// Keep a value inside the given range.
static float clampFloat(float fValue, float fMin, float fMax)
{
if(fValue < fMin) return fMin;
if(fValue > fMax) return fMax;
return fValue;
}
// Move a value toward its target by one step.
static float approach(float current, float target, float delta)
{
if(current < target) return (current + delta > target) ? target : (current + delta);
if(current > target) return (current - delta < target) ? target : (current - delta);
return current;
}
// Enable or disable smooth movement on all layers.
static bool setSmoothForAllLayers(bool bEnable)
{
if(!g_pState || !g_pState->pScene) return false;
bool bResult = true;
const Sint32 iLayerCount = g_pState->pScene->getLayerCount();
for(Sint32 i = 1; i <= iLayerCount; i++) bResult &= g_pState->pScene->setLayerFeatures(i, SLF_SMOOTHSCROLL, bEnable);
return bResult;
}
class PlatformPlayer : public SceneObject
{
public:
void initialize() override
{
if(getWidth() <= 0.0f || getHeight() <= 0.0f) setSize(96.0f, 128.0f);
}
void update(Scene*, SceneLayerContext& rContext) override
{
if(!g_pState) return;
if(this != g_pState->pPlayer || !g_pState->pGroundLayer) return;
Main& mC64 = Main::instance();
const bool bLeft = mC64.getKeyState(SDLK_LEFT);
const bool bRight = mC64.getKeyState(SDLK_RIGHT);
const Sint32 iMoveAxis = (bRight ? 1 : 0) - (bLeft ? 1 : 0);
const float fDT = rContext.fDeltaTime;
const float fTargetVX = static_cast<float>(iMoveAxis) * fPlayerSpeed;
const float fAccel = g_pState->bOnGround ? fGroundAccel : fAirAccel;
g_pState->fVelocityX = approach(g_pState->fVelocityX, fTargetVX, fAccel * fDT);
if(g_pState->bJumpQueued && g_pState->bOnGround)
{
g_pState->fVelocityY = fJumpVelocity;
g_pState->bOnGround = false;
}
g_pState->bJumpQueued = false;
g_pState->fVelocityY += fGravity * fDT;
g_pState->fVelocityY = clampFloat(g_pState->fVelocityY, -2000.0f, fMaxFallSpeed);
const float fWidth = getWidth();
const float fHeight = getHeight();
const SDL_FRect rectStart = { getX(), getY(), fWidth, fHeight };
SDL_FRect rectResolved = rectStart;
bool bHitX = false;
bool bHitY = false;
bool bGroundedSweep = false;
const bool bSweepOK = mC64.physics().sweepAABBOnLayerTile(
rectStart,
g_pState->fVelocityX * fDT,
g_pState->fVelocityY * fDT,
g_pState->pGroundLayer,
&rectResolved,
&bHitX,
&bHitY,
&bGroundedSweep,
&PlatformPlayer::solidTileForTutorial,
nullptr
);
/*
Optional one-way vertical behavior (jump-through platforms):
- Keep normal X collision.
- Disable Y collision while moving upward (fVelocityY < 0).
To enable, replace the single sweep above with a split X/Y sweep and
use the callback below:
fnYSolid = (fVelocityY < 0) ? nonSolidWhenJumpingUp : solidTileForTutorial;
Uncomment nonSolidWhenJumpingUp() at the bottom of this class.
*/
if(!bSweepOK) return;
if(bHitX) g_pState->fVelocityX = 0.0f;
if(bHitY) g_pState->fVelocityY = 0.0f;
bool bGroundedProbe = false;
if(!bGroundedSweep && fabsf(g_pState->fVelocityY) <= 0.001f)
{
bGroundedProbe = mC64.physics().isGroundedOnLayerTile(
rectResolved,
g_pState->pGroundLayer,
fMaxStepDown,
&PlatformPlayer::solidTileForTutorial,
nullptr
);
}
g_pState->bOnGround = (bGroundedSweep || bGroundedProbe);
setPosition(rectResolved.x, rectResolved.y);
}
private:
static bool solidTileForTutorial(const SceneLayerTile* pTileLayer, Sint32 iCellX, Sint32 iCellY, Uint32 iCellValue, void*)
{
if(!pTileLayer) return false;
const Sint32 iMapW = pTileLayer->getWidth();
const Sint32 iMapH = pTileLayer->getHeight();
if(iCellX < 0 || iCellX >= iMapW) return true;
if(iCellY >= iMapH) return true;
if(iCellY < 0) return false;
return iCellValue != 0;
}
// Uncomment to enable one-way vertical behavior (see update() comment above).
// static bool nonSolidWhenJumpingUp(const SceneLayerTile*, Sint32, Sint32, Uint32, void*)
// {
// return false;
// }
};
class TutorialObjectFactory : public ISceneObjectFactory
{
public:
SceneObject* create(const string& rType) override
{
if(rType == "hero" || rType == "player") return new(std::nothrow) PlatformPlayer();
return nullptr;
}
};
// Adapt the renderer to the engine callback signature.
static Sint32 renderWrapper(Sint32)
{
if(!g_pState || !g_pState->pScene) return 0;
if(g_pState->idBgImage > 0)
{
Image* pBG = Main::instance().imageMgr().get(g_pState->idBgImage);
if(pBG) pBG->render();
}
g_pState->pScene->render();
return 0;
}
// Find the player object in a scene layer.
static SceneObject* findPlayerObject(SceneLayerObject* pLayer)
{
if(!pLayer) return nullptr;
SceneObject* pObj = pLayer->findFirstByType("hero");
if(pObj) return pObj;
pObj = pLayer->findFirstByType("player");
if(pObj) return pObj;
pObj = pLayer->findFirstByName("hero");
if(pObj) return pObj;
return pLayer->findFirstByName("player");
}
// Find the first object with the requested type.
static SceneObject* findObjectByType(SceneLayerObject* pLayer, const string& rTypeWanted)
{
if(!pLayer) return nullptr;
return pLayer->findFirstByType(rTypeWanted);
}
// Check whether an object represents the player.
static bool isPlayerType(const SceneObject* pObj)
{
if(!pObj) return false;
const string& rType = pObj->getType();
const string& sName = pObj->getName();
return (rType == "hero" || rType == "player" || sName == "hero" || sName == "player");
}
// Handle the player entering the house trigger.
static void onHouseTrigger(const SceneTriggerEvent& ev)
{
if(!g_pState) return;
if(!isPlayerType(ev.pOther)) return;
if(ev.eType == STET_STAY)
{
g_pState->bHouseTriggerInside = true;
}
else if(ev.eType == STET_EXIT)
{
g_pState->bHouseTriggerInside = false;
g_pState->bHousePromptLock = false;
}
}
// Process the player response to the house prompt.
static bool handleHousePrompt()
{
if(!g_pState) return false;
if(!g_pState->bHouseTriggerInside)
{
g_pState->bHousePromptLock = false;
return false;
}
if(g_pState->bHousePromptLock) return false;
g_pState->bHousePromptLock = true;
g_pState->fVelocityX = 0.0f;
g_pState->fVelocityY = 0.0f;
// Freeze engine/layers before opening a modal dialog.
g_pState->pScene->pause();
const bool bExit = (Main::instance().tool().messageBox("House Found", "House found, do you want to exit?", MBB_YES | MBB_NO, MBT_QUESTION) == MBB_YES);
if(!bExit)
{
g_pState->pPlayer->setPosition(g_pState->pPlayer->getX() - 24.0f, g_pState->pPlayer->getY());
}
g_pState->pScene->resume();
return bExit;
}
// Close resources in ownership order, including partially completed setup.
static void closeTutorial()
{
Main& mC64 = Main::instance();
mC64.sceneMgr().close(0);
mC64.imageMgr().close(0);
}
// Run the tutorial application and release its resources.
int main(int argc, char* argv[])
{
if(!runParameterReadRender(argc, argv, eRenderer)) return 1;
atexit(CMem::destroy);
Main& mC64 = Main::instance();
TutorialState state;
g_pState = &state;
mC64.logMgr().get()->init("Tutorial_16_Scene_Platform", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_16_Scene_Platform.log");
if(!mC64.timer().init() || !mC64.timer().setRate(0, iLogicRate))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to initialize tutorial timing.\n");
return -1;
}
Screen* pScreen = mC64.configMgr().get();
pScreen->setSize(iScreenW, iScreenH);
pScreen->setDriver(eRenderer);
pScreen->setTitle("Tutorial 16 Scene - Platform");
if(pScreen->show() < 0)
{
return -1;
}
if(!pScreen->setRenderCallback(renderWrapper))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to set the render callback.\n");
return -1;
}
g_pState->idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
if(g_pState->idBgImage <= 0)
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to load the tutorial background.\n");
closeTutorial();
return -1;
}
static TutorialObjectFactory sFactory;
const Sint32 idScene = mC64.sceneMgr().loadFromFile(szMapFile, &sFactory);
if(idScene < 0)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Failed to load map: %s\n", szMapFile);
closeTutorial();
return -1;
}
g_pState->pScene = mC64.sceneMgr().get(idScene);
if(!g_pState->pScene)
{
closeTutorial();
return -1;
}
if(g_pState->pScene->getLayerCount() < 5)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Map must contain at least 5 layers. Found=%d\n", g_pState->pScene->getLayerCount());
closeTutorial();
return -1;
}
if(g_pState->pScene->getLayerType(1) != SLT_IMAGE || g_pState->pScene->getLayerType(2) != SLT_IMAGE)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Layers 1 and 2 must be image layers.\n");
closeTutorial();
return -1;
}
if(g_pState->pScene->getLayerType(iGroundLayer) != SLT_TILE)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Layer %d must be a tile layer (platforms).\n", iGroundLayer);
closeTutorial();
return -1;
}
g_pState->pGroundLayer = g_pState->pScene->accessLayerTile(iGroundLayer);
if(!g_pState->pGroundLayer)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Missing required tile layer for platforms. ground=%d\n", iGroundLayer);
closeTutorial();
return -1;
}
if(g_pState->pGroundLayer->getCellWidth() <= 0 || g_pState->pGroundLayer->getCellHeight() <= 0)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Invalid tile size in layer %d.\n", iGroundLayer);
closeTutorial();
return -1;
}
if(!g_pState->pScene->isObjectLayer(iPlayerLayer))
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Layer %d must be an object layer (objects/player).\n", iPlayerLayer);
closeTutorial();
return -1;
}
SceneLayerObject* pPlayerLayer = g_pState->pScene->accessLayerObject(iPlayerLayer);
g_pState->pPlayer = findPlayerObject(pPlayerLayer);
SceneObject* pHouse = findObjectByType(pPlayerLayer, "house");
if(!g_pState->pPlayer || !pPlayerLayer)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Missing player object layer. player=%d\n", iPlayerLayer);
closeTutorial();
return -1;
}
if(dynamic_cast<PlatformPlayer*>(g_pState->pPlayer) == nullptr)
{
mC64.logMgr().get()->msg(LL_CRITICAL, "Player object must use type 'hero' or 'player' in TMX.\n");
closeTutorial();
return -1;
}
if(pHouse != nullptr)
{
SceneObjectTrigger mHouseTrigger;
mHouseTrigger.sName = "house_entry";
mHouseTrigger.ShapeLocal.eType = SST_RECTANGLE;
mHouseTrigger.ShapeLocal.calculateAABB(fHouseCheckX, fHouseCheckY, fHouseCheckW, fHouseCheckH);
mHouseTrigger.iTargetLayer = iPlayerLayer;
mHouseTrigger.iStayDelayMs = 500;
mHouseTrigger.addIncludeType("player");
mHouseTrigger.addIncludeType("hero");
if(!pHouse->addTrigger(mHouseTrigger))
{
mC64.logMgr().get()->msg(LL_WARNING, "Failed to add house trigger zone.\n");
}
pHouse->setOnTriggerEvent(onHouseTrigger);
}
else
{
mC64.logMgr().get()->msg(LL_WARNING, "No house object found. House prompt trigger disabled.\n");
}
SDL_Rect rView = { iViewX, iViewY, iViewW, iViewH };
if(!g_pState->pScene->setViewport(&rView))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene viewport.\n");
closeTutorial();
return -1;
}
const float fStartCamX = g_pState->pPlayer->getX() + (g_pState->pPlayer->getWidth() * 0.5f) - (iViewW * 0.5f);
const float fStartCamY = g_pState->pPlayer->getY() + (g_pState->pPlayer->getHeight() * 0.75f) - (iViewH * 0.5f);
// Dead-zone rectangle size (centered on screen anchor): camera moves only when target leaves this area.
mCam.rDeadZone = { 0.0f, 0.0f, fCameraDeadZoneX * 2.0f, fCameraDeadZoneY * 2.0f };
// Follow damping in 1/seconds. Higher values follow faster.
mCam.fDamping = 12.0f;
// Clamp camera to map/layer bounds.
mCam.bClampToBounds = true;
// Follow point on player: X and Y center
mCam.ptTargetAnchor = { 0.5f, 0.5f };
// Screen focus point where target anchor should appear: exact viewport center.
mCam.ptScreenAnchor = { 0.5f, 0.5f };
if(!g_pState->pScene->setCameraPosition(iGroundLayer, fStartCamX, fStartCamY, true) ||
!g_pState->pScene->setCameraTarget(iGroundLayer, g_pState->pPlayer) ||
!g_pState->pScene->setCameraParams(iGroundLayer, mCam) ||
!g_pState->pScene->setCameraMode(iGroundLayer, SCM_DEADZONE))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene camera.\n");
closeTutorial();
return -1;
}
if(!setSmoothForAllLayers(g_pState->bSmooth))
{
mC64.logMgr().get()->msg(LL_ERROR, "Failed to enable smooth scrolling.\n");
closeTutorial();
return -1;
}
mC64.logMgr().get()->msg(LL_INFO, "\nCONTROLS:\n [Left/Right cursors] Move\n [Space] Jump\n [S] Toggle smooth scroll\n [P] Pause\n [R] Resume\n [D] Toggle object AABB\n [F1] Toggle trigger zones\n [F2] Toggle tile grid\n [F3] Toggle camera dead zone\n [Q/Esc] Quit\n\n");
SDL_Event ev;
bool bDone = false;
while(!bDone)
{
while(mC64.update(&ev))
{
if(ev.type == SDL_EVENT_QUIT) bDone = true;
if(ev.type == SDL_EVENT_KEY_DOWN)
{
if(ev.key.key == SDLK_ESCAPE || ev.key.key == SDLK_Q) bDone = true;
if(ev.key.key == SDLK_SPACE) g_pState->bJumpQueued = true;
if(ev.key.key == SDLK_P) g_pState->pScene->pause();
if(ev.key.key == SDLK_R) g_pState->pScene->resume();
if(ev.key.key == SDLK_D)
{
g_pState->bDebugAABB = !g_pState->bDebugAABB;
g_pState->pScene->setDebugObjectOverlay(SDO_OBJECT_AABB, SDOT_STANDARD, g_pState->bDebugAABB);
mC64.logMgr().get()->msg(LL_INFO, "Debug AABB: %s\n", g_pState->bDebugAABB ? "ON" : "OFF");
}
if(ev.key.key == SDLK_F1)
{
g_pState->bDebugTriggers = !g_pState->bDebugTriggers;
g_pState->pScene->setDebugObjectOverlay(SDO_TRIGGER_ZONES, SDOT_STANDARD, g_pState->bDebugTriggers);
mC64.logMgr().get()->msg(LL_INFO, "Debug Trigger Zones: %s\n", g_pState->bDebugTriggers ? "ON" : "OFF");
}
if(ev.key.key == SDLK_F2)
{
g_pState->bDebugTileGrid = !g_pState->bDebugTileGrid;
g_pState->pScene->setDebugOverlay(SDO_TILE_CELLGRID, g_pState->bDebugTileGrid);
mC64.logMgr().get()->msg(LL_INFO, "Debug Tile Grid: %s\n", g_pState->bDebugTileGrid ? "ON" : "OFF");
}
if(ev.key.key == SDLK_F3)
{
g_pState->bDebugCameraDeadZone = !g_pState->bDebugCameraDeadZone;
g_pState->pScene->setDebugOverlay(SDO_CAMERA_DEADZONE, g_pState->bDebugCameraDeadZone);
mC64.logMgr().get()->msg(LL_INFO, "Debug camera dead zone: %s\n", g_pState->bDebugCameraDeadZone ? "ON" : "OFF");
}
if(ev.key.key == SDLK_S)
{
g_pState->bSmooth = !g_pState->bSmooth;
if(!setSmoothForAllLayers(g_pState->bSmooth)) mC64.logMgr().get()->msg(LL_ERROR, "Failed to change smooth scrolling.\n");
mC64.logMgr().get()->msg(LL_INFO, "Smooth: %s\n", g_pState->bSmooth ? "ON" : "OFF");
}
}
}
if(bDone) break;
g_pState->pScene->update();
if(handleHousePrompt()) bDone = true;
}
mC64.sceneMgr().info();
closeTutorial();
return 0;
}
Screen * get(Sint32 idScreen=0)
Get a pointer to the screen using its handle.
Definition CoreConfig.cpp:1497
void setMTFriendly(Uint8 iDelayMS)
Set the multithread-friendly scheduler yield.
Definition CoreConfig.cpp:1183
Factory interface for creating game objects from Tiled type strings.
Definition CRM64Pro.h:10711
Image Object class.
Definition CRM64Pro.h:3356
Sint32 render(Sint32 idRes=0, SDL_FRect *pSrc=nullptr, SDL_FRect *pDst=nullptr)
Render the image.
Definition GraphicsImage.cpp:1575
Sint32 load(const string &sCDCFile, const string &sName)
Load an image stored in a CDC file.
Definition GraphicsImage.cpp:2476
Sint32 close(Sint32 idImg) override
Close and destroy an image.
Definition GraphicsImage.cpp:2178
CRM64Pro GDK Main class.
Definition CRM64Pro.h:502
bool sweepAABBOnLayerTile(const SDL_FRect &rRectStart, float fDX, float fDY, const SceneLayerTile *pSceneLayer, SDL_FRect *pRectResolved, bool *pHitX=nullptr, bool *pHitY=nullptr, bool *pGrounded=nullptr, TileSolidCallback fnSolid=nullptr, void *pUserData=nullptr)
Sweep an AABB on a tile layer and resolve collisions (axis separated: X then Y).
Definition GraphicsPhysics.cpp:1361
bool isGroundedOnLayerTile(const SDL_FRect &rRect, const SceneLayerTile *pSceneLayer, float fProbeDistance=1.0f, TileSolidCallback fnSolid=nullptr, void *pUserData=nullptr)
Ground probe helper against a tile layer.
Definition GraphicsPhysics.cpp:1496
Scene class.
Definition CRM64Pro.h:11200
Object layer containing game objects with spatial partitioning.
Definition CRM64Pro.h:10986
TileLayer class for tile-based map layers.
Definition CRM64Pro.h:10368
Sint32 loadFromFile(const string &sTMXFile, ISceneObjectFactory *pFactory=nullptr)
Load a level from a TMX file.
Definition SceneMapLoad.cpp:1575
Base class for all Scene game objects.
Definition CRM64Pro.h:10725
Screen Object class.
Definition CRM64Pro.h:1055
Sint32 show()
Show the screen.
Definition CoreConfig.cpp:362
Sint32 setDriver(eConfigRendererDriver eCRD)
Set the renderer driver.
Definition CoreConfig.cpp:454
bool setRenderCallback(RenderCallback fnCallback)
Set a callback function for rendering graphics at Render Frame Rate.
Definition CoreConfig.cpp:765
Sint32 setSize(Sint32 iWidth, Sint32 iHeight)
Set the size of the screen.
Definition CoreConfig.cpp:526
bool setTitle(const string &sName)
Set the title.
Definition CoreConfig.cpp:674
void setStatsLevel(eMemStatsLevel eMSL)
Set the memory stats gathering level.
Definition CMemoryManager.cpp:618
void destroy()
Releases the internal heap and displays final stats.
Definition CMemoryManager.cpp:954
@ MSL_HIGH
MSL_NORMAL plus deallocations and sizes logging for each module.
Definition CRM64Pro.h:387
eConfigRendererDriver
Screen renderer drivers.
Definition CRM64Pro.h:1013
@ CRD_SOFTWARE
Screen::setDriver(): software renderer driver. Slow but works on all platforms.
Definition CRM64Pro.h:1015
bool msg(eLogLevel eLL, const char *szFmt,...)
Write a message to the log.
Definition CoreLog.cpp:448
Log * get(Sint32 idLog=0)
Get a pointer to the log object.
Definition CoreLog.cpp:921
bool init(const string &sAppname, eLogLevel eLL, eLogMode eLM, const string &sFilename="", const string &sAuthor="", const string &sEmail="", const string &sWeb="")
Initialize the log object.
Definition CoreLog.cpp:338
@ LM_FILE
Log output mode for the 'lm' parameter in CRM64Pro::Log::init(). Output to a file and overwrite exist...
Definition CRM64Pro.h:748
@ LM_STDOUT
Log output mode for the 'lm' parameter in CRM64Pro::Log::init(). Output to standard output (stdout).
Definition CRM64Pro.h:750
@ LL_WARNING
Undesirable states or use of deprecated APIs.
Definition CRM64Pro.h:763
@ LL_INFO
Runtime events (startup/shutdown). Messages without timestamp.
Definition CRM64Pro.h:764
@ LL_DEBUG
Detailed information for flow control.
Definition CRM64Pro.h:765
@ LL_CRITICAL
Fatal errors, application crash, or assertions.
Definition CRM64Pro.h:761
@ LL_ERROR
Runtime errors that do not require immediate termination.
Definition CRM64Pro.h:762
Timer & timer()
Get reference to Timer module.
Definition CoreMain.cpp:730
Sint32 update(SDL_Event *evUser=nullptr)
Manage the GDK update cycle with a fixed virtual logic frame rate.
Definition CoreMain.cpp:451
ImageMgr & imageMgr()
Get reference to Image Manager.
Definition CoreMain.cpp:754
Physics & physics()
Get reference to Physics module.
Definition CoreMain.cpp:790
SceneMgr & sceneMgr()
Get reference to Scene Manager.
Definition CoreMain.cpp:814
static void terminate()
Main explicit destructor.
Definition CoreMain.cpp:266
static Main & instance()
Access the Main singleton.
Definition CoreMain.cpp:252
LogMgr & logMgr()
Get reference to Log Manager.
Definition CoreMain.cpp:724
bool getKeyState(SDL_Keycode keycode) const
Get the current state of a key.
Definition CoreMain.cpp:686
ConfigMgr & configMgr()
Get reference to Config Manager.
Definition CoreMain.cpp:712
@ SCM_DEADZONE
Damped deadzone tracking; deadzone is centered on SceneCameraParams::ptScreenAnchor.
Definition CRM64Pro.h:10185
@ SST_RECTANGLE
Default rectangular area defined by x, y, width, height.
Definition CRM64Pro.h:10139
@ SLF_SMOOTHSCROLL
Enable the smooth rendering feature. Check render() for further details.
Definition CRM64Pro.h:10080
@ SDO_OBJECT_AABB
Draw object AABBs for selected object types.
Definition CRM64Pro.h:10093
@ SDO_CAMERA_DEADZONE
Draw camera dead-zone rectangles for rendered dead-zone camera layers.
Definition CRM64Pro.h:10098
@ SDO_TRIGGER_ZONES
Draw trigger zone bounds for selected object types.
Definition CRM64Pro.h:10096
@ SDO_TILE_CELLGRID
Draw tile cell grid overlays for rendered tile layers.
Definition CRM64Pro.h:10097
@ SLT_TILE
Layer contains tile data (SceneLayerTile).
Definition CRM64Pro.h:10151
@ SLT_IMAGE
Layer displays an image (SceneLayerImage).
Definition CRM64Pro.h:10153
@ SDOT_STANDARD
Standard and user-defined object types.
Definition CRM64Pro.h:10109
@ STET_EXIT
Fired when overlap ends.
Definition CRM64Pro.h:10172
@ STET_STAY
Fired once after configured delay while overlap remains.
Definition CRM64Pro.h:10171
CRM64PRO_API void addIncludeType(const string &sType)
Add one type identifier to the include filter.
Definition SceneLayerObject.cpp:540
const string & getType() const
Get the type identifier.
Definition SceneLayerObject.cpp:1505
bool addTrigger(const SceneObjectTrigger &rTrigger)
Add a trigger zone to this object.
Definition SceneLayerObject.cpp:1535
SceneObject * findFirstByName(const string &sName)
Find first object matching an exact name string.
Definition SceneLayerObject.cpp:3371
SceneObject * findFirstByType(const string &sType)
Find first object matching an exact type string.
Definition SceneLayerObject.cpp:3349
const string & getName() const
Get the object name.
Definition SceneLayerObject.cpp:1510
CRM64PRO_API void calculateAABB(float fObjX, float fObjY, float fObjWidth, float fObjHeight)
Recalculate AABB from object position and dimensions.
Definition SceneLayerObject.cpp:210
void setOnTriggerEvent(TriggerEventCallback fnCallback)
Set trigger event callback.
Definition SceneLayerObject.cpp:1635
Sint32 getHeight() const
Get map height.
Definition SceneLayerTile.cpp:101
Sint32 getWidth() const
Get map width.
Definition SceneLayerTile.cpp:98
Sint32 close(Sint32 idScene) override
Close and destroy a Scene.
Definition Scene.cpp:2298
bool info(Sint32 iMode=0) override
Request Scene Manager information.
Definition Scene.cpp:2215
Scene * get(Sint32 idScene)
Get a pointer to the Scene using its handle.
Definition Scene.cpp:2355
bool init(eTimerState eState=TS_INIT)
Initialize the timer system.
Definition CoreTimer.cpp:543
bool setRate(Uint16 iR, Uint16 iL)
Set preferred Render and Logic Frame Rate.
Definition CoreTimer.cpp:556
@ MBB_NO
No button is displayed.
Definition CRM64Pro.h:2513
@ MBB_YES
Yes button is displayed.
Definition CRM64Pro.h:2512
@ MBT_QUESTION
Question icon is displayed.
Definition CRM64Pro.h:2503
CRM64Pro GDK.
Definition AudioTrack.cpp:36
Camera configuration parameters.
Definition CRM64Pro.h:10194
SDL_FPoint ptScreenAnchor
Normalized viewport anchor [0..1]x[0..1] where the target anchor should appear on screen....
Definition CRM64Pro.h:10200
SDL_FRect rDeadZone
Deadzone rectangle in viewport pixels for SCM_DEADZONE. Currently only w/h are used (x/y are ignored)...
Definition CRM64Pro.h:10195
SDL_FPoint ptTargetAnchor
Normalized target anchor [0..1]x[0..1] on the followed object (0,0=top-left, 0.5,1....
Definition CRM64Pro.h:10199
float fDamping
Target-following exponential damping strength in 1/second. Ignored by SCM_AUTOSCROLL....
Definition CRM64Pro.h:10197
bool bClampToBounds
If true, clamp camera to explicit camera bounds (if set) or to layer/world bounds....
Definition CRM64Pro.h:10198
float fDeltaTime
Time elapsed since last frame (for update). 0 during render.
Definition CRM64Pro.h:10230
Object trigger zone definition.
Definition CRM64Pro.h:10623
Sint32 iTargetLayer
-1 same layer, 0 all object layers, >0 specific object layer ID.
Definition CRM64Pro.h:10630
string sName
Trigger name unique within owner object.
Definition CRM64Pro.h:10624
Uint32 iStayDelayMs
Delay in ms before one-shot STAY event is emitted after ENTER.
Definition CRM64Pro.h:10627
SceneShapeData ShapeLocal
Trigger shape in owner-local coordinates.
Definition CRM64Pro.h:10626
eSceneShapeType eType
Shape type (rectangle, ellipse, polygon, etc.).
Definition CRM64Pro.h:10603
Trigger event payload.
Definition CRM64Pro.h:10662
const SceneObject * pOther
Other object involved in the overlap.
Definition CRM64Pro.h:10667
eSceneTriggerEventType eType
Event type.
Definition CRM64Pro.h:10663

Navigation

Previous: Tutorial 16.2 - Island Game v2

Back to the tutorials index

Next: Tutorial 16.4 - Scene Shooter