This is the final lesson of the tutorial series — and the most ambitious. We're going to build a complete side-scrolling platformer from scratch: a scrolling tilemap level, a player with gravity and jumping, an enemy to avoid, and a goal to reach. By the end you'll have a real playable game and the knowledge to extend it into something much bigger.
⚠️The Sprite Engine is a separate download — grab it from the HyperXTalk community forums and add it to your stack before using any of the commands in this lesson. From v0.9.16 onwards it will be available directly in HyperXTalk via Tools → Package Manager → Sprite Engine.
🎯 Learning Objectives
Build a tile-based level with solid platforms using defineTile and loadTilemap
Add a scrolling background layer
Implement gravity and jumping with a physics loop
Use spriteOnGround, snapSpriteToGround, and spriteHitsCeiling
Add an enemy that patrols back and forth
Detect a win condition and reset the game
Explore the full sample game included with HyperXTalk
🎮 What We're Building
Our game will have:
🗺️ A tile map level with ground and platforms the player can run and jump on.
🏃 A player character controlled with A (left), D (right), and W (jump).
🌊 Gravity that pulls the player down and tile collision that stops them falling through the floor.
👾 A patrolling enemy that bounces back and forth — touching it resets the level.
⭐ A goal sprite — reaching it shows a win message.
We'll build it step by step. Each section adds one new piece. All code goes in the card script unless stated otherwise.
💡This lesson uses placeholder coloured rectangles instead of image files so you can get the game working without any art assets. Once it's running, swap in your own images at any point using setSpriteImage.
🗺️ Tilemaps
A tilemap describes a level as a grid of characters. Each character maps to a tile image. The engine renders the tiles and handles collision automatically for any tile you mark as solid.
Set up your tiles and load the map in openCard:
card script — openCard
-- Define what each map character looks like-- (pass empty for a coloured placeholder)defineTile"G", empty-- Ground tile (green placeholder)defineTile"P", empty-- Platform tile-- Mark which tiles block movementdefineSolidTile"G", truedefineSolidTile"P", true-- Set tile dimensions in pixelssetTileSize64, 64-- Build the level as a string-- Each character = one tile. Space = empty air.-- Each line = one row, top to bottom.localtMapput"................................................"&returnintotMapput"................................................"&returnaftertMapput"......PPPP......................................P"&returnaftertMapput"................................................"&returnaftertMapput"...............PPPP............................."&returnaftertMapput"................................................"&returnaftertMapput"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"aftertMaploadTilemaptMap
Dots (.) and spaces both represent empty air. Any other character you define becomes a tile. The map above creates a ground row at the bottom and two floating platforms — a classic platformer layout.
🟢When using a tile sheet image, call defineTileSheet "tileset.png", 16, 16 first, then reference tiles by column and row: defineTile "G", 0, 0. This is more efficient than one image per tile and is how the sample game works.
🌅 Scrolling Backgrounds
A background layer tiles and scrolls behind all sprites. Link its scroll velocity to the camera so it moves as the player explores the level — creating a parallax effect:
-- Set a background image (stretched to fill the canvas)setSpriteBackgroundImage"sky.png"setSpriteBackgroundStretchtrue-- Or scroll it for a parallax effectsetSpriteBackgroundImage"hills.png"setSpriteBackgroundVelocity-1, 0-- scroll left slowly-- Multi-layer parallaxaddSpriteBackground"sky", "sky.png", -0.5, 0addSpriteBackground"hills", "hills.png", -1.5, 0addSpriteBackground"trees", "trees.png", -3.0, 0
🏃 The Player
Create the player sprite, set up the camera to follow it, and track the movement keys. The physics variables that control jumping and falling live as script-local variables at the top of the card script:
card script — top of script
localgPlayerVY-- vertical velocity (we manage this ourselves)localgOnGround-- true when player is standing on a solid tilelocalgKeyRight, gKeyLeft, gJumpPressed
card script — openCard (after loadTilemap)
-- Create the player and set a tight hitboxcreateSprite"player", empty, 100, 320, 40, 56setSpriteHitbox"player", 5, 4, 30, 52-- Lock the camera to follow the playersetCameraFollow"player"-- Initialise physics stateput0intogPlayerVYputfalseintogOnGroundputfalseintogKeyRightputfalseintogKeyLeftputfalseintogJumpPressedresumeSprites
The physics loop runs inside spriteTick. Each tick it applies gravity, checks whether the player is on the ground, handles jumping, and applies horizontal movement. The key insight is that we manage vertical velocity ourselves — gPlayerVY accumulates gravity each tick, and we set it into the sprite at the end:
card script — spriteTick
onspriteTicklocalkTileSize, kGravity, kMaxFall, kSpeed, kJumpPowerput64intokTileSizeput0.5intokGravityput10intokMaxFallput3intokSpeedput-9intokJumpPower-- Jump if W was pressed and player is on the groundifgJumpPressedandgOnGroundthenputkJumpPowerintogPlayerVYend ifputfalseintogJumpPressed-- Ground check and gravityifspriteOnGround("player") thensnapSpriteToGround"player", kTileSizeput0intogPlayerVYputtrueintogOnGroundelseaddkGravitytogPlayerVYifgPlayerVY > kMaxFallthenputkMaxFallintogPlayerVYputfalseintogOnGroundend if-- Ceiling check: cancel upward velocity if head hits a blockifspriteHitsCeiling("player") andgPlayerVY < 0thenput0intogPlayerVYend if-- Horizontal movementlocaltVXput0intotVXifgKeyRightthenputkSpeedintotVXifgKeyLeftthenput-kSpeedintotVX-- Apply combined velocitysetSpriteVelocity"player", tVX, gPlayerVYendspriteTick
spriteOnGround probes three points along the bottom of the sprite's hitbox. If any of them touch a solid tile, it returns true. snapSpriteToGround then nudges the sprite up so it sits flush on the tile surface rather than slightly overlapping it.
⚠️Cap your fall speed (kMaxFall) to less than the tile height. If a sprite is moving faster than one tile height per tick it can "tunnel" straight through a platform without the ground check firing.
👾 Adding an Enemy
A patrolling enemy is straightforward — create it, give it a horizontal velocity, and in spriteTick reverse that velocity when it hits a wall. If it collides with the player, restart the level:
-- Bounce the enemy off wallsifspriteHitsWall("enemy", "right") thensetSpriteVelocity"enemy", -2, 0end ififspriteHitsWall("enemy", "left") thensetSpriteVelocity"enemy", 2, 0end if-- Player touches enemy = restartifspritesCollide("player", "enemy") thenpauseSpritesanswer"Oh no! Try again."send"openCard"to this cardend if
⭐ Win Condition
Place a goal sprite at the end of the level. When the player reaches it, pause the engine and show a win message:
card script — openCard
createSprite"goal", empty, 2800, 320, 40, 40
card script — inside spriteTick
-- Win conditionifspritesCollide("player", "goal") thenpauseSpritesanswer"You win! 🎉"send"openCard"to this cardend if-- Fall-off-the-world kill zoneifspriteY("player") > 700thensend"openCard"to this cardend if
🟢send "openCard" to this card re-fires the openCard handler, which destroys all sprites, resets all variables, and rebuilds the level from scratch — a simple and clean way to restart the game.
🎮 The Sample Game
Everything in this lesson is a simplified version of the full side-scroller included with HyperXTalk. The sample game adds sprite sheet animation, ladders, multiple enemies with death animations, an attack mechanic, a scrolling parallax background, and a full tileset — but it's built on exactly the same foundations you've just learned.
📦
SideScrollerSample.hyperxtalk is available to download from the HyperXTalk community forums. Open it in HyperXTalk to play the game and read the full commented card script. Every technique in this lesson — and many more — is demonstrated there in production-quality code.
Some highlights from the sample game worth studying:
Sprite sheet animation — the player and enemies use sprite sheets with multiple animation states (idle, run, jump, attack). setSpriteSheet configures the sheet and setSpriteAnimation switches between frame ranges.
from SideScrollerSample
-- Load a 10-column, 17-row sheet (170 total frames, 8 ticks per frame)setSpriteSheet"player", 10, 17, 170, 8-- Switch animation range based on player stateifpAnimis"run"thensetSpriteAnimation"player", 90, 97, 4ifpAnimis"jump"thensetSpriteAnimation"player", 110, 113, 8ifpAnimis"idle"thensetSpriteAnimation"player", 70, 74, 8ifpAnimis"attack"thensetSpriteAnimation"player", 140, 145, 8
Enemy groups — enemies are tracked in a comma-separated list and an associative array stores their state ("alive", "dying", "dead"). This pattern scales cleanly to any number of enemies.
Ladders — ladder hitbox sprites are generated automatically from the tilemap using a scan that finds vertical runs of ladder characters, then builds invisible sprites to handle the climb collision.
Horizontal flipping — setSpriteFlipX mirrors the player sprite when they change direction, so you only need one set of animation frames.
📝 Review Questions
Question 1
In the tilemap string, what does a dot (.) or space represent?
Empty air — nothing is drawn and there is no collision. Any other character you define with defineTile renders as a tile, and any tile you mark with defineSolidTile "X", true blocks sprite movement.
Question 2
Why do we track vertical velocity ourselves in gPlayerVY rather than just using setSpriteVelocity with a fixed Y value?
Because gravity must accumulate over time — each tick we add a small gravity value to gPlayerVY, making the player fall faster and faster until they hit the ground. A fixed Y velocity would mean the player falls at constant speed with no sense of weight or acceleration.
Question 3
What does snapSpriteToGround do, and why is it needed?
It nudges the sprite upward so its bottom edge sits exactly flush with the tile surface. Without it, the sprite may be a few pixels inside the tile (depending on how fast it was falling that tick), causing it to visually overlap the ground or drift downward on slopes.
Question 4
How does the enemy patrol back and forth without any complex logic?
By checking spriteHitsWall each tick and reversing the X velocity when it returns true. The enemy starts with a positive velocity (moving right), and whenever it hits a wall the velocity is flipped to negative (moving left), and vice versa.
Question 5
What is the simplest way to restart the game when the player dies or wins?
send "openCard" to this card — this re-fires the openCard handler which destroys all sprites, resets all script-local variables, rebuilds the tilemap, and re-creates all sprites from scratch. It's a clean full reset with a single line of code.