Home
JAQForum Ver 24.01
Log In or Join  
Active Topics
Local Time 08:12 22 Apr 2026 Privacy Policy
Jump to

Notice. New forum software under development. It's going to miss a few functions and look a bit ugly for a while, but I'm working on it full time now as the old forum was too unstable. Couple days, all good. If you notice any issues, please contact me.

Forum Index : Microcontroller and PC projects : How to program a platform game with the TILEMAP command, example code

Author Message
matherp
Guru

Joined: 11/12/2012
Location: United Kingdom
Posts: 11216
Posted: 06:14pm 11 Mar 2026
Copy link to clipboard 
Print this post

By request, attached is a sort of proforma example platform game programmed using the TILEMAP command. It is fully functional (HDMI or RP2350VGA) and extensively commented so should be useful as a starting point for any sort of platform type game.

'=============================================
' PLATFORM - A Tilemap-Based Platform Game
'
' A complete reference implementation showing
' how to build a side-scrolling platform game
' using the TILEMAP engine. Techniques covered:
'
'   - Procedural tileset generation (drawing
'     tiles at runtime, saving as BMP, loading
'     into FLASH for the TILEMAP engine)
'   - Scrolling camera across a world wider
'     than the screen (map is 2x screen width)
'   - Tile-based collision detection using
'     TILEMAP(COLLISION) with attribute masks
'   - Gravity, jumping, and ladder climbing
'   - One-way platforms (stand on top, pass
'     through from below)
'   - Coin collection via TILEMAP SET to
'     replace tiles at runtime
'   - Sprite animation using TILEMAP SPRITE
'     SET to swap between tile frames
'   - Double-buffered rendering with
'     FRAMEBUFFER for flicker-free display
'
' Game Structure:
'   1. GenerateTileset draws tiles to screen,
'      saves as BMP, loads into FLASH
'   2. Title screen waits for input
'   3. TILEMAP CREATE builds the map from DATA
'   4. Main loop: Input -> Physics -> Collision
'      -> Animation -> Draw -> Repeat
'   5. Win/Lose screens loop back or exit
'
' Controls: Left/Right arrows to move
'           Up arrow to jump / climb ladder
'           Down arrow to descend ladder
'           A = jump diagonally left
'           S = jump diagonally right
'           Q to quit
'
' Requires: MODE 2 (320x240 RGB121), SD card
'=============================================

' OPTION EXPLICIT forces all variables to be
' declared with DIM before use - catches typos.
OPTION EXPLICIT

' OPTION BASE 0 means arrays and DATA indices
' start at 0. Tile index 0 = empty/transparent.
OPTION BASE 0

' MODE 2 sets 320x240 resolution with RGB121
' (4-bit colour, 16 colours). This is the most
' common mode for PicoMite game development.
MODE 2

' ---- Display constants ----
' These match MODE 2 resolution. Used to
' calculate camera bounds and HUD positions.
CONST SCR_W = 320
CONST SCR_H = 240

' ---- Tile dimensions ----
' Each tile is 16x16 pixels. The tileset image
' is arranged in a grid with TPR tiles per row.
' TILEMAP CREATE uses these to slice the BMP
' into individual tile images.
CONST TW = 16           ' tile width in pixels
CONST TH = 16           ' tile height in pixels
CONST TPR = 8           ' tiles per row in tileset image

' ---- Map dimensions ----
' The map is wider than the screen to enable
' horizontal scrolling. 40 cols x 16px = 640px
' = exactly 2 screen widths. The camera slides
' across this wider world to follow the player.
' Height is 15 rows x 16px = 240px = 1 screen.
CONST MCOLS = 40         ' map columns (2x screen)
CONST MROWS = 15         ' map rows (1x screen)

' ---- Tile indices ----
' Each tile type has a unique index (1-based).
' Index 0 is reserved for empty/transparent.
' Tiles 1-8 appear in the map DATA. Tiles 9-12
' are used only for sprite animation frames
' (never placed on the map itself).
CONST T_EMPTY  = 0       ' empty/transparent tile
CONST T_GRASS  = 1       ' solid ground with grass top
CONST T_BRICK  = 2       ' solid brick platform
CONST T_LADDER = 3       ' climbable ladder
CONST T_COIN   = 4       ' collectible coin
CONST T_SKY    = 5       ' background sky (passable)
CONST T_DIRT   = 6       ' solid underground fill
CONST T_PLAYER = 7       ' player standing pose (sprite)
CONST T_PLAT   = 8       ' one-way platform (stand on top)
CONST T_WALK1  = 9       ' walk frame 1 (sprite only)
CONST T_WALK2  = 10      ' walk frame 2 (sprite only)
CONST T_CLIMB1 = 11      ' climb frame 1 (sprite only)
CONST T_CLIMB2 = 12      ' climb frame 2 (sprite only)

' ---- Attribute bits ----
' Each tile type has a bitmask of attributes,
' loaded via TILEMAP ATTR. TILEMAP(COLLISION)
' tests a rectangular area against a specific
' attribute bit, returning >0 if any tile in
' that area has the bit set. Using separate
' bits allows a tile to have multiple properties
' (e.g. a tile could be both solid AND a ladder
' if needed). We test with AND in code:
'   IF (attr AND A_SOLID) THEN ...
CONST A_SOLID  = &b0001  ' bit 0: blocks movement
CONST A_LADDER = &b0010  ' bit 1: climbable
CONST A_COIN   = &b0100  ' bit 2: collectible
CONST A_PLAT   = &b1000  ' bit 3: one-way platform

' ---- RGB121 colours ----
' MODE 2 uses RGB121 encoding: 1 bit red, 2
' bits green, 1 bit blue = 16 possible colours.
' The RGB() function maps 24-bit values to the
' nearest RGB121 colour. Defining named colour
' constants improves readability throughout.
CONST C_BLACK  = RGB(0,0,0)
CONST C_RED    = RGB(255,0,0)
CONST C_YELLOW = RGB(255,255,0)
CONST C_GREEN  = RGB(0,255,0)
CONST C_BLUE   = RGB(0,0,255)
CONST C_WHITE  = RGB(255,255,255)
CONST C_GREY   = RGB(128,128,128)
CONST C_CYAN   = RGB(0,255,255)
CONST C_BROWN  = RGB(128,128,0)
CONST C_SKY    = RGB(0,128,255)
CONST C_DGREEN = RGB(0,128,0)
CONST C_ORANGE = RGB(255,128,0)

' ---- Game state ----
' Positions use "world coordinates" (pixels
' within the full 640px-wide map), not screen
' coordinates. The camera offset converts world
' to screen position for rendering.
' Float variables (!) give sub-pixel precision
' for smooth movement at fractional speeds.
DIM score, coins_left
DIM px!, py!             ' player world position (float for sub-pixel)
DIM vx!, vy!             ' player velocity (pixels/frame)
DIM cam_x                ' camera X offset in world pixels
DIM on_ground            ' 1 if standing on solid surface
DIM on_ladder            ' 1 if player centre overlaps ladder tile
DIM k$                   ' current keypress from INKEY$
DIM hit_t, hit_a         ' collision query temporaries
DIM tcol, trow           ' tile grid coordinate temporaries
DIM new_px!, new_py!     ' proposed position before collision check
DIM i                    ' general loop variable
DIM anim_count           ' frame counter for animation timing
DIM anim_tile            ' current tile index for sprite display
DIM walk_timer           ' countdown: frames to show walk pose
DIM climb_timer          ' countdown: frames to show climb pose

' ---- Physics constants ----
' These control the "feel" of the game. Adjust
' to taste: higher GRAVITY = heavier; higher
' JUMP_VEL (more negative) = higher jumps;
' higher MOVE_SPEED = faster walking.
' All values are in pixels per frame.
CONST GRAVITY! = 0.3     ' downward acceleration per frame
CONST JUMP_VEL! = -4.5   ' initial upward velocity on jump
CONST MOVE_SPEED! = 2.0  ' horizontal walk speed
CONST CLIMB_SPEED! = 1.5 ' vertical climb speed on ladders
CONST MAX_FALL! = 5.0    ' terminal velocity (max fall speed)

' Player collision box: narrower than the tile
' to allow the player to slip between gaps and
' feel less "sticky" against walls. PW < TW
' means 2px margin on each side of the sprite.
CONST PW = 12            ' collision box width (< TW)
CONST PH = 16            ' collision box height (= TH)

' Animation hold timers: INKEY$ only returns a
' character when a key repeat fires, leaving
' most frames with k$="". Without timers the
' sprite flickers to standing between repeats.
' These counters hold the walk/climb pose for
' N frames after each keypress, bridging the
' gap between INKEY$ repeat events.
CONST WALK_FRAMES = 10   ' frames to sustain walk pose
CONST CLIMB_FRAMES = 10  ' frames to sustain climb pose

' ---- Derived constants ----
' World width in pixels and maximum camera
' offset. Camera is clamped to [0..MAX_CAM]
' so the viewport never shows beyond the map.
CONST WORLD_W = MCOLS * TW     ' 640 pixels
CONST MAX_CAM = WORLD_W - SCR_W ' 320 pixels

' ============================================
' Setup: Tileset Generation Pipeline
' ============================================
' Step 1: Draw all tile graphics to the screen
'         using LINE, BOX, CIRCLE, PIXEL.
' Step 2: SAVE IMAGE captures the tileset area
'         to a BMP file on the SD card.
' Step 3: FLASH LOAD IMAGE stores the BMP into
'         FLASH memory slot 1. The O flag means
'         "overwrite" if the slot is already used.
'         Once in FLASH, TILEMAP CREATE can use
'         it without needing the SD card again.
PRINT "Generating tileset..."
GenerateTileset
FLASH LOAD IMAGE 1, "platform_tiles.bmp", O

' Create a framebuffer for double-buffered
' rendering. We draw to framebuffer F (off-screen)
' then copy to N (on-screen) each frame. This
' eliminates flicker from partial draws.
FRAMEBUFFER CREATE

' ============================================
' Title Screen
' ============================================
' Draw to the framebuffer (F), then copy to
' the display (N) in one operation. This
' pattern is used everywhere for flicker-free
' rendering.
TitleScreen:
FRAMEBUFFER WRITE F
CLS C_BLACK
TEXT SCR_W\2, 40, "PLATFORM", "CM", 7, 2, C_GREEN
TEXT SCR_W\2, 100, "Arrow keys to move", "CM", 1, 1, C_WHITE
TEXT SCR_W\2, 120, "UP to jump or climb", "CM", 1, 1, C_WHITE
TEXT SCR_W\2, 140, "A=jump-left  S=jump-right", "CM", 1, 1, C_WHITE
TEXT SCR_W\2, 160, "DOWN to descend ladders", "CM", 1, 1, C_WHITE
TEXT SCR_W\2, 180, "Collect all coins!", "CM", 1, 1, C_YELLOW
TEXT SCR_W\2, 200, "Press SPACE to start", "CM", 1, 1, C_CYAN
FRAMEBUFFER COPY F, N
DO : k$ = INKEY$ : LOOP UNTIL k$ = " " OR UCASE$(k$) = "Q"
IF UCASE$(k$) = "Q" THEN GOTO Cleanup
' Flush keyboard buffer before game starts
DO WHILE INKEY$ <> "" : LOOP

' ============================================
' New Game - Initialise TILEMAP and state
' ============================================
score = 0

' Close any previous tilemap (needed when
' restarting after win/lose via GOTO TitleScreen).
TILEMAP CLOSE

' TILEMAP CREATE arguments:
'   mapdata  = DATA label containing tile indices
'   1        = FLASH image slot for the tileset
'   1        = tilemap ID (for multi-tilemap games)
'   TW, TH   = tile dimensions in pixels
'   TPR      = tiles per row in the tileset image
'   MCOLS    = map width in tiles
'   MROWS    = map height in tiles
TILEMAP CREATE mapdata, 1, 1, TW, TH, TPR, MCOLS, MROWS

' TILEMAP ATTR loads the attribute bitmask for
' each tile type. The DATA label "tileattrs"
' provides one value per tile type (1-based).
' The third argument (12) is the number of entries.
TILEMAP ATTR tileattrs, 1, 12

' Count coins in the map by scanning every tile.
' This uses the two-step query pattern:
'   1. TILEMAP(TILE id, x, y) returns the tile
'      index at world pixel coordinates.
'   2. TILEMAP(ATTR id, tileIndex) returns the
'      attribute bitmask for that tile type.
' Note: TILEMAP(ATTR) takes a tile INDEX, not
' pixel coordinates. Always check tile > 0 first
' since tile 0 (empty) has no attributes.
coins_left = 0
FOR trow = 0 TO MROWS - 1
 FOR tcol = 0 TO MCOLS - 1
   hit_t = TILEMAP(TILE 1, tcol * TW + 1, trow * TH + 1)
   IF hit_t > 0 THEN
     hit_a = TILEMAP(ATTR 1, hit_t)
     IF (hit_a AND A_COIN) THEN coins_left = coins_left + 1
   END IF
 NEXT tcol
NEXT trow

' ---- Create player sprite ----
' TILEMAP SPRITE CREATE arguments:
'   1 = sprite ID
'   1 = tilemap ID it belongs to
'   T_PLAYER = initial tile to display
'   x, y = initial screen position
' The sprite moves independently of the map
' and is drawn on top by TILEMAP SPRITE DRAW.
' Start at column 2, row 11 (just above ground).
px! = 2 * TW
py! = 11 * TH
vx! = 0 : vy! = 0
on_ground = 0 : on_ladder = 0
cam_x = 0
anim_count = 0 : anim_tile = T_PLAYER
walk_timer = 0 : climb_timer = 0
TILEMAP SPRITE CREATE 1, 1, T_PLAYER, INT(px!) - cam_x, INT(py!)

' ============================================
' Main Game Loop
' ============================================
' Each iteration is one frame. The loop follows
' the standard game loop pattern:
'   1. Read input
'   2. Update physics (velocity, position)
'   3. Resolve collisions
'   4. Collect items
'   5. Update camera and animation
'   6. Render everything
'   7. Check win/lose conditions
DO
 ' Start each frame by directing drawing to
 ' the off-screen framebuffer, then clear it.
 FRAMEBUFFER WRITE F
 CLS C_SKY

 ' ---- Input ----
 ' INKEY$ returns one character per call from
 ' the keyboard buffer, or "" if no key is
 ' pending. Arrow keys return CHR$(128)-131.
 ' Limitation: INKEY$ can only report one key
 ' per frame, so we use A/S as combo keys for
 ' simultaneous jump + horizontal movement.
 k$ = INKEY$
 IF UCASE$(k$) = "Q" THEN GOTO Cleanup

 ' ---- Ladder detection ----
 ' Check what tile is under the player's centre
 ' point. If it has the A_LADDER attribute, the
 ' player can climb. This two-step query
 ' (TILE then ATTR) is the standard pattern for
 ' checking tile properties at a point.
 hit_t = TILEMAP(TILE 1, INT(px!) + TW\2, INT(py!) + TH\2)
 on_ladder = 0
 IF hit_t > 0 THEN
   hit_a = TILEMAP(ATTR 1, hit_t)
   IF (hit_a AND A_LADDER) THEN on_ladder = 1
 END IF

 ' ---- Horizontal movement ----
 ' When on ground or ladder, velocity resets to
 ' zero each frame (player stops when no key is
 ' pressed). When AIRBORNE, vx! is preserved
 ' from the previous frame so the player keeps
 ' momentum through the jump arc - this makes
 ' gap-jumping possible with INKEY$ input.
 IF on_ground THEN vx! = 0
 IF on_ladder THEN vx! = 0
 ' Arrow keys and A/S set horizontal velocity.
 ' A/S also trigger a jump (handled below),
 ' giving diagonal jump with a single keypress.
 ' walk_timer is reset on each keypress to keep
 ' the walking animation visible between INKEY$
 ' repeat events.
 IF k$ = CHR$(130) THEN vx! = -MOVE_SPEED! : walk_timer = WALK_FRAMES
 IF k$ = CHR$(131) THEN vx! = MOVE_SPEED! : walk_timer = WALK_FRAMES
 IF LCASE$(k$) = "a" THEN vx! = -MOVE_SPEED! : walk_timer = WALK_FRAMES
 IF LCASE$(k$) = "s" THEN vx! = MOVE_SPEED! : walk_timer = WALK_FRAMES

 ' ---- Vertical movement ----
 ' Two completely different physics modes:
 ' ON LADDER: no gravity, up/down to climb,
 '   A/S to jump off the ladder sideways.
 ' NOT ON LADDER: gravity pulls down, jump
 '   only when standing on ground.
 IF on_ladder THEN
   ' Ladder mode: direct vertical control
   vy! = 0
   IF k$ = CHR$(128) THEN vy! = -CLIMB_SPEED! : climb_timer = CLIMB_FRAMES
   IF k$ = CHR$(129) THEN vy! = CLIMB_SPEED! : climb_timer = CLIMB_FRAMES
   ' A/S = leap off ladder sideways (vx! was
   ' already set above; here we add jump velocity
   ' and exit ladder mode)
   IF LCASE$(k$) = "a" THEN
     vy! = JUMP_VEL!
     on_ladder = 0
   END IF
   IF LCASE$(k$) = "s" THEN
     vy! = JUMP_VEL!
     on_ladder = 0
   END IF
 ELSE
   ' Normal physics: gravity + jump
   ' Jump is only allowed when on_ground.
   ' Up arrow = vertical jump; A/S = diagonal
   ' jump (vx! was already set above).
   ' Note: MMBasic IF does not short-circuit,
   ' so we nest IF checks instead of using AND.
   IF k$ = CHR$(128) THEN
     IF on_ground THEN
       vy! = JUMP_VEL!
       on_ground = 0
     END IF
   END IF
   IF LCASE$(k$) = "a" THEN
     IF on_ground THEN
       vy! = JUMP_VEL!
       on_ground = 0
     END IF
   END IF
   IF LCASE$(k$) = "s" THEN
     IF on_ground THEN
       vy! = JUMP_VEL!
       on_ground = 0
     END IF
   END IF
   ' Gravity accumulates each frame (constant
   ' acceleration). Terminal velocity prevents
   ' the player from falling unreasonably fast.
   vy! = vy! + GRAVITY!
   IF vy! > MAX_FALL! THEN vy! = MAX_FALL!
 END IF

 ' ============================================
 ' COLLISION DETECTION
 ' ============================================
 ' Collisions use "proposed position" testing:
 '   1. Calculate new position from velocity
 '   2. Test the new position for overlaps
 '   3. If blocked, revert to old position
 ' Horizontal and vertical axes are resolved
 ' independently to prevent diagonal sticking.
 '
 ' TILEMAP(COLLISION id, x, y, w, h, attrMask)
 ' tests a rectangle at world pixel coords and
 ' returns >0 if any tile in that area has the
 ' specified attribute bits set.
 ' ============================================

 ' ---- Horizontal collision ----
 new_px! = px! + vx!
 ' Clamp to world bounds
 IF new_px! < 0 THEN new_px! = 0
 IF new_px! > WORLD_W - TW THEN new_px! = WORLD_W - TW

 ' Check horizontal collision with solid tiles.
 ' Only test the LEADING EDGE of the collision
 ' box (1px-wide strip) in the direction of
 ' movement. The collision box is centred within
 ' the tile: offset = (TW - PW) / 2 on each side.
 ' The vertical range is inset by 1px top and
 ' bottom to avoid false positives at tile seams.
 IF vx! < 0 THEN
   ' Moving left: test 1px strip at left edge
   hit_t = TILEMAP(COLLISION 1, INT(new_px!) + (TW - PW)\2, INT(py!) + 1, 1, PH - 2, A_SOLID)
   IF hit_t > 0 THEN new_px! = px!  ' blocked: revert
 END IF
 IF vx! > 0 THEN
   ' Moving right: test 1px strip at right edge
   hit_t = TILEMAP(COLLISION 1, INT(new_px!) + (TW + PW)\2 - 1, INT(py!) + 1, 1, PH - 2, A_SOLID)
   IF hit_t > 0 THEN new_px! = px!  ' blocked: revert
 END IF
 px! = new_px!  ' commit horizontal position

 ' ---- Vertical collision ----
 ' Reset on_ground each frame; it gets set to 1
 ' only if we detect solid ground or a platform
 ' below the player's feet.
 new_py! = py! + vy!
 on_ground = 0

 IF vy! >= 0 THEN
   ' FALLING / STATIONARY: test 1px strip below
   ' the player's feet (at new_py! + PH).
   hit_t = TILEMAP(COLLISION 1, INT(px!) + (TW - PW)\2, INT(new_py!) + PH, PW, 1, A_SOLID)
   IF hit_t > 0 THEN
     ' Snap player to the top of the tile they
     ' collided with. Integer division finds the
     ' tile row, then multiply back to get the
     ' pixel-aligned landing position.
     trow = (INT(new_py!) + PH) \ TH
     new_py! = trow * TH - PH
     vy! = 0
     on_ground = 1
   ELSE
     ' No solid tile below - check one-way
     ' platforms (A_PLAT). These are only solid
     ' when the player is falling ONTO them from
     ' above, not when jumping up through them.
     hit_t = TILEMAP(COLLISION 1, INT(px!) + (TW - PW)\2, INT(new_py!) + PH, PW, 1, A_PLAT)
     IF hit_t > 0 THEN
       ' Only land if the player's feet were
       ' ABOVE the platform on the previous frame.
       ' This prevents "catching" on platforms
       ' when jumping up through them from below.
       IF INT(py!) + PH <= ((INT(new_py!) + PH) \ TH) * TH THEN
         trow = (INT(new_py!) + PH) \ TH
         new_py! = trow * TH - PH
         vy! = 0
         on_ground = 1
       END IF
     END IF
   END IF
 END IF

 IF vy! < 0 THEN
   ' JUMPING UP: test 1px strip above head.
   ' When on a ladder, skip the head-bump check
   ' so the player can climb through platforms
   ' that have ladder tiles passing through them.
   ' The map places ladder tiles in the platform
   ' row (replacing one brick) and one row above
   ' to create a climbable passage.
   IF on_ladder = 0 THEN
     hit_t = TILEMAP(COLLISION 1, INT(px!) + (TW - PW)\2, INT(new_py!), PW, 1, A_SOLID)
     IF hit_t > 0 THEN
       ' Snap to bottom of the tile above
       trow = INT(new_py!) \ TH
       new_py! = (trow + 1) * TH
       vy! = 0  ' cancel upward velocity
     END IF
   END IF
 END IF
 py! = new_py!  ' commit vertical position

 ' ---- Coin collection ----
 ' Check the tile at the player's centre point.
 ' If it has the A_COIN attribute, replace it
 ' with T_EMPTY using TILEMAP SET. This is how
 ' you modify the map at runtime - the tile is
 ' permanently removed until the map is recreated.
 hit_t = TILEMAP(TILE 1, INT(px!) + TW\2, INT(py!) + TH\2)
 IF hit_t > 0 THEN
   hit_a = TILEMAP(ATTR 1, hit_t)
   IF (hit_a AND A_COIN) THEN
     tcol = (INT(px!) + TW\2) \ TW
     trow = (INT(py!) + TH\2) \ TH
     TILEMAP SET 1, tcol, trow, T_EMPTY  ' remove coin from map
     score = score + 100
     coins_left = coins_left - 1
   END IF
 END IF

 ' ---- Camera scrolling ----
 ' The camera tracks the player horizontally,
 ' keeping them centred on screen. Clamping to
 ' [0..MAX_CAM] prevents showing empty space
 ' beyond the map edges.
 cam_x = INT(px!) - SCR_W \ 2
 IF cam_x < 0 THEN cam_x = 0
 IF cam_x > MAX_CAM THEN cam_x = MAX_CAM

 ' ---- Update sprite screen position ----
 ' The sprite's screen X = world X minus camera
 ' offset. Screen Y = world Y (no vertical
 ' scrolling in this game).
 TILEMAP SPRITE MOVE 1, INT(px!) - cam_x, INT(py!)

 ' ---- Animation via tile substitution ----
 ' TILEMAP SPRITE SET swaps the tile a sprite
 ' displays without destroying/recreating it.
 ' This is the key technique for animation:
 ' define multiple tile frames in the tileset,
 ' then cycle between them each frame.
 '
 ' The walk/climb timers keep the animation
 ' pose visible between INKEY$ repeat events.
 ' anim_count increments every frame; dividing
 ' by 6 and taking MOD 2 gives a 0/1 toggle
 ' that flips every 6 frames (the animation
 ' rate). Adjust the divisor to speed up or
 ' slow down the animation.
 anim_count = anim_count + 1
 IF walk_timer > 0 THEN walk_timer = walk_timer - 1
 IF climb_timer > 0 THEN climb_timer = climb_timer - 1
 IF on_ladder THEN
   ' Climbing animation: alternate climb frames
   IF climb_timer > 0 THEN
     IF (anim_count \ 6) MOD 2 = 0 THEN
       anim_tile = T_CLIMB1
     ELSE
       anim_tile = T_CLIMB2
     END IF
   ELSE
     anim_tile = T_CLIMB1
   END IF
 ELSE
   IF walk_timer > 0 THEN
     ' Walking: alternate walk frames every 6 game loops
     IF (anim_count \ 6) MOD 2 = 0 THEN
       anim_tile = T_WALK1
     ELSE
       anim_tile = T_WALK2
     END IF
   ELSE
     ' Standing still
     anim_tile = T_PLAYER
   END IF
 END IF
 ' Apply the selected animation tile to the sprite
 TILEMAP SPRITE SET 1, anim_tile

 ' ---- Render ----
 ' Drawing order matters! The tilemap is drawn
 ' first (background), then sprites on top,
 ' then HUD text over everything.
 ' TILEMAP DRAW arguments:
 '   1 = tilemap ID
 '   F = destination (framebuffer)
 '   cam_x, 0 = world pixel offset to start
 '              drawing from (scrolling!)
 '   0, 0 = screen destination position
 '   SCR_W, SCR_H = size of viewport
 '   -1 = draw all layers
 TILEMAP DRAW 1, F, cam_x, 0, 0, 0, SCR_W, SCR_H, -1

 ' Draw all sprites belonging to this tilemap.
 ' The 0 flag means draw at the sprite's current
 ' screen position (set by SPRITE MOVE above).
 TILEMAP SPRITE DRAW F, 0

 ' HUD overlay: score and remaining coins.
 ' TEXT is drawn directly over the tilemap/
 ' sprite layers. "LT" = left-top aligned,
 ' "RT" = right-top aligned.
 TEXT 4, 2, "SCORE:" + STR$(score), "LT", 1, 1, C_WHITE
 TEXT SCR_W - 4, 2, "COINS:" + STR$(coins_left), "RT", 1, 1, C_YELLOW

 ' Copy the completed frame from framebuffer
 ' to the visible display in one operation.
 FRAMEBUFFER COPY F, N

 ' ---- Win/lose checks ----
 ' Check AFTER rendering so the final frame
 ' is visible before switching screens.
 IF coins_left <= 0 THEN GOTO YouWin
 IF py! > MROWS * TH THEN GOTO GameOver  ' fell off bottom
LOOP

' ============================================
' End Screens
' ============================================
' Win and Game Over screens use the same
' pattern: draw to framebuffer, copy to display,
' wait for input, then either restart or exit.
' GOTO TitleScreen loops back to recreate the
' tilemap and reset all game state.

' ============================================
' You Win
' ============================================
YouWin:
FRAMEBUFFER WRITE F
CLS C_BLACK
TEXT SCR_W\2, 80, "YOU WIN!", "CM", 7, 2, C_GREEN
TEXT SCR_W\2, 130, "Score: " + STR$(score), "CM", 1, 2, C_WHITE
TEXT SCR_W\2, 170, "All coins collected!", "CM", 1, 1, C_YELLOW
TEXT SCR_W\2, 210, "SPACE=Play Again  Q=Quit", "CM", 1, 1, C_GREY
FRAMEBUFFER COPY F, N
DO : k$ = INKEY$ : LOOP UNTIL k$ = " " OR UCASE$(k$) = "Q"
IF k$ = " " THEN GOTO TitleScreen
GOTO Cleanup

' ============================================
' Game Over
' ============================================
GameOver:
FRAMEBUFFER WRITE F
CLS C_BLACK
TEXT SCR_W\2, 80, "GAME OVER", "CM", 7, 2, C_RED
TEXT SCR_W\2, 130, "Score: " + STR$(score), "CM", 1, 2, C_WHITE
TEXT SCR_W\2, 200, "SPACE=Play Again  Q=Quit", "CM", 1, 1, C_GREY
FRAMEBUFFER COPY F, N
DO : k$ = INKEY$ : LOOP UNTIL k$ = " " OR UCASE$(k$) = "Q"
IF k$ = " " THEN GOTO TitleScreen

' ============================================
' Cleanup - Release Resources
' ============================================
' Always close TILEMAP and FRAMEBUFFER before
' ending. TILEMAP CLOSE frees all sprites, maps,
' and attribute data. FRAMEBUFFER CLOSE releases
' the off-screen buffer memory.
Cleanup:
TILEMAP CLOSE
FRAMEBUFFER CLOSE
CLS
PRINT "Thanks for playing!"
PRINT "Final score: "; score
END

' ============================================
' SUBROUTINES
' ============================================

SUB GenerateTileset
 ' ================================================
 ' Procedural Tileset Generation
 ' ================================================
 ' Instead of creating tile graphics externally,
 ' we draw them programmatically using LINE, BOX,
 ' CIRCLE, and PIXEL. This approach:
 '   - Makes the game self-contained (no external
 '     image editor needed)
 '   - Lets readers see exactly how each tile is
 '     constructed
 '   - Uses only CONST colours for consistency
 '
 ' Layout: 8 tiles per row, 2 rows = 128x32 px
 '   Row 1 (y=0):  Map tiles 1-8 (grass, brick,
 '                 ladder, coin, sky, dirt,
 '                 player standing, thin platform)
 '   Row 2 (y=16): Animation-only tiles 9-12
 '                 (walk1, walk2, climb1, climb2)
 '
 ' Tiles are drawn at position (tx, ty) where:
 '   tx = tile_index_in_row * TW
 '   ty = row * TH
 '
 ' After drawing, SAVE IMAGE captures the
 ' rectangle to a BMP on the SD card.
 ' ================================================
 LOCAL tx, ty, cx, cy

 ' Clear screen to black - this becomes the
 ' background for tiles that don't fill their
 ' entire 16x16 area (e.g. stick figures).
 CLS C_BLACK

 ' Tile 1: Grass (green top, brown body)
 tx = 0 : ty = 0
 BOX tx, ty, TW, TH, 0, C_BROWN, C_BROWN
 BOX tx, ty, TW, 4, 0, C_GREEN, C_GREEN
 LINE tx, ty + 3, tx + TW - 1, ty + 3, 1, C_DGREEN

 ' Tile 2: Brick (brown with mortar lines)
 tx = TW : ty = 0
 BOX tx, ty, TW, TH, 0, C_BROWN, C_BROWN
 BOX tx, ty, TW, TH, 1, C_GREY
 LINE tx, ty + TH\2, tx + TW - 1, ty + TH\2, 1, C_GREY
 LINE tx + TW\2, ty, tx + TW\2, ty + TH\2, 1, C_GREY
 LINE tx + TW\4, ty + TH\2, tx + TW\4, ty + TH, 1, C_GREY

 ' Tile 3: Ladder (brown rails with rungs)
 tx = TW * 2 : ty = 0
 BOX tx, ty, TW, TH, 0, C_SKY, C_SKY
 LINE tx + 3, ty, tx + 3, ty + TH - 1, 1, C_BROWN
 LINE tx + TW - 4, ty, tx + TW - 4, ty + TH - 1, 1, C_BROWN
 LINE tx + 3, ty + 3, tx + TW - 4, ty + 3, 1, C_ORANGE
 LINE tx + 3, ty + 8, tx + TW - 4, ty + 8, 1, C_ORANGE
 LINE tx + 3, ty + 13, tx + TW - 4, ty + 13, 1, C_ORANGE

 ' Tile 4: Coin (yellow circle on sky)
 tx = TW * 3 : ty = 0
 BOX tx, ty, TW, TH, 0, C_SKY, C_SKY
 CIRCLE tx + TW\2, ty + TH\2, 5, 1, 1, C_YELLOW, C_YELLOW
 CIRCLE tx + TW\2, ty + TH\2, 3, 1, 1, C_ORANGE, C_ORANGE

 ' Tile 5: Sky (plain blue)
 tx = TW * 4 : ty = 0
 BOX tx, ty, TW, TH, 0, C_SKY, C_SKY

 ' Tile 6: Dirt (solid brown)
 tx = TW * 5 : ty = 0
 BOX tx, ty, TW, TH, 0, C_BROWN, C_BROWN
 PIXEL tx + 3, ty + 4, C_GREY
 PIXEL tx + 10, ty + 7, C_GREY
 PIXEL tx + 6, ty + 12, C_GREY

 ' Tile 7: Player STANDING pose (stick figure)
 ' This is the default sprite tile. The figure
 ' is drawn with LINE and CIRCLE on a black
 ' background. The black pixels become visible
 ' (not transparent) - a deliberate choice so
 ' the player is clearly visible against the
 ' bright sky/grass background.
 ' cx = horizontal centre of the tile.
 tx = TW * 6 : ty = 0
 cx = tx + 8 : cy = ty
 BOX tx, ty, TW, TH, 0, C_BLACK, C_BLACK
 CIRCLE cx, cy + 3, 2, 1, 1, C_WHITE, C_WHITE     ' head
 LINE cx, cy + 5, cx, cy + 10, 1, C_WHITE          ' body
 LINE cx, cy + 7, cx - 3, cy + 9, 1, C_WHITE      ' left arm down
 LINE cx, cy + 7, cx + 3, cy + 9, 1, C_WHITE      ' right arm down
 LINE cx, cy + 10, cx - 2, cy + 15, 1, C_WHITE    ' left leg
 LINE cx, cy + 10, cx + 2, cy + 15, 1, C_WHITE    ' right leg

 ' Tile 8: Thin platform (grey line on sky)
 tx = TW * 7 : ty = 0
 BOX tx, ty, TW, TH, 0, C_SKY, C_SKY
 BOX tx, ty, TW, 4, 0, C_GREY, C_GREY
 LINE tx, ty + 3, tx + TW - 1, ty + 3, 1, C_WHITE

 ' ---- Row 2: Sprite Animation Frames ----
 ' These tiles are NEVER placed on the map.
 ' They exist solely for TILEMAP SPRITE SET to
 ' swap between, creating animation. Each frame
 ' is a variation of the standing pose with
 ' limbs repositioned. Two frames per action
 ' (walk, climb) gives a simple oscillating
 ' animation when alternated.

 ' Tile 9: Walk frame 1
 ' Left leg forward, right arm swings forward
 ' (opposite arm/leg = natural walking motion).
 tx = 0 : ty = TH
 cx = tx + 8
 BOX tx, ty, TW, TH, 0, C_BLACK, C_BLACK
 CIRCLE cx, ty + 3, 2, 1, 1, C_WHITE, C_WHITE     ' head
 LINE cx, ty + 5, cx, ty + 10, 1, C_WHITE          ' body
 LINE cx, ty + 7, cx + 4, ty + 8, 1, C_WHITE      ' right arm forward
 LINE cx, ty + 7, cx - 3, ty + 10, 1, C_WHITE     ' left arm back
 LINE cx, ty + 10, cx - 4, ty + 15, 1, C_WHITE    ' left leg forward
 LINE cx, ty + 10, cx + 2, ty + 15, 1, C_WHITE    ' right leg back

 ' Tile 10: Walk frame 2
 ' Mirror of frame 1: right leg forward, left
 ' arm swings forward. Alternating tiles 9 and
 ' 10 creates the walking animation cycle.
 tx = TW : ty = TH
 cx = tx + 8
 BOX tx, ty, TW, TH, 0, C_BLACK, C_BLACK
 CIRCLE cx, ty + 3, 2, 1, 1, C_WHITE, C_WHITE     ' head
 LINE cx, ty + 5, cx, ty + 10, 1, C_WHITE          ' body
 LINE cx, ty + 7, cx - 4, ty + 8, 1, C_WHITE      ' left arm forward
 LINE cx, ty + 7, cx + 3, ty + 10, 1, C_WHITE     ' right arm back
 LINE cx, ty + 10, cx + 4, ty + 15, 1, C_WHITE    ' right leg forward
 LINE cx, ty + 10, cx - 2, ty + 15, 1, C_WHITE    ' left leg back

 ' Tile 11: Climb frame 1
 ' Left arm reaches up, right arm hangs down,
 ' legs spread wide for ladder grip.
 tx = TW * 2 : ty = TH
 cx = tx + 8
 BOX tx, ty, TW, TH, 0, C_BLACK, C_BLACK
 CIRCLE cx, ty + 3, 2, 1, 1, C_WHITE, C_WHITE     ' head
 LINE cx, ty + 5, cx, ty + 10, 1, C_WHITE          ' body
 LINE cx, ty + 7, cx - 3, ty + 5, 1, C_WHITE      ' left arm up
 LINE cx, ty + 7, cx + 3, ty + 9, 1, C_WHITE      ' right arm down
 LINE cx, ty + 10, cx - 3, ty + 15, 1, C_WHITE    ' left leg out
 LINE cx, ty + 10, cx + 3, ty + 15, 1, C_WHITE    ' right leg out

 ' Tile 12: Climb frame 2
 ' Arms swapped from frame 1: right arm up,
 ' left arm down. Alternating 11/12 gives a
 ' hand-over-hand climbing animation.
 tx = TW * 3 : ty = TH
 cx = tx + 8
 BOX tx, ty, TW, TH, 0, C_BLACK, C_BLACK
 CIRCLE cx, ty + 3, 2, 1, 1, C_WHITE, C_WHITE     ' head
 LINE cx, ty + 5, cx, ty + 10, 1, C_WHITE          ' body
 LINE cx, ty + 7, cx - 3, ty + 9, 1, C_WHITE      ' left arm down
 LINE cx, ty + 7, cx + 3, ty + 5, 1, C_WHITE      ' right arm up
 LINE cx, ty + 10, cx + 3, ty + 15, 1, C_WHITE    ' right leg out
 LINE cx, ty + 10, cx - 3, ty + 15, 1, C_WHITE    ' left leg out

 ' Save the tileset region to BMP on SD card.
 ' Width = TPR * TW = 8 * 16 = 128 pixels
 ' Height = TH * 2 = 32 pixels (2 rows)
 SAVE IMAGE "platform_tiles.bmp", 0, 0, TPR * TW, TH * 2
END SUB

' ============================================
' MAP DATA: 40 cols x 15 rows = 600 tile values
' ============================================
' Row 0 = top of screen, Row 14 = bottom.
' Each value is a tile index (0-8).
'
' Level design notes:
' - Row 12 is the main ground (grass). Gaps at
'   cols 11-12, 22-24, and 36-37 create pits the
'   player must jump over (or fall to death).
' - Two vertical ladders at cols 7 and 17 connect
'   ground level up through mid-level platforms.
' - A long ladder at col 29 reaches the highest
'   platform (row 3).
' - Ladder tiles REPLACE one brick in each
'   platform row (e.g. row 7 col 7 = ladder tile
'   3 instead of brick tile 2). This lets the
'   player climb through the platform.
' - An EXTRA ladder tile extends one row ABOVE
'   each platform (e.g. row 6 col 7). Without
'   this, the player would be stuck halfway into
'   the platform with no non-solid tile to step
'   off onto.
' - One-way platforms (tile 8) let the player
'   jump up through them from below but stand on
'   them when falling from above.
' - Coins (tile 4) are placed to reward
'   exploration of all routes.
'
' Legend:
'   0=empty  1=grass  2=brick  3=ladder
'   4=coin   5=sky    6=dirt   8=platform
' ============================================
mapdata:
' Row 0: sky
DATA 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5
' Row 1: sky with coins on high platforms
DATA 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5
' Row 2: upper platforms (ladder at col 29 extends above row 3 platform)
DATA 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,3,5,5,5,5,5,5,5,5,5,5
' Row 3: upper platforms (ladder at col 29 passes through)
DATA 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,2,2,3,2,2,5,5,5,5,5,5,5,5
' Row 4: sky
DATA 5,5,5,5,5,5,5,5,5,5,2,2,2,2,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5
' Row 5: mid-level platforms
DATA 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5
' Row 6: coins and platforms (ladders at col 7,17 extend above row 7 platforms)
DATA 5,5,5,5,5,5,4,3,5,5,5,5,5,5,5,5,4,3,5,5,5,5,8,8,8,8,5,5,5,3,5,5,5,5,4,4,5,5,5,5
' Row 7: mid platforms (ladders at col 7,17 pass through)
DATA 5,5,5,5,5,2,2,3,2,5,5,5,5,5,5,2,2,3,2,5,5,5,5,5,5,5,5,5,5,3,5,5,5,8,8,8,8,5,5,5
' Row 8: ladder connections
DATA 5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5
' Row 9: lower platforms with coins
DATA 5,5,5,4,5,5,5,3,5,5,4,4,4,4,5,5,5,3,5,5,5,5,5,4,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5
' Row 10: lower level
DATA 5,5,2,2,2,5,5,3,5,5,8,8,8,8,8,5,5,3,5,5,5,8,8,8,8,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5
' Row 11: near ground
DATA 5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,5,5,5,3,5,5,5,4,5,5,5,5,5,5
' Row 12: ground level (main floor with gaps)
DATA 1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1
' Row 13: underground
DATA 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6
' Row 14: underground
DATA 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6

' ============================================
' TILE ATTRIBUTES (12 tile types)
' ============================================
' One attribute value per tile type (1 through
' 12), read sequentially by TILEMAP ATTR.
' Each value is a bitmask. TILEMAP(COLLISION)
' tests attribute bits to determine what kind
' of tile the player overlaps:
'   bit 0 (1) = A_SOLID  - blocks movement
'   bit 1 (2) = A_LADDER - enables climbing
'   bit 2 (4) = A_COIN   - collectible item
'   bit 3 (8) = A_PLAT   - one-way platform
' Sprite-only tiles (9-12) have attribute 0
' since they never appear in the map.
' ============================================
tileattrs:
DATA 1      ' tile 1: grass    - A_SOLID
DATA 1      ' tile 2: brick    - A_SOLID
DATA 2      ' tile 3: ladder   - A_LADDER
DATA 4      ' tile 4: coin     - A_COIN
DATA 0      ' tile 5: sky      - passable
DATA 1      ' tile 6: dirt     - A_SOLID
DATA 0      ' tile 7: player   - none
DATA 8      ' tile 8: platform - A_PLAT
DATA 0      ' tile 9: walk 1   - none (sprite only)
DATA 0      ' tile 10: walk 2  - none (sprite only)
DATA 0      ' tile 11: climb 1 - none (sprite only)
DATA 0      ' tile 12: climb 2 - none (sprite only)
 
gadgetjack
Senior Member

Joined: 15/07/2016
Location: United States
Posts: 208
Posted: 06:51pm 11 Mar 2026
Copy link to clipboard 
Print this post

Thank you Peter. Always like to see your work.
 
Peter63
Senior Member

Joined: 28/07/2017
Location: Sweden
Posts: 153
Posted: 02:36pm 12 Mar 2026
Copy link to clipboard 
Print this post

Now I get that happy again like a penguin...
/ Peter63
 
Print this page


To reply to this topic, you need to log in.

The Back Shed's forum code is written, and hosted, in Australia.
© JAQ Software 2026