Language version 1.57.0 · docs generated 2026-08-13 · 411 keywords
Complete reference for the Crash BASIC language: every keyword, statement, and built-in function.
This file is auto-generated from docs/api-docs.json by scripts/generate-api-markdown.py. Do not edit by hand.
Contents
Control Flow
ASYNC · BOOM · BYREF · BYVAL · CALL · CALL EVENT · COOLDOWN · DEBUGGER · DECLARE · DELAY · DO · END · EVENT · EXIT · FOR · FOR EACH · FUNCTION · IF · INSTATE$ · RESOLVE · SELECT CASE · SET STATE · SET YIELD · STOP RENDER · STOP THREAD · STOP TIMER · SUB · WHILE · WITH · YIELD
Structured Programming
BYVAL · DECLARE · FUNCTION · IMPORT · NAMESPACE · SUB
Variables
Array Literal · BEGIN ENUM · BEGIN SORT · CLONE · CONST · DATA · DIM · ERASE · FALSE · MUTATION · MUTATIONCOUNT · MUTATIONPOINTX · MUTATIONPOINTY · OPEN DATA · Object Literal · PUSH · QUEUE · READ · REDIM · RESTORE · SELF · SET MUTATIONPOINT · TRUE · TYPE
Functions
ASARRAY · ASC · CHR$ · COUNT · DATE$ · DEQUEUE · EXISTS · FPS · HEX$ · INSTR · JOIN$ · LCASE$ · LEFT$ · LEN · LTRIM$ · MATCH · MID$ · OCT$ · POP · REGEX · REPLACE$ · REVERSE$ · RIGHT$ · RTRIM$ · SPACE$ · SPLIT$ · STR$ · STRING$ · TIME$ · TIMER · TRIM$ · UCASE$ · VAL · VERSION$
Math
ABS · ACOS · ASIN · ATN · ATN2 · CEIL · CLAMP · COS · EXP · FIX · FIXED · FLOOR · INT · LBOUND · LERP · LOG · LOG10 · MAG · MAX · MIN · MOD · PI · POW · RANDOMIZE · RND · ROUND · SGN · SIN · SQR · TAN · UBOUND
Bitwise
BITAND · BITCLEAR · BITNOT · BITOR · BITSET · BITTEST · BITXOR · SHL · SHR
Input
DRAGX · DRAGY · GAMEPAD · GAMEPAD ALPHA · GAMEPAD BUTTON · GAMEPAD BUTTON AT · GAMEPAD SETIMAGE · INKEY$ · INPUT · ISCONSOLE · ISMOBILE · JOYAXIS · JOYBUTTON · JOYCONNECTED · KEYDOWN · MOUSEDOWN · MOUSEX · MOUSEY · PAUSE · RUMBLE · TAP · TOUCHX · TOUCHY
Graphics
ALPHA · ASIMAGE · BEGIN FRAME · BEGIN MEMORYBUFFER · BEGIN SCENE · CIRCLE · CLS · COLOR · CREATE LAYER · CREATE MAP · DESTROY POLYGON · END FRAME · FONT · GETIMAGE · GETSCROLL · IMAGEHEIGHT · IMAGEWIDTH · LINE · LOADFONT · LOCATE · MAPTILE · MAPTILEX · MAPTILEY · MAPWORLDTILEX · MAPWORLDTILEY · NEWIMAGE · OFFSET · PALETTE · POINT · POINTRGB · POLYGON · PRINT · PSET · REGION · RESET LAYERS · ROTATE · SCALE · SCALETO · SCREEN · SCREEN TEXT · SCREENHEIGHT · SCREENWIDTH · SET CURSOR · SET IMAGE · SET LAYER · SET PALETTE · SET POLYGON · SET SCROLL · SETFONT · SHADER · TEXTTOIMAGE · TILEMAP · TOSPRITESHEET · TRANSITION · TRANSITION$ · TYPING · UPDATE MAP · WINDOW
Sprites
DISABLE SPRITE · EMITPARTICLE · ENABLE SPRITE · FOR COLLISIONS · FOR SPRITES · FRAMECOUNT · GETSPRITEMETA · HIDE SPRITE · IMAGEBBOX · MOVE SPRITE · NEXTFREESPRITE · RESERVE SPRITE · SET SPRITE · SHOW SPRITE · SPRITE RESET · SPRITEBBOX · SPRITECOLLISIONID · SPRITECOUNT · SPRITEDIRECTION · SPRITEDIST · SPRITEELEVATION · SPRITEENABLED · SPRITEHEIGHT · SPRITETAG$ · SPRITEVISIBLE · SPRITEWIDTH · SPRITEX · SPRITEY
Behaviors
AFTERCOLLISION · BEHAVIOR · HASBEHAVIOR · HASPATTERN · ONCOLLISION · PATTERN · SAVE BEHAVIOR · SAVE RIG · SET BEHAVIOR RATE · SET MUTATION BEHAVIOR · SPRITEBEHAVIORS$ · SPRITEPATTERN$ · SPRITEVELOCITYX · SPRITEVELOCITYY · THISSPRITE · VARIANCE
Collisions
BOUNDARY ADD · BOUNDARY ADD FROM · BOUNDARY CLEAR · BOUNDARY DELETE · COLLISION · ENCLOSURE (boundary modifier) · SOLID (boundary modifier) · SPRITECOLLISION · SPRITETOUCHING$ · SPRITETOUCHINGX · SPRITETOUCHINGY
Maps
FOR MAPDATA · MAPHEIGHT · MAPMETA$ · MAPWIDTH
Sound
ADD ARRANGEMENT · APPLY MUSIC · ARRANGEMENTCOUNT · ARRANGEMENTLAYER$ · ARRANGEMENTLAYERCOUNT · ARRANGEMENTPATTERN$ · ARRANGEMENTREPEAT · BEEP · CREATE MUSIC PATTERN · CREATE TRACK · DELETE ARRANGEMENT · DELETE MUSIC · DELETE MUSIC PATTERN · DELETE TRACK · FADEMUSIC · GENERATOR · INSERT ARRANGEMENT · INSTRUMENTMODE · LIPSYNC · LOADMUSIC · MUSIC · MUSICBEATS · MUSICBEATUNIT · MUSICEXISTS · MUSICLENGTH · MUSICLOOP · MUSICPATTERNCOUNT · MUSICPATTERNLENGTH · MUSICPATTERNNAME$ · MUSICPLAYING · MUSICPOSITION · MUSICTEMPO · MUTE TRACK · Music File Format · PATTERNAUDIO$ · PATTERNAUDIOVOLUME · PAUSEMUSIC · PLAY · PLAYMUSIC · RESUMEMUSIC · SET ARRANGEMENT · SET AUDIO · SET MUSIC · SET MUSIC PATTERN · SET TRACK · SETINSTRUMENTWAV · SOLO TRACK · SONGCOUNT · SONGNAME$ · SOUND · STOP PLAY · STOPMUSIC · TRACKCHANNEL · TRACKCOUNT · TRACKGENERATOR$ · TRACKINSTRUMENT · TRACKMUTED · TRACKNAME$ · TRACKNOTES$ · TRACKPAN · TRACKREPEAT · TRACKSOLOED · TRACKVOLUME
Video
LOADVIDEO · PAUSEVIDEO · PLAYVIDEO · RESUMEVIDEO · SEEKVIDEO · STOPVIDEO · WAITVIDEO
File I/O
CALL SQL · CLOSE · DESERIALIZE$ · EOF · FETCH · FILEEXISTS · FILES · ISOPEN · JMES · LINE INPUT · LOADJSON · LOADTEXT · LOC · LOF · OPEN · OPEN DATABASE · PRINT # · SAVEJSON · SAVETEXT · SERIALIZE$ · SQL
Resources
BASE64TOIMG · BASE64TOMP3 · BASE64TOWAV · FADEMP3 · FREE · GETIMAGE · LOADIMG · LOADMP3 · LOADPNG · LOADWAV · PLAYMP3 · PLAYWAV · PUTIMAGE · STOPMP3 · STOPWAV
Multiplayer
CALL HOST · CALL PLAYER · CRASHNET · CRASHNET (object) · GAMECONTEXT$ · HOST DATA · ME · OnServerBroadcast · PLAYER DATA · PLAYERS · SET ROOM · STOP HOST · STOP PLAYER · SYNC
In-App Purchase
FOR PURCHASE · PURCHASE · PURCHASE NEW · PURCHASE RESTORE
AI
ASTAR · CHOOSE · DISTANCE · FLEE · FLOODFILL · MAZE · NEAREST · NEIGHBORS · RAYCAST · SEEK · WANDER · WEIGHTED
String
Control Flow
ASYNC
Control Flow · since 1.30.0
Prefix keyword that spawns a worker thread to run the function and immediately returns a Promise value. Arguments are evaluated eagerly in the caller's scope, so changes to caller-local variables after the spawn don't leak into the worker. Works on BASIC FUNCTIONs, BASIC SUBs, and native built-ins.
The promise is consumed by RESOLVE(). Re-resolving the same promise returns the cached value (single-write, many-read). Wrong-arity / undefined-function errors halt at the ASYNC site, not deep inside a never-resolved promise.
Errors inside an async body halt the program — ASYNC is for parallelism, not error isolation.
Syntax
promise = ASYNC funcName(args)Parameters
- funcName - Any FUNCTION, SUB, or native function name
- args - Argument expressions, evaluated eagerly in the caller's scope
Examples
FUNCTION Double(n)
Double = n * 2
END FUNCTION
p = ASYNC Double(21)
result = RESOLVE(p) ' 42REM Three loads in parallel; total wall-clock = max(durations)
p1 = ASYNC LoadLevel(1)
p2 = ASYNC LoadLevel(2)
p3 = ASYNC LoadLevel(3)
levels = RESOLVE(p1, p2, p3) ' array of three values, in orderREM Parallel network calls
p = ASYNC FETCH("https://cdn.example.com/sprite.png", "GET")
CALL DoOtherStuff()
r = RESOLVE(p)
ok = BASE64TOIMG(r.MESSAGE, "sprite")See also: RESOLVE, FETCH, CALL THREAD, FUNCTION
BOOM
Control Flow · since 1.27.0
Inline guard that halts the program with a fatal runtime error when the condition is truthy. If the condition is falsy, BOOM is a silent no-op. Use it to bail out at the exact line where an impossible state is detected, instead of letting the program continue and fail somewhere downstream that's harder to trace. The message is a full expression evaluated only when BOOM fires (when the condition is falsy, the message is not evaluated, so it's safe to put expensive string-builders there). The error surfaces through the normal runtime-error path — same UI, same logs, same halt semantics — there is no catch / recover / on-error mechanism.
Syntax
BOOM <condition>, <message>Parameters
- condition - Any expression. Truthy fires BOOM; falsy is a no-op (TRUE = 1, FALSE = 0; non-zero values are truthy).
- message - Any expression, stringified at runtime. Shows up as the runtime-error message.
Examples
' Guard against impossible state
BOOM gamePhase < 0, "gamePhase went negative"' Validate inputs at function entry
SUB ApplyDamage(amount)
BOOM amount < 0, "ApplyDamage got a negative amount"
hp = hp - amount
END SUB' Inline message composition
BOOM playerSlot > MAX_PLAYERS, "slot " + STR$(playerSlot) + " exceeds max " + STR$(MAX_PLAYERS)' Single-line IF style
IF host.gamePercentageDone > 100 THEN BOOM 1, "percentage overflowed"
BYREF
Control Flow · since 1.0.3
Pass parameter by reference to SUB. This is the default behavior for SUB parameters when neither BYVAL nor BYREF is specified. Changes to the parameter inside the subroutine will modify the caller's original variable. BYREF needs a caller's variable to write back into, so it applies only when the argument IS a plain variable. A literal, an expression, an array element (rooms(1)) or a function result has nothing to write back to and is passed by value regardless of how the parameter was declared. Not allowed in FUNCTION definitions (use SUB if you need to modify caller's variables).
Syntax
BYREF parameter AS datatypeParameters
- parameter - Parameter name
- datatype - INTEGER, STRING, etc.
Examples
' BYREF allows modifying the caller's variable
SUB DoubleIt(BYREF n AS INTEGER)
n = n * 2
END SUB
value = 5
CALL DoubleIt(value)
PRINT value ' Prints 10' Classic swap example using BYREF
SUB Swap(BYREF a AS INTEGER, BYREF b AS INTEGER)
DIM temp AS INTEGER
temp = a
a = b
b = temp
END SUB
x = 10
y = 20
CALL Swap(x, y)
PRINT x; y ' Prints 20 10' Mix BYVAL and BYREF in the same SUB
SUB UpdateScore(BYVAL points AS INTEGER, BYREF total AS INTEGER)
total = total + points ' Modifies caller's total
points = 0 ' Only affects local copy
END SUB
score = 100
bonus = 50
CALL UpdateScore(bonus, score)
PRINT score ' Prints 150
PRINT bonus ' Still 50' BYREF with string variables
SUB AppendWorld(BYREF text AS STRING)
text = text + " World"
END SUB
msg$ = "Hello"
CALL AppendWorld(msg$)
PRINT msg$ ' Prints "Hello World"
BYVAL
Control Flow · since 0.23.0
Pass parameter by value (copy). In SUB, use BYVAL to override the default BYREF behavior. In FUNCTION, all parameters are implicitly BYVAL. Changes to the parameter inside the subroutine/function do not affect the caller's variable. For a reference type (an array or object), an explicit BYVAL parameter is a genuinely independent deep copy — writing to an element through the parameter cannot reach the caller's data. Note that this is the effect of writing BYVAL, not of a BYREF parameter that fell back to by-value because its argument was not a plain variable: in that case a reference-typed argument still aliases the caller's data.
Syntax
BYVAL parameter AS datatypeParameters
- parameter - Parameter name
- datatype - INTEGER, STRING, etc.
Examples
SUB Test(BYVAL x AS INTEGER)
x = x + 1 ' Changes local copy only
END SUB
a = 5
CALL Test(a)
PRINT a ' Still prints 5' SUB defaults to BYREF, use BYVAL to prevent modification:
SUB ProcessValue(BYVAL n AS INTEGER)
n = n + 1 ' Only affects local copy
END SUBSee also: BYREF, SUB, FUNCTION
CALL
Control Flow · since 0.23.0
Calls subroutines directly, schedules them for timer execution, runs them in background threads, or executes them synchronized with the behavior engine tick.
CALL RENDER is particularly important for game loops that need to be synchronized with sprite movement. Unlike CALL TIMER which runs on an independent timer, CALL RENDER callbacks execute at the end of each behavior engine tick (60 FPS), immediately after sprite positions are updated by behaviors and patterns. This eliminates timing desynchronization that can cause jitter in features like camera tracking or scroll position calculations.
Method receivers. A type-bound SUB is called on the instance it belongs to, and the receiver may be any path to that instance — a variable (CALL p.Take(3)), an array element (CALL enemies(i).Update()), a field of a type (CALL squad.leader.Hit(5)), or any depth of those combined (CALL army.squads(0).members(2).Hit(5)). The trailing argument list belongs to the method, never to the receiver. Inside the body SELF refers to the instance itself rather than a copy, so writes are visible through the same path afterwards. The async forms (TIMER / THREAD / RENDER / EVENT) take the same receivers, evaluated once at registration and captured as SELF. A FUNCTION method is called in expression position instead (IF army.squads(0).members(2).IsDead() THEN ...), with the same receiver rules.
A two-part name with no index is read as a namespaced sub first (CALL Utils.Double(5)), falling back to a method on a variable of that name.
The optional GUARD(flags) modifier restricts where the call/registration runs and/or guarantees non-overlapping invocations. Flags break down into two orthogonal kinds:
Context flags (combinable, OR-matched): SINGLE (only fires in solo / single-player launches), HOST (only when ME.ISHOST), PLAYER (only when ME.ISSERVER AND NOT ME.ISHOST), CLIENT (only when ME.ISCLIENT). List any subset; the handler fires when the current interpreter matches ANY listed context. Empty context list = no restriction.
Invocation lock: ONCE (skip overlapping invocations of this AS-id). Orthogonal to the context flags — combinable with any subset, or stand-alone.
Without FAILS, a guard violation is a silent no-op (skip the call/registration, or drop the overlapping invocation). With FAILS, the same violation halts the program with a runtime error.
Strict context matching (no solo short-circuit): SINGLE is its own context. GUARD(HOST) does NOT fire in solo — solo and multiplayer-host are different things, and writing a Free-tier solo flow that accidentally fires host-only logic was the motivating regression. To cover both solo and host, list both: GUARD(HOST, SINGLE). The intended pattern is "write a complete game using context-targeted GUARDs throughout, deploy to solo or multiplayer; the same .bas runs by adding SINGLE wherever the handler should also fire solo."
ONCE semantics by call kind:
- plain CALL — keyed by SUB name (shared across threads). Re-entrant / recursive calls into the same SUB while it is in flight are skipped (or fail).
- CALL THREAD — registration-time check: refuse to respawn if a thread with this ID is already running.
- CALL TIMER / RENDER / EVENT / PLAYER / HOST — per-invocation lock keyed by AS-id; overlapping ticks / firings / handler invocations are dropped.
The parentheses are part of the call, empty or not: CALL Restart(), not CALL Restart. This is true of every form — plain, method, TIMER, THREAD and RENDER.
Syntax
CALL subroutine_name (arguments) [GUARD(flags) [FAILS]]
CALL receiver.Method (arguments) [GUARD(flags) [FAILS]]
CALL TIMER subroutine_name (arguments) [GUARD(flags) [FAILS]] EVERY time_interval time_unit [LOCAL] AS "timer_id"
CALL TIMER subroutine_name (arguments) [GUARD(flags) [FAILS]] AFTER time_interval time_unit [LOCAL] AS "timer_id"
CALL THREAD subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "thread_id"
CALL RENDER subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "render_id"Parameters
- subroutine_name - Name of SUB to call
- arguments - Values to pass to subroutine parameters. The parentheses are required even when there are none: CALL DoThing()
- time_interval - Numeric value for timer delay/interval
- time_unit - SECOND, SECONDS, MILLISECONDS, or MINUTES
- timer_id - String identifier for managing the timer
- thread_id - String identifier for managing the thread
- render_id - String identifier for managing the render callback
- GUARD(flags) - Optional registration / invocation guard. Context flags (combinable, OR-matched): SINGLE (solo only), HOST, PLAYER, CLIENT. Lock flag: ONCE (no-overlap invocation lock). Combine with comma: GUARD(HOST, SINGLE) or GUARD(HOST, ONCE) or GUARD(HOST, SINGLE, ONCE).
- FAILS - Optional. Turns a guard violation into a runtime error instead of a silent no-op.
Examples
CALL PrintMessage("Hello World")CALL DrawBox(10, 10, 50, 50, 15)' Plain CALL with GUARD(HOST, SINGLE) — fires both on the multiplayer host AND in solo, but never on player-server or client.
CALL InitWorldState() GUARD(HOST, SINGLE)' Plain CALL with GUARD(ONCE) FAILS — refuse re-entrant SaveGame and halt loudly if it happens.
CALL SaveGame() GUARD(ONCE) FAILSCALL TIMER GameUpdate() EVERY 1 SECOND AS "gameLoop"CALL TIMER ShowMessage() AFTER 30 SECONDS AS "reminder"CALL THREAD LoadResources() AS "loader"CALL RENDER DrawHUD(playerHealth, score) AS "hud"' Use CALL RENDER for camera tracking to avoid jitter:
CALL RENDER GameLoop() AS "gameloop"
SUB GameLoop()
' This runs synchronized with sprite movement
scrollX = INT(SPRITEX(0) - 320)
IF scrollX < 0 THEN scrollX = 0
CALL RenderGame()
END SUB' GUARD(HOST, SINGLE, ONCE) FAILS — fires on the multiplayer host AND in solo, with an invocation lock that halts loudly if a tick overruns.
CALL TIMER WorldStep() GUARD(HOST, SINGLE, ONCE) FAILS EVERY 100 MILLISECONDS AS "world"' GUARD(CLIENT, SINGLE) — render only on the local client interpreter (multiplayer) or in solo. Player-server and host don't render here.
CALL RENDER DrawHUD() GUARD(CLIENT, SINGLE) AS "hud"' Type-bound methods: the receiver can be any path to the instance.
TYPE Unit
hp AS INTEGER
END TYPE
SUB Unit::Hit(n)
SELF.hp = SELF.hp - n
END SUB
FUNCTION Unit::IsDead()
IsDead = SELF.hp <= 0
END FUNCTION
TYPE Squad
members(0 TO 2) AS Unit
leader AS Unit
END TYPE
DIM squad AS Squad
squad.leader.hp = 10
squad.members(2).hp = 10
CALL squad.leader.Hit(3) ' field of a type
CALL squad.members(2).Hit(10) ' array field element
PRINT squad.leader.hp ' 7 — SELF wrote through the path
IF squad.members(2).IsDead() THEN PRINT "down"See also: SUB, STOP TIMER, STOP THREAD, STOP RENDER, DECLARE, BEHAVIOR, PATTERN, ME, CRASHNET
CALL EVENT
Control Flow · since 1.0.4
Registers a subroutine to be executed when a named event is triggered. Events enable decoupled communication between different parts of your program. If called with empty parentheses, the handler accepts dynamic arguments from the event source (e.g., behavior frame events pass sprite_id and frame_number). Event handlers run asynchronously in their own threads, so they do not block the main program or tick loop. Multiple events of the same type can run concurrently. Use STOP THREAD "event_name" to stop all running handlers for that event.
In CrashNet multiplayer, the same registration mechanism handles both in-process EVENT triggers AND cross-the-wire EVENT SERVER "name"(args) / EVENT HOST "name"(args) signals from clients. When invoked via a wire signal, the handler's parameter count must match the args sent — mismatch is a fatal runtime error. Register the handler on the side that should receive it: a player-server handler registered in the player-server interpreter receives EVENT SERVER from that player's client; a host-side handler receives EVENT HOST.
The optional GUARD(flags) modifier (see CALL) restricts where the handler is registered (HOST/PLAYER/CLIENT) and/or guarantees that overlapping firings of the same event don't run in parallel (ONCE — per-event invocation lock). Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.
Syntax
CALL EVENT subroutine_name (arguments) [GUARD(flags) [FAILS]] [LOCAL] AS "event_name"Parameters
- subroutine_name - Name of SUB to execute when event is triggered
- arguments - Values to pass to the subroutine (captured at registration time). If empty parens (), accepts dynamic args from event source
- event_name - String identifier for the event
Examples
DECLARE SUB OnPlayerWin()
CALL EVENT OnPlayerWin() AS "player_victory"
' Later in the game...
IF score >= 1000 THEN
EVENT "player_victory"
END IF
SUB OnPlayerWin()
PRINT "Congratulations! You won!"
PLAY "victory.wav"
END SUBDECLARE SUB ShowMessage(text AS STRING)
CALL EVENT ShowMessage("Game Over!") AS "game_end"
CALL EVENT ShowMessage("Level Complete!") AS "level_win"
IF lives <= 0 THEN
EVENT "game_end"
ELSE
EVENT "level_win"
END IF
SUB ShowMessage(text AS STRING)
PRINT text
END SUB' Multiple events can trigger the same handler
DECLARE SUB UpdateDisplay()
CALL EVENT UpdateDisplay() AS "score_changed"
CALL EVENT UpdateDisplay() AS "lives_changed"
CALL EVENT UpdateDisplay() AS "level_changed"
' Trigger from anywhere in your program
score = score + 100
EVENT "score_changed"
SUB UpdateDisplay()
LOCATE 1, 1
PRINT "Score:"; score; " Lives:"; lives
END SUB' Dynamic args from behavior frame events
SUB OnAnimFrame(sprite_id, frame)
PRINT "Sprite"; sprite_id; "hit frame"; frame
END SUB
CALL EVENT OnAnimFrame() AS "PLAYER_FRAME" ' Empty parens = accept dynamic args from ANIMATE "player" EVENTSON' CrashNet: same handler receives client EVENT SERVER signals
' Register on the player-server side. The captured arg values are placeholders;
' actual values come from the client EVENT SERVER call.
CALL EVENT OnFire(0, 0) AS "Fire"
SUB OnFire(x, y)
' Authoritative: validate range, apply damage, etc.
END SUB
' Then on the client (in IF ME.ISCLIENT):
' EVENT SERVER "Fire" (MOUSEX, MOUSEY)' Stop all running handlers for an event
STOP THREAD "PLAYER_FRAME" ' Stops all PLAYER_FRAME event handlersSee also: EVENT, SUB, DECLARE, BEHAVIOR, STOP THREAD, CRASHNET, ME
COOLDOWN
Control Flow · since 1.18.1
Executes the enclosed code at most once per ticks / resolution seconds of wall-clock time, rate-limiting code inside a fast game loop. resolution is optional and defaults to 60, so COOLDOWN N means once per N/60 seconds (one tick = 1/60 s) — the same as before. Pass a larger resolution for finer, sub-tick pacing: COOLDOWN 1, 240 runs at most once per 1/240 second (about 240 Hz). The first execution always runs immediately. Each COOLDOWN block is tracked independently by its position in the source. Timing is measured against a monotonic wall clock, so a block paces the same regardless of how fast the surrounding loop spins.
Syntax
COOLDOWN ticks [, resolution]
statements
END COOLDOWNParameters
- ticks - How many resolution-units to wait before the block may run again
- resolution - Optional units-per-second (default 60). The block runs at most once per ticks/resolution seconds
Examples
' Fire bullets at most once per second
DO
COOLDOWN 60
CALL FireBullet()
END COOLDOWN
YIELD
LOOP' Spawn enemies every 3 seconds
DO
COOLDOWN 180
CALL SpawnEnemy()
END COOLDOWN
CALL UpdateGame()
YIELD
LOOP' Pace a render block to ~120 Hz from a loop that spins much faster
DO
COOLDOWN 1, 120
CALL DrawFrame()
END COOLDOWN
CALL UpdateSimulation()
YIELD
LOOP' Multiple cooldowns in same loop
DO
COOLDOWN 30 ' 2x per second
CALL UpdateAnimation()
END COOLDOWN
COOLDOWN 60 ' 1x per second
CALL CheckCollisions()
END COOLDOWN
YIELD
LOOP
DEBUGGER
Control Flow · since 1.0.0
Triggers a breakpoint in the debugger, similar to JavaScript's debugger statement. When a debugger is attached and the program reaches this statement, execution will pause and enter debug mode, allowing inspection of variables and stepping through code. If no debugger is attached, this statement is a no-op and execution continues normally.
Syntax
DEBUGGERExamples
' Break unconditionally for inspection
x = CalculateValue()
DEBUGGER
PRINT x' Conditional debugging
IF score < 0 THEN
DEBUGGER ' Something went wrong
END IF' Debug a loop iteration
FOR i = 1 TO 100
IF i = 50 THEN DEBUGGER ' Inspect at iteration 50
NEXTSee also: STOP, END
DECLARE
Control Flow · since 0.23.0
Forward declares a SUB or FUNCTION so its signature is known up front. It is optional for ordinary calls: module-level FUNCTION/SUB definitions are auto-hoisted, so you can call them before they appear in the source — including mutual recursion and a 'main at the top, helpers below' layout — with or without DECLARE. DECLARE remains useful for documenting an interface and for QuickBASIC compatibility. Only module-level definitions (top level and inside NAMESPACE blocks) are hoisted; a FUNCTION/SUB nested inside a control-flow block (IF/WHILE/FOR/DO) registers when execution reaches it, so define those before use.
The parentheses are required, empty or not: DECLARE SUB Restart(), not DECLARE SUB Restart.
Syntax
DECLARE SUB name(parameters)
DECLARE FUNCTION name(parameters) [AS datatype]Parameters
- name - Subroutine or function name
- parameters - Parameter list matching the actual definition. The parentheses are required even for a routine that takes none: DECLARE SUB Restart()
Example
DECLARE FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
DECLARE SUB PrintMessage(BYVAL msg AS STRING)
PRINT Add(5, 3)
CALL PrintMessage("Test")
FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
Add = a + b
END FUNCTION
SUB PrintMessage(BYVAL msg AS STRING)
PRINT msg
END SUB
DELAY
Control Flow · since 1.0.0
Pauses program execution until a time or event condition is met. With a numeric argument, waits that many milliseconds. With UNTIL event$, blocks until the next time that named event fires via the EVENT statement (broadcast wake — every thread waiting on the same event resumes). Only firings that occur after the DELAY UNTIL begins count; earlier firings are not buffered. An optional TIMEOUT clause resumes execution after the given number of milliseconds even if the event never fires.
Syntax
DELAY milliseconds
DELAY UNTIL event$ [TIMEOUT milliseconds]Parameters
- milliseconds - Number of milliseconds to pause (must be >= 0)
- event$ - Name of an event that will be fired by an EVENT statement
- TIMEOUT milliseconds - Optional maximum wait time for DELAY UNTIL
Examples
PRINT "Starting..."
DELAY 1000
PRINT "Done!"' Animation with delays
FOR frame = 1 TO 10
CIRCLE (100 + frame * 10, 100), 5, 14
DELAY 100 ' 100ms between frames
NEXT frame' Sync to a pattern event
DELAY UNTIL "beat"
PRINT "Hit on the beat!"' Wait for event, but cap the wait at 500ms
DELAY UNTIL "playerReady" TIMEOUT 500
PRINT "Continuing either way."See also: YIELD, CALL TIMER, EVENT, CALL EVENT, FOR
DO
Control Flow · since 0.23.0
Loop with flexible condition testing at start or end.
Syntax
DO [WHILE condition]
statements
LOOP [WHILE condition]
DO [UNTIL condition]
statements
LOOP [UNTIL condition]Parameters
- condition - Expression to test for loop continuation
Examples
DO WHILE x < 10
PRINT x
x = x + 1
LOOPDO
INPUT "Continue? (Y/N): ", ans$
LOOP UNTIL UCASE$(ans$) = "N"
END
Control Flow · since 0.23.0
Terminates program execution cleanly without error dialog.
Syntax
ENDExample
PRINT "Program finished"
ENDSee also: STOP, SYSTEM
EVENT
Control Flow · since 0.23.0
Trigger a registered event. The optional target tells the runtime who runs the handler — and that "who" cascades into where authority lives, which routing the call takes, and which interpreter context the handler executes in.
In-process (the default)
EVENT "button_click"Fires the matching CALL EVENT handler in the current interpreter, asynchronously on a background thread. No network involvement, no targeting — the right tool for single-interpreter dispatch, state-machine transitions, internal event buses, debounced UI, etc.
Client → server-side (only callable from a local-client interpreter)
| Form | Where it fires |
|---|---|
EVENT SERVER "name" (args) |
this player's player-server (their own slot) |
EVENT HOST "name" (args) |
the host (slot 0) |
Use these when the client wants the server to make a decision. Client says "I pressed jump"; the server-side handler runs the authoritative reaction — position update, sound trigger, score change.
Server → server-side (callable from host or player-server)
| Form | Target | Authority |
|---|---|---|
EVENT SERVER n "name" (args) |
slot n's player-server | host can target any n; a player-server can only target its own slot |
EVENT SERVERS "name" (args) |
every player-server | host-only |
EVENT SERVERS ROOM "R" "name" (args) |
every player-server whose player is in room R | host-only |
For server-side fan-out — round-end ticks, per-room game-state changes, cross-slot coordination.
Server → client (callable from host or player-server)
| Form | Target | Authority |
|---|---|---|
EVENT CLIENT n "name" (args) |
slot n's local-client | host can target any n; a player-server can only target its own slot |
EVENT CLIENTS "name" (args) |
every connected client | host-only |
EVENT CLIENTS ROOM "R" "name" (args) |
every client whose player is in room R | host-only |
Server tells clients to do client-side things — render an animation, play a sound, show a notification. This is the right channel for anything in the "server wants the player to see X" bucket. Rendering and audio are client-side, period.
Authority
host > player-server > local-client. Cross-slot writes from a non-host server interpreter are a fatal runtime error pointing you at EVENT HOST (the right channel for client→host coordination). Broadcast and room-scoped variants from non-host interpreters are also fatal. The runtime won't let you accidentally write code that works on the host and errors elsewhere.
Arity must match
The matching CALL EVENT handler's parameter count must match the call exactly. A handler registered as CALL EVENT MyHandler(0, 0) AS "Foo" expects exactly two args on any EVENT ... "Foo" — extras or shortfalls are a fatal runtime error.
Args that travel over the wire
Cross-the-wire EVENTs (every form except the in-process default) accept these arg types: Integer, Float, String, Coordinate, Mutation (serialized as a coordinate sequence), Array (1D only), and Object of those types. Pass anything else and the runtime fails loudly with the offending argument's index — function values, multi-dimensional arrays, and Range are the usual suspects.
Single-player passthrough
Cross-the-wire EVENTs are a silent no-op in single-player — there's no other interpreter to deliver to. Intentional: the same .bas runs in both modes, so you can author the network-aware path without sprinkling IF ME.ISHOST THEN everywhere just to dodge runtime errors at startup.
Syntax
EVENT [<target>] "event_name" [(arg1, arg2, ...)]
target ::= SERVER | SERVER n | SERVERS | SERVERS ROOM "name"
| HOST
| CLIENT n | CLIENTS | CLIENTS ROOM "name"Parameters
- Optional. See description for each form's routing + authority. Absent = in-process synchronous-style dispatch in the calling interpreter. - n - Slot number (0..255). Required for EVENT SERVER n / EVENT CLIENT n. The host is slot 0.
- "R" - Room name (string expression). For ROOM variants, only player-servers / clients whose ME.ROOM$ matches receive the event.
- event_name - String identifier of the event to trigger (must match a registered CALL EVENT handler in the target context).
- (arg1, arg2, ...) - Optional positional arguments. For in-process EVENT, passed as dynamic args to a handler registered with empty parens. For cross-the-wire forms, the handler's arity must match exactly.
Examples
' In-process — single-interpreter dispatch (the default)
DECLARE SUB PlaySound()
CALL EVENT PlaySound() AS "button_click"
EVENT "button_click"
SUB PlaySound()
BEEP
END SUB' Client → its own player-server (CrashNet)
IF ME.ISCLIENT AND KEYDOWN("SPACE") THEN
EVENT SERVER "Jump" (MOUSEX, MOUSEY)
END IF
IF ME.ISSERVER AND NOT ME.ISHOST THEN
CALL EVENT OnJump(0, 0) AS "Jump"
END IF
SUB OnJump(x, y)
' Authoritative reaction on the player-server
END SUB' Client → host (authoritative)
IF ME.ISCLIENT AND KEYDOWN("B") THEN
EVENT HOST "Build" ("barracks", MOUSEX, MOUSEY)
END IF
IF ME.ISHOST THEN
CALL EVENT OnBuild("", 0, 0) AS "Build"
END IF
SUB OnBuild(kind$, x, y)
' Host-side validation + state mutation
HOST.buildings = HOST.buildings + 1
END SUB' Host → all clients (broadcast — system message overlay)
IF ME.ISHOST THEN
EVENT CLIENTS "GameStart" ("Round 1 — fight!")
END IF
SUB OnGameStart(message$)
IF ME.ISCLIENT THEN LOCATE 1, 1: PRINT message$
END SUB
CALL EVENT OnGameStart("") AS "GameStart"' Host → one specific client (private notification)
IF ME.ISHOST AND playerWon THEN
EVENT CLIENT winnerSlot "Victory" ("You win!")
END IF' Host → all clients in a room (room-scoped announcement)
IF ME.ISHOST THEN
EVENT CLIENTS ROOM "dungeon" "BossSpawn" ("Dragon appears!")
END IF' Host → all player-servers (server-side fan-out)
IF ME.ISHOST THEN
EVENT SERVERS "RoundEnd" (HOST.roundNumber)
END IF
IF ME.ISSERVER AND NOT ME.ISHOST THEN
CALL EVENT OnRoundEnd(0) AS "RoundEnd"
END IF
SUB OnRoundEnd(round)
' Each player-server runs its own end-of-round logic
END SUBSee also: CALL EVENT, SUB, STOP THREAD, CRASHNET, ME
EXIT
Control Flow · since 0.23.0
Exits early from loops or procedures.
Syntax
EXIT FOR
EXIT DO
EXIT WHILE
EXIT SUB
EXIT FUNCTIONExamples
FOR I = 1 TO 100
IF I = 50 THEN EXIT FOR
PRINT I
NEXTDO
INPUT "Enter number: ", n
IF n = 0 THEN EXIT DO
LOOPSee also: FOR, DO, WHILE, SUB, FUNCTION
FOR
Control Flow · since 0.23.0
Repeats a block of code a specific number of times.
Syntax
FOR variable = start TO end [STEP increment]
statements
NEXT [variable]Parameters
- variable - Loop counter
- start - Initial value
- end - Final value
- increment - Step amount (default: 1)
Examples
FOR I = 1 TO 10
PRINT I
NEXT IFOR X = 0 TO 320 STEP 10
PSET (X, 100), 15
NEXTSee also: FOR EACH, WHILE, DO, EXIT
FOR EACH
Control Flow · since 1.12.0
Iterates over all elements of an array or all entries of an object. The single-variable form sets the loop variable to each value. The two-variable form sets the first variable to the key (string for objects, 0-based index for arrays) and the second to the value. The collection is snapshot at loop start, so modifications during iteration are safe. Supports EXIT FOR to break out early.
Syntax
FOR EACH value IN collection
statements
NEXT
FOR EACH key, value IN collection
statements
NEXTParameters
- value - Loop variable set to each element value
- key - (Optional) Loop variable set to the key (object property name or array index)
- collection - Array or object expression to iterate over
Examples
DIM scores(3) AS INTEGER
scores(0) = 10
scores(1) = 20
scores(2) = 30
total = 0
FOR EACH s IN scores
total = total + s
NEXT
PRINT total ' 60names = ["Alice", "Bob", "Charlie"]
FOR EACH name IN names
PRINT name
NEXT' Iterate object values
player = {"HP": 100, "MP": 50, "STR": 12}
FOR EACH stat IN player
PRINT stat
NEXT' Iterate object key-value pairs
player = {"HP": 100, "MP": 50}
FOR EACH name, value IN player
PRINT name; " = "; value
NEXT' Array with index
scores = [100, 85, 92]
FOR EACH i, score IN scores
PRINT "Player"; i; ": "; score
NEXT
FUNCTION
Control Flow · since 0.23.0
Defines a function that returns a value when called. All parameters are always pass-by-value (BYVAL) to prevent side effects. BYREF is not allowed in FUNCTION — use SUB if you need to modify the caller's variables.
Type-bound methods. Qualify the name with Type:: to bind the function as a method of a user TYPE. Inside the body, SELF is an implied reference to the instance, and you return as usual (Method = value or RETURN value). Call a function method in expression position: x = obj.Method(), IF obj.Method() THEN ..., or on array elements (x = enemies(i).GetHP()).
Syntax
FUNCTION name [(parameters)] [AS datatype]
statements
name = return_value
END FUNCTION
FUNCTION Type::Method [(parameters)] [AS datatype] ' type-bound method
Method = return_value
END FUNCTIONParameters
- name - Function name
- parameters - Optional parameter list (BYVAL only, BYREF not allowed)
- datatype - Return type (INTEGER, STRING, etc.)
Examples
FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
Add = a + b
END FUNCTION
result = Add(5, 3)FUNCTION GetPlayerName$() AS STRING
INPUT "Enter name: ", n$
GetPlayerName$ = n$
END FUNCTION' Parameters are always passed by value in FUNCTION
FUNCTION IncrementAndReturn(n AS INTEGER) AS INTEGER
n = n + 1 ' Only affects local copy
IncrementAndReturn = n
END FUNCTION
value = 5
result = IncrementAndReturn(value)
PRINT value ' Still 5 (not modified)
PRINT result ' Prints 6See also: SUB, DECLARE, BYVAL, TYPE, SELF
IF
Control Flow · since 0.23.0
Executes different code blocks based on conditions.
Syntax
IF condition THEN statement [ELSE statement]
IF condition THEN
statements
[ELSE
statements]
END IFParameters
- condition - Expression that evaluates to true/false
- statement - Code to execute
Examples
IF x > 10 THEN PRINT "Large"IF score >= 90 THEN
PRINT "Grade: A"
ELSE
PRINT "Grade: B"
END IFSee also: ELSEIF, SELECT CASE
INSTATE$
Control Flow · since 0.23.0
Returns 1 if the current state should continue executing, 0 if the state needs to advance and the subroutine should willingly exit loops and return. Takes no arguments.
Syntax
INSTATE$Examples
SUB GameLoop()
DO
CALL UpdateGame()
CALL DrawScreen()
IF NOT INSTATE$ THEN EXIT DO ' Exit if state should advance
LOOP
END SUBFOR i = 1 TO 1000
CALL ProcessItem(i)
IF NOT INSTATE$ THEN EXIT FOR ' Allow state transition
NEXT iSUB LongRunningTask()
FOR step = 1 TO 10000
CALL DoWork(step)
' Check if state should advance
IF NOT INSTATE$ THEN
CALL Cleanup()
EXIT FOR
END IF
NEXT step
END SUB
RESOLVE
Control Flow · since 1.30.0
Blocks the current thread until the promise (or every promise) completes, then returns the resolved value(s). One promise → the value as-is (an async that returned an array stays an array). Multiple promises → an array of values in declaration order. Re-resolving the same promise returns the cached value, so you can ask twice without re-running the async work.
Fatal runtime error if any argument isn't a Promise, or if no arguments are given. If an async function inside the chain failed with a runtime error, the program halts (async errors are not isolated to the promise — see ASYNC).
Syntax
value = RESOLVE(promise)
values = RESOLVE(p1, p2, ..., pN)Parameters
- promise(s) - One or more Promise values returned by ASYNC. Other types halt loud.
Examples
p = ASYNC ComputeStuff()
result = RESOLVE(p)REM Multiple promises → array result
p1 = ASYNC FetchA()
p2 = ASYNC FetchB()
results = RESOLVE(p1, p2)
a = results(0)
b = results(1)REM Re-resolve returns the cached value
p = ASYNC Slow()
first = RESOLVE(p)
second = RESOLVE(p) ' same value, no second wait
SELECT CASE
Control Flow · since 0.23.0
Multi-way branching based on expression value. Each CASE clause can match a single value, multiple comma-separated values, a range (value TO value), or a comparison (IS followed by a comparison operator and value).
Syntax
SELECT CASE expression
CASE value1, value2, ...
statements
CASE value3 TO value4
statements
CASE IS > value5
statements
CASE ELSE
statements
END SELECTParameters
- expression - Value to compare against cases
- CASE value - Matches a single value
- CASE value1, value2, ... - Matches any of the listed values
- CASE value1 TO value2 - Matches any value in the range (inclusive)
- CASE IS
value - Matches using a comparison operator (=, <, >, <=, >=, <>)
Examples
SELECT CASE grade
CASE 90 TO 100
PRINT "A"
CASE 80 TO 89
PRINT "B"
CASE ELSE
PRINT "C"
END SELECTSELECT CASE direction$
CASE "N", "S"
PRINT "Vertical"
CASE "E", "W"
PRINT "Horizontal"
CASE ELSE
PRINT "Unknown"
END SELECTSELECT CASE score
CASE IS >= 100
PRINT "High score!"
CASE IS > 50
PRINT "Not bad"
CASE ELSE
PRINT "Try again"
END SELECTSee also: IF, CASE
SET STATE
Control Flow · since 0.23.0
Changes the current program state for state machine-based control flow. The transition to the next state occurs when the current state SUB exits. If no state is queued, program flow continues normally after the SUB completes.
Syntax
SET STATE "state_name"Parameters
- state_name - String identifier matching an existing SUB name
Examples
SET STATE "MenuState"
SET STATE "PlayingState"
SET STATE "GameOverState"' State-based game with matching SUB names
DECLARE SUB MenuState()
DECLARE SUB PlayingState()
DECLARE SUB GameOverState()
' Start with menu state
SET STATE "MenuState"
SUB MenuState()
DO
PRINT "Main Menu - Press S to start"
keyPressed$ = INKEY$
IF keyPressed$ = "s" THEN
SET STATE "PlayingState"
EXIT DO
END IF
IF INSTATE$=0 THEN EXIT DO
DELAY 1
LOOP
END SUB
SUB PlayingState()
DO
PRINT "Playing... Health:"; playerHealth
IF playerHealth <= 0 THEN
SET STATE "GameOverState"
EXIT DO
END IF
IF INSTATE$=0 THEN EXIT DO
DELAY 1
LOOP
END SUB
SUB GameOverState()
PRINT "Game Over!"
END SUBSee also: INSTATE$
SET YIELD
Control Flow
Sets how long YIELD pauses, in milliseconds. The default is 16 (~60fps), which keeps an unpaced game loop from free-running and flooding the graphics command stream on devices whose renderer can't drain it that fast. SET YIELD 1 (or 0) frees the loop to run as fast as the machine allows — do this only when you pace your own rendering, for example with COOLDOWN, or motion may run far faster than intended. The setting persists until changed and applies to every subsequent YIELD.
Syntax
SET YIELD millisecondsParameters
- milliseconds - How long YIELD pauses. 16 is the default (~60fps); 1 is ultrafast/near-free-running
Examples
' Opt into a free-running loop, but pace rendering with COOLDOWN
SET YIELD 1
DO
COOLDOWN 1, 60
CALL DrawFrame()
END COOLDOWN
CALL UpdateSimulation()
YIELD
LOOP' Restore the default ~60fps cadence
SET YIELD 16See also: YIELD, COOLDOWN, DELAY
STOP RENDER
Control Flow · since 0.23.0
Stops render callback execution. Without render_id, stops the current render callback.
Syntax
STOP RENDER ["render_id"]Parameters
- render_id - String identifier of render callback to stop (optional)
Examples
STOP RENDER "hud"STOP RENDER ' Stop current render contextSee also: CALL RENDER, STOP TIMER, STOP THREAD
STOP THREAD
Control Flow · since 0.23.0
Terminates background thread execution. Without thread_id, stops the current thread. Also stops event handlers registered via CALL EVENT - use the event name directly (e.g., STOP THREAD "PLAYER_FRAME" stops all running PLAYER_FRAME handlers). When multiple handlers of the same event are running concurrently, this stops all of them.
Syntax
STOP THREAD ["thread_id"]Parameters
- thread_id - String identifier of thread or event to stop (optional)
Examples
STOP THREAD "loader"STOP THREAD ' Stop current thread contextSTOP THREAD "PLAYER_FRAME" ' Stop all running PLAYER_FRAME event handlersSee also: CALL THREAD, CALL EVENT, STOP TIMER, STOP RENDER
STOP TIMER
Control Flow · since 0.23.0
Stops scheduled timer execution. Without timer_id, stops the current timer.
Syntax
STOP TIMER ["timer_id"]Parameters
- timer_id - String identifier of timer to stop (optional)
Examples
STOP TIMER "gameLoop"STOP TIMER ' Stop current timer contextSee also: CALL TIMER, STOP THREAD, STOP RENDER
SUB
Control Flow · since 0.23.0
Defines a subroutine that can be called with CALL statement. Parameters default to BYREF (pass by reference) unless BYVAL is specified. Literals are always passed by value regardless of mode. Use BYVAL when you want to protect the caller's variables from modification.
Type-bound methods. Qualify the name with Type:: to bind the sub as a method of a user TYPE. Inside the body, SELF is an implied reference to the instance. Invoke it with CALL obj.Method(args) — including on array elements (CALL enemies(i).Method()) and in the async forms (CALL THREAD obj.Method() AS "id"). A method behaves exactly like any other sub otherwise; the only addition is SELF.
Syntax
SUB name [(parameters)]
statements
END SUB
SUB Type::Method [(parameters)] ' type-bound method
statements
END SUBParameters
- name - Subroutine name
- parameters - Optional parameter list with BYVAL/BYREF modifiers
Examples
SUB PrintMessage(BYVAL msg AS STRING)
PRINT "Message: "; msg
END SUB
CALL PrintMessage("Hello")SUB DrawBox(x1, y1, x2, y2, color)
LINE (x1, y1)-(x2, y2), color, B
END SUBscore = 100 ' Global variable
SUB UpdateScore(points)
score = score + points ' Accesses global
PRINT "New score:"; score
END SUB
CALL UpdateScore(50)
PRINT "Final score:"; score ' Prints 150' Using BYREF to modify caller's variable
SUB Increment(BYREF n AS INTEGER)
n = n + 1
END SUB
counter = 0
CALL Increment(counter)
PRINT counter ' Prints 1' Swap two values using BYREF
SUB Swap(BYREF a AS INTEGER, BYREF b AS INTEGER)
DIM temp AS INTEGER
temp = a: a = b: b = temp
END SUB
x = 10: y = 20
CALL Swap(x, y)
PRINT x; y ' Prints 20 10See also: FUNCTION, DECLARE, CALL, BYVAL, BYREF, TYPE, SELF
WHILE
Control Flow · since 0.23.0
Repeats code while a condition remains true.
Syntax
WHILE condition
statements
WENDParameters
- condition - Expression to test each iteration
Example
INPUT "Enter number: ", num
WHILE num < 1 OR num > 10
PRINT "Invalid!"
INPUT "Enter number: ", num
WEND
WITH
Control Flow · since 1.29.0
WITH ... END WITH is the context primitive — step into a temporary scope, do some stuff, and the moment you leave the block (normal exit, EXIT FOR/DO, RETURN, or even a halting error) everything snaps back to how it was. Four flavors, split into two camps:
Ambient context (LAYER, ROOM) — pushes a frame onto an internal stack, the body reads from it, the frame pops on exit. Pure save-and-restore.
Compile-time hoisting (PLAYER, CLIENT) — the AST builder lifts the body out into a synthetic top-level SUB, auto-registers it as an event handler via CALL EVENT, and replaces the WITH site with the appropriate EVENT fire. WITH PLAYER becomes EVENT SERVER <slot> (ships to that player's PLAYER-SERVER interpreter); WITH CLIENT becomes EVENT CLIENTS-with-contextual-routing (broadcast from host, own-slot from player-server). Same .crashcart runs on every interpreter, so the synthetic SUB is already registered everywhere by the time the event lands.
`WITH LAYER n`
Block-scoped SET LAYER. The current draw layer becomes n for the body of the block, then snaps back to whatever it was before. No more save-the-old-value / do-the-work / restore-it boilerplate. Negative values clamp to 0 (matching what SET LAYER already tolerates).
WITH LAYER 3
CLS 0
PRINT "HUD"
END WITH
' previous layer restored automatically`WITH ROOM "name"`
Every tilemap, sprite, and layer registration inside the block gets tagged with that room. The host's CREATE MAP / UPDATE MAP ships broadcasts that only reach clients whose ME.ROOM matches. Late-join replay walks the per-room snapshot. Single-player gets level-switching for free — WITH ROOM "level1" to set things up, later SET ROOM "level1" and the engine auto-mounts everything tagged level1. No manual destroy-and-recreate.
`WITH PLAYER n SEND (args)`
Ship the block body to player n's PLAYER-SERVER interpreter (the slot-N server-side runner) for one-shot execution. Server-authoritative state changes — SET ROOM, mutating PLAYERS(n).<field>, calling HOST.<field>, etc. — are legal inside because player-server is the right interpreter to make those changes "as that player." The compiler wraps the block in a synthetic SUB __with_anon_N(args), registers it as an event handler, and replaces the WITH site with EVENT SERVER n "__with_anon_N" (args). The args get evaluated BYVAL in the host's current scope at the WITH site and travel with the event.
' Host-side: handler fires when a new player joins, ships SET ROOM to
' the player-server so the new player lands in the entrance.
SUB OnPlayerJoin(p)
WITH PLAYER p.slot SEND (p)
ENABLE SPRITE p.slot
SET SPRITE p.slot, 32, 32, "player", 0
SET SPRITE p.slot SYNC("Entrance")
SET ROOM "Entrance"
END WITH
END SUB
CALL PLAYER OnPlayerJoin() GUARD(HOST) AS "PlayerHandler"The SEND (args) clause is optional. Skip it for a zero-arg block.
For client-side rendering dispatch (overlays on a single player's screen), use raw EVENT CLIENT n — there's no block-form equivalent because WITH PLAYER is now firmly server-side.
`WITH PLAYER n SYNC (args)`
Same shape as SEND, but the caller waits for the remote body to finish before continuing past END WITH. Use this when ordering matters — "run this player-server setup, then continue with host bookkeeping that depends on it being done."
The contract is ack-on-complete, no return value. The body just signals "done" — whether it ran cleanly or halted with a runtime error, the caller resumes. If the player goes away mid-body, the player-server interpreter is torn down by disconnect cleanup and the host's wait exits.
' Wait for the player-server to finish populating its initial state
' before announcing the join to other players.
SUB OnPlayerJoin(p)
WITH PLAYER p.slot SYNC (p)
ENABLE SPRITE p.slot
SET SPRITE p.slot SYNC("Lobby")
SET ROOM "Lobby"
END WITH
' At this point the new player is fully set up.
EVENT SERVERS "PlayerReady" (p.slot, p.name$)
END SUBSYNC is host-only for WITH PLAYER. A player-server interpreter doing WITH PLAYER ME.SLOT SYNC on itself would deadlock its own pattern-tick (the thread that needs to dispatch the body is the one doing the waiting). Use SEND for own-slot dispatch from a player-server. If you genuinely need cross-slot SYNC from a player-server, route through EVENT HOST + a CALL HOST handler on the host that does the WITH PLAYER n SYNC for you.
While the host is waiting, its pattern-tick is parked — other host-side timers / CALL RENDER / OnPlayerJoin for additional incoming players are paused until the wait resolves. By design; that's what "wait" means. Use SEND if you want concurrent fire-and-forget.
`WITH CLIENT SEND (args)`
Same shape as WITH PLAYER, but the target depends on which server-side context you're calling from:
- From the host: broadcasts to every connected client (same as
EVENT CLIENTS). Game-start countdowns, lobby chat, "the boss is here, scatter" announcements. - From a player-server: fires on that player's own client only (same as
EVENT CLIENT ME.SLOT). Useful when shared code wants "tell the client this server-side context is for, no broadcasting required." - From a multiplayer local-client: fatal runtime error. Clients don't have authority to fire client-side handlers through the network — the design constraint exists so client code can't impersonate the server.
- In single-player: collapses to in-process dispatch (the synthetic SUB runs locally). Same
.crashcartruns in solo and multiplayer without gating.
WITH CLIENT SEND (countdown)
PRINT "Starting in " + STR$(countdown) + "..."
END WITHWriting WITH CLIENT from shared code that runs in both host and player-server contexts is the intended use — the routing collapses to "the right thing" without IF ME.ISHOST gating.
`WITH CLIENT SYNC (args)`
Player-server waits for the body to finish on its own client before continuing. Useful for client-side animations / transitions that have to complete before server-side state advances.
' Player-server side: wait for the client's fade-to-black to finish
' before swapping rooms server-authoritatively.
WITH CLIENT SYNC
WITH LAYER 1
CLS 0 EFFECT(DISSOLVE) SPEED 30
WHILE TRANSITION() <> ""
YIELD
WEND
END WITH
END WITH
SET ROOM "Lobby"SYNC is player-server-only for WITH CLIENT. It targets the calling player-server's own client. Host-side WITH CLIENT SYNC is fatal — broadcasting to all clients and waiting for every one to ack has no coherent semantic (slowest client dictates the wait, single disconnect blocks the host). If you need confirmation from every client after a broadcast, fire async (WITH CLIENT SEND) and have each client's handler write a PLAYER DATA flag the host polls.
Nesting
LAYER and ROOM compose orthogonally — they're different scoping axes, so an outer WITH LAYER 3 and an inner WITH ROOM "lobby" both apply inside the inner block. Same-kind nesting shadows: inner wins, and when the inner block exits, the outer's value comes back.
PLAYER and CLIENT can't be meaningfully nested inside each other — they're one-shot fires, not ambient contexts.
Defaults outside any `WITH` block
LAYERresolves to whateverSET LAYERlast set (default0).ROOMfalls back to the_globalsentinel on the host (visible to every connected player). Everywhere else, it falls back toME.ROOM.
Why `SEND` / `SYNC`?
The keyword separates the slot expression from the args list because BASIC's expression parser eagerly consumes trailing parens as subscripts. Without it, WITH PLAYER 2 (host_score) would parse as WITH PLAYER 2(host_score) — a function call on the literal 2. The keyword also doubles as the async-vs-sync selector: SEND = fire-and-forget, SYNC = wait for ack. Omitting the keyword entirely is equivalent to SEND (zero-arg async).
Args are BYVAL
Full stop. The block body executes on a different interpreter context — sometimes a different machine. It can't mutate host state remotely, and there's no sync-back path even with SYNC (which is ack-only, not return-value). If you need to mutate server state, write to PLAYERS(n).field directly; the existing PLAYER DATA machinery handles propagation.
Syntax
WITH LAYER <expr>
statements
END WITH
WITH ROOM <expr>
statements
END WITH
WITH PLAYER <slot> [SEND|SYNC (arg1, arg2, ...)]
statements
END WITH
WITH CLIENT [SEND|SYNC (arg1, arg2, ...)]
statements
END WITHParameters
- LAYER expr - WITH LAYER variant: numeric layer ID (0-99) for the block-scoped active draw layer.
- ROOM expr - WITH ROOM variant: string room name. Tilemap commands inside the block tag with this room for replication/filter purposes.
- PLAYER slot - WITH PLAYER variant: numeric slot expression for the target player. The block ships to that player's player-server interpreter for server-authoritative execution.
- CLIENT - WITH CLIENT variant: no slot expression — broadcasts to every connected client.
- SEND (args) - Optional. Comma-separated identifier list. Each name binds a host-scope value (looked up via Expr::Var) to a same-named BYVAL parameter on the synthetic SUB. Omit to ship a zero-arg block.
Examples
REM Block-scoped draw layer — replaces the SET LAYER save/restore dance
WITH LAYER 3 ' HUD layer
CLS 0
PRINT "Score: " + STR$(score)
END WITH ' previous layer auto-restoredREM Host sets up four levels' worth of tilemap data, room-tagged
IF ME.ISHOST THEN
FOR i = 1 TO 4
WITH ROOM "level" + STR$(i)
TILEMAP "lvl" + STR$(i)
TILESET "grass.png"
TILESIZE 32, 32
SIZE 100, 60
TILES 0 FROM tiles
END TILEMAP
CREATE MAP "lvl" + STR$(i) AS LAYER 1
END WITH
NEXT i
END IFREM Composing two orthogonal scopes
WITH ROOM "lobby"
WITH LAYER 2
CLS 0
PRINT "Welcome to the lobby"
END WITH
END WITHREM Single-player level switching (with the same code)
WITH ROOM "level1"
CREATE MAP "level1" AS LAYER 1
END WITH
SET ROOM "level1" ' mounts everything tagged level1 automaticallyREM Per-player state setup on join — block runs on player 2's player-server, where SET ROOM and PLAYERS(n) writes are authoritative
SUB OnPlayerJoin(p)
WITH PLAYER p.slot SEND (p)
ENABLE SPRITE p.slot
SET SPRITE p.slot, 32, 32, "player", 0
SET SPRITE p.slot SYNC("Entrance")
SET ROOM "Entrance"
END WITH
END SUB
CALL PLAYER OnPlayerJoin() GUARD(HOST) AS "PlayerHandler"REM Broadcast game-start countdown to every connected client
countdown = 3
WITH CLIENT SEND (countdown)
PRINT "Game starts in " + STR$(countdown) + "..."
END WITHREM Zero-arg form — no SEND clause, the block ships as-is
WITH PLAYER 1
PRINT "Welcome, you're the first to arrive."
END WITHSee also: SET LAYER, SET ROOM, TILEMAP, CREATE MAP, UPDATE MAP, EVENT, CALL EVENT
YIELD
Control Flow · since 1.0.3
Yields execution so the renderer, audio, behavior animations, and input each get a slice, then resumes — a cooperative yield point. By default YIELD pauses about 16ms (~60fps), which keeps an unpaced game loop from free-running and flooding the graphics command stream on devices whose renderer can't drain it that fast. Tune the pause with SET YIELD <ms>: SET YIELD 1 frees the loop to run as fast as the machine allows — pair that with COOLDOWN so you pace your own rendering. Use YIELD in tight loops — especially game loops — to stay responsive; a loop with no YIELD trips the yield-starvation guard.
Express motion as a rate over time (e.g. pixels per second scaled by elapsed time) rather than a fixed step per iteration, so gameplay speed stays consistent regardless of the YIELD rate.
Syntax
YIELDExamples
' Basic game loop
DO
' Update game logic
YIELD
LOOP' Smooth sprite animation with behaviors
DO
IF KEYDOWN("LEFT") THEN playerX = playerX - 2
IF KEYDOWN("RIGHT") THEN playerX = playerX + 2
MOVE SPRITE 0, playerX, playerY
YIELD ' Allow behaviors to animate
LOOP' Polling loop
WHILE NOT done
IF INKEY$ <> "" THEN done = 1
YIELD
WEND
Structured Programming
BYVAL
Structured Programming · since 0.23.0
Pass parameter by value (copy) to SUB or FUNCTION.
Syntax
BYVAL parameter AS datatypeParameters
- parameter - Parameter name
- datatype - INTEGER, STRING, etc.
Example
SUB Test(BYVAL x AS INTEGER)
x = x + 1 ' Changes local copy only
END SUB
a = 5
CALL Test(a)
PRINT a ' Still prints 5See also: BYREF, SUB, FUNCTION
DECLARE
Structured Programming · since 0.23.0
Forward declares a SUB or FUNCTION so its signature is known up front. It is optional for ordinary calls: module-level FUNCTION/SUB definitions are auto-hoisted, so you can call them before they appear in the source — including mutual recursion and a 'main at the top, helpers below' layout — with or without DECLARE. DECLARE remains useful for documenting an interface and for QuickBASIC compatibility. Only module-level definitions (top level and inside NAMESPACE blocks) are hoisted; a FUNCTION/SUB nested inside a control-flow block (IF/WHILE/FOR/DO) registers when execution reaches it, so define those before use.
The parentheses are required, empty or not: DECLARE SUB Restart(), not DECLARE SUB Restart.
Syntax
DECLARE SUB name(parameters)
DECLARE FUNCTION name(parameters) [AS datatype]Parameters
- name - Subroutine or function name
- parameters - Parameter list matching the actual definition. The parentheses are required even for a routine that takes none: DECLARE SUB Restart()
Example
DECLARE FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
DECLARE SUB PrintMessage(BYVAL msg AS STRING)
PRINT Add(5, 3)
CALL PrintMessage("Test")
FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
Add = a + b
END FUNCTION
SUB PrintMessage(BYVAL msg AS STRING)
PRINT msg
END SUB
FUNCTION
Structured Programming · since 0.23.0
Defines a function that returns a value when called. All parameters are always pass-by-value (BYVAL). BYREF is not allowed.
Syntax
FUNCTION name [(parameters)] [AS datatype]
statements
name = return_value
END FUNCTIONParameters
- name - Function name
- parameters - Optional parameter list (BYVAL only, BYREF not allowed)
- datatype - Return type (INTEGER, STRING, etc.)
Examples
FUNCTION Add(a AS INTEGER, b AS INTEGER) AS INTEGER
Add = a + b
END FUNCTION
result = Add(5, 3)FUNCTION GetPlayerName$() AS STRING
INPUT "Enter name: ", n$
GetPlayerName$ = n$
END FUNCTION
IMPORT
Structured Programming · since 0.23.0
Imports functions, subroutines, and type definitions from another BASIC file in the same bundle. IMPORT may appear anywhere in the file — the imported file's contents are spliced in at the IMPORT's exact position, so you can pull a reusable block of code straight into the middle of an app, not just declare modules at the top. Errors in an imported file are reported against that file's name and line. IMPORT is a plain text splice with no include-once guard, by design: importing the same file more than once splices it each time, which simply re-declares its functions/subs (harmless — later definitions win) and keeps IMPORT predictable and flexible. Circular imports are still detected and rejected.
Syntax
IMPORT "filename.bas"Parameters
- filename - Name of the .bas file to import (within current bundle)
- .bas extension is optional and will be added automatically
- Files must exist in the same resource bundle (no subfolders supported)
Examples
IMPORT "math.bas"
IMPORT "graphics.bas"
' Now use imported functions
result = Calculate(10, 20)
CALL DrawSprite(100, 100)' Import utility modules
IMPORT "utils"
IMPORT "physics.bas"' main.bas
IMPORT "player.bas"
IMPORT "enemies.bas"
SCREEN 13
CALL InitPlayer()
CALL SpawnEnemies()See also: DECLARE, SUB, FUNCTION, TYPE
NAMESPACE
Structured Programming · since 1.12.0
Groups functions, subroutines, and variables under a named scope. Inside the block, members are accessed directly by name. Outside the block, use Namespace.Member syntax (e.g., Utils.Clamp()) for both reads and writes. Multiple namespaces can define functions with the same name without conflict. Cross-namespace access works — code in one namespace can read and write variables in another namespace using the full Namespace.var path. See docs/variable-scoping.md for the full scoping rules.
Syntax
NAMESPACE name
statements
END NAMESPACEParameters
- name - Namespace identifier
Examples
NAMESPACE Utils
FUNCTION Clamp(value, lo, hi)
IF value < lo THEN
Clamp = lo
ELSEIF value > hi THEN
Clamp = hi
ELSE
Clamp = value
END IF
END FUNCTION
END NAMESPACE
x = Utils.Clamp(150, 0, 100) ' Returns 100REM Namespace variables are readable and writable from outside.
REM Use mutator subs for complex logic.
NAMESPACE Player
health = 100
SUB TakeDamage(amount)
health = health - amount
IF health < 0 THEN health = 0
END SUB
END NAMESPACE
PRINT Player.health ' 100
Player.health = 75 ' Direct write from outside
PRINT Player.health ' 75
CALL Player.TakeDamage(25) ' Or use a mutator sub
PRINT Player.health ' 50
SUB
Structured Programming · since 0.23.0
Defines a subroutine that can be called with CALL statement.
Syntax
SUB name [(parameters)]
statements
END SUBParameters
- name - Subroutine name
- parameters - Optional parameter list with BYVAL/BYREF modifiers
Examples
SUB PrintMessage(BYVAL msg AS STRING)
PRINT "Message: "; msg
END SUB
CALL PrintMessage("Hello")SUB DrawBox(x1, y1, x2, y2, color)
LINE (x1, y1)-(x2, y2), color, B
END SUBscore = 100 ' Global variable
SUB UpdateScore(points)
score = score + points ' Accesses global
PRINT "New score:"; score
END SUB
CALL UpdateScore(50)
PRINT "Final score:"; score ' Prints 150See also: FUNCTION, DECLARE, CALL, BYVAL
Variables
Array Literal
Variables · since 1.12.0
Creates an inline array from a list of expressions. Elements are 0-indexed. The resulting array supports subscript access and FOR EACH iteration. Can be nested inside object literals.
Syntax
[expr, expr, ...]Parameters
- expr - Any expression for each element
Examples
scores = [10, 20, 30]
PRINT scores(0) ' 10names = ["Alice", "Bob"]
FOR EACH n IN names
PRINT n
NEXTdata = {"values": [1, 2, 3], "label": "test"}See also: Object Literal, FOR EACH, DIM
BEGIN ENUM
Variables · since 1.35.0
Declare a group of named integer constants, auto-numbered from 0.
List one member per line between BEGIN ENUM and END ENUM. Each member becomes a global constant (exactly like CONST — read-only, case-insensitive), numbered in order starting at 0:
BEGIN ENUM
IDLE ' 0
WALKING ' 1
JUMPING ' 2
END ENUM
state = IDLE
IF state = JUMPING THEN ...This is cleaner than a stack of CONST lines when you just want a sequence of related values — game states, directions, message types, menu items.
Explicit values
A member can pin its own value with = expr (any integer expression). Members after it continue counting from there:
BEGIN ENUM
OK ' 0
WARN = 10 ' 10
ERROR ' 11 (continues from 10)
FATAL = 20 ' 20
END ENUMThe name is optional
You may give the block a name for readability — BEGIN ENUM Direction — but it's decorative: members are still bare global constants (NORTH, not Direction.NORTH). Choose member names that won't collide with other constants or variables.
Notes
Members are defined when the BEGIN ENUM statement runs, just like CONST, so declare your enums before you use them (top of the program is the usual spot). Use trailing comments (RED ' the danger colour) rather than standalone comment lines between members.
Members are constants, so the same rule applies: a member name already in use as a variable is a loud error, not a silent promotion.
Syntax
BEGIN ENUM [Name]
MEMBER1
MEMBER2 [= value]
...
END ENUMParameters
- Name - Optional, decorative block name (does not namespace the members)
- MEMBER - One identifier per line; becomes a global integer constant
- = value - Optional explicit integer value for a member; later members continue from value+1
Examples
REM Auto-numbered game states
BEGIN ENUM
TITLE
PLAYING
PAUSED
GAMEOVER
END ENUM
mode = TITLE
IF mode = PLAYING THEN CALL UpdateWorld()REM Named enum with explicit values
BEGIN ENUM Direction
NORTH = 0
EAST = 90
SOUTH = 180
WEST = 270
END ENUM
facing = EASTREM Explicit value, then continue auto-numbering
BEGIN ENUM
HTTP_OK = 200
HTTP_CREATED ' 201
HTTP_NOTFOUND = 404
HTTP_TEAPOT ' 405
END ENUMNotes
Each member is registered via the same mechanism as CONST (read-only, namespace-aware via the active NAMESPACE, case-insensitive). Numbering starts at 0 and increments per member; an explicit = expr (evaluated like a CONST value) sets the member and resets the running counter to value+1. The optional name is parsed but not stored as a namespace — members are bare globals. Members are one-per-line (like TYPE fields); trailing comments and blank lines between members are fine, standalone comment lines are not. Defined at runtime when the statement executes (no hoisting), so declare before use.
See also: CONST, DIM, SELECT CASE
BEGIN SORT
Variables · since 1.38.0
Sorts a one-dimensional array IN PLACE using a custom comparator block. For each pair being compared, the two elements are bound to the named variables (a, b) and the block runs; its RETURN value is read as a three-way sign: negative if 'a' should sort BEFORE 'b' (ascending), 0 if they are equal, positive if 'a' sorts AFTER 'b'. The sort is stable (equal elements keep their original order). A RETURN inside the block yields only that comparison — it does NOT return from the enclosing FUNCTION/SUB. Sorting a non-1-D array, or a value that isn't an array, is a runtime error.
Syntax
BEGIN SORT array AS a, b
' compare a and b; RETURN a three-way sign
END SORTParameters
- array - The one-dimensional array to sort in place (a variable, or any expression that evaluates to an array)
- a - Name bound to the first element of each compared pair
- b - Name bound to the second element of each compared pair
Examples
BEGIN SORT scores AS a, b
RETURN a - b ' ascending
END SORTBEGIN SORT scores AS a, b
RETURN b - a ' descending
END SORT' Sort an array of objects by a field
BEGIN SORT enemies AS a, b
RETURN a.hp - b.hp
END SORT' Explicit three-way comparator (e.g. for strings)
BEGIN SORT names AS a, b
IF a < b THEN
RETURN -1
ELSEIF a > b THEN
RETURN 1
END IF
RETURN 0
END SORTSee also: DIM, FOR EACH, CLONE
CLONE
Variables · since 1.38.0
Returns a deep, independent copy of a value. Crash BASIC arrays and objects are reference types: 'b = a' makes b an ALIAS of a (they share storage), so mutating one is seen through the other. CLONE(a) breaks that link, recursively duplicating nested arrays and objects so later mutations never bleed across. For scalars (Integer/Float/String) there is no shared storage, so CLONE just returns an equal value.
Syntax
CLONE(value)Parameters
- value - Any value. Arrays and Objects are deep-copied (recursively); scalars pass through unchanged.
Examples
DIM original(3) AS INTEGER
original(0) = 10
copy = CLONE(original) ' independent snapshot
copy(0) = 99
PRINT original(0) ' 10 (without CLONE it would be 99)snapshot = CLONE(gameState) ' deep copy of an objectSee also: DIM, LET
CONST
Variables · since 0.23.0
Defines a constant that cannot be changed during execution.
A name is one thing or the other. Declaring a constant over a name already in use as a variable is a loud error rather than a silent promotion — every earlier assignment would become retroactively meaningless, and every later one would fail with "cannot assign to constant" pointing at a line that looks perfectly reasonable. Pick a different name, or drop the CONST.
Syntax
CONST name = valueParameters
- name - Constant name
- value - Constant value (number or string)
Example
CONST PI = 3.14159
CONST MAX_PLAYERS = 4
CONST GAME_TITLE$ = "My Game"See also: DIM, LET, BEGIN ENUM
DATA
Variables · since 1.0.0
Defines a named block of constant data that can be read sequentially at runtime. DATA blocks support numbers, strings, and expressions. Multiple data values can span multiple lines within the block. Use OPEN DATA to access the block and READ to retrieve values.
Syntax
DATA "blockname"
value1, value2, value3
value4, value5, value6
END DATAParameters
- blockname - String name to identify the data block
- values - Comma-separated numbers, strings, or expressions
Examples
DATA "colors"
255, 0, 0
0, 255, 0
0, 0, 255
END DATA
OPEN DATA "colors" AS #1
DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
READ #1, r, g, b
PRINT "Red: R=" + STR$(r) + " G=" + STR$(g) + " B=" + STR$(b)
CLOSE #1DATA "enemies"
"Goblin", 10, 2
"Orc", 25, 5
"Dragon", 100, 20
END DATA
OPEN DATA "enemies" AS #1
DIM name AS STRING, hp AS INTEGER, damage AS INTEGER
READ #1, name, hp, damage
PRINT "Enemy: " + name + " HP=" + STR$(hp)
CLOSE #1DATA "mixed"
"Player1", 100, 3.5
END DATA
OPEN DATA "mixed" AS #1
DIM name AS STRING, score AS INTEGER, multiplier AS SINGLE
READ #1, name, score, multiplier
PRINT name + " scored " + STR$(score)
CLOSE #1See also: OPEN DATA, READ, RESTORE, CLOSE, EOF
DIM
Variables · since 0.23.0
Declares variables and allocates memory. AS datatype is required — there is no untyped DIM, and no inline initialization (DIM x = 0 does not parse; declare, then assign). Supports basic types, arrays, custom types, and Objects. Arrays are ZERO-BASED: DIM arr(N) allocates N elements indexed 0..N-1. A range bound is allowed only with a lower bound of 0 (e.g. DIM arr(0 TO 9) = 10 elements, indices 0-9); a non-zero lower bound such as DIM arr(1 TO 10) is a loud runtime error, not a silent re-base.
Custom types. DIM x AS SomeType (where SomeType is declared with TYPE) creates an instance pre-populated with the declared fields at their type defaults and tagged with the type, so it can carry methods. The type must be declared before the DIM; an unknown type name is a loud error, not a silent untyped object. Arrays of a custom type (DIM enemies(8) AS Enemy) give each element its own independent, tagged instance.
Syntax
DIM variable AS datatype
DIM variable(size) AS datatype
DIM variable(0 TO upper) AS datatype
DIM variable AS customtypeParameters
- variable - Variable name
- datatype - INTEGER, LONG, SINGLE, DOUBLE, STRING, Object, or custom type name
- size - Array element count (indices 0..size-1)
- upper - Upper index in a 0 TO upper range bound (lower bound must be 0)
Examples
DIM name AS STRINGDIM scores(10) AS INTEGERDIM grid(0 TO 9) AS INTEGERDIM config AS ObjectDIM player AS PlayerTypeSee also: LET, AS, TYPE, Object
ERASE
Variables · since 0.23.0
Clears arrays and frees memory.
Syntax
ERASE array_name [, array_name, ...]Parameters
- array_name - Names of arrays to erase
Example
DIM scores(100) AS INTEGER, names$(50) AS STRING
ERASE scores, names$
FALSE
Variables · since 1.57.0
The integer 0. Crash BASIC uses modern truthiness: TRUE is 1, FALSE is 0, and any non-zero value is truthy.
A pseudo-variable, so it needs no parentheses and cannot be assigned to.
Syntax
FALSEExample
running = FALSE
WHILE running = FALSE
YIELD
WEND
MUTATION
Variables · since 1.4.3
Declares a MUTATION variable for polygon-style game entities. Mutations hold lists of 2D coordinates that can have behaviors applied, causing all points to move together as a unit. Can be rendered with POLYGON command. An array of mutations (DIM rocks(24) AS MUTATION) can be indexed wherever a mutation is expected, which is how a game pools bound polygons.
Reading is free; writing takes over. While a behavior drives a mutation you can read its points as often as you like — MUTATIONPOINTX / MUTATIONPOINTY report where the behavior has got to, which makes a mutation a perfectly good position tracker even when nothing renders it. WRITING it — re-assigning the variable, or SET MUTATIONPOINT — ends the behavior driving it and hands control to your program. That is deliberate: the shape a behavior was animating is no longer the shape you asserted, so the behavior is removed rather than fighting you for the points.
The consequence worth planning for: a shape that must KEEP its behavior can only be moved through its sprite half — SET POLYGON n, LOCATION(x, y) ROTATE(a) — never by writing points every frame.
Syntax
DIM varName AS MUTATION
DIM varName(maxPoints) AS MUTATION
varName = MUTATION((x1, y1), (x2, y2), ...)Parameters
- varName - Variable name for the mutation
- maxPoints - Optional maximum number of points (omit for unlimited)
- (x, y) - Point coordinates for mutation literal
Examples
'Fixed-size mutation (3 points max)
DIM triangle(3) AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))'Dynamic mutation (unlimited points)
DIM path AS MUTATION
path = MUTATION((10, 20), (30, 40), (50, 60), (70, 80))'Combine mutations with + operator
DIM combined AS MUTATION
combined = triangle + path'Render mutation as polygon
POLYGON triangle, 14, 12 ' outline yellow, fill redSee also: SET MUTATION BEHAVIOR, SET MUTATIONPOINT, MUTATIONPOINTX, MUTATIONPOINTY, MUTATIONCOUNT, POLYGON, DIM
MUTATIONCOUNT
Variables · since 1.4.3
Returns the number of points in a MUTATION variable. Returns 0 if the variable is not a mutation or is empty. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.
Syntax
MUTATIONCOUNT(mutation)Parameters
- mutation - MUTATION variable or variable name
Examples
'Get point count
DIM triangle AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))
count = MUTATIONCOUNT(triangle) ' Returns 3'Iterate over all points
FOR i = 0 TO MUTATIONCOUNT(shape) - 1
x = MUTATIONPOINTX(shape, i)
y = MUTATIONPOINTY(shape, i)
PSET (x, y), 15
NEXT i'Check if mutation is empty
IF MUTATIONCOUNT(path) = 0 THEN
PRINT "No points in path"
END IFSee also: MUTATIONPOINTX, MUTATIONPOINTY, SET MUTATIONPOINT, MUTATION
MUTATIONPOINTX
Variables · since 1.4.3
Returns the X coordinate of a point in a MUTATION variable at the specified index. Returns 0 if the index is out of bounds or the variable is not a mutation. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.
Syntax
MUTATIONPOINTX(mutation, index)Parameters
- mutation - MUTATION variable or variable name
- index - Zero-based index of the point
Examples
'Get X coordinate of first point
DIM path AS MUTATION
path = MUTATION((100, 50), (200, 150))
x = MUTATIONPOINTX(path, 0) ' Returns 100'Print all point coordinates
FOR i = 0 TO MUTATIONCOUNT(path) - 1
PRINT "Point " + STR$(i) + ": " + STR$(MUTATIONPOINTX(path, i)) + ", " + STR$(MUTATIONPOINTY(path, i))
NEXT iSee also: MUTATIONPOINTY, MUTATIONCOUNT, SET MUTATIONPOINT, MUTATION
MUTATIONPOINTY
Variables · since 1.4.3
Returns the Y coordinate of a point in a MUTATION variable at the specified index. Returns 0 if the index is out of bounds or the variable is not a mutation. Reading is non-destructive: a mutation driven by a behavior can be read as often as you like and keeps running. It is WRITING it (SET MUTATIONPOINT, re-assignment) that ends the behavior.
Syntax
MUTATIONPOINTY(mutation, index)Parameters
- mutation - MUTATION variable or variable name
- index - Zero-based index of the point
Examples
'Get Y coordinate of first point
DIM path AS MUTATION
path = MUTATION((100, 50), (200, 150))
y = MUTATIONPOINTY(path, 0) ' Returns 50'Calculate bounding box
minY = MUTATIONPOINTY(shape, 0)
maxY = minY
FOR i = 1 TO MUTATIONCOUNT(shape) - 1
y = MUTATIONPOINTY(shape, i)
IF y < minY THEN minY = y
IF y > maxY THEN maxY = y
NEXT iSee also: MUTATIONPOINTX, MUTATIONCOUNT, SET MUTATIONPOINT, MUTATION
OPEN DATA
Variables · since 1.0.0
Opens a named DATA block for sequential reading. The file number is used with READ to retrieve values from the block. Multiple DATA blocks can be open simultaneously with different file numbers.
Syntax
OPEN DATA "blockname" AS #filenumberParameters
- blockname - String name of the DATA block to open
- filenumber - Numeric file handle (1-255) for accessing the data
Examples
DATA "scores"
100, 200, 300
END DATA
OPEN DATA "scores" AS #1
DIM score AS INTEGER
READ #1, score
PRINT STR$(score)
CLOSE #1' Open multiple DATA blocks simultaneously
DATA "block1"
1, 2, 3
END DATA
DATA "block2"
100, 200, 300
END DATA
OPEN DATA "block1" AS #1
OPEN DATA "block2" AS #2
DIM a AS INTEGER, b AS INTEGER
READ #1, a
READ #2, b
PRINT STR$(a) + " and " + STR$(b)
CLOSE #1
CLOSE #2See also: DATA, READ, CLOSE, RESTORE
Object Literal
Variables · since 1.12.0
Creates an inline object with key-value pairs. Keys can be quoted strings or bare identifiers. Values can be any expression, including nested objects and array literals. The resulting object supports dot notation (obj.key) and subscript access (obj("key")).
Syntax
{key: value, key2: value2, ...}Parameters
- key - Property name (string or identifier)
- value - Any expression
Examples
player = {"name": "Dan", "score": 100}
PRINT player.NAMEconfig = {x: 10, y: 20, size: 32}
PRINT config.X + config.Ygame = {"player": {"x": 100, "y": 200}, "level": 1}
PRINT game.PLAYER.XSee also: Array Literal, DIM
PUSH
Variables · since 0.23.0
Adds an element to the end of a 1D array. This is an alias for QUEUE, but typically used with POP for LIFO (Last In, First Out) stack operations. It always appends a new element to the end (the array grows by one each call; it does not fill any pre-DIMmed empty slots). Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.
Syntax
PUSH array, valueParameters
- array - 1D array variable to push onto
- value - Expression or value to add to the end of the array
Examples
DIM stack(10) AS INTEGER
PUSH stack, 10
PUSH stack, 20
PUSH stack, 30' LIFO stack: use with POP
PUSH myStack, 100
PUSH myStack, 200
value = POP(myStack) ' Returns 200 (last in)DIM history(5) AS STRING
PUSH history, "Action 1"
PUSH history, "Action 2"See also: QUEUE, POP, DIM, REDIM
QUEUE
Variables · since 0.23.0
Adds an element to the end of a 1D array. It always appends a new element to the end (the array grows by one each call; it does not fill any pre-DIMmed empty slots). Used for FIFO (First In, First Out) queue operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error, since appending one element would leave the array's shape inconsistent with its data.
Syntax
QUEUE array, valueParameters
- array - 1D array variable to append to
- value - Expression or value to add to the end of the array
Examples
DIM tasks(10) AS STRING
QUEUE tasks, "Process order"
QUEUE tasks, "Send email"DIM numbers(5) AS INTEGER
FOR i = 1 TO 10
QUEUE numbers, i * 2
NEXT' FIFO queue: use with DEQUEUE
QUEUE myQueue, "first"
QUEUE myQueue, "second"
item$ = DEQUEUE(myQueue) ' Returns "first"See also: PUSH, DEQUEUE, DIM, REDIM
READ
Variables · since 1.0.0
Reads the next value(s) from an open handle into variables.
For a DATA block handle (opened with OPEN DATA), values are read sequentially from where the last READ left off — one DATA element per variable, preserving the original type.
For a file-stream handle (opened with OPEN ... FOR INPUT), one line of the file is consumed per variable and auto-parsed: integers go to INTEGER, decimals to FLOAT, anything else stays a STRING. To capture a line verbatim without auto-parsing, use LINE INPUT instead.
Reading past the end raises a runtime error — gate the loop on EOF.
Syntax
READ #filenumber, var1 [, var2, var3, ...]Parameters
- filenumber - Handle from OPEN DATA or OPEN ... FOR INPUT
- var1, var2, ... - Variables to receive the values (one DATA element OR one file line each)
Examples
DATA "numbers"
10, 20, 30, 40, 50, 60
END DATA
OPEN DATA "numbers" AS #1
DIM a AS INTEGER, b AS INTEGER, c AS INTEGER
READ #1, a, b, c
PRINT STR$(a) + " " + STR$(b) + " " + STR$(c)
READ #1, a, b, c
PRINT STR$(a) + " " + STR$(b) + " " + STR$(c)
CLOSE #1' Read mixed data types from a DATA block
DATA "player"
"Hero", 100, 5.5
END DATA
OPEN DATA "player" AS #1
DIM name AS STRING, health AS INTEGER, speed AS SINGLE
READ #1, name, health, speed
PRINT name + " - HP: " + STR$(health)
CLOSE #1' Read from a file stream — one line per variable, auto-parsed
OPEN "player.txt" FOR INPUT AS #1
DIM name$ AS STRING, level AS INTEGER
READ #1, name$, level
CLOSE #1See also: DATA, OPEN DATA, OPEN, LINE INPUT, RESTORE, EOF
REDIM
Variables · since 0.23.0
Resizes an existing array, always preserving the elements that still fit. There is no PRESERVE keyword and none is needed — preserving is the default and only behavior (a non-preserving resize is just DIM). Growing keeps every existing element and default-fills the new cells; shrinking keeps the elements that remain in range. The resize happens in place, so other references to the same array (a BYREF parameter, a second binding) see the new size too. Rank is fixed: you may change the size of each dimension but not the number of dimensions, and the last dimension is the safe one to grow on multi-dimensional arrays. Changing the dimension count is a runtime error. If the array doesn't exist yet, REDIM allocates it like DIM.
Syntax
REDIM array_name(dimensions) AS datatypeParameters
- array_name - Name of the array to resize
- dimensions - New size for each dimension (same number of dimensions as the original)
- datatype - Element type (INTEGER, FLOAT, STRING, etc.)
Examples
DIM scores(10) AS INTEGER
scores(0) = 99
REDIM scores(20) AS INTEGER ' grows to 20, scores(0) still 99DIM log$(100) AS STRING
REDIM log$(50) AS STRING ' shrinks to 50, first 50 kept
RESTORE
Variables · since 1.0.0
Resets the read pointer for a DATA block back to the beginning. Without a file number, restores all open DATA blocks. This allows re-reading the same data multiple times.
The handle keeps its #, as it does everywhere else — RESTORE #1, not RESTORE 1. A bare number does not parse.
Syntax
RESTORE [#filenumber]Parameters
- #filenumber - Optional file handle to restore, written with the # (omit to restore all open DATA blocks)
Examples
DATA "test"
1, 2, 3
END DATA
OPEN DATA "test" AS #1
DIM a AS INTEGER, b AS INTEGER
READ #1, a, b
RESTORE #1
READ #1, a
PRINT STR$(a) ' Prints 1 again
CLOSE #1' Re-read data for multiple passes
DATA "palette"
255, 0, 0
0, 255, 0
0, 0, 255
END DATA
OPEN DATA "palette" AS #1
FOR pass = 1 TO 2
RESTORE #1
DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
READ #1, r, g, b
PRINT "Pass " + STR$(pass) + ": R=" + STR$(r)
NEXT pass
CLOSE #1See also: DATA, READ, OPEN DATA
SELF
Variables · since 1.44.0
Inside a type-bound method (SUB Type::Method or FUNCTION Type::Method), SELF is an implied reference to the instance the method was called on. Reading or assigning SELF.field reads or mutates that instance in place — a method changes the object it was called on, with no need to pass it in or return it. You can call other methods on the same instance with CALL SELF.Other() (sub) or x = SELF.Other() (function). SELF is only meaningful inside a method body.
Syntax
SELF.field
CALL SELF.Method(args)
x = SELF.Method(args)Example
TYPE Counter
n AS INTEGER
END TYPE
SUB Counter::Inc()
SELF.n = SELF.n + 1
END SUB
SUB Counter::IncTwice()
CALL SELF.Inc()
CALL SELF.Inc()
END SUB
DIM c AS Counter
c.n = 0
CALL c.IncTwice()
PRINT c.n ' 2
SET MUTATIONPOINT
Variables · since 1.4.3
Sets the coordinates of a specific point in a MUTATION variable. The index is 0-based. If the index is beyond the current number of points, the mutation is automatically extended with (0, 0) points to accommodate.
Writing a point ends any behavior on that mutation. Reading is free — MUTATIONPOINTX / MUTATIONPOINTY report where a behavior has got to — but writing is the program taking control, so the behavior is removed and the points you wrote become the shape's new baseline (a pose derived from the old one is reset with it). Re-apply the behavior if you still want it. To move a shape WITHOUT ending its behavior, position it through its sprite half: SET POLYGON n, LOCATION(x, y).
Syntax
SET MUTATIONPOINT varName, index, x, y
SET MUTATIONPOINT varName, index, coord (the point also accepts a coordinate: (x,y) or a coordinate variable)
SET MUTATIONPOINT mutationArray(slot), index, coordParameters
- varName - Name of the MUTATION variable
- index - Zero-based index of the point to set
- x - New X coordinate for the point
- y - New Y coordinate for the point
Examples
'Create and modify a triangle
DIM triangle AS MUTATION
triangle = MUTATION((0, 0), (50, 0), (25, 43))
'Move the top vertex
SET MUTATIONPOINT triangle, 2, 25, 30'Build a square point by point
DIM square AS MUTATION
SET MUTATIONPOINT square, 0, 0, 0
SET MUTATIONPOINT square, 1, 50, 0
SET MUTATIONPOINT square, 2, 50, 50
SET MUTATIONPOINT square, 3, 0, 50'Animate a point
FOR i = 0 TO 100
SET MUTATIONPOINT path, 0, i, SIN(i * 0.1) * 20
POLYGON path, 14
DELAY 16
NEXT iSee also: MUTATION, MUTATIONPOINTX, MUTATIONPOINTY, MUTATIONCOUNT
TRUE
Variables · since 1.57.0
The integer 1. Crash BASIC uses modern truthiness: TRUE is 1, FALSE is 0, and any non-zero value is truthy.
A pseudo-variable, so it needs no parentheses and cannot be assigned to.
Syntax
TRUEExamples
done = TRUE
IF done THEN PRINT "finished"IF HASPATTERN(id) = TRUE THEN PRINT "running"
TYPE
Variables · since 1.56.0
Defines a custom data type with named fields. DIM x AS typename creates an instance pre-populated with the declared fields at their type defaults, and tags the instance with the type — which is what lets it carry methods (see below). Fields are accessed with dot notation, and the object stays dynamic: you can add extra fields beyond those declared.
Define before use. A TYPE must appear before any DIM ... AS typename that uses it (same rule as defining a SUB before you CALL it). Using an undeclared type name in DIM is a loud error rather than a silent untyped object, so typos are caught.
Array fields. A field may declare array bounds exactly as DIM does — invaders(0 TO 4, 0 TO 9) AS SpaceInvader — in any number of dimensions, with either the bare-size (cells(8)) or explicit zero-based range (cells(0 TO 7)) form. Arrays are zero-based, so a non-zero lower bound is an error here just as it is in DIM. The bounds are evaluated once, when the TYPE statement runs, and every instance is allocated from those sizes; use CONSTs rather than variables that change later. Each instance gets its own array — two instances never share cells.
Fields of another type. A field declared AS another type name is pre-populated as a tagged instance of that type, so its own declared fields are already there and its methods dispatch on it: a SUB with CALL fleet.invaders(r, c).Hit(1) and a FUNCTION in expression position with IF fleet.invaders(r, c).IsDead() THEN .... SELF refers to that cell itself, so what a method writes is visible through the same path afterwards and no other cell moves. This applies to array fields cell by cell. A type that refers to itself (next AS Node) cannot be filled in all the way down, so the self-referring field starts as an empty instance of that type — still tagged, ready to be assigned into, which is how you build lists and trees.
Methods. Bind a method to a type by qualifying a SUB or FUNCTION name with typename::. Inside the body, SELF is an implied reference to the instance the method was called on. Call a SUB method with CALL obj.Method(args) and a FUNCTION method in expression position (x = obj.Method()); both also work on array elements (CALL enemies(i).Hit()). Field and method types resolve case-insensitively.
Field types can be INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN, COORDINATE, MUTATION, OBJECT, or another type name.
Syntax
TYPE typename
fieldname AS datatype
fieldname(bounds) AS datatype
...
END TYPEParameters
- typename - Name for the custom type
- fieldname - Name of each field
- bounds - Optional array bounds for the field, spelled as in DIM: (N) or (0 TO N), comma-separated for more dimensions
- datatype - Type of each field (INTEGER, STRING, etc.)
Examples
TYPE Player
name AS STRING
score AS INTEGER
lives AS INTEGER
END TYPE
DIM p AS Player
p.name = "Mario"
p.score = 0
p.lives = 3' A type with methods. SELF is the instance.
TYPE Player
health AS INTEGER
END TYPE
SUB Player::TakeDamage(amount)
SELF.health = SELF.health - amount
END SUB
FUNCTION Player::IsDead()
IsDead = SELF.health <= 0
END FUNCTION
DIM p AS Player
p.health = 100
CALL p.TakeDamage(30)
IF p.IsDead() THEN PRINT "game over"' Methods on an array of typed objects
TYPE Enemy
hp AS INTEGER
END TYPE
SUB Enemy::Hit(n)
SELF.hp = SELF.hp - n
END SUB
DIM enemies(8) AS Enemy
DIM i AS INTEGER
FOR i = 0 TO 7
CALL enemies(i).Hit(5)
NEXT' Array fields, including arrays of another type.
CONST ROWS = 5
CONST COLS = 11
TYPE SpaceInvader
hp AS INTEGER
alive AS BOOLEAN
END TYPE
SUB SpaceInvader::Hit(n)
SELF.hp = SELF.hp - n
SELF.alive = SELF.hp > 0
END SUB
TYPE SpaceInvaderFleet
invaders(0 TO ROWS - 1, 0 TO COLS - 1) AS SpaceInvader
speed AS SINGLE
END TYPE
DIM fleet AS SpaceInvaderFleet
fleet.speed = 2.5
DIM r AS INTEGER
DIM c AS INTEGER
FOR r = 0 TO ROWS - 1
FOR c = 0 TO COLS - 1
fleet.invaders(r, c).hp = 3
fleet.invaders(r, c).alive = TRUE
NEXT
NEXT
CALL fleet.invaders(0, 0).Hit(1)
PRINT fleet.invaders(0, 0).hpSee also: DIM, SUB, FUNCTION, SELF, Object
Functions
ASARRAY
Functions · since 1.0.3
Creates an array from a DATA block with specified dimensions. Converts embedded DATA block values directly into a multi-dimensional array without requiring OPEN DATA, READ loops, or manual element assignment. Supports 1D, 2D, 3D or higher dimensional arrays. The total number of elements in the DATA block must match the array dimensions.
Syntax
array = ASARRAY(data_block_name$, dim1 [, dim2, ...])Parameters
- data_block_name$ - Name of the DATA block (as a string)
- dim1 - First dimension upper bound (0 to dim1)
- dim2 - Optional second dimension upper bound
- ... - Additional dimensions as needed
Examples
REM Simple 1D array from DATA block
DATA "scores"
100, 200, 300, 400, 500
END DATA
scores = ASARRAY("scores", 4)
PRINT STR$(scores(0)) ' 100
PRINT STR$(scores(4)) ' 500REM 2D array for tile map (3x3 = 9 elements)
DATA "tilemap"
0, 1, 0
1, 2, 1
0, 1, 0
END DATA
tileMap = ASARRAY("tilemap", 2, 2)
PRINT STR$(tileMap(1, 1)) ' Center tile: 2REM Color palette (4 colors x 3 RGB values = 12 elements)
DATA "palette"
255, 0, 0
0, 255, 0
0, 0, 255
255, 255, 255
END DATA
colors = ASARRAY("palette", 3, 2)
PRINT "Red R: " + STR$(colors(0, 0)) ' 255
PRINT "White B: " + STR$(colors(3, 2)) ' 255REM Mixed value types (strings and numbers)
DATA "enemies"
"Goblin", 10, 2
"Orc", 25, 5
"Dragon", 100, 20
END DATA
enemies = ASARRAY("enemies", 2, 2)
PRINT enemies(0, 0) ' Goblin
PRINT STR$(enemies(2, 1)) ' 100 (Dragon HP)REM Use with FOR loop after creation
DATA "values"
5, 10, 15, 20, 25
END DATA
nums = ASARRAY("values", 4)
DIM total AS INTEGER
total = 0
FOR i = 0 TO UBOUND(nums)
total = total + nums(i)
NEXT
PRINT "Sum: " + STR$(total) ' 75See also: DATA, DIM, UBOUND, LBOUND, OPEN DATA, READ
ASC
Functions · since 0.23.0
Returns ASCII code of first character.
Syntax
ASC(string_expression)Parameters
- string_expression - String with at least one character
Examples
PRINT ASC("A") ' Prints 65code = ASC(key$)IF ASC(char$) >= 65 AND ASC(char$) <= 90 THEN PRINT "Uppercase"
CHR$
Functions · since 0.23.0
Converts ASCII code to character.
Syntax
CHR$(ascii_code)Parameters
- ascii_code - ASCII value (0-255)
Examples
PRINT CHR$(65) ' Prints "A"PRINT CHR$(7) ' Bell charactername$ = CHR$(74) + CHR$(111) + CHR$(104) + CHR$(110)
COUNT
Functions · since 0.23.0
Counts the number of non-overlapping occurrences of a substring within a string.
Syntax
COUNT(string, search_for)Parameters
- string - String to search in
- search_for - Substring to count
Examples
n = COUNT("Hello World", "l") ' Returns 3n = COUNT("banana", "an") ' Returns 2words = COUNT(text$, " ") + 1 ' Count words (spaces + 1)See also: INSTR, LEN, REPLACE$
DATE$
Functions · since 0.23.0
Returns the current system date as a string in MM/DD/YYYY format.
Syntax
DATE$Parameters
- No parameters required
Examples
PRINT "Today is: "; DATE$today$ = DATE$PRINT "Program run on "; DATE$; " at "; TIME$See also: TIME$
DEQUEUE
Functions · since 0.23.0
Removes and returns the first element from a 1D array (FIFO - First In, First Out). All remaining elements shift down by one position, and the array automatically shrinks. Use with QUEUE for queue operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.
Syntax
value = DEQUEUE(array)Parameters
- array - 1D array variable to dequeue from
Examples
DIM tasks(20) AS STRING
QUEUE tasks, "Task 1"
QUEUE tasks, "Task 2"
QUEUE tasks, "Task 3"
nextTask$ = DEQUEUE(tasks) ' Returns "Task 1" (first in)
nextTask$ = DEQUEUE(tasks) ' Returns "Task 2"' Message queue system
DIM messages(100) AS STRING
QUEUE messages, "Login"
QUEUE messages, "Update"
QUEUE messages, "Logout"
WHILE UBOUND(messages) >= 0
msg$ = DEQUEUE(messages)
PRINT "Processing: "; msg$
WEND' Event processing queue
DIM events(50) AS STRING
QUEUE events, "KeyPress"
QUEUE events, "MouseClick"
IF UBOUND(events) >= 0 THEN
currentEvent$ = DEQUEUE(events)
PRINT "Handling: "; currentEvent$
END IFSee also: QUEUE, POP, UBOUND, LBOUND
EXISTS
Functions · since 1.0.3
Checks if a resource exists in the resource manager. Returns 1 if the resource exists, 0 if it does not.
Syntax
EXISTS(resource_name)Parameters
- resource_name - Name of the resource to check (image, sound, music, data, or text)
Examples
IF EXISTS("player.png") = 1 THEN
PRINT "Player image found!"
ELSE
PRINT "Player image not found"
END IFREM Check for optional save data before loading
IF EXISTS("savegame.json") THEN
gameData$ = LOADTEXT("savegame.json")
PRINT "Save data loaded"
END IFSee also: LOADIMG, LOADTEXT, LOADJSON, LOADWAV, LOADMP3
FPS
Functions · since 1.10.0
Returns the current frames per second as a floating-point number. Uses a smoothed average to avoid frame-to-frame jitter. Useful for adaptive performance tuning — games can check FPS and reduce visual complexity on slower devices.
Syntax
FPSParameters
- No parameters required
Examples
PRINT "FPS: "; INT(FPS)' Adaptive quality:
IF FPS < 30 THEN
' Reduce particle count or disable effects
maxParticles = 50
ELSE
maxParticles = 200
END IF' Display FPS counter:
LOCATE 1, 1
PRINT FIXED$(FPS, 1)
HEX$
Functions · since 0.23.0
Converts a number to its hexadecimal string representation.
Syntax
HEX$(number)Parameters
- number - Numeric value to convert (integer part used)
Examples
hex$ = HEX$(255) ' Returns "FF"hex$ = HEX$(16) ' Returns "10"PRINT "Color: &H" + HEX$(color_value)
INSTR
Functions · since 0.23.0
Finds the 1-based position of the first occurrence of a substring within a string. Returns 0 if not found.
Syntax
INSTR(string1, string2)Parameters
- string1 - String to search in
- string2 - Substring to find
Examples
pos = INSTR("Hello World", "World")IF INSTR(filename$, ".BAS") > 0 THEN PRINT "Basic file"
JOIN$
Functions · since 0.23.0
Joins an array of strings into a single string using a delimiter.
Syntax
JOIN$(array, delimiter)Parameters
- array - Array of strings to join
- delimiter - Character or string to join with
Examples
result$ = JOIN$(parts$, ",") ' Join array with commassentence$ = JOIN$(words$, " ") ' Join words with spacescsv_line$ = JOIN$(fields$, ",") ' Create CSV format
LCASE$
Functions · since 0.23.0
Converts a string to lowercase letters.
Syntax
LCASE$(string)Parameters
- string - Input string to convert
Examples
result$ = LCASE$("Hello World") ' Returns "hello world"filename$ = LCASE$(input_file$)See also: UCASE$
LEFT$
Functions · since 0.23.0
Returns the leftmost characters from a string.
Syntax
LEFT$(string_expression, count)Parameters
- string_expression - Source string
- count - Number of characters to return
Examples
first3 = LEFT$("Hello", 3)prefix$ = LEFT$(filename$, 8)PRINT LEFT$("QBasic", 2) ' Prints "QB"
LEN
Functions · since 0.23.0
Returns the length of a string.
Syntax
LEN(string_expression)Parameters
- string_expression - String to measure
Examples
length = LEN("Hello")IF LEN(name$) > 10 THEN PRINT "Name too long"FOR i = 1 TO LEN(text$): PRINT MID$(text$, i, 1): NEXT
LTRIM$
Functions · since 0.23.0
Removes leading spaces from the left side of a string.
Syntax
LTRIM$(string)Parameters
- string - Input string to trim
Examples
result$ = LTRIM$(" Hello World ") ' Returns "Hello World "cleaned$ = LTRIM$(user_input$)
MATCH
Functions · since 0.23.0
Pattern matching with wildcards. Returns 1 if match, 0 if not.
Syntax
MATCH(string, pattern)Parameters
- string - String to test
- pattern - Pattern with wildcards (* = any sequence, ? = any single character)
Examples
IF MATCH(filename$, "*.BAS") THEN PRINT "Basic file"IF MATCH(name$, "J?hn") THEN PRINT "John or similar"IF MATCH(text$, "*error*") THEN PRINT "Contains error"
MID$
Functions · since 0.23.0
Returns a substring from the middle of a string.
Syntax
MID$(string_expression, start [, length])Parameters
- string_expression - Source string
- start - Starting position (1-based)
- length - Number of characters (optional, defaults to rest of string)
Examples
middle = MID$("Hello", 2, 3)rest$ = MID$(text$, 5)char$ = MID$(alphabet$, i, 1)See also: LEFT$, RIGHT$, INSTR
OCT$
Functions · since 0.23.0
Converts a number to its octal string representation.
Syntax
OCT$(number)Parameters
- number - Numeric value to convert (integer part used)
Examples
oct$ = OCT$(64) ' Returns "100"oct$ = OCT$(8) ' Returns "10"PRINT "Octal: " + OCT$(value)
POP
Functions · since 0.23.0
Removes and returns the last element from a 1D array (LIFO - Last In, First Out). The array automatically shrinks by one element. Use with PUSH for stack operations. Only works on 1-dimensional arrays — calling it on a multi-dimensional array is a runtime error.
Syntax
value = POP(array)Parameters
- array - 1D array variable to pop from
Examples
DIM stack(10) AS INTEGER
PUSH stack, 10
PUSH stack, 20
PUSH stack, 30
value = POP(stack) ' Returns 30 (last in)
value = POP(stack) ' Returns 20' Undo system using stack
DIM history(50) AS STRING
PUSH history, "Action 1"
PUSH history, "Action 2"
lastAction$ = POP(history) ' Get most recent action' Stack-based expression evaluator
DIM operands(100) AS INTEGER
PUSH operands, 5
PUSH operands, 3
b = POP(operands)
a = POP(operands)
result = a + bSee also: PUSH, DEQUEUE, UBOUND, LBOUND
REGEX
Functions · since 0.23.0
Advanced regular expression pattern matching. Returns 1 if the pattern matches, 0 if not. Matching is case-sensitive; embed an inline flag such as (?i) at the start of the pattern for case-insensitive matching.
Syntax
REGEX(string, pattern)Parameters
- string - String to test
- pattern - Regular expression pattern
Examples
IF REGEX(email$, "^[\w.-]+@[\w.-]+\.[a-z]{2,}$") THEN PRINT "Valid email"IF REGEX(phone$, "^\d{3}-\d{3}-\d{4}$") THEN PRINT "Valid phone"IF REGEX(text$, "(?i)hello") THEN PRINT "Contains hello (any case)"
REPLACE$
Functions · since 0.23.0
Replaces all occurrences of a substring with another string.
Syntax
REPLACE$(string, search_for, replace_with)Parameters
- string - Source string
- search_for - Substring to find
- replace_with - Replacement string
Examples
result$ = REPLACE$("Hello World", "World", "QBasic")clean$ = REPLACE$(text$, " ", "_")fixed$ = REPLACE$(data$, "old", "new")
REVERSE$
Functions · since 0.23.0
Reverses the order of characters in a string.
Syntax
REVERSE$(string)Parameters
- string - String to reverse
Examples
result$ = REVERSE$("Hello") ' Returns "olleH"result$ = REVERSE$("12345") ' Returns "54321"IF text$ = REVERSE$(text$) THEN PRINT "Palindrome!"
RIGHT$
Functions · since 0.23.0
Returns the rightmost characters from a string.
Syntax
RIGHT$(string_expression, count)Parameters
- string_expression - Source string
- count - Number of characters to return
Examples
last3 = RIGHT$("Hello", 3)extension$ = RIGHT$(filename$, 3)PRINT RIGHT$("QBasic", 5) ' Prints "Basic"
RTRIM$
Functions · since 0.23.0
Removes trailing spaces from the right side of a string.
Syntax
RTRIM$(string)Parameters
- string - Input string to trim
Examples
result$ = RTRIM$(" Hello World ") ' Returns " Hello World"cleaned$ = RTRIM$(line_data$)
SPACE$
Functions · since 0.23.0
Creates a string of spaces.
Syntax
SPACE$(count)Parameters
- count - Number of spaces to create
Examples
spaces$ = SPACE$(10) ' Returns " "indent$ = SPACE$(level * 4)PRINT "Name:" + SPACE$(5) + name$See also: STRING$
SPLIT$
Functions · since 0.23.0
Splits a string into an array using a delimiter. Returns array of strings.
Syntax
SPLIT$(string, delimiter)Parameters
- string - String to split
- delimiter - Character or string to split on
Examples
parts$ = SPLIT$("apple,banana,cherry", ",") ' Returns ["apple", "banana", "cherry"]words$ = SPLIT$("Hello World", " ") ' Returns ["Hello", "World"]lines$ = SPLIT$(text$, CHR$(10)) ' Split on newlines
STR$
Functions · since 0.23.0
Converts a number to its string representation. No leading space is added for positive numbers.
Syntax
STR$(number)Parameters
- number - Numeric value to convert
Examples
text$ = STR$(123) ' Returns "123"text$ = STR$(-45) ' Returns "-45"PRINT "Value:" + STR$(score)
STRING$
Functions · since 0.23.0
Creates a string by repeating a character or ASCII code.
Syntax
STRING$(count, character)Parameters
- count - Number of repetitions
- character - String character or ASCII code number
Examples
line$ = STRING$(40, "-") ' Returns "----------------------------------------"spaces$ = STRING$(10, 32) ' Returns 10 spaces (ASCII 32)border$ = STRING$(20, "*")
TIME$
Functions · since 0.23.0
Returns the current system time as a string in HH:MM:SS format.
Syntax
TIME$Parameters
- No parameters required
Examples
PRINT "Current time: "; TIME$startTime$ = TIME$IF TIME$ > "12:00:00" THEN PRINT "Good afternoon"
TIMER
Functions · since 0.23.0
Returns the number of seconds since midnight as a floating-point number. Includes fractional seconds for high-precision timing. Useful for performance measurement, animation timing, and seeding random numbers. Range is 0.0 to 86399.999...
Syntax
TIMERParameters
- No parameters required
Examples
PRINT "Seconds since midnight: "; TIMER' Measure execution time:
startTime = TIMER
' ... do something ...
endTime = TIMER
PRINT "Elapsed: "; endTime - startTime; " seconds"' Seed random number generator:
RANDOMIZE TIMER' Animation timing:
frameStart = TIMER
' ... render frame ...
elapsed = TIMER - frameStart
IF elapsed < 0.016667 THEN DELAY (0.016667 - elapsed) * 1000' Time-based game logic:
IF TIMER - lastShot > 0.5 THEN
' Allow shooting every 0.5 seconds
CALL FireBullet()
lastShot = TIMER
END IFSee also: TIME$, DATE$, RANDOMIZE, DELAY
TRIM$
Functions · since 0.23.0
Removes leading and trailing spaces from a string.
Syntax
TRIM$(string)Parameters
- string - Input string to trim
Examples
cleaned$ = TRIM$(" Hello World ") ' Returns "Hello World"result$ = TRIM$(INPUT_TEXT$)
UCASE$
Functions · since 0.23.0
Converts a string to uppercase letters.
Syntax
UCASE$(string)Parameters
- string - Input string to convert
Examples
result$ = UCASE$("Hello World") ' Returns "HELLO WORLD"command$ = UCASE$(user_input$)See also: LCASE$
VAL
Functions · since 0.23.0
Converts a string to a number. The whole string must be one — surrounding spaces are fine, anything else is not.
A string that is not a number is a runtime error. It does not come back as 0, because 0 is indistinguishable from the string "0": a typo, an empty input box and a genuine zero would all read the same, and the number reaching the rest of the program would be a guess nobody made.
It does not read a number off the front of something else. VAL("10ABC") is an error, not 10. If you need the leading digits of a mixed string, cut them out yourself with LEFT$ / MID$ first.
Check the string first if it might not be a number. There is no sentinel return to test for afterwards — that is the point.
Syntax
VAL(string_expression)Parameters
- string_expression - A string that is entirely a number, such as "42", "-3.14" or " 7 ". Anything else is a runtime error.
Examples
num = VAL("123.45") ' 123.45PRINT VAL(" -7 ") ' Prints -7REM Guard user input rather than trusting it
IF entry$ <> "" THEN
score = VAL(entry$)
END IFSee also: STR$, ASC, CHR$, FIXED$, LEFT$
VERSION$
Functions · since 1.10.0
Returns the CrashCore engine version as a string (e.g., "1.10.0"). Useful for displaying engine information or checking compatibility.
Syntax
VERSION$Parameters
- No parameters required
Examples
PRINT "CrashCore version: "; VERSION$' Check minimum version:
IF VERSION$ < "1.10.0" THEN
PRINT "This game requires CrashCore 1.10.0 or later"
END
END IF
Math
ABS
Math · since 0.23.0
Returns the absolute value of a number.
Syntax
ABS(number)Parameters
- number - Any numeric expression
Examples
PRINT ABS(-42) ' Prints 42PRINT ABS(42) ' Prints 42PRINT ABS(-3.14) ' Prints 3.14See also: SGN
ACOS
Math · since 1.0.3
Returns the arccosine (inverse cosine) of a number, in radians. The argument should be in the range -1 to 1.
Syntax
ACOS(number)Parameters
- number - A value in the range -1 to 1
Examples
PRINT ACOS(1) ' Prints 0PRINT ACOS(0) ' Prints 1.570796 (PI/2)degrees = ACOS(ratio) * 180 / PI
ASIN
Math · since 1.0.3
Returns the arcsine (inverse sine) of a number, in radians. The argument should be in the range -1 to 1.
Syntax
ASIN(number)Parameters
- number - A value in the range -1 to 1
Examples
PRINT ASIN(0) ' Prints 0PRINT ASIN(1) ' Prints 1.570796 (PI/2)degrees = ASIN(ratio) * 180 / PI
ATN
Math · since 0.23.0
Returns the arctangent (inverse tangent) in radians.
Syntax
ATN(number)Parameters
- number - Any numeric expression
Examples
PRINT ATN(0) ' Prints 0PRINT ATN(1) ' Prints 0.785398 (PI/4)degrees = ATN(1) * 180 / 3.14159 ' Convert to degreesPRINT degrees ' Prints 45
ATN2
Math · since 1.0.3
Returns the two-argument arctangent (atan2) in radians. Computes the angle from the positive X axis to the point (x, y), handling all four quadrants correctly. This is the preferred function for computing angles between two points or from velocity components.
Syntax
ATN2(y, x)Parameters
- y - The Y component (vertical delta or sine component)
- x - The X component (horizontal delta or cosine component)
Examples
angle = ATN2(1, 0) ' Returns PI/2 (90 degrees)angle = ATN2(0, 1) ' Returns 0 (0 degrees)angle = ATN2(-1, 0) ' Returns -PI/2 (-90 degrees)' Compute angle between two sprites:
dx = SPRITEX(1, 0.5) - SPRITEX(0, 0.5)
dy = SPRITEY(1, 0.5) - SPRITEY(0, 0.5)
angle = ATN2(dy, dx)' Convert to degrees:
degrees = ATN2(dy, dx) * 180 / PI
CEIL
Math · since 0.23.0
Returns the smallest integer greater than or equal to the number (rounds up).
Syntax
CEIL(number)Parameters
- number - Any numeric expression
Examples
PRINT CEIL(3.2) ' Prints 4PRINT CEIL(-2.8) ' Prints -2PRINT CEIL(42) ' Prints 42pages = CEIL(total_items / items_per_page)
CLAMP
Math · since 1.0.3
Constrains a value to lie within a specified range. If value is less than min, returns min. If value is greater than max, returns max. Otherwise returns value unchanged.
Syntax
CLAMP(value, min, max)Parameters
- value - The value to clamp
- min - The minimum bound
- max - The maximum bound
Examples
x = CLAMP(x, 0, screenWidth) ' Keep x within screen boundshp = CLAMP(hp - damage, 0, maxHp) ' Clamp health between 0 and maxalpha = CLAMP(fade, 0, 255) ' Keep alpha in valid rangespeed = CLAMP(speed + accel, -maxSpeed, maxSpeed)
COS
Math · since 0.23.0
Returns the cosine of an angle in radians.
Results are full-precision floats, and PRINT shows all of it. TAN(PI/4) is 0.9999999999999999, not 1 — that is the arithmetic being honest, not a bug. Compare with a tolerance rather than = when it matters: IF ABS(x - 1) < 0.0001 THEN. Use FIXED$(value, places) to show a rounded number.
Syntax
COS(angle)Parameters
- angle - Angle in radians
Examples
PRINT COS(0) ' Prints 1PRINT COS(PI) ' Prints -1angle = 60 * PI / 180 ' Convert degrees to radiansPRINT COS(angle) ' Cos of 60 degrees
EXP
Math · since 0.23.0
Returns e raised to the power of number (e^x).
Syntax
EXP(number)Parameters
- number - Any numeric expression
Examples
PRINT EXP(0) ' Prints 1PRINT EXP(1) ' Prints 2.718281828459045 (e)PRINT EXP(2) ' Prints 7.38905609893065 (e^2)PRINT LOG(EXP(5)) ' Prints 5
FIX
Math · since 0.23.0
Returns the integer part by truncating towards zero.
Syntax
FIX(number)Parameters
- number - Any numeric expression
Examples
PRINT FIX(3.7) ' Prints 3PRINT FIX(-3.7) ' Prints -3PRINT FIX(5) ' Prints 5' INT vs FIX for negative numbers:PRINT INT(-2.5), FIX(-2.5) ' Prints -3, -2See also: INT, CINT
FIXED
Math · since 1.41.0
Rounds a number to a fixed number of decimal places and returns it as a number. Useful for smoothing noisy inputs such as joystick axis values. For exact textual display, use FIXED$ instead (it avoids floating-point representation artifacts).
Syntax
FIXED(value, places)Parameters
- value - Any numeric expression
- places - Number of decimal places (0-15; negative values are treated as 0)
Examples
PRINT FIXED(3.14159, 2) ' Prints 3.14PRINT FIXED(-0.56789, 2) ' Prints -0.57smooth = FIXED(JOYAXIS(0), 2)
FLOOR
Math · since 0.23.0
Returns the largest integer less than or equal to the number (rounds down).
Syntax
FLOOR(number)Parameters
- number - Any numeric expression
Examples
PRINT FLOOR(3.7) ' Prints 3PRINT FLOOR(-2.3) ' Prints -3PRINT FLOOR(42) ' Prints 42page = FLOOR(item / items_per_page)
INT
Math · since 0.23.0
Returns the largest integer less than or equal to number (floor function).
Syntax
INT(number)Parameters
- number - Any numeric expression
Examples
PRINT INT(3.7) ' Prints 3PRINT INT(-3.7) ' Prints -4PRINT INT(5) ' Prints 5dice = INT(RND * 6) + 1 ' Random die roll (1-6)See also: FIX, CINT
LBOUND
Math · since 1.0.3
Returns the lower bound (lowest valid index) of a one-dimensional array. Always returns 0 since arrays use 0-based indexing.
Syntax
LBOUND(array)Parameters
- array - An array variable
Examples
DIM arr(10) AS INTEGER
PRINT LBOUND(arr) ' Prints 0FOR i = LBOUND(arr) TO UBOUND(arr)
PRINT arr(i)
NEXT
LERP
Math · since 1.0.3
Linear interpolation between two values by factor t. When t=0 returns a, when t=1 returns b, when t=0.5 returns the midpoint. Works with both scalar values and coordinates for smooth 2D/3D interpolation.
Syntax
LERP(a, b, t)Parameters
- a - Start value (scalar or coordinate)
- b - End value (scalar or coordinate)
- t - Interpolation factor (typically 0 to 1)
Examples
' Smooth camera movement
camX = LERP(camX, targetX, 0.1)' Fade between colors
r = LERP(startR, endR, progress)' Smooth 2D position interpolation
pos = LERP((0, 0), (100, 200), 0.5) ' Returns (50, 100)' Ease-in movement with coordinate lerp
start = (playerX, playerY)
dest = (goalX, goalY)
current = LERP(start, dest, t)
LOG
Math · since 0.23.0
Returns the natural logarithm (base e) of a number.
Syntax
LOG(number)Parameters
- number - Any positive numeric expression
Examples
PRINT LOG(1) ' Prints 0PRINT LOG(2.718282) ' Prints 1 (log of e)PRINT LOG(10) ' Prints 2.302585log10 = LOG(100) / LOG(10) ' Base 10 logarithm
LOG10
Math · since 1.0.3
Returns the base-10 logarithm of a number.
Syntax
LOG10(number)Parameters
- number - A positive numeric expression
Examples
PRINT LOG10(1) ' Prints 0PRINT LOG10(100) ' Prints 2PRINT LOG10(1000) ' Prints 3
MAG
Math · since 1.0.3
Returns the magnitude (Euclidean length) of a coordinate vector. For scalar values, returns the absolute value. Use MAG(a - b) to compute the distance between two coordinates.
Syntax
MAG(value)Parameters
- value - A coordinate or scalar value
Examples
pos = (3, 4)
PRINT MAG(pos) ' Prints 5 (3-4-5 triangle)a = (10, 20)
b = (40, 60)
dist = MAG(a - b) ' Distance between two pointsvel = (vx, vy, vz)
speed = MAG(vel) ' Speed from 3D velocity vectorPRINT MAG(-7) ' Prints 7 (absolute value for scalars)See also: SQR, ABS, SPRITEDIST
MAX
Math · since 1.0.3
Returns the larger of two values. Preserves the original value type (integer or float) of the result.
Syntax
MAX(a, b)Parameters
- a - First value to compare
- b - Second value to compare
Examples
PRINT MAX(5, 3) ' Prints 5PRINT MAX(-1, 1) ' Prints 1scrollX = MAX(scrollX, 0) ' Clamp to zerohealth = MAX(health - damage, 0) ' Don't go below zero
MIN
Math · since 1.0.3
Returns the smaller of two values. Preserves the original value type (integer or float) of the result.
Syntax
MIN(a, b)Parameters
- a - First value to compare
- b - Second value to compare
Examples
PRINT MIN(5, 3) ' Prints 3PRINT MIN(-1, 1) ' Prints -1speed = MIN(speed + 1, maxSpeed) ' Clamp to maxscrollX = MIN(scrollX, mapWidth - SCREENWIDTH) ' Clamp camera
MOD
Math · since 1.0.3
Returns the remainder of dividing a by b (modulo operation). Also available as the MOD operator: a MOD b. A zero divisor is a runtime error (matching division by zero).
Syntax
MOD(a, b)Parameters
- a - The dividend
- b - The divisor (a value of 0 is a runtime error)
Examples
PRINT MOD(10, 3) ' Prints 1PRINT 10 MOD 3 ' Prints 1 (operator form)IF MOD(frame, 2) = 0 THEN PRINT "even frame"tile = MOD(x, tileSize) ' Position within tile
PI
Math · since 1.57.0
The mathematical constant pi, 3.141592653589793, as a Float. A pseudo-variable, not a function — no parentheses.
Useful for converting between degrees and radians: Crash BASIC's trigonometric functions take radians, while ROTATE, BROTATE and STARTANGLE take degrees.
Syntax
PIExamples
radians = degrees * PI / 180circumference = 2 * PI * radiusPRINT SIN(PI / 2) ' 1
POW
Math · since 1.0.3
Returns base raised to the power of exponent. Also available as the ^ operator: base ^ exponent. The result is always a float, so fractional and negative exponents work. ^ binds tighter than every arithmetic operator including unary minus, so -2 ^ 2 is -(2 ^ 2) = -4; write (-2) ^ 2 to raise a negative base. It is right-associative, so 2 ^ 3 ^ 2 is 2 ^ (3 ^ 2) = 512.
Syntax
POW(base, exponent)Parameters
- base - The base number
- exponent - The power to raise the base to
Examples
PRINT POW(2, 3) ' Prints 8PRINT 2 ^ 3 ' Prints 8 (operator form)PRINT POW(9, 0.5) ' Prints 3 (square root)PRINT 2 ^ -1 ' Prints 0.5 (negative exponent)PRINT POW(10, 0) ' Prints 1PRINT POW(2, 10) ' Prints 1024PRINT 2 * 3 ^ 2 ' Prints 18 (^ before *)PRINT -2 ^ 2 ' Prints -4 (^ before unary minus)PRINT 2 ^ 3 ^ 2 ' Prints 512 (right-associative)
RANDOMIZE
Math · since 0.23.0
Seeds the random number generator for RND function.
Syntax
RANDOMIZE [seed]Parameters
- seed - Optional seed value (uses timer if omitted)
Examples
RANDOMIZE ' Seed with current timeRANDOMIZE 12345 ' Seed with specific value
PRINT RND
RND
Math · since 0.23.0
Returns a random number between 0 and 1 (exclusive of 1).
Syntax
RND[(n)]Parameters
- n - Optional argument (currently ignored)
Examples
PRINT RND ' Random decimal 0 to 0.999...dice = INT(RND * 6) + 1 ' Random integer 1-6x = RND * 100 ' Random number 0-100IF RND < 0.5 THEN PRINT "Heads" ELSE PRINT "Tails"
ROUND
Math · since 0.23.0
Returns the number rounded to the nearest integer.
Syntax
ROUND(number)Parameters
- number - Any numeric expression
Examples
PRINT ROUND(3.7) ' Prints 4PRINT ROUND(3.2) ' Prints 3PRINT ROUND(-2.8) ' Prints -3rounded = ROUND(precise_value)
SGN
Math · since 0.23.0
Returns the sign of a number: 1 if positive, -1 if negative, 0 if zero.
Syntax
SGN(number)Parameters
- number - Any numeric expression
Examples
PRINT SGN(42) ' Prints 1PRINT SGN(-42) ' Prints -1PRINT SGN(0) ' Prints 0direction = SGN(velocity) ' Get movement directionSee also: ABS
SIN
Math · since 0.23.0
Returns the sine of an angle in radians.
Syntax
SIN(angle)Parameters
- angle - Angle in radians
Examples
PRINT SIN(0) ' Prints 0PRINT SIN(3.14159/2) ' Prints 1 (sin of PI/2)angle = 45 * 3.14159 / 180 ' Convert degrees to radiansPRINT SIN(angle) ' Sin of 45 degrees
SQR
Math · since 0.23.0
Returns the square root of a number. A negative argument is a runtime error (the result would be NaN, which is not a representable value).
Syntax
SQR(number)Parameters
- number - A non-negative numeric expression (a negative value is a runtime error)
Examples
PRINT SQR(16) ' Prints 4PRINT SQR(2) ' Prints 1.414213hypotenuse = SQR(3^2 + 4^2) ' Pythagorean theoremPRINT hypotenuse ' Prints 5
TAN
Math · since 0.23.0
Returns the tangent of an angle in radians.
Results are full-precision floats, and PRINT shows all of it. TAN(PI/4) is 0.9999999999999999, not 1 — that is the arithmetic being honest, not a bug. Compare with a tolerance rather than = when it matters: IF ABS(x - 1) < 0.0001 THEN. Use FIXED$(value, places) to show a rounded number.
Syntax
TAN(angle)Parameters
- angle - Angle in radians
Examples
PRINT TAN(0) ' Prints 0PRINT TAN(PI/4) ' Prints 0.9999999999999999angle = 45 * PI / 180 ' Convert degrees to radiansPRINT TAN(angle) ' Tan of 45 degrees
UBOUND
Math · since 1.0.3
Returns the upper bound (highest valid index) of a one-dimensional array. Returns -1 if the array is empty or the argument is not an array.
Syntax
UBOUND(array)Parameters
- array - An array variable
Examples
DIM arr(10) AS INTEGER
PRINT UBOUND(arr) ' Prints 9 (indices 0-9)FOR i = 0 TO UBOUND(names)
PRINT names(i)
NEXTIF i <= UBOUND(arr) THEN value = arr(i) ' Bounds check
Bitwise
BITAND
Bitwise · since 1.33.0
Bitwise AND of two integers (operates on the 64-bit pattern). Distinct from the boolean AND operator: BITAND(12, 10) is 8, whereas (12 AND 10) is 1.
Syntax
BITAND(a, b)Parameters
- a - Integer (floats are truncated)
- b - Integer (floats are truncated)
Examples
mask = BITAND(flags, 15) ' low nibblePRINT BITAND(12, 10) ' 8See also: BITOR, BITXOR, BITNOT, SHL, SHR
BITCLEAR
Bitwise · since 1.33.0
Returns a with bit number bit set to 0 (other bits unchanged). Bit 0 is the least significant. A negative index halts; an index >= 64 returns a unchanged.
Syntax
BITCLEAR(a, bit)Parameters
- a - Integer (floats are truncated)
- bit - Bit index 0..63 (0 = least significant)
Examples
flags = BITCLEAR(flags, 3) ' turn bit 3 offbitboard = BITCLEAR(bitboard, square)See also: BITSET, BITTEST, BITAND
BITNOT
Bitwise · since 1.33.0
One's complement (flips every bit) of an integer. BITNOT(0) is -1 because the value is a signed 64-bit integer.
Syntax
BITNOT(a)Parameters
- a - Integer (floats are truncated)
Examples
PRINT BITNOT(0) ' -1PRINT BITNOT(5) ' -6See also: BITAND, BITOR, BITXOR
BITOR
Bitwise · since 1.33.0
Bitwise OR of two integers (operates on the 64-bit pattern). Distinct from the boolean OR operator.
Syntax
BITOR(a, b)Parameters
- a - Integer (floats are truncated)
- b - Integer (floats are truncated)
Examples
flags = BITOR(flags, 4) ' set bit 2PRINT BITOR(12, 10) ' 14See also: BITAND, BITXOR, BITNOT, SHL, SHR
BITSET
Bitwise · since 1.33.0
Returns a with bit number bit set to 1 (other bits unchanged). Bit 0 is the least significant. A negative index halts; an index >= 64 returns a unchanged.
Syntax
BITSET(a, bit)Parameters
- a - Integer (floats are truncated)
- bit - Bit index 0..63 (0 = least significant)
Examples
flags = BITSET(flags, 3) ' turn bit 3 onbitboard = BITSET(bitboard, square)See also: BITCLEAR, BITTEST, BITOR
BITTEST
Bitwise · since 1.33.0
Returns 1 if bit number bit of a is set, else 0. Bit 0 is the least significant bit. A negative index halts; an index >= 64 reads as 0 (no such bit in a 64-bit value).
Syntax
BITTEST(a, bit)Parameters
- a - Integer (floats are truncated)
- bit - Bit index 0..63 (0 = least significant)
Examples
IF BITTEST(flags, 3) THEN PRINT "bit 3 set"occupied = BITTEST(bitboard, square)See also: BITSET, BITCLEAR, BITAND
BITXOR
Bitwise · since 1.33.0
Bitwise XOR (exclusive or) of two integers.
Syntax
BITXOR(a, b)Parameters
- a - Integer (floats are truncated)
- b - Integer (floats are truncated)
Examples
flags = BITXOR(flags, 4) ' toggle bit 2PRINT BITXOR(12, 10) ' 6See also: BITAND, BITOR, BITNOT, SHL, SHR
SHL
Bitwise · since 1.33.0
Shift the bits of a left by n positions. n must be >= 0 (negative halts); shifting by >= 64 yields 0.
Syntax
SHL(a, n)Parameters
- a - Integer to shift (floats are truncated)
- n - Number of bit positions, >= 0
Examples
PRINT SHL(1, 4) ' 16bit = SHL(1, n) ' bitmask for bit n
SHR
Bitwise · since 1.33.0
Logical (zero-fill) shift of the bits of a right by n positions. Treats the value as a 64-bit pattern, so the sign bit does not propagate (correct for masks/bitboards). n must be >= 0 (negative halts); shifting by >= 64 yields 0.
Syntax
SHR(a, n)Parameters
- a - Integer to shift (floats are truncated)
- n - Number of bit positions, >= 0
Examples
PRINT SHR(16, 2) ' 4PRINT SHR(-1, 1) ' 9223372036854775807 (logical, not arithmetic)
Input
DRAGX
Input · since 0.23.0
Returns normalized horizontal drag value from -1 to 1 while actively dragging, 0 when not dragging. Works with both touch (mobile) and mouse (desktop). -1 is maximum left drag, 0 is no drag, 1 is maximum right drag. Values are normalized based on half the screen width. Automatically returns 0 when pointer is released.
Syntax
DRAGX()Examples
REM Real-time camera pan
DO
dx = DRAGX()
IF dx <> 0 THEN cameraX = cameraX - dx * 10
DELAY 16
LOOPREM Swipe-based menu navigation
DO
dx = DRAGX()
IF dx < -0.5 THEN
CALL NextPage()
DELAY 300 ' Debounce
END IF
IF dx > 0.5 THEN
CALL PrevPage()
DELAY 300
END IF
DELAY 16
LOOPREM Horizontal scrolling with momentum
velocity = 0
DO
dx = DRAGX()
IF dx <> 0 THEN
velocity = dx * 15
ELSE
velocity = velocity * 0.9 ' Decay when released
END IF
scrollX = scrollX + velocity
DELAY 16
LOOPSee also: DRAGY, TAP, MOUSEX, MOUSEY, MOUSEDOWN
DRAGY
Input · since 0.23.0
Returns normalized vertical drag value from -1 to 1 while actively dragging, 0 when not dragging. Works with both touch (mobile) and mouse (desktop). -1 is maximum up drag, 0 is no drag, 1 is maximum down drag. Values are normalized based on half the screen height. Automatically returns 0 when pointer is released.
Syntax
DRAGY()Examples
REM Real-time vertical scrolling
DO
dy = DRAGY()
IF dy <> 0 THEN scrollY = scrollY - dy * 100
DELAY 16
LOOPREM Drag-to-aim mechanic
DO
dx = DRAGX()
dy = DRAGY()
IF dx <> 0 OR dy <> 0 THEN
aimAngle = ATN2(dy, dx)
aimPower = SQR(dx * dx + dy * dy)
PRINT "Aim:", aimAngle, "Power:", aimPower
END IF
DELAY 16
LOOPREM Pull-down-to-refresh gesture
DO
dy = DRAGY()
IF dy > 0.7 AND scrollY = 0 THEN
PRINT "Refreshing..."
CALL RefreshContent()
END IF
DELAY 16
LOOPSee also: DRAGX, TAP, MOUSEX, MOUSEY, MOUSEDOWN
GAMEPAD
Input · since 1.4.8
Controls virtual and physical gamepad input. The virtual gamepad displays on-screen buttons (D-pad + A/B/X/Y + START) for mobile touch controls. Physical gamepad support enables input from USB/Bluetooth controllers. Both use the same GAMEPAD MAP configuration to inject virtual key presses, allowing existing KEYDOWN-based games to work with any input method. The D-pad can optionally be displayed as an analog joystick that supports diagonal movement.
tvOS: the virtual (touch) gamepad is never shown on tvOS — there's no touchscreen to drive it — so GAMEPAD ON is a no-op there. Gate your UI with ISMOBILE() (false on tvOS) and use GAMEPAD PHYSICAL ON for the Siri Remote / game controllers instead. Older carts that call GAMEPAD ON unconditionally are handled safely: the overlay simply won't appear.
GAMEPAD PHYSICAL ON is a gate, and it comes first. Physical controller support starts off. That statement is what initializes the controller backend and begins polling — so until it runs, no controller is read and none is reported. JOYCONNECTED, JOYBUTTON, JOYAXIS and RUMBLE all return nothing beforehand, however many pads are plugged in or paired.
Enable it early — next to SCREEN — not when you first want to read a pad. On desktop and the web a controller is discovered by polling, so it turns up on the first poll after you enable and the ordering rarely bites. On Apple TV, iOS and Android it is pushed: the system announces a controller once, when it pairs. A pad that was already paired before your program enabled physical input announced itself while the subsystem was switched off. Enabling re-announces controllers it already knows about, precisely to cover this — but anything you read before enabling still comes back empty, and on Apple TV a controller is almost always already paired when your game starts. That's the failure to expect: a game that works on your desktop and sees no controller on the TV.
GAMEPAD PHYSICAL OFF stops polling again.
Syntax
GAMEPAD ON | OFF | MAP "button", "key" | BUTTON "button" ON|OFF | BUTTON "button" AT x, y | RESET | PHYSICAL ON | PHYSICAL OFF | DPAD JOYSTICK | DPAD BUTTONS | DPAD DRAG | ALPHA valueParameters
- ON - Show the virtual gamepad overlay (mobile touch)
- OFF - Hide the virtual gamepad overlay
- MAP "button", "key" - Map a gamepad button to simulate a keyboard key (affects both virtual and physical)
- BUTTON "button" ON - Show a specific virtual button (default)
- BUTTON "button" OFF - Hide a specific virtual button so it is neither drawn nor touch-interactive
- BUTTON "button" AT x, y - Move a specific virtual button (0.0-1.0 window %); hit box follows automatically
- RESET - Clear all button-to-key mappings
- PHYSICAL ON - Enable physical gamepad/joystick polling
- PHYSICAL OFF - Disable physical gamepad polling
- DPAD JOYSTICK - Display D-pad as analog joystick (supports diagonal movement)
- DPAD BUTTONS - Display D-pad as 4 separate buttons (default)
- DPAD DRAG - Tap and drag anywhere on screen for directional input; touch point becomes the center, drag direction maps to UP/DOWN/LEFT/RIGHT
- ALPHA value - Set the transparency of the virtual gamepad buttons (0.0 = fully transparent, 1.0 = fully opaque)
- "button" - One of: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", "START"
Examples
REM Enable virtual gamepad on mobile, physical on desktop
IF ISMOBILE() THEN
GAMEPAD ON
ELSE
GAMEPAD PHYSICAL ON
END IF
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD MAP "A", "SPACE"
GAMEPAD MAP "START", "ESCAPE"REM Game loop works the same for keyboard, virtual, and physical gamepad
DO
IF KEYDOWN("W") THEN playerY = playerY - 2
IF KEYDOWN("S") THEN playerY = playerY + 2
IF KEYDOWN("A") THEN playerX = playerX - 2
IF KEYDOWN("D") THEN playerX = playerX + 2
IF KEYDOWN("SPACE") THEN CALL PlayerJump()
DELAY 16
LOOPREM Use joystick mode for diagonal movement on mobile
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD ON
GAMEPAD DPAD JOYSTICK
REM Now players can move diagonally by dragging the virtual joystick!REM Use analog stick for smooth movement (physical gamepad)
GAMEPAD PHYSICAL ON
DO
lx = JOYAXIS(0)
ly = JOYAXIS(1)
REM lx/ly are left stick X/Y (-1 to 1)
IF ABS(lx) > 0.2 THEN playerX = playerX + lx * 5
IF ABS(ly) > 0.2 THEN playerY = playerY + ly * 5
DELAY 16
LOOPREM Check if controller is connected
IF JOYCONNECTED() THEN
PRINT "Controller detected!"
GAMEPAD PHYSICAL ON
ELSE
PRINT "No controller - use keyboard"
END IFREM Drag-to-move: tap anywhere and drag for directional input
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD MAP "A", "SPACE"
GAMEPAD DPAD DRAG
GAMEPAD ON
REM D-pad buttons are hidden; drag anywhere to move, tap A/B/X/Y for actionsSee also: GAMEPAD SETIMAGE, GAMEPAD ALPHA, GAMEPAD BUTTON, GAMEPAD BUTTON AT, KEYDOWN, ISMOBILE, JOYAXIS, JOYCONNECTED, TAP, TOUCHX, TOUCHY
GAMEPAD ALPHA
Input · since 1.7.3
Sets the transparency of the virtual gamepad buttons. This allows you to make the on-screen controls more or less visible, useful for adjusting visibility based on the game's visuals or player preference. The alpha value affects both the button fills and labels.
Syntax
GAMEPAD ALPHA valueParameters
- value - Transparency level from 0.0 (fully transparent/invisible) to 1.0 (fully opaque). Values outside this range are clamped.
Examples
REM Make the gamepad semi-transparent
GAMEPAD ON
GAMEPAD ALPHA 0.5REM Make the gamepad nearly invisible
GAMEPAD ON
GAMEPAD ALPHA 0.2REM Restore full opacity
GAMEPAD ALPHA 1.0REM Fade gamepad based on player preference
alpha = LOAD("gamepad_alpha")
IF alpha = "" THEN alpha = 0.5
GAMEPAD ALPHA VAL(alpha)See also: GAMEPAD, GAMEPAD SETIMAGE
GAMEPAD BUTTON
Input · since 1.19.3
Enables or disables an individual virtual gamepad button. Disabled buttons are not drawn and do not respond to touch, letting games show only the controls they actually use (for example, a puzzle game with just a D-pad and an A button). Buttons are enabled by default. Turning a pressed button OFF releases any mapped key before hiding it so no input gets stuck down. Only affects the on-screen virtual gamepad; physical gamepad buttons are unaffected.
Syntax
GAMEPAD BUTTON button$ ON|OFF
GAMEPAD BUTTON button$ AT x, y (the position also accepts a coordinate: AT (x,y) or AT coord)Parameters
- button$ - The button to toggle: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
- ON - Show and enable the button (default state)
- OFF - Hide the button and disable touch hit-testing for it
Examples
REM Puzzle game: only D-pad and A
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD ONREM Runner game: only A for jump
GAMEPAD BUTTON "UP" OFF
GAMEPAD BUTTON "DOWN" OFF
GAMEPAD BUTTON "LEFT" OFF
GAMEPAD BUTTON "RIGHT" OFF
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD MAP "A", "SPACE"
GAMEPAD ONREM Re-enable a button later
GAMEPAD BUTTON "START" ONSee also: GAMEPAD, GAMEPAD BUTTON AT, GAMEPAD SETIMAGE, GAMEPAD ALPHA
GAMEPAD BUTTON AT
Input · since 1.19.3
Repositions a virtual gamepad button. Coordinates are expressed as a fraction of the window size (0.0-1.0), matching the default layout. Values are clamped to that range. The hit box follows automatically so touches still register where the button is drawn. Use this to custom-lay-out controls per game without having to rebuild the engine, for example moving the A button further from the D-pad for thumb comfort or relocating START out of the way of gameplay.
Syntax
GAMEPAD BUTTON button$ AT x, yParameters
- button$ - The button to move: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
- x - Horizontal center as a fraction of window width (0.0 = left edge, 1.0 = right edge)
- y - Vertical center as a fraction of window height (0.0 = top edge, 1.0 = bottom edge)
Examples
REM Push the A button toward the bottom-right thumb zone
GAMEPAD BUTTON "A" AT 0.92, 0.85
GAMEPAD ONREM Compact single-button layout: only A, centered low on the right
GAMEPAD BUTTON "UP" OFF
GAMEPAD BUTTON "DOWN" OFF
GAMEPAD BUTTON "LEFT" OFF
GAMEPAD BUTTON "RIGHT" OFF
GAMEPAD BUTTON "B" OFF
GAMEPAD BUTTON "X" OFF
GAMEPAD BUTTON "Y" OFF
GAMEPAD BUTTON "START" OFF
GAMEPAD BUTTON "A" AT 0.85, 0.8
GAMEPAD MAP "A", "SPACE"
GAMEPAD ONREM Move START out of the way in the top-center
GAMEPAD BUTTON "START" AT 0.5, 0.08See also: GAMEPAD, GAMEPAD BUTTON, GAMEPAD SETIMAGE
GAMEPAD SETIMAGE
Input · since 1.7.0
Sets a custom image for a virtual gamepad button. This allows you to replace the default button appearance with your own graphics, useful for theming the gamepad to match your game's art style. The image must be previously loaded with LOADIMG. Use an empty string to reset a button to its default appearance.
Syntax
GAMEPAD SETIMAGE button$, image$Parameters
- button$ - The button to customize: "UP", "DOWN", "LEFT", "RIGHT", "A", "B", "X", "Y", or "START"
- image$ - The name of a loaded image to use, or empty string "" to reset to default
Examples
REM Load custom button images
LOADIMG "sprites/btn_jump.png", "jump_btn"
LOADIMG "sprites/btn_attack.png", "attack_btn"
REM Apply custom images to A and B buttons
GAMEPAD SETIMAGE "A", "jump_btn"
GAMEPAD SETIMAGE "B", "attack_btn"
REM Show the gamepad
GAMEPAD ONREM Reset a button to default appearance
GAMEPAD SETIMAGE "A", ""REM Theme all buttons for a retro game
LOADIMG "ui/dpad_up.png", "dpad_u"
LOADIMG "ui/dpad_down.png", "dpad_d"
LOADIMG "ui/dpad_left.png", "dpad_l"
LOADIMG "ui/dpad_right.png", "dpad_r"
GAMEPAD SETIMAGE "UP", "dpad_u"
GAMEPAD SETIMAGE "DOWN", "dpad_d"
GAMEPAD SETIMAGE "LEFT", "dpad_l"
GAMEPAD SETIMAGE "RIGHT", "dpad_r"See also: GAMEPAD, GAMEPAD ALPHA, LOADIMAGE
INKEY$
Input · since 0.23.0
Returns the currently held key as a string, respecting case. Pressing 'a' returns "a", pressing Shift+A returns "A". Returns an empty string when no key is held. The value clears when the key is released. For case-insensitive key checking, use KEYDOWN() instead.
Syntax
variable$ = INKEY$Examples
key$ = INKEY$
IF key$ <> "" THEN PRINT key$DO
k$ = INKEY$
LOOP WHILE k$ = ""IF INKEY$ = "q" THEN END ' Quit on lowercase q onlyIF UCASE$(INKEY$) = "Q" THEN END ' Quit on Q or qSee also: INPUT, ASC, KEYDOWN, PAUSE
INPUT
Input · since 0.23.0
Gets input from the user. By default, displays a simple text input. Use the WITH CURSOR modifier for a themed input field with a blinking cursor that uses the current foreground color for text and border, matching the CrashBasic visual style. The cursor character can be customized (e.g., "_" or "█"). User can type and edit input, then press Enter to submit. Multiple variables can be entered separated by commas.
Syntax
INPUT [WITH CURSOR(char$)] [prompt ;|,] variable [, variable ...]Parameters
- WITH CURSOR(char$) - Optional modifier to display a themed input field with blinking cursor using the specified character
- prompt - Optional text to display before the input field
- variable - Variable(s) to store input (string or numeric)
Examples
INPUT name$INPUT "Enter your name: "; name$INPUT "Enter your name: ", name$INPUT WITH CURSOR("_") "Enter your name: "; name$INPUT WITH CURSOR("█") "Password: "; pass$COLOR 14: INPUT WITH CURSOR("_") "Yellow prompt: "; value$INPUT "X, Y coordinates: "; x, ySee also: PRINT, INKEY$, COLOR, LOCATE
ISCONSOLE
Input · since 1.56.1
Returns 1 on a living-room console — tvOS today, with Xbox and Switch as those platforms land — and 0 everywhere else.
A console is the third case beside mobile and desktop: no touchscreen, but no keyboard and mouse either. It is driven by a remote or a game controller on a screen across the room. Checking ISMOBILE() alone can't find it, and treating "not mobile" as "desktop" lands a cart on mouse and keyboard assumptions that do not hold there.
The three platform classes are mutually exclusive, so a cart can pick its control scheme directly:
| Platform | ISMOBILE() | ISCONSOLE() |
|---|---|---|
| iOS, Android | 1 | 0 |
| tvOS | 0 | 1 |
| Desktop, web | 0 | 0 |
Both report 0 until the frontend has reported a platform, so a program that asks before its first SCREEN reads as desktop.
Syntax
ISCONSOLE()Examples
REM Pick a control scheme per platform class.
IF ISCONSOLE() THEN
GAMEPAD PHYSICAL ON ' Siri Remote / controller
PRINT "Use the remote to steer"
ELSEIF ISMOBILE() THEN
GAMEPAD ON ' on-screen touch pad
PRINT "Tap to jump, swipe to move"
ELSE
PRINT "SPACE to jump, WASD to move"
END IFREM A console has no pointer, so drive a cursor with the stick instead.
IF ISCONSOLE() THEN
cursorX = cursorX + JOYAXIS(0) * speed
cursorY = cursorY + JOYAXIS(1) * speed
ELSE
cursorX = MOUSEX()
cursorY = MOUSEY()
END IFSee also: ISMOBILE, JOYCONNECTED, JOYAXIS, JOYBUTTON, GAMEPAD
ISMOBILE
Input · since 1.4.8
Returns 1 in the native iOS and Android players, 0 everywhere else — desktop, web and tvOS. Use it to gate touch-specific UI: an on-screen gamepad, a "tap to continue" prompt, touch control hints.
tvOS is not mobile. It is a television driven by a remote, so it reports 0 — touch UI shown there would be something nobody can press. GAMEPAD ON (the virtual, on-screen gamepad) is a no-op on tvOS for the same reason; use GAMEPAD PHYSICAL ON for the Siri Remote and controllers.
It answers for the BUILD, not the device. The flag is the platform the player was compiled for — iOS or Android — so it is 1 in the native iOS and Android apps and 0 in a web browser, including a browser on a phone. There is no user-agent sniffing and no touch-capability probe.
That is deliberate rather than an oversight: the handheld experience ships as the native iOS and Android players, which is where touch UI, the virtual gamepad and the store integrations live. The web build targets desktop browsers, so ISMOBILE() reporting 0 there is the answer a cart wants. If you specifically need to serve a phone browser, detect it outside the cart and pass it in — ISMOBILE() will not tell you.
Returns 0 on tvOS — use ISCONSOLE() to detect a console.
Syntax
ISMOBILE()Examples
REM Enable the on-screen gamepad only where it can be touched.
REM (0 on tvOS, so the Siri Remote path is taken instead.)
IF ISMOBILE() THEN
GAMEPAD MAP "UP", "W"
GAMEPAD MAP "DOWN", "S"
GAMEPAD MAP "LEFT", "A"
GAMEPAD MAP "RIGHT", "D"
GAMEPAD MAP "A", "SPACE"
GAMEPAD ON
ELSE
GAMEPAD PHYSICAL ON
END IFREM Show different control hints
IF ISMOBILE() THEN
PRINT "Tap to jump, swipe to move"
ELSE
PRINT "SPACE to jump, WASD to move"
END IFREM Use touch or mouse based on device
IF ISMOBILE() THEN
x = TOUCHX()
y = TOUCHY()
ELSE
x = MOUSEX()
y = MOUSEY()
END IFSee also: GAMEPAD, JOYCONNECTED, TOUCHX, TOUCHY, TAP, DRAGX, DRAGY
JOYAXIS
Input · since 1.4.8
Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.
Returns the value of a physical gamepad axis. Sticks range -1 to 1 (small deadzone applied); triggers range 0 to 1. Returns 0 if the controller is not connected or the axis is not available.
Axis indices:
0 Left stick X 1 Left stick Y
2 Right stick X 3 Right stick Y
4 D-pad X (-1 left, +1 right) 5 D-pad Y (-1 up, +1 down)
6 Left trigger (0..1) 7 Right trigger (0..1)
The d-pad axes (4/5) are derived from the d-pad buttons so they read the same on every platform (+X right, +Y down). On native desktop the analog stick Y sign follows the hardware convention, which can differ from the d-pad's screen-style sign.
The optional player index (0-based) selects which connected controller to read; it defaults to 0 (the first controller), so existing single-controller code is unchanged. Up to 4 controllers are tracked (players 0 through 3). An out-of-range player returns 0.
Use GAMEPAD PHYSICAL ON to enable physical gamepad polling.
Syntax
JOYAXIS(axis [, player])Parameters
- axis - Axis index 0-7: 0/1 left stick, 2/3 right stick, 4/5 d-pad, 6/7 triggers (see description)
- player - Optional 0-based controller index (0-3). Defaults to 0.
Examples
REM Basic analog stick movement
GAMEPAD PHYSICAL ON
DO
lx = JOYAXIS(0)
ly = JOYAXIS(1)
playerX = playerX + lx * speed
playerY = playerY + ly * speed
DELAY 16
LOOPREM Right stick for aiming
aimX = JOYAXIS(2)
aimY = JOYAXIS(3)
IF ABS(aimX) > 0.2 OR ABS(aimY) > 0.2 THEN
aimAngle = ATN2(aimY, aimX)
END IFREM Dual stick controls
DO
REM Left stick for movement
moveX = JOYAXIS(0) * moveSpeed
moveY = JOYAXIS(1) * moveSpeed
REM Right stick for camera
camX = camX + JOYAXIS(2) * camSpeed
camY = camY + JOYAXIS(3) * camSpeed
DELAY 16
LOOPSee also: JOYCONNECTED, GAMEPAD, KEYDOWN
JOYBUTTON
Input · since 1.41.0
Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.
Returns 1 if the given physical gamepad button is currently held, 0 otherwise. The optional player index (0-based) selects which connected controller to read; it defaults to 0 (the first controller). Up to 4 controllers are tracked (players 0 through 3). An out-of-range player returns 0.
Button indices use the standard gamepad layout (the same physical button maps to the same index on every platform):
0 A 1 B 2 X 3 Y
4 LB (left shoulder) 5 RB (right shoulder)
6 LT (left trigger) 7 RT (right trigger)
8 SELECT/Back 9 START
10 L3 (left stick click) 11 R3 (right stick click)
12 D-pad Up 13 D-pad Down 14 D-pad Left 15 D-pad Right
16 Guide/Xbox 17 Share/Capture
LT/RT (6/7) report as pressed past a threshold; for their analog pressure use JOYAXIS(6) / JOYAXIS(7). Guide (16) and Share (17) are reported where the controller and platform expose them (Share is not standardized on the web).
Use GAMEPAD PHYSICAL ON to enable physical gamepad polling.
Syntax
JOYBUTTON(button [, player])Parameters
- button - Standard button index (0-17, see description).
- player - Optional 0-based controller index (0-3). Defaults to 0.
Examples
REM Jump when the A button is pressed
GAMEPAD PHYSICAL ON
IF JOYBUTTON(0) THEN CALL player.jump()REM Read face buttons + start
IF JOYBUTTON(0) THEN CALL fire()
IF JOYBUTTON(1) THEN CALL dash()
IF JOYBUTTON(9) THEN CALL pauseGame()REM Two players, each on their own pad
IF JOYBUTTON(0, 0) THEN CALL p1Fire()
IF JOYBUTTON(0, 1) THEN CALL p2Fire()See also: JOYAXIS, JOYCONNECTED, GAMEPAD, KEYDOWN
JOYCONNECTED
Input · since 1.4.8
Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.
Reports physical gamepad/joystick connectivity. Called with no argument, it returns the count of connected controllers (0 to 4). Called with a player index (0-based), it returns 1 if that specific controller is connected, or 0 if not.
Because any non-zero count is truthy, IF JOYCONNECTED() THEN ... still works to detect "at least one controller". Use the count to support local multiplayer, and JOYCONNECTED(n) to check a specific player slot. Up to 4 controllers are tracked (players 0 through 3).
Syntax
JOYCONNECTED([player])Parameters
- player - Optional 0-based controller index (0-3). When omitted, returns the total connected count instead.
Examples
REM Check for controller on startup
IF JOYCONNECTED() THEN
PRINT "Press START to begin"
GAMEPAD PHYSICAL ON
ELSE
PRINT "Press ENTER to begin"
END IFREM Local multiplayer: how many pads are plugged in?
pads = JOYCONNECTED()
PRINT pads; " controller(s) connected"
FOR p = 0 TO pads - 1
PRINT "Player "; p; " ready"
NEXTREM Check a specific player slot
IF JOYCONNECTED(1) THEN
PRINT "Player 2 has a controller"
END IFSee also: JOYAXIS, JOYBUTTON, GAMEPAD, ISMOBILE
KEYDOWN
Input · since 0.23.0
Returns 1 if the specified key is currently pressed, 0 if not. Ideal for real-time game input and smooth character movement.
Key names are the ones the input boundary reports: Escape, Enter, Space, Tab, Backspace, Delete, Insert, Up, Down, Left, Right, Home, End, PageUp, PageDown, Shift, Control, Alt, CapsLock, F1-F12, or a single character such as "A" or "7". Case does not matter.
A name that names no key is not an error — it answers 0, because a key that cannot be pressed is never down. Watch the spelling for that reason: the key is called Escape, so KEYDOWN("ESC") returns 0 forever rather than telling you why.
Syntax
KEYDOWN(key_name)Parameters
- key_name - String name of key to check (e.g., "SPACE", "LEFT", "RIGHT", "UP", "DOWN", "A", "ESCAPE")
Examples
IF KEYDOWN("SPACE") THEN PRINT "Space is pressed"IF KEYDOWN("LEFT") THEN playerX = playerX - 2IF KEYDOWN("RIGHT") THEN playerX = playerX + 2IF KEYDOWN("ESCAPE") THEN END
MOUSEDOWN
Input · since 0.23.0
Returns 1 if the specified mouse button is currently pressed, 0 if not. Button parameter is optional and defaults to left mouse button.
Syntax
MOUSEDOWN([button])Parameters
- button - Mouse button number (0=left, 1=middle, 2=right) - optional, defaults to 0
Examples
IF MOUSEDOWN() THEN PRINT "Left mouse button pressed"IF MOUSEDOWN(0) THEN PRINT "Left button"IF MOUSEDOWN(2) THEN PRINT "Right button"IF MOUSEDOWN() THEN
IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
CALL HandleButtonClick()
END IF
END IFSee also: MOUSEX, MOUSEY, KEYDOWN, COLLISION
MOUSEX
Input · since 0.23.0
Returns the current X coordinate of the mouse cursor relative to the game canvas.
Syntax
MOUSEX()Examples
mouseX = MOUSEX()PRINT "Mouse at X:", MOUSEX()IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
PRINT "Mouse over button"
END IFSee also: MOUSEY, MOUSEDOWN, COLLISION
MOUSEY
Input · since 0.23.0
Returns the current Y coordinate of the mouse cursor relative to the game canvas.
Syntax
MOUSEY()Examples
mouseY = MOUSEY()PRINT "Mouse at Y:", MOUSEY()IF COLLISION("POINT", MOUSEX(), MOUSEY(), targetX, targetY, targetW, targetH) THEN
PRINT "Mouse over target"
END IFSee also: MOUSEX, MOUSEDOWN, COLLISION
PAUSE
Input · since 1.4.6
Halts program execution until the user presses any key. Useful for displaying messages that the user needs to read before continuing, or for simple "press any key to continue" prompts.
Syntax
PAUSEExamples
PRINT "Press any key to continue..."
PAUSEPRINT "Game Over!"
PRINT "Final Score:"; score
PAUSE
ENDSCREEN 13
PRINT "Loading complete!"
PAUSE
CLSSee also: INKEY$, INPUT, DELAY, KEYDOWN
RUMBLE
Input · since 1.41.0
Requires GAMEPAD PHYSICAL ON. Physical controllers are not polled at all until your program enables them, so before that call this reports nothing — no matter what is plugged in or paired. Enable it early, next to SCREEN, rather than lazily when you first want to read a pad. See GAMEPAD for why a late enable is especially easy to get wrong on Apple TV.
Vibrates a physical controller. Controllers have two motors: a low-frequency "heavy" motor and a high-frequency "light" motor.
Forms:
RUMBLE STRENGTH s DURATION ms drives both motors at magnitude s.RUMBLE LOW lo HIGH hi DURATION ms drives the heavy and light motors separately.RUMBLE STOP cancels rumble immediately.
Magnitudes are 0.0 to 1.0 (clamped). DURATION is in milliseconds; the rumble auto-stops when it elapses. PLAYER p (0-based) targets a specific controller and defaults to 0. RUMBLE PLAYER p STOP stops a specific controller.
Rumble targets a physical controller that supports vibration (turn on GAMEPAD PHYSICAL ON so controllers are detected). Strength is delivered as faithfully as each platform's haptics allow.
On mobile (iOS/Android), when no physical controller is connected for that slot — for example with the on-screen virtual gamepad (GAMEPAD ON) or touch play — RUMBLE falls back to the device's own vibrator so haptics still reach the player (on hardware that has one; iPads have no Taptic engine). On desktop and web, rumble drives the controller only.
Syntax
RUMBLE [PLAYER p] (STRENGTH s | LOW lo HIGH hi) DURATION msParameters
- PLAYER p - Optional 0-based controller index (0-3). Defaults to 0.
- STRENGTH s - Both motors at magnitude s (0.0 to 1.0).
- LOW lo HIGH hi - Heavy (low-freq) and light (high-freq) motor magnitudes separately.
- DURATION ms - How long to rumble, in milliseconds (required for the play forms).
Examples
REM Quick buzz on hit
GAMEPAD PHYSICAL ON
IF playerHit THEN RUMBLE STRENGTH 0.8 DURATION 150REM Heavy engine rumble on player 2's pad
RUMBLE PLAYER 1 LOW 0.9 HIGH 0.2 DURATION 500REM Stop all rumble when paused
IF paused THEN RUMBLE STOPSee also: JOYBUTTON, JOYAXIS, GAMEPAD
TAP
Input · since 0.23.0
Returns 1 while pointer/finger is held down (without dragging), 0 when released or when dragging starts. Works with both touch (mobile) and mouse (desktop) input. This is a state, not an event - it remains 1 continuously while tapping, making it easy to detect in game loops.
Syntax
TAP()Examples
REM Continuous fire while tapping
DO
IF TAP() THEN
PRINT "Firing at:", MOUSEX(), MOUSEY()
CALL FireWeapon()
END IF
DELAY 16
LOOPREM Button detection with visual feedback
IF TAP() THEN
IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, 64, 32) THEN
PRINT "Button pressed!" ' Shows continuously while held
END IF
END IFREM Charge-up mechanic
chargeTime = 0
DO
IF TAP() THEN
chargeTime = chargeTime + 1
PRINT "Charging:", chargeTime
ELSE
IF chargeTime > 0 THEN CALL ReleaseAttack(chargeTime)
chargeTime = 0
END IF
DELAY 16
LOOPSee also: DRAGX, DRAGY, MOUSEX, MOUSEY, MOUSEDOWN, COLLISION
TOUCHX
Input · since 1.4.8
Returns the current X coordinate of an active touch point in logical game coordinates. Returns -1 if there is no active touch (finger is not touching the screen). Unlike MOUSEX(), this only responds to touch input and ignores mouse input, making it useful for distinguishing between mouse and touch events.
Syntax
TOUCHX()Examples
REM Check for active touch
touchX = TOUCHX()
IF touchX >= 0 THEN
PRINT "Touching at X:", touchX
END IFREM Track touch position for painting
DO
x = TOUCHX()
y = TOUCHY()
IF x >= 0 AND y >= 0 THEN
PSET (x, y), 15
END IF
DELAY 16
LOOPSee also: TOUCHY, MOUSEX, TAP, DRAGX, ISMOBILE
TOUCHY
Input · since 1.4.8
Returns the current Y coordinate of an active touch point in logical game coordinates. Returns -1 if there is no active touch (finger is not touching the screen). Unlike MOUSEY(), this only responds to touch input and ignores mouse input.
Syntax
TOUCHY()Examples
REM Check for active touch
touchY = TOUCHY()
IF touchY >= 0 THEN
PRINT "Touching at Y:", touchY
END IFREM Draw line from touch start to current position
IF TOUCHX() >= 0 THEN
LINE (startX, startY)-(TOUCHX(), TOUCHY()), 14
END IFSee also: TOUCHX, MOUSEY, TAP, DRAGY, ISMOBILE
Graphics
ALPHA
Graphics · since 0.23.0
Graphics modifier used with PUTIMAGE to control transparency and fade effects. Value ranges from 0 (fully transparent) to 1 (fully opaque). Creates smooth transparency blending for advanced visual effects.
Syntax
ALPHA(fade_value)Parameters
- fade_value - Transparency level (0.0-1.0, where 0=transparent, 1=opaque)
Examples
PUTIMAGE (100, 100), "ghost", ALPHA(0.5) ' 50% transparentPUTIMAGE (50, 50), "sprite", 2, ALPHA(0.8) ' Frame 2, 80% opaque' Fade in effect:
FOR i = 0 TO 10
alpha = i / 10
PUTIMAGE (200, 100), "item", ALPHA(alpha)
NEXT' Fade out effect:
FOR i = 10 TO 0 STEP -1
alpha = i / 10
PUTIMAGE (200, 100), "item", ALPHA(alpha)
NEXTSee also: PUTIMAGE, LOADIMG, PALETTE, SCALE, REGION
ASIMAGE
Graphics · since 1.27.0
Builds a named image from a DATA block of palette color indices, one value per pixel in row-major order (left-to-right, top-to-bottom). The result is an ordinary image resource: display it with PUTIMAGE, use it as a sprite texture, or capture and combine it like any other image. Ideal for hand-authored pixel art, icons, and tiles embedded directly in source.
The DATA block must contain exactly width * height values. Each value is a palette color index (0 or greater) or -1 for a transparent pixel. There is no upper bound on the index — the palette is dynamically sized. A value below -1, a missing DATA block, a value count that does not match width * height, or a zero/negative dimension is a runtime error.
In CrashNet multiplayer, ASIMAGE runs on every interpreter (host, player-server, client) so the image's dimensions register on each one's collision engine, just like NEWIMAGE / LOADIMG. The pixels are only realized where there is a renderer (the user's CrashPlayer client).
Syntax
ASIMAGE dataBlock$, imageName$, width, heightParameters
- dataBlock$ - Name of the DATA block holding the pixel values
- imageName$ - String name to identify the created image
- width - Image width in pixels (must be positive)
- height - Image height in pixels (must be positive)
- Each DATA value is a palette index (0+) or -1 for transparent
- DATA values are read row-major: left-to-right, top-to-bottom
- The block must hold exactly width * height values
Examples
REM 8x8 tile: red border, yellow ring, cyan center
DATA "tile"
4,4,4,4,4,4,4,4
4,14,14,14,14,14,14,4
4,14,11,11,11,11,14,4
4,14,11,11,11,11,14,4
4,14,11,11,11,11,14,4
4,14,11,11,11,11,14,4
4,14,14,14,14,14,14,4
4,4,4,4,4,4,4,4
END DATA
ASIMAGE "tile", "box", 8, 8
PUTIMAGE (100, 100), "box", SCALE(8, 8)REM Transparent pixels with -1 (a white plus on a clear background)
DATA "plus"
-1,15,-1
15,15,15
-1,15,-1
END DATA
ASIMAGE "plus", "cursor", 3, 3
PUTIMAGE (50, 50), "cursor"See also: NEWIMAGE, PUTIMAGE, GETIMAGE, SET IMAGE, DATA
BEGIN FRAME
Graphics · since 0.23.0
Starts frame buffering for optimized graphics performance. All graphics operations between BEGIN FRAME and END FRAME are buffered and displayed simultaneously when END FRAME is reached. Essential for smooth animation and eliminating flicker.
Syntax
BEGIN FRAME
graphics commands
END FRAMEExamples
BEGIN FRAME
FOR i = 0 TO 100
PSET (i, 50), 15
NEXT i
END FRAMEWHILE INKEY$ = ""
BEGIN FRAME
CLS
CIRCLE (x, y), 10, 14
x = x + 1
END FRAME
WENDREM Star field animation
WHILE running
BEGIN FRAME
CLS
FOR i = 0 TO 49
PSET (starX(i), starY(i)), 7
starX(i) = starX(i) - 1
NEXT
END FRAME
WEND
BEGIN MEMORYBUFFER
Graphics · since 1.35.0
Draw a bunch of primitives into an offscreen buffer and bake the result into a single texture — efficiently.
Everything you draw between BEGIN MEMORYBUFFER and END MEMORYBUFFER (PSET, PRESET, LINE, CIRCLE, POLYGON, CLS) is recorded, then rasterized once on the CPU into one RGBA texture named textureName and uploaded a single time at END. This is dramatically cheaper than drawing the same primitives to the screen one at a time — instead of N round-trips to the renderer (each PSET its own command), the whole batch travels and uploads as a single operation. Use it for procedural textures, static backgrounds, generated sprites, starfields, icons — anything you build out of primitives once and then reuse.
The resulting texture works like any loaded image: hand it to SET SPRITE, PUTIMAGE, particles, etc.
Size
If you omit width, height, the buffer matches the current SCREEN resolution (so a SCREEN must be active). Pass an explicit width, height for a specific texture size. Maximum is 8192×8192. With FROM, the buffer is sized to its source instead — passing explicit width, height alongside FROM is a fatal error.
Seeding from an existing texture (FROM)
BEGIN MEMORYBUFFER "out" FROM "source" starts the buffer as a copy of an existing texture (any loaded image — LOADIMG, TEXTTOIMAGE, NEWIMAGE, or another memory buffer) and sizes it to that source. Your draws composite on top, so you can recolor, outline, mask, or otherwise procedurally alter existing pixels — ideal for post-processing TEXTTOIMAGE output. A transparent PRESET erases seeded pixels, giving cut-out/masking for free. Reading and overwriting the same name (FROM "spr" into "spr") is allowed. FROM reads pixel data, so it is client-only in multiplayer — wrap it in IF ME.ISCLIENT THEN.
Reading pixels with POINT / POINTRGB
Inside a FROM block, POINT(x, y) returns the palette index of the source pixel at (x, y) — or -1 if transparent — and POINTRGB(x, y) returns its raw packed colour (0xAARRGGBB). Both sample the frozen source as it was at BEGIN, never your in-block draws, so a convolution (blur, edge-detect, outline) reads pristine neighbours while writing results to the buffer without smearing. Out-of-range coordinates read as transparent. POINT/POINTRGB outside a memory-buffer block are a fatal error.
What's allowed inside
Primitive drawing (PSET, PRESET, LINE, CIRCLE, POLYGON, CLS) plus control flow (IF, FOR, WHILE, DO), variable assignment, and SUB/FUNCTION calls (so a helper that only draws primitives composes fine). Anything else — sprites, PRINT, PUTIMAGE, resource loads, SETIMAGE, BEGIN FRAME, SCREEN, audio, a nested BEGIN MEMORYBUFFER — is a fatal runtime error. The buffer is for primitive rasterization only.
CLS [color] inside the block fills the whole buffer (no color = transparent). The buffer starts fully transparent. Later draws overdraw earlier ones, exactly like drawing to the screen.
Textured polygons stay on the GPU
A POLYGON with a texture argument is intentionally rejected inside a memory buffer — texture sampling is a GPU operation and always will be, so it has no place in a CPU-rasterized buffer. Solid-color and fill polygons work fine here. When you want a textured polygon, draw it directly to a layer or the screen (outside the buffer), where the GPU does the sampling.
Multiplayer
Like TEXTTOIMAGE, this is a computed resource: it runs on every interpreter and registers the texture name/size everywhere, but is not broadcast (no per-pixel bandwidth). Build it per-client, e.g. inside IF ME.ISCLIENT THEN. CPU rasterization is deterministic, so every client produces identical pixels.
Syntax
BEGIN MEMORYBUFFER "textureName" [FROM "source"] [, width, height]
primitive draws...
idx = POINT(x, y) ' only inside a FROM block
END MEMORYBUFFERParameters
- textureName - String expression naming the texture to create (case-insensitive, like all image handles)
- source - Optional (FROM clause). Seed the buffer from this existing texture and size it to the source. Mutually exclusive with width/height. Client-only in multiplayer. Enables POINT/POINTRGB sampling.
- width - Optional buffer width in pixels (defaults to the current SCREEN width; not allowed with FROM)
- height - Optional buffer height in pixels (defaults to the current SCREEN height; not allowed with FROM)
Examples
REM Procedural starfield baked into one texture
SCREEN 320, 240
BEGIN MEMORYBUFFER "stars"
FOR i = 1 TO 200
PSET (RND * 320, RND * 240), 15
NEXT
END MEMORYBUFFER
PUTIMAGE (0, 0), "stars"REM A 32x32 icon, then used as a sprite
BEGIN MEMORYBUFFER "icon", 32, 32
CLS 0
CIRCLE (16, 16), 14, 2, 1
LINE (8, 16)-(24, 16), 15
END MEMORYBUFFER
ENABLE SPRITE 0
SET SPRITE 0, 100, 100, "icon"REM Multiplayer: build the texture on each client only
IF ME.ISCLIENT THEN
BEGIN MEMORYBUFFER "grid", 256, 256
FOR x = 0 TO 256 STEP 16
LINE (x, 0)-(x, 256), 8
NEXT
END MEMORYBUFFER
END IFREM Recolor a rendered label in place: turn its glyph pixels red
IF ME.ISCLIENT THEN
TEXTTOIMAGE "HI", "label", 32
BEGIN MEMORYBUFFER "label" FROM "label"
FOR y = 0 TO 31
FOR x = 0 TO 200
IF POINT(x, y) <> -1 THEN PSET (x, y), 4
NEXT
NEXT
END MEMORYBUFFER
END IFNotes
Primitives in the block are recorded as a batch and flushed at END as a single RasterizeMemoryBuffer gfx command; the renderer CPU-rasterizes them into one RGBA8 texture (reusing the same drawing algorithms as the live primitive path) and uploads it once via the texture store — the same path TEXTTOIMAGE uses. Only primitives + control flow + variable ops + calls are permitted inside; the stmt-level guard rejects everything else with a located error. Size defaults to SCREEN resolution, capped at 8192x8192. Textured POLYGON is rejected by design — texture mapping is GPU-only and never CPU-rasterized; draw textured polygons directly to a layer/screen instead. Not broadcast in multiplayer; runs everywhere and self-registers texture dimensions. FROM seeds the work buffer renderer-side from the named source (seed_from on RasterizeMemoryBuffer — no pixel bytes cross the wire); the interpreter separately fetches the source's CPU pixels once via a ReadImageData → PixelsReady round-trip to hold as the frozen POINT/POINTRGB snapshot (no GPU readback). POINT reverse-looks-up the live palette (exact match, else nearest by RGB); transparent reads -1. FROM is a fatal error on a server-side interpreter (no pixel mirror) and when combined with explicit dimensions.
See also: PSET, LINE, CIRCLE, POLYGON, CLS, POINT, POINTRGB, TEXTTOIMAGE, NEWIMAGE, SETIMAGE, SET SPRITE, PUTIMAGE
BEGIN SCENE
Graphics · since 1.4.3
Save the whole world, mess with it, restore it. That's BEGIN SCENE.
At push, the entire game state gets snapshotted — sprite states (position, velocity, behaviors, patterns, visibility, solid, tags, metadata), boundaries, mutation-bound polygons, active pattern instances, layer metadata (dimensions, scroll, alpha, wrap), tilemap assignments, current drawing layer, per-layer text state, palette. During the scene, any resource you create gets tracked (threads, timers, callbacks, sprites, layers, images). At END SCENE, the scene's resources get cleaned up and the pre-scene snapshot is restored — sprites snap back to where they were, behaviors resume, bound polygons return to their pre-scene shape, layers and scroll come back, tilemaps re-mount.
Scenes are nestable. Use them for battle transitions, dungeon dives, shop overlays, anything where you want to do a bunch of work and have the world go back to exactly how it was when you're done.
Retention mode (default, 1.29.0+)
The renderer-side snapshot moves the live GPU texture handles into the snapshot at push, leaving the scene body with a blank slate. On END SCENE, the saved handles move back. Restoration is a single-frame reference swap — no destroy, no recreate, no re-bake of tilemap textures. Visually instant.
Tradeoff: the pre-scene textures sit in VRAM untouched for the duration of the scene (~58MB per scene level for a 1280×720 game with 8 layers, less for smaller resolutions). Deeply nested scenes stack VRAM cost. If you're memory-constrained, consider FAST for those branches.
`BEGIN SCENE FAST "name"` — opt out of retention
The snapshot stores metadata only (plus a CPU clone of tile data); the scene body inherits the live layer state and overwrites it freely. On END SCENE, layers are destroyed and recreated from metadata, and tilemap textures re-bake from the saved tile arrays. Lower VRAM, but the re-render is visible — a few frames where things repaint. Good for one-way transitions (title screens, game-over screens), deeply nested scene stacks, or low-end hardware.
Tile-array sync after `END SCENE` (option B)
After the renderer restores its pre-scene state, BASIC arrays bound to tilemaps still hold whatever values they had at END SCENE time — not the pre-scene values. The renderer and your arrays are temporarily out of sync. Call UPDATE MAP "name" after END SCENE if you want the renderer resynced to the array's current state.
This is intentional. Scene retention is a render checkpoint, not a full state checkpoint. If you wanted full sync you'd also have to track every array assignment during the scene and roll them back, which is a deeper kind of save/load than a scene block should be doing.
Bound polygons
A polygon bound with SET POLYGON is a world object, so it obeys the same promise a sprite does. Move its shape inside a scene and END SCENE puts the geometry back — the drawn shape, the MUTATION it is bound to, and its SOLID collider all return together, so it never ends up drawn in one place and hit in another.
A polygon created inside a scene belongs to the scene: at END SCENE it is destroyed, binding and collider with it.
What does not restore is a MUTATION variable that was never bound to a polygon. That is an ordinary variable, and scenes do not roll variables back (same rule as the tile arrays above). Bind it to a polygon and its geometry becomes scene state; leave it unbound and it keeps whatever the scene left in it.
Screen restoration
If SCREEN w, h runs inside the scene, the logical screen dimensions get restored on END SCENE. The physical window stays at whatever size it last reached — only the user's coordinate space reverts. So you can SCREEN 800, 600 inside a scene, draw a high-res cutscene, and come back to your SCREEN 320, 240 retro game cleanly.
Multiplayer
The host's NetState::room_tilemaps is pushed and popped in lockstep with the renderer snapshot — under retention mode only. Late-joiners who connect during a scene see the scene's tilemap state; late-joiners who connect after END SCENE see the pre-scene state. Multiplayer-coherent without any extra work from you. FAST mode doesn't push the network-state stack (matches its no-retention-on-renderer semantics).
What survives, what doesn't
- Under retention: layer pixel contents from freehand drawing (
LINE,PSET,PRINT) ARE preserved. The textures themselves come back intact. - Under FAST: those are NOT preserved. The renderer rebuilds layers from scratch, so anything you drew gets re-baked from tilemap data only — freehand pixels are gone.
Syntax
BEGIN SCENE [FAST] "sceneName"
statements
END SCENEParameters
- FAST - Optional modifier. Opt-out of retention mode; use the pre-1.29.0 metadata-only snapshot path. Lower VRAM, visible re-render latency on END SCENE.
- sceneName - A string expression identifying the scene (e.g., "menu", "level1", "cutscene")
Examples
REM Battle transition — overworld is fully restored, instantly
BEGIN SCENE "battle"
CLS
' Set up battle sprites, UI, boundaries...
WHILE battleActive
' Battle loop
YIELD
WEND
END SCENE
' Overworld sprites, scroll, patterns, AND rendered layer textures all back exactly as they wereREM Nested scenes — each restores its own level
BEGIN SCENE "dungeon"
CREATE MAP ("dungeon.map.json", 0) AS LAYER 0
' ... explore dungeon ...
BEGIN SCENE "boss"
' Boss fight in a separate scene
END SCENE
' Back in dungeon, everything restored
END SCENE
' Back in overworldREM Shop menu overlay
BEGIN SCENE "shop"
CREATE LAYER 5, 320, 240
' Draw shop UI on layer 5
' ... shop interaction ...
END SCENE
' Layer 5 destroyed, overworld layers and scroll restoredREM Memory-constrained / one-way transition — opt out of retention
BEGIN SCENE FAST "titleScreen"
' Title screen drawing
END SCENE ' visible re-render of the previous state, but no VRAM taxREM Resync renderer to mutated tilemap array after scene
BEGIN SCENE "editor"
tiles(5, 5) = NEW_TILE_ID
UPDATE MAP "level1"
END SCENE
UPDATE MAP "level1" ' resync the restored renderer to the array's current contentsNotes
At BEGIN SCENE: full world state is snapshotted. During scene: new resources are tracked. At END SCENE: (1) cleanup scene-created resources (threads, timers, callbacks, behaviors, patterns, shaders, sprites, layers, images), (2) restore snapshotted sprites/boundaries/bound polygons/patterns to behavior engine and re-sync GFX — a bound polygon's coordinates are written back to its MUTATION variable, which is where the renderer reads its vertices from, (3) restore layer metadata/scroll/tilemaps to GFX engine, (4) restore interpreter layer state, (5) restore palette. Layer pixel contents (freehand LINE/PSET/PRINT) are NOT preserved — only structural state. Tile maps regenerate automatically.
See also: SCREEN, LOADIMG, SET SPRITE, CREATE LAYER, SET PALETTE, CALL THREAD, CALL TIMER, CALL RENDER, CALL EVENT
CIRCLE
Graphics · since 0.23.0
Draws circles with specified center and radius. Can draw both outlined and filled circles. For optimal performance with multiple circles, use BEGIN FRAME/END FRAME.
Syntax
CIRCLE (x, y), radius [, color] [, fillColor]Parameters
- x, y - Center coordinates
- radius - Circle radius
- color - Outline color
- fillColor - Optional fill color (if provided, circle is filled)
Examples
CIRCLE (160, 100), 50, 15CIRCLE (160, 100), 50, 15, 12 ' Filled circleFOR R = 10 TO 100 STEP 10
CIRCLE (160, 100), R, 14
NEXT' High-performance multiple circles:
BEGIN FRAME
FOR R = 10 TO 100 STEP 10
CIRCLE (160, 100), R, 14
NEXT
END FRAMESee also: LINE, POLYGON, BEGIN FRAME, END FRAME
CLS
Graphics · since 0.23.0
Clears the screen and optionally sets background color or transitions to an image. Use CLS -1 to clear to fully transparent (useful for overlay layers). The TOIMAGE modifier specifies a loaded image to transition to instead of a solid color. The EFFECT modifier adds GPU-accelerated shader-based screen transition effects that are non-blocking - program execution continues while the effect plays. Each layer can have its own active transition. When the transition completes, the layer is filled with the target color or image.
In multiplayer, the bare CLS [color] form is client-only and fatal-errors on a server-side interpreter — clearing a layer / screen has no useful semantic on the host. Use EVENT CLIENTS "RefreshHud" to drive client-side clears from the host. Note: CLS paired with a target-image inside a NEWIMAGE/SET IMAGE setup block is allowed everywhere, since procedural-image creation runs on every interpreter for dimension registration.
Syntax
CLS [color] [TOIMAGE("imageName")] [EFFECT(effectName) [SPEED speed] [SIZE blockSize]]Parameters
- color - Background color (0-255, or -1 for transparent)
- TOIMAGE("imageName") - Name of a loaded image to transition to (alternative to solid color)
- EFFECT(effectName) - Screen transition effect: built-in effects (MELT, FADE, DISSOLVE, WIPEX, WIPEY, BLOCK) or a custom shader name in quotes
- SPEED speed - Effect speed in ticks per second (default: 60). SPEED(60) = 1 second transition, SPEED(30) = 2 seconds, SPEED(120) = 0.5 seconds
- SIZE blockSize - Block size in pixels for BLOCK effect (default: 8)
Examples
CLSCLS 1 ' Blue backgroundCLS -1 ' Clear to transparent (for overlay layers)CLS EFFECT(FADE) ' Fade to blackCLS 4 EFFECT(MELT) SPEED 30 ' Slow melt to red (Doom-style, 2 seconds)CLS EFFECT(DISSOLVE) SPEED 120 ' Fast pixel dissolve (0.5 seconds)CLS EFFECT(WIPEX) ' Horizontal wipe left to rightCLS EFFECT(WIPEY) ' Vertical wipe top to bottomCLS EFFECT(BLOCK) SIZE 16 ' Random 16x16 block clearingCLS EFFECT("my_custom_shader") ' Use a custom shader by nameCLS TOIMAGE("titleScreen") ' Immediately show loaded imageCLS TOIMAGE("scene2") EFFECT(FADE) ' Fade transition to imageCLS TOIMAGE("background") EFFECT(MELT) SPEED 60 ' Melt to image' Wait for transition to complete:
CLS EFFECT(FADE)
WHILE TRANSITION() <> ""
' Continue other tasks
WENDSee also: COLOR, SCREEN, BEGIN FRAME, TRANSITION, SET LAYER, DEFINE SHADER, LOADIMG
COLOR
Graphics · since 0.23.0
Sets text foreground and background colors. The foreground argument is required; background is optional. Values are palette indices (0-255); in direct-color mode (SET PALETTE OFF) a full 0xAARRGGBB literal is also accepted, and a background of -1 means "no background".
Layer-scoped: color is remembered per layer. Setting COLOR while one layer is active (SET LAYER) does not change the text color of another layer; each layer keeps its own (default white, no background). CLS preserves a layer's color.
In multiplayer, COLOR is client-only and halts with a runtime error if called from a server-side interpreter. Color state is per-client renderer; drive it from a GUARD(CLIENT) handler triggered by EVENT CLIENTS.
Syntax
COLOR foreground [, background]Parameters
- foreground - Text color (palette index 0-255; required)
- background - Background color (palette index 0-255, or -1 for none; optional)
Examples
COLOR 14 ' Yellow textCOLOR 15, 1 ' White on blueCOLOR 12: PRINT "Red text"
CREATE LAYER
Graphics · since 0.23.0
Creates a new graphics layer for multi-layer rendering and scrolling effects. PRINT and drawing go to the layer chosen by SET LAYER, which defaults to layer 0 — so drawing works before any SET LAYER, and layer 0 is always the default. Creating a layer does not change which layer is the draw target.
Syntax
CREATE LAYER layer_id, width, heightParameters
- layer_id - Layer number. By convention 0-99 are user layers and 100+ are reserved for internal use, but this is not enforced (any value 0-255 is accepted).
- width - Layer width in pixels
- height - Layer height in pixels
Examples
CREATE LAYER 0, 320, 200 ' Main game layer (layer 0 is the default draw target)PRINT "Text goes to Layer 0" ' Layer 0 is the default target until SET LAYERCREATE LAYER 1, 640, 200 ' Scrolling backgroundCREATE LAYER 2, 320, 200 ' UI overlaySee also: SET LAYER, SET SCROLL, BEGIN FRAME, CREATE MAP
CREATE MAP
Graphics · since 1.4.3
Three ways to put a tilemap on a screen layer — pick by where the data lives.
Terminology first (the word "layer" is overloaded)
- Tile layer is a horizontal slice INSIDE the tilemap (background grass, foreground decor, etc.). Indexed
0..Nwithin theTILEMAPblock. Selected byLAYER .... - Screen layer is the GPU's render layer (
0..99). Where the tilemap actually gets painted. Selected byAS LAYER ....
These are different things sharing one word. LAYER (no AS) is always a tile layer. AS LAYER is always a screen layer. Don't conflate them.
Form 1: file-loaded (the original)
CREATE MAP("level1.map.json") AS LAYER 0Loads a .map.json produced by the Map Editor widget. The paren form predates the named and inline forms; still works, still supported.
Form 2: named (renders a `TILEMAP` DEF)
CREATE MAP "level1" AS LAYER 0 ' tile layer 0 → screen 0
CREATE MAP "level1" LAYER "foreground" AS LAYER 2 ' named tile layer → screen 2
CREATE MAP "level1" LAYER 0 TO 2 AS LAYER 0 TO 2 ' splat layers 0..=2 onto screens 0..=2Renders a tilemap defined earlier with a TILEMAP block. The LAYER selector picks one or more tile layers from the named tilemap. Omit it and tile layer 0 is used — or, when AS LAYER is a range, tile layers 0..=N auto-splat onto the screen range.
This is the canonical form for programmatic tilemaps. Use it with TILEMAP DEF (which sets up the bindings) and UPDATE MAP (which pushes array mutations to the GPU). Tilemap data lives in your BASIC arrays; the renderer is downstream of them. There's no SETMAPTILE statement and never will be — tileArr(x, y) = newIdx is the way.
Form 3: inline (anonymous from a BASIC array)
CREATE MAP FROM tileArr TILESET "grass.png" TILESIZE 32 AS LAYER 0One-shot, no TILEMAP DEF needed. Optional META metaArr binds a string array as metadata. Dimensions are inferred from the tile array's shape unless you pass SIZE w, h explicitly.
What you can do with a rendered map layer
- Scroll it via
SET SCROLL. - Make it transparent via
SET LAYER ALPHA. - Animate tiles automatically — each animation runs at the
frameTimedeclared in the basemap (default ~200ms per frame).SPEED fpsis an optional global override that forces every animation on the map onto a fixed cadence (SPEED 4= 4 frames/sec); omit it and per-animation timing is preserved. - Query metadata via
MAPMETA$()andFOR MAPDATA. Metadata is single characters or emojis at grid positions — handy for marking spawn points, items, triggers, anything you want to look up by tile coord. Works identically against all three forms once the map is rendered.
What you can't do
- Draw on a map layer with
PSET,LINE, etc. Map layers are read-only at the renderer. CLSa map layer. That destroys the tilemap.- Go bigger than 512×512 tiles. That's the GPU-accelerated rendering limit (16384×16384 pixels at 32px tiles).
- Use more than 500 animated tile definitions or 5 frames each. Tileset animation budget. (Bumped from 8 in 1.36.4 — the old cap was a habit, not a hardware limit; 500 is more animations than any tileset will ever have unique tiles to trigger them, so the practical ceiling is now "as many animations as your tileset has unique source tiles.")
Room-aware replication
When called from the host inside a WITH ROOM "name" block, the mount tags with that room. Outgoing broadcasts to clients filter on ME.ROOM = name, so only players in that room render it. Late-join replay (replay_room_snapshot_to_player) and SET ROOM transitions walk the same per-room snapshot. Outside any WITH ROOM block, the host falls back to a _global sentinel that's visible to every connected player. Non-host interpreters don't write to the snapshot store — the host is the sole authoritative author.
One array is one map layer
AS LAYER x TO y spreads a map's layers across a range of screen layers, so it belongs to the file-loaded and named forms. The inline FROM tiles_array form describes exactly one map layer — one array, one layer — so there is nothing for a range to spread, and writing one is a runtime error.
This is a constraint rather than an omission: map layers are not required to share dimensions. A multi-layer map binds each layer explicitly so each can declare its own size:
TILEMAP "level1"
BASEMAP "level1.map.json"
TILES 0 INTO ground
TILES 1 INTO decor
END TILEMAP
CREATE MAP "level1" LAYER 0 TO 1 AS LAYER 0 TO 1Syntax
CREATE MAP("map_file" [, map_layer_index [TO map_layer_end]]) AS LAYER layer_id [TO layer_end] [SPEED fps] [WRAPX] [WRAPY]
CREATE MAP "tilemap_name" [LAYER tile_layer_selector] AS LAYER layer_id [TO layer_end] [SPEED fps] [WRAPX] [WRAPY]
CREATE MAP FROM tiles_array [META meta_array] TILESET "image" TILESIZE w[, h] [SIZE w, h] AS LAYER layer_id [SPEED fps] [WRAPX] [WRAPY]Parameters
- map_file - File-loaded form: path to the .map.json file (created with Map Editor widget).
- map_layer_index - File-loaded form: which tile layer from the map file to load (0-based). Defaults to 0.
- tilemap_name - Named form: a TILEMAP defined earlier with TILEMAP "name" ... END TILEMAP. If the string ends with .map.json or .json AND no TILEMAP is registered under that name, falls back to file-loaded form.
- tile_layer_selector - Named form: optional selector picking which tile layer in the named TILEMAP to render. Three shapes: a string AS-name (LAYER "foreground"), a numeric index (LAYER 1), or a numeric range (LAYER 0 TO 2). The range form must be paired with AS LAYER X TO Y of the same width and splats each tile layer onto consecutive screen layers. Defaults: when AS LAYER is a single layer, tile layer 0; when AS LAYER is a range (X TO Y), tile layers 0..=(Y-X) auto-splat onto X..=Y — equivalent to writing LAYER 0 TO (Y-X) explicitly.
- tiles_array - Inline form: globally-scoped 2D INTEGER array holding tile indices [x][y].
- meta_array - Inline form: optional globally-scoped 2D STRING array holding per-cell metadata.
- TILESET / TILESIZE / SIZE - Inline form: tileset image name, per-tile pixel dimensions, optional explicit map size in tiles (default: array shape).
- layer_id - Target screen layer number (0-99). Layers 100+ are reserved for internal use.
- SPEED fps - Optional global override forcing every animation on the map onto a fixed cadence (SPEED 4 = 4 fps). Omit it and each animation plays at its basemap-authored frame timing (default ~200ms per frame, i.e. ~5 fps).
- WRAPX / WRAPY - Optional: enable infinite scrolling by wrapping at map edges.
- Metadata layers (file-loaded or via TILEMAP DEF) are automatically queryable via MAPMETA$() / FOR MAPDATA.
Examples
REM File-loaded form (original; still supported)
CREATE MAP("level1.map.json") AS LAYER 0REM Named form — renders a TILEMAP DEF
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
TILESET "grass.png"
TILESIZE 32, 32
SIZE 100, 60
TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0REM Named form with multi-layer tilemap, picked by name
CREATE MAP "level1" LAYER "background" AS LAYER 0
CREATE MAP "level1" LAYER "foreground" AS LAYER 2REM Named form with multi-layer tilemap, picked by numeric index
CREATE MAP "level1" LAYER 0 AS LAYER 0
CREATE MAP "level1" LAYER 2 AS LAYER 2REM Named form splatting a tile-layer range onto consecutive screen layers
CREATE MAP "level1" LAYER 0 TO 2 AS LAYER 0 TO 2 ' tile layer 0 → screen 0, 1 → 1, 2 → 2REM AS LAYER range with no explicit LAYER selector — auto-splats from tile layer 0
CREATE MAP "level1" AS LAYER 1 TO 2 ' equivalent to LAYER 0 TO 1 AS LAYER 1 TO 2REM Room-aware: inside WITH ROOM, the mount tags with that room
WITH ROOM "lobby"
CREATE MAP "lobbyMap" AS LAYER 1 ' only players in "lobby" see it
END WITHREM Inline form — anonymous one-shot, dimensions inferred
DIM proceduralTiles(0 TO 49, 0 TO 29) AS INTEGER
CREATE MAP FROM proceduralTiles TILESET "grass.png" TILESIZE 32 AS LAYER 0REM Pathfinding pattern: same array drives renderer + AI logic
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
TILESET "grass.png"
TILESIZE 16, 16
SIZE 100, 60
TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0
' AStar reads the same array the GPU is rendering
DIM path AS COORDINATE
path = AStar(tiles, startX, startY, endX, endY)
' Mutate to open a door, push to GPU
tiles(doorX, doorY) = OPEN_DOOR_TILE
UPDATE MAP "level1"REM Find spawn points from metadata
CREATE MAP("level1.map.json") AS LAYER 0
FOR MAPDATA (0, 0)-(mapWidth, mapHeight) AS x, y, m$
IF m$ = "P" THEN
playerX = x * 16
playerY = y * 16
END IF
NEXT MAPDATASee also: TILEMAP, UPDATE MAP, CREATE LAYER, SET LAYER, SET SCROLL, RESET LAYERS, MAPMETA$, MAPWIDTH, MAPHEIGHT, FOR MAPDATA
DESTROY POLYGON
Graphics · since 1.43.0
Removes a mutation-bound polygon created with SET POLYGON, unbinding its mutation and freeing the slot. The id is the same 0-based handle passed to SET POLYGON. Like SET POLYGON, this is client-only in multiplayer — calling it server-side in a CrashNet game is a fatal error. The shape's sprite half goes with it — its collider, behavior and pose are removed together, so nothing is left behind to collide with. The MUTATION itself is untouched: unbinding a polygon does not destroy the shape's data, and the same mutation can be bound again.
Syntax
DESTROY POLYGON idParameters
- id - 0-based polygon handle to destroy
Example
SET POLYGON 0 BIND blob FILL 4
REM ... later ...
DESTROY POLYGON 0See also: SET POLYGON, MUTATION
END FRAME
Graphics · since 0.23.0
Ends frame buffering and displays all buffered graphics operations at once. This eliminates flickering and provides smooth animation by updating the entire screen simultaneously. Optimized for maximum performance.
Syntax
END FRAMEExamples
BEGIN FRAME
CLS
FOR i = 0 TO 319
PSET (i, 100), RND * 16
NEXT
END FRAMEREM High-performance animation loop
WHILE running
BEGIN FRAME
CLS
REM Draw all objects here
CIRCLE (playerX, playerY), 5, 15
LINE (enemyX, enemyY)-(enemyX+10, enemyY+10), 12, BF
END FRAME
WENDSee also: BEGIN FRAME, CLS
FONT
Graphics · since 0.23.0
Sets the font family and size for text rendering, or resets to default font. Built-in fonts "IBM BIOS" and "IBM EGA 8x8" are available without loading. Custom fonts can be loaded with LOADFONT. The CURRENT sentinel keeps whatever font is already active and only changes the size — useful in render loops that mix sizes of the same face. FONT and SETFONT are interchangeable keywords.
Layer-scoped: font is remembered per layer. FONT set while one layer is active (SET LAYER) does not change another layer's font; each layer keeps its own and defaults to "IBM EGA 8x8" at size 8. CLS preserves a layer's font. (CURRENT keeps the active layer's current font.)
Syntax
FONT fontname$, size
FONT CURRENT, size
FONT RESET
SETFONT fontname$, size
SETFONT CURRENT, sizeParameters
- fontname$ - Font family name (built-in: "IBM BIOS", "IBM EGA 8x8", or custom loaded font)
- CURRENT - Sentinel meaning "keep the current font, only change size"
- size - Font size in pixels
Examples
FONT "IBM EGA 8x8", 16
PRINT "Retro pixel font"FONT "IBM BIOS", 8
PRINT "Classic BIOS font"' Keep the same font, just resize for a heading
FONT "MyBody", 12
PRINT "body text"
FONT CURRENT, 24
PRINT "HEADING"
FONT CURRENT, 12
PRINT "back to body"FONT RESET ' Return to default fontSee also: LOADFONT, COLOR, PRINT, LOCATE
GETIMAGE
Graphics · since 0.23.0
Captures a rectangular region of the current layer and stores it as a named image that can be displayed with PUTIMAGE. The captured image becomes a reusable resource.
Syntax
GETIMAGE (x1, y1)-(x2, y2), imageName$Parameters
- x1, y1 - Top-left corner of capture region
- x2, y2 - Opposite corner of the capture region (coordinates may be given in any order)
- imageName$ - String name to identify the captured image
Examples
REM Capture a sprite from the screen
CIRCLE (50, 50), 25, 15
GETIMAGE (25, 25)-(75, 75), "circle"REM Use the captured image
PUTIMAGE (100, 100), "circle"
PUTIMAGE (200, 100), "circle", SCALETO(64, 64)REM Capture and reuse UI elements
LINE (10, 10)-(90, 40), 15, BF
LOCATE 2, 2: PRINT "BUTTON"
GETIMAGE (10, 10)-(90, 40), "button"
CLS
PUTIMAGE (50, 50), "button"
PUTIMAGE (50, 100), "button"REM Capture gameplay screenshots
GETIMAGE (0, 0)-(319, 199), "screenshot"
SAVEJSON "game_state.json", screenshot$ ' Can serialize the nameSee also: PUTIMAGE, NEWIMAGE, LOADIMG
GETSCROLL
Graphics · since 1.0.0
Returns the current scroll offset of a layer as a coordinate. If no layer is specified, returns the scroll offset of the current layer. The returned coordinate can be used directly or decomposed into X and Y components.
Syntax
GETSCROLL([layer])Parameters
- layer - Optional layer number. Defaults to the current layer if omitted
Examples
pos = GETSCROLL() ' Get current layer scroll as coordinatepos = GETSCROLL(1) ' Get scroll offset of layer 1' Use with coordinate decomposition:
scroll = GETSCROLL()
PRINT scroll.X, scroll.Y' Save and restore scroll position:
saved = GETSCROLL()
SET SCROLL(0, 0) ' Reset
' ... draw UI ...
SET SCROLL(saved.X, saved.Y) ' RestoreSee also: SET SCROLL, CREATE LAYER, SET LAYER
IMAGEHEIGHT
Graphics · since 1.36.2
Returns the base height in pixels of a loaded image texture identified by its string handle (the name passed to LOADIMG, NEWIMAGE, TEXTTOIMAGE, BASE64TOIMG, etc.). The result is the texture's intrinsic dimension, independent of any sprite that references it. Image handles are case-insensitive. Returns 0 if no image with that handle is loaded.
Syntax
IMAGEHEIGHT(name$)Parameters
- name$ - String handle of a loaded image texture
Examples
LOADIMG "player.png", "player"
h = IMAGEHEIGHT("player") ' Height of the player texture' Stack two images vertically:
PUTIMAGE 0, 0, "top"
PUTIMAGE 0, IMAGEHEIGHT("top"), "bottom"See also: IMAGEWIDTH, SPRITEWIDTH, SPRITEHEIGHT, LOADIMG, NEWIMAGE, PUTIMAGE
IMAGEWIDTH
Graphics · since 1.36.2
Returns the base width in pixels of a loaded image texture identified by its string handle (the name passed to LOADIMG, NEWIMAGE, TEXTTOIMAGE, BASE64TOIMG, etc.). The result is the texture's intrinsic dimension, independent of any sprite that references it. Image handles are case-insensitive. Returns 0 if no image with that handle is loaded.
Syntax
IMAGEWIDTH(name$)Parameters
- name$ - String handle of a loaded image texture
Examples
LOADIMG "player.png", "player"
w = IMAGEWIDTH("player") ' Width of the player texture' Place an image flush against the right edge of the screen:
LOADIMG "hud.png", "hud"
PUTIMAGE SCREENWIDTH - IMAGEWIDTH("hud"), 0, "hud"' Center an image on screen:
x = (SCREENWIDTH - IMAGEWIDTH("logo")) / 2
y = (SCREENHEIGHT - IMAGEHEIGHT("logo")) / 2
PUTIMAGE x, y, "logo"See also: IMAGEHEIGHT, SPRITEWIDTH, SPRITEHEIGHT, LOADIMG, NEWIMAGE, PUTIMAGE
LINE
Graphics · since 0.23.0
Draws lines and rectangles. A color or a B/BF style is required — LINE (x1,y1)-(x2,y2) with no color and no style does not parse. For smooth animation with multiple lines, use BEGIN FRAME/END FRAME to prevent flicker.
Syntax
LINE (x1, y1)-(x2, y2), color [, B[F]]
LINE (x1, y1)-(x2, y2), B[F]Parameters
- x1, y1 - Start coordinates
- x2, y2 - End coordinates
- color - Line color (required unless a B/BF style is given instead)
- B - Box mode
- BF - Filled box
Examples
LINE (0, 0)-(100, 100), 15LINE (50, 50)-(150, 100), 12, BLINE (10, 10)-(90, 90), 4, BFREM Animated wireframe
BEGIN FRAME
CLS
FOR i = 0 TO 10
LINE (i*30, 0)-(i*30, 200), 14
NEXT
END FRAMESee also: PSET, CIRCLE, BEGIN FRAME
LOADFONT
Graphics · since 1.2.0
Loads a custom font file (TTF, OTF, or WOFF2) from disk and registers it with the given name for use with FONT or SETFONT. The font is cached for the duration of the program. Built-in fonts like "IBM BIOS" and "IBM EGA 8x8" are available without loading.
In CrashNet multiplayer, LOADFONT runs on every interpreter so subsequent SETFONT / GPRINT / TEXTTOIMAGE references resolve consistently. The actual font bytes are dropped by the headless host's GfxClient (it doesn't render text), but the name registration happens locally on each interpreter — call LOADFONT once at top-level and host / player-server / client all know the font exists.
Syntax
LOADFONT filename$, name$Parameters
- filename$ - Path to the font file (TTF, OTF, or WOFF2 format)
- name$ - Name to register the font as for later use
Examples
LOADFONT "fonts/custom.ttf", "MyFont"
FONT "MyFont", 16
PRINT "Custom font text"REM Load multiple fonts
LOADFONT "assets/title.ttf", "TitleFont"
LOADFONT "assets/body.ttf", "BodyFont"
FONT "TitleFont", 32
PRINT "GAME TITLE"
FONT "BodyFont", 12
PRINT "Press any key to start"See also: FONT, TEXTTOIMAGE
LOCATE
Graphics · since 0.23.0
Positions text cursor at specific coordinates with optional pixel-level offset. LOCATE RELATIVE offsets the cursor from its current position by the given number of rows and columns, allowing you to move backward to overwrite previously printed text.
In multiplayer, LOCATE is client-only — a server-side call halts with a runtime error. The text cursor lives in the client's renderer.
Syntax
LOCATE [row] [, column] [ORIGIN(x, y)]
LOCATE RELATIVE deltaRow [, deltaCol]
(ORIGIN also accepts a coordinate: ORIGIN(coord); ORIGIN alone defaults to (0,0))Parameters
- row - Row position (1-based)
- column - Column position (1-based)
- ORIGIN(x, y) - Pixel offset from column/row position
- RELATIVE - Use relative positioning (delta from current cursor position)
- deltaRow - Number of rows to offset (negative = up)
- deltaCol - Number of columns to offset (negative = left)
Examples
LOCATE 10, 20LOCATE 5, 1: PRINT "Top left"LOCATE , 40: PRINT "Column 40"LOCATE 10, 10 ORIGIN(20, 5)LOCATE 15, 10 ORIGIN(40, 10)' Overwrite previous text
LOCATE 1, 1: PRINT "Score: 0"
LOCATE RELATIVE 0, -1: PRINT "5"' Move cursor back to rewrite a value
PRINT "HP: 100";
LOCATE RELATIVE 0, -3: PRINT "075";
MAPTILE
Graphics · since 1.13.0
Returns the 0-based tile index (the source Tiled tile id minus 1) at the given map coordinates. Defaults to the current screen layer if you omit screenLayer. Returns -1 if no tile exists at the position or the queried layer isn't a map layer.
Need to know what's under the mouse? Use MAPTILEX / MAPTILEY to convert pixel coordinates to tile coordinates first.
Server-side lookup
Player-server interpreters have no renderer of their own and never receive CreateTilemap broadcasts — they ARE the broadcast source. So their local map cache is empty. To make them useful for collision / pathfinding / map-aware logic, MAPTILE falls back to the per-room tilemap snapshot on NetState when the local cache misses.
Which room's snapshot? Resolved in this order:
- The
WITH ROOMcontext if one is active. - The interpreter's
ME.ROOM. - The
_globalsentinel.
Limit: the snapshot fallback only handles tile_layer == 0. Multi-tile-layer tilemaps live as separate snapshots per (room, screen_layer) (see CREATE MAP's splat form), so querying a deeper tile layer through the snapshot route returns -1.
Syntax
MAPTILE(tileX, tileY [, screenLayer])Parameters
- tileX - Tile X coordinate (map space, not pixels)
- tileY - Tile Y coordinate (map space, not pixels)
- screenLayer - Optional: screen layer to query (defaults to the current layer)
Examples
' Check tile at map coordinates
tile = MAPTILE(5, 10)
IF tile = 5 THEN PRINT "Solid wall!"' Check tile at mouse position (convert pixels to tiles first)
tx = MAPTILEX(MOUSEX(), 1)
ty = MAPTILEY(MOUSEY(), 1)
tile = MAPTILE(tx, ty, 1)
PRINT "Tile: " + STR$(tile)' Collision detection using tile coordinates
tx = MAPTILEX(playerX + 8)
ty = MAPTILEY(playerY + 8)
IF MAPTILE(tx, ty) > 0 THEN
playerBlocked = 1
END IFSee also: MAPTILEX, MAPTILEY, MAPMETA$, CREATE MAP, SET LAYER
MAPTILEX
Graphics · since 1.13.0
Converts a SCREEN pixel X coordinate to tile X coordinate by adding the layer's scroll offset. This accounts for scrolling so the result maps to the correct tile even when the map has scrolled. Use this for MOUSEX(), TOUCHX(), and other screen-relative coordinates. For world coordinates like SPRITEX() that are already in map space, use MAPWORLDTILEX instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: screen input (MOUSEX) → MAPTILEX, world position (SPRITEX) → MAPWORLDTILEX.
Syntax
MAPTILEX(screenPixelX [, layerNum])Parameters
- screenPixelX - Screen pixel X coordinate (e.g., MOUSEX)
- layerNum - Optional: Layer number to query (defaults to current layer)
Examples
' Track mouse on a scrolled map
tx = MAPTILEX(MOUSEX())
ty = MAPTILEY(MOUSEY())
tile = MAPTILE(tx, ty)
IF tile > 0 THEN PRINT "Hovering tile " + STR$(tile)' Tile-based click detection on scrolled map
IF MOUSEDOWN() THEN
tx = MAPTILEX(MOUSEX(), 1)
ty = MAPTILEY(MOUSEY(), 1)
PRINT "Clicked tile (" + STR$(tx) + "," + STR$(ty) + ")"
END IFSee also: MAPTILEY, MAPWORLDTILEX, MAPWORLDTILEY, MAPTILE, MOUSEX
MAPTILEY
Graphics · since 1.13.0
Converts a SCREEN pixel Y coordinate to tile Y coordinate by adding the layer's scroll offset. This accounts for scrolling so the result maps to the correct tile even when the map has scrolled. Use this for MOUSEY(), TOUCHY(), and other screen-relative coordinates. For world coordinates like SPRITEY() that are already in map space, use MAPWORLDTILEY instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: screen input (MOUSEY) → MAPTILEY, world position (SPRITEY) → MAPWORLDTILEY.
Syntax
MAPTILEY(screenPixelY [, layerNum])Parameters
- screenPixelY - Screen pixel Y coordinate (e.g., MOUSEY)
- layerNum - Optional: Layer number to query (defaults to current layer)
Examples
' Get tile row at mouse position
ty = MAPTILEY(MOUSEY(), 1)
PRINT "Mouse at tile row: " + STR$(ty)' Check if mouse click is in specific tile row
IF MOUSEDOWN() AND MAPTILEY(MOUSEY()) = 5 THEN
PRINT "Row 5 clicked!"
END IFSee also: MAPTILEX, MAPWORLDTILEX, MAPWORLDTILEY, MAPTILE, MOUSEY
MAPWORLDTILEX
Graphics · since 1.13.0
Converts a WORLD pixel X coordinate to tile X coordinate without adding scroll offset. Use this for SPRITEX() and other world/logical coordinates that are already in map space. For screen-relative coordinates like MOUSEX() that need scroll adjustment, use MAPTILEX instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: world position (SPRITEX) → MAPWORLDTILEX, screen input (MOUSEX) → MAPTILEX.
Syntax
MAPWORLDTILEX(worldPixelX [, layerNum])Parameters
- worldPixelX - World pixel X coordinate (e.g., SPRITEX)
- layerNum - Optional: Layer number to query (defaults to current layer)
Examples
' Get tile column where sprite is located
tx = MAPWORLDTILEX(SPRITEX(0), 1)
PRINT "Sprite at tile column: " + STR$(tx)' Check sprite collision with tile
tx = MAPWORLDTILEX(SPRITEX(playerSprite))
ty = MAPWORLDTILEY(SPRITEY(playerSprite))
IF MAPTILE(tx, ty) = SOLID_TILE THEN
PRINT "Sprite hit solid tile!"
END IF' Get tile under sprite center
centerX = SPRITEX(0) + 8 ' Assuming 16px sprite
tx = MAPWORLDTILEX(centerX)
PRINT "Sprite center at tile: " + STR$(tx)See also: MAPWORLDTILEY, MAPTILEX, MAPTILEY, MAPTILE, SPRITEX
MAPWORLDTILEY
Graphics · since 1.13.0
Converts a WORLD pixel Y coordinate to tile Y coordinate without adding scroll offset. Use this for SPRITEY() and other world/logical coordinates that are already in map space. For screen-relative coordinates like MOUSEY() that need scroll adjustment, use MAPTILEY instead. Uses the current map layer if layerNum is omitted. Returns -1 if the layer is not a map layer. Quick rule: world position (SPRITEY) → MAPWORLDTILEY, screen input (MOUSEY) → MAPTILEY.
Syntax
MAPWORLDTILEY(worldPixelY [, layerNum])Parameters
- worldPixelY - World pixel Y coordinate (e.g., SPRITEY)
- layerNum - Optional: Layer number to query (defaults to current layer)
Examples
' Get tile row where sprite is located
ty = MAPWORLDTILEY(SPRITEY(0), 1)
PRINT "Sprite at tile row: " + STR$(ty)' Platform collision detection
feetY = SPRITEY(playerSprite) + 16 ' Bottom of sprite
ty = MAPWORLDTILEY(feetY)
IF MAPTILE(tx, ty) > 0 THEN
onGround = 1
END IF' Check if sprite is above certain tile row
IF MAPWORLDTILEY(SPRITEY(0)) < 5 THEN
PRINT "Sprite is in upper area!"
END IFSee also: MAPWORLDTILEX, MAPTILEX, MAPTILEY, MAPTILE, SPRITEY
NEWIMAGE
Graphics · since 0.23.0
Creates an offscreen image buffer with specified dimensions and background color. The buffer can be drawn to using SET IMAGE and displayed with PUTIMAGE. With AS SPRITESHEET, the image is automatically divided into frames for animation. Perfect for compositing complex graphics, preparing sprites, or creating procedural sprite sheets.
Syntax
NEWIMAGE width, height, backgroundColor, imageName$ [AS SPRITESHEET(frameWidth, frameHeight)]Parameters
- width - Width in pixels (must be positive; a zero or negative value is a runtime error)
- height - Height in pixels (must be positive; a zero or negative value is a runtime error)
- backgroundColor - Palette color index (0-255) to fill the buffer
- imageName$ - String name to identify the image buffer
- AS SPRITESHEET(frameWidth, frameHeight) - Optional: Divide image into animation frames
- Frame numbering: Frames start at 0 (top-left), increment left-to-right, top-to-bottom
Examples
REM Create offscreen buffer for compositing
NEWIMAGE 100, 100, 0, "composite"
SET IMAGE "composite"
CIRCLE (50, 50), 40, 15
LINE (10, 10)-(90, 90), 14
SET LAYER 0
PUTIMAGE (100, 50), "composite"REM Create procedural sprite sheet with 4 frames (32x32 each)
NEWIMAGE 128, 32, 0, "anim" AS SPRITESHEET(32, 32)
SET IMAGE "anim"
FOR i = 0 TO 3
CIRCLE (i * 32 + 16, 16), 10 + i * 2, 15, 15
NEXT
SET LAYER 0
PUTIMAGE (100, 100), "anim", frame ' Use frame 0-3REM Create sprite atlas
NEWIMAGE 320, 200, 0, "sprites"
SET IMAGE "sprites"
FOR i = 0 TO 9
CIRCLE (i * 32 + 16, 100), 12, i + 1, i + 1
NEXT
SET LAYER 0
' Now use sprites from atlas
GETIMAGE (0, 88)-(32, 112), "sprite0"REM Prepare UI panels
NEWIMAGE 200, 100, 8, "panel"
SET IMAGE "panel"
LINE (0, 0)-(199, 99), 15, B
LOCATE 2, 5: PRINT "GAME MENU"
SET LAYER 0
PUTIMAGE (60, 50), "panel", ALPHA(0.8)REM Multiple buffers for double buffering
NEWIMAGE 320, 200, 0, "buffer1"
NEWIMAGE 320, 200, 0, "buffer2"See also: SET IMAGE, GETIMAGE, PUTIMAGE, CREATE LAYER, TOSPRITESHEET, FRAMECOUNT
OFFSET
Graphics · since 1.0.3
Graphics modifier used with POLYGON to translate (move) all vertices by specified x and y amounts. Applied after SCALE and ROTATE transformations.
Syntax
OFFSET(x, y)Parameters
- x - Horizontal offset in pixels (can be negative)
- y - Vertical offset in pixels (can be negative)
Examples
' Move polygon to position:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, OFFSET(100, 100)' Define shape at origin, position with OFFSET:
POLYGON ((-25, -25), (25, -25), (25, 25), (-25, 25)), 14, OFFSET(160, 100)' Combined with rotation:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(angle), OFFSET(centerX, centerY)' Animation using OFFSET:
FOR x = 0 TO 320 STEP 10
CLS
POLYGON ((0, 0), (30, 0), (15, 30)), 15, 12, OFFSET(x, 100)
DELAY 50
NEXTSee also: POLYGON, ROTATE, SCALE
PALETTE
Graphics · since 0.23.0
Modifies color palette with RGB values and transparency. The palette is dynamically sized: any non-negative color_index is valid and the palette grows to fit (there is no 256-entry limit).
Syntax
PALETTE [color_index, rgb_value]
PALETTE [color_index, red, green, blue [, alpha]]Parameters
- color_index - Palette color number (0 or greater; palette grows to fit)
- rgb_value - RGB color value
- red, green, blue - RGB components (0-255)
- alpha - Transparency (0-255, optional)
Examples
PALETTE 1, 255, 128, 64 ' Orange color
PSET (100, 100), 1PALETTE 2, 0, 255, 0, 128 ' Semi-transparent greenPALETTE 300, &HFF112233 ' High index is fine (palette grows)See also: COLOR, SCREEN, SET PALETTE
POINT
Graphics · since 1.35.0
Sample a pixel from a BEGIN MEMORYBUFFER ... FROM source and return its palette index (0-255), or -1 if the pixel is transparent.
Valid only inside a memory-buffer block — calling it elsewhere is a fatal error (reading a texture's pixels outside this context is unsupported). It reads the frozen snapshot of the FROM source captured at BEGIN, never the draws you've made inside the block, so a filter that reads neighbouring pixels and writes results won't read its own half-finished output.
An opaque colour that isn't an exact palette entry resolves to the nearest palette index, so POINT always yields a usable index for opaque pixels. For the exact colour of a non-palette pixel (e.g. from a photo), use POINTRGB. Inside a block with no FROM, the snapshot is transparent everywhere (POINT returns -1). Out-of-range coordinates read as transparent.
Syntax
POINT(x, y)
POINT(coord) (also accepts a single coordinate value)Parameters
- x - X coordinate in the source/buffer (same coordinate space as PSET)
- y - Y coordinate in the source/buffer
Examples
REM Outline: draw an edge around a rendered glyph
IF ME.ISCLIENT THEN
TEXTTOIMAGE "PLAY", "label", 48
BEGIN MEMORYBUFFER "outlined" FROM "label"
FOR y = 1 TO 46
FOR x = 1 TO 200
IF POINT(x, y) = -1 THEN
IF POINT(x-1, y) <> -1 OR POINT(x+1, y) <> -1 OR POINT(x, y-1) <> -1 OR POINT(x, y+1) <> -1 THEN
PSET (x, y), 0
END IF
END IF
NEXT
NEXT
END MEMORYBUFFER
END IFREM Recolor: every non-transparent pixel becomes red
BEGIN MEMORYBUFFER "out" FROM "src"
FOR y = 0 TO 31
FOR x = 0 TO 31
IF POINT(x, y) <> -1 THEN PSET (x, y), 4
NEXT
NEXT
END MEMORYBUFFERNotes
Reads the interpreter-side snapshot of the FROM source (linear RGBA, captured once at BEGIN via ReadImageData → PixelsReady — no GPU readback). Transparent (alpha 0) returns -1; otherwise reverse-looks-up the live palette via Palette::nearest_index (first exact RGB match, else nearest by squared RGB distance). Fatal outside a MEMORYBUFFER block; out-of-range and no-FROM blocks read transparent. Coordinates share PSET's space 1:1.
See also: POINTRGB, BEGIN MEMORYBUFFER, PSET, PRESET, PALETTE
POINTRGB
Graphics · since 1.35.0
Sample a pixel from a BEGIN MEMORYBUFFER ... FROM source and return its raw packed colour as 0xAARRGGBB (alpha in the high byte), or 0 if transparent.
The companion to POINT: where POINT gives a palette index, POINTRGB gives the exact colour — use it for FROM'd photos and anything that isn't a palette colour. Like POINT, it samples the frozen FROM snapshot (not in-block draws) and is valid only inside a memory-buffer block (fatal error otherwise). Out-of-range coordinates read as 0 (transparent).
Pull individual channels with the bitwise helpers:
c = POINTRGB(x, y)
r = BITAND(SHR(c, 16), 255)
g = BITAND(SHR(c, 8), 255)
b = BITAND(c, 255)
a = BITAND(SHR(c, 24), 255)Syntax
POINTRGB(x, y)
POINTRGB(coord) (also accepts a single coordinate value)Parameters
- x - X coordinate in the source/buffer (same coordinate space as PSET)
- y - Y coordinate in the source/buffer
Examples
REM Read the exact colour of a photo pixel
BEGIN MEMORYBUFFER "out" FROM "photo"
c = POINTRGB(10, 10)
r = BITAND(SHR(c, 16), 255)
END MEMORYBUFFERREM Tint: halve the red channel of every opaque pixel
BEGIN MEMORYBUFFER "dim" FROM "photo"
FOR y = 0 TO 63
FOR x = 0 TO 63
c = POINTRGB(x, y)
IF BITAND(SHR(c, 24), 255) > 0 THEN
r = BITAND(SHR(c, 16), 255) / 2
g = BITAND(SHR(c, 8), 255)
b = BITAND(c, 255)
PSET (x, y), RGB(r, g, b)
END IF
NEXT
NEXT
END MEMORYBUFFERNotes
Returns the snapshot pixel's packed 0xAARRGGBB (alpha high byte) as an integer; transparent reads 0. Same frozen-snapshot, block-scoped semantics as POINT — fatal outside a MEMORYBUFFER block, out-of-range reads transparent. Composes with BITAND/SHR for channel extraction (0xRRGGBB packing convention shared with PALETTE and RGB).
See also: POINT, BEGIN MEMORYBUFFER, RGB, BITAND, SHR
POLYGON
Graphics · since 0.23.0
Draws polygons with any number of vertices using GPU-accelerated ear-clipping triangulation, so both convex and concave shapes fill correctly. Can draw outlined, filled, or textured polygons. Supports two syntaxes: inline point coordinates or an array of points. Array-based syntax is useful for dynamic polygons or large point sets. Supports OFFSET, ROTATE, SCALE, and SCALETO modifiers for transformation. For shader effects, use SET LAYER SHADER on the target layer. For textured polygons, UV coordinates specify texture mapping for each vertex.
Transforms use the shape's own centre as their origin by default. ROTATE(angle) spins the polygon about the average of its own vertices, and SCALE/SCALETO grow it about the same point. That is right for a shape that stands alone, and wrong for one piece of a composed object: a flame drawn behind a ship, authored around the ship's origin, spins about its own middle and never swings round to the correct side.
Give an explicit origin for that case — ROTATE(angle, 0, 0) rotates about the coordinate system the points were authored in, so several polygons sharing an origin rotate as one object. OFFSET is applied last, after any rotation or scale, so it moves the finished shape into place.
Syntax
POLYGON ((x1, y1) [UV (u1, v1)], (x2, y2) [UV (u2, v2)], ...), color [, fillColor] [, textureName] [, modifiers]
POLYGON arrayName, color [, fillColor] [, textureName] [, UV(uvArray | (u1,v1), (u2,v2), ...)] [, modifiers]
modifiers: ROTATE(angle [, originX, originY]) SCALE(sx, sy) SCALETO(w, h) OFFSET(x, y)
(SCALE, SCALETO and OFFSET each accept a coordinate: SCALE(coord), OFFSET(coord))Parameters
- (x, y) - Vertex coordinates (at least 3 vertices required)
- UV (u, v) - Optional UV texture coordinates for each vertex (0.0-1.0, where 0,0 is top-left)
- arrayName - A MUTATION, or an array of points: coordinate values ((x,y)), objects with .x/.y fields, or an Nx2 numeric array of [x, y] rows
- color - Outline color (palette index 0-255, or -1 for no outline)
- fillColor - Optional fill color (palette index 0-255, ignored when texture is specified)
- textureName - Optional texture name for textured fill (requires UV coordinates on vertices)
- OFFSET(x, y) - Translate polygon by x, y pixels
- ROTATE(angle [, originX, originY]) - Rotate polygon by angle degrees (default: around centroid)
- SCALE(scaleX, scaleY) - Scale polygon dimensions (applied around centroid)
- SCALETO(width, height) - Scale polygon to fit target dimensions
- UV(uvArray) - UV modifier: specify UV coordinates from a MUTATION or coordinate array (normalized 0.0-1.0, for array/mutation-based polygons)
- UV((u1,v1), (u2,v2), ...) - UV modifier: specify inline UV coordinates (normalized 0.0-1.0, where 0,0 is top-left and 1,1 is bottom-right)
- UV(..., SCALE(sx, sy)) - Scale UV coordinates by factors (applied around UV centroid)
- UV(..., SCALETO(w, h)) - Scale UV coordinates to fit target dimensions
- UV(..., ROTATE(angle)) - Rotate UV coordinates by angle degrees (around UV centroid)
- UV(..., OFFSET(u, v)) - Translate UV coordinates by u, v amounts
Examples
POLYGON ((50, 50), (150, 50), (100, 150)), 15 ' Triangle outlinePOLYGON ((50, 50), (150, 50), (100, 150)), 15, 12 ' Filled trianglePOLYGON ((10, 10), (90, 10), (90, 90), (10, 90)), 14, 10 ' Filled rectangle' Star shape:
POLYGON ((160, 50), (170, 80), (200, 80), (175, 100), (185, 130), (160, 110), (135, 130), (145, 100), (120, 80), (150, 80)), 15' Array-based polygon:
DIM points(2) AS OBJECT
points(0).x = 50: points(0).y = 50
points(1).x = 150: points(1).y = 50
points(2).x = 100: points(2).y = 150
POLYGON points, 15, 12' Textured quad with UV mapping:
LOADIMG "beam.png", "beam"
POLYGON ((0, 0) UV (0, 0), (100, 0) UV (1, 0), (100, 20) UV (1, 1), (0, 20) UV (0, 1)), -1, 0, "beam"' UV modifier with inline coordinates (for array/mutation polygons):
DIM quad AS MUTATION
quad = MUTATION((0, 0), (100, 0), (100, 100), (0, 100))
LOADIMG "tile.png", "tile"
POLYGON quad, -1, 0, "tile" UV((0, 0), (1, 0), (1, 1), (0, 1))' UV modifier with array variable:
DIM uvCoords(3) AS OBJECT
uvCoords(0).u = 0: uvCoords(0).v = 0
uvCoords(1).u = 1: uvCoords(1).v = 0
uvCoords(2).u = 1: uvCoords(2).v = 1
uvCoords(3).u = 0: uvCoords(3).v = 1
POLYGON quad, -1, 0, "tile" UV(uvCoords)' Translated polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, OFFSET(100, 100)' Rotated polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45)' Rotated around custom point:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45, 25, 25)' Scaled polygon:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(2, 2)' Combined modifiers:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, 12, SCALE(2, 2), ROTATE(angle), OFFSET(200, 200)See also: LINE, CIRCLE, BEGIN FRAME, END FRAME, OFFSET, ROTATE, SCALE, SCALETO, SET LAYER, LOADIMG
Graphics · since 0.23.0
Displays text and values on the screen. Use the WORDS modifier to enable word wrapping, which breaks text at word boundaries instead of mid-word when reaching the edge of the screen. Use RATE to create a typewriter effect with a delay between each character - this is non-blocking, meaning program execution continues while text is being typed.
In multiplayer, PRINT is client-only — calling it from a server-side interpreter (host or player-server) raises a fatal runtime error pointing at the EVENT CLIENTS pattern. Update server-authoritative state, then fire EVENT CLIENTS "RefreshHud" and have a GUARD(CLIENT) handler do the actual PRINT. State is server-authoritative; pixels are client-authoritative.
Syntax
PRINT [WORDS] [RATE(delayMs)] [expression [; | ,] [expression] ...]Parameters
- WORDS - Optional modifier for word-wrapped output
- RATE(delayMs) - Optional typewriter effect with delay in milliseconds between characters
- expression - Value to display
- ; - Keep on same line
- , - Tab to next column
Examples
PRINT "Hello, World!"PRINT "Name:", name$PRINT x; y; zPRINT WORDS "This long text will wrap at word boundaries instead of breaking mid-word"PRINT RATE(100) "Slow typewriter effect..."PRINT WORDS RATE(30) "Fast typewriter with word wrap for dialog boxes"' Wait for typing to complete:
PRINT RATE(50) "Loading..."
WHILE TYPING() = 1
' Continue other tasks
WENDSee also: LOCATE, TYPING, WINDOW
PSET
Graphics · since 0.23.0
Sets individual pixels at coordinates. For optimal performance when drawing many pixels in loops, use BEGIN FRAME/END FRAME to buffer operations and eliminate flicker.
Syntax
PSET (x, y) [, color]Parameters
- x, y - Pixel coordinates
- color - Color value
Examples
PSET (100, 100), 15FOR I = 0 TO 319
PSET (I, 100), 14
NEXTREM High-performance pixel drawing
BEGIN FRAME
FOR I = 0 TO 319
PSET (I, 100), 14
NEXT
END FRAMESee also: LINE, CIRCLE, BEGIN FRAME
REGION
Graphics · since 0.23.0
Graphics modifier used with PUTIMAGE to copy only a specific rectangular region from the source image. Useful for extracting parts of tilesets, creating viewport effects, or implementing custom sprite clipping.
Syntax
REGION(x1, y1, x2, y2)Parameters
- x1, y1 - Top-left corner of the source region
- x2, y2 - Bottom-right corner of the source region
Examples
PUTIMAGE (10, 10), "tileset", REGION(0, 0, 32, 32) ' Copy 32x32 tilePUTIMAGE (100, 100), "atlas", REGION(64, 0, 96, 32) ' Extract specific spritePUTIMAGE (0, 0), "room", REGION(20, 20, 400, 400), SCALETO(640, 480) ' Crop and scale to target size' Viewport scrolling:
viewX = playerX - 160
viewY = playerY - 120
PUTIMAGE (0, 0), "level", REGION(viewX, viewY, viewX+320, viewY+240)' Extract multiple tiles from tileset:
FOR row = 0 TO 3
FOR col = 0 TO 7
tileX = col * 32
tileY = row * 32
PUTIMAGE (col*32, row*32), "tiles", REGION(tileX, tileY, tileX+32, tileY+32)
NEXT
NEXTSee also: PUTIMAGE, LOADIMG, SCALE, SCALETO, ALPHA
RESET LAYERS
Graphics · since 0.23.0
Destroys all custom layers and restores the graphics system to its default state with only layer 0. Useful for cleaning up complex multi-layer scenes or resetting the graphics environment. All layer data is cleared and layer 0 is recreated with default screen dimensions.
Syntax
RESET LAYERSExamples
CREATE LAYER 1, 640, 200 ' Create custom layers
CREATE LAYER 2, 320, 200
RESET LAYERS ' Clean up - back to layer 0 onlyREM Reset graphics between game scenes
RESET LAYERS
SCREEN 13
CLSSee also: CREATE LAYER, CREATE MAP, SET LAYER, CLS
ROTATE
Graphics · since 0.23.0
Graphics modifier used with PUTIMAGE and POLYGON to rotate by a specified angle in degrees. Supports custom rotation origin point for precise control over the rotation center.
Syntax
ROTATE(angle [, originX, originY])Parameters
- angle - Rotation angle in degrees (0-360)
- originX - Optional X origin for rotation. For PUTIMAGE: normalized 0-1 value. For POLYGON: pixel coordinates
- originY - Optional Y origin for rotation. For PUTIMAGE: normalized 0-1 value. For POLYGON: pixel coordinates
- For PUTIMAGE: (0, 0) = top-left, (0.5, 0.5) = center (default when omitted), (1, 1) = bottom-right
- For POLYGON: rotation occurs around centroid by default, or custom pixel coordinates
Examples
PUTIMAGE (100, 100), "ship", ROTATE(45) ' Rotate 45 degrees around center (default)PUTIMAGE (x, y), "ship", ROTATE(45, 0.5, 0.5) ' Rotate 45 degrees around center (explicit)PUTIMAGE (200, 150), "wheel", 0, ROTATE(rotation) ' Animated rotation with frame 0' Spinning effect:
angle = (angle + 5) MOD 360
PUTIMAGE (centerX, centerY), "spinner", ROTATE(angle, 0.5, 0.5)' Combined with other modifiers:
PUTIMAGE (x, y), "sprite", FLIPX, ROTATE(45, 0.5, 0.5) ' Flip and rotatePUTIMAGE (x, y), "player", ALPHA(0.8), SCALE(2, 2), ROTATE(angle, 0.5, 0.5) ' Transparent, scaled, and rotated' Polygon rotation:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45) ' Rotate around centroidPOLYGON ((0, 0), (50, 0), (25, 50)), 15, ROTATE(45, 25, 25) ' Rotate around custom pointSee also: PUTIMAGE, POLYGON, LOADIMG, ALPHA, SCALE, FLIPX, FLIPY, OFFSET
SCALE
Graphics · since 0.23.0
Graphics modifier used with PUTIMAGE, SET SPRITE, and POLYGON. Uniformly applies scale factors where 1.0 = original size, 0.5 = half size, 2.0 = double size. For POLYGON, scales vertices around the centroid. Use SCALETO for scaling to specific pixel dimensions.
Syntax
SCALE(scaleX, scaleY)Parameters
- scaleX - Horizontal scale factor (1.0 = original, 2.0 = double, 0.5 = half)
- scaleY - Vertical scale factor (1.0 = original, 2.0 = double, 0.5 = half)
Examples
PUTIMAGE (10, 10), "icon", SCALE(2, 2) ' Draw at double sizePUTIMAGE (100, 100), "player", 0, SCALE(0.5, 0.5) ' Draw at half sizePUTIMAGE (50, 50), "enemy", ALPHA(0.7), SCALE(3, 3) ' Transparent and 3x scaledSET SPRITE 0, x, y, "player", SCALE(2, 2) ' Double-size sprite' Polygon scaling:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(2, 2) ' Double sizePOLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALE(0.5, 1) ' Squash horizontallySee also: SCALETO, PUTIMAGE, POLYGON, SET SPRITE, LOADIMG, ALPHA, REGION, OFFSET, ROTATE
SCALETO
Graphics · since 0.30.0
Graphics modifier used with PUTIMAGE, SET SPRITE, and POLYGON. Scales to specific target pixel dimensions. For POLYGON, computes the bounding box and derives scale factors to fit the shape within the target dimensions. Use SCALE for factor-based scaling.
Syntax
SCALETO(width, height)Parameters
- width - Target width in pixels
- height - Target height in pixels
Examples
PUTIMAGE (10, 10), "icon", SCALETO(64, 64) ' Scale to exactly 64x64 pixelsPUTIMAGE (100, 100), "player", 0, SCALETO(128, 128) ' Scale sprite frame to 128x128PUTIMAGE (50, 50), "enemy", ALPHA(0.7), SCALETO(48, 48) ' Transparent and scaled to 48x48SET SPRITE 0, x, y, "player", SCALETO(64, 64) ' Sprite scaled to 64x64' Dynamic size effect:
FOR size = 16 TO 64 STEP 8
PUTIMAGE (100, 100), "powerup", SCALETO(size, size)
NEXT' Polygon scaled to target size:
POLYGON ((0, 0), (50, 0), (25, 50)), 15, SCALETO(100, 100) ' Fit into 100x100See also: SCALE, PUTIMAGE, POLYGON, SET SPRITE, LOADIMG, ALPHA, REGION, ROTATE
SCREEN
Graphics · since 0.23.0
Set the video display mode, a custom resolution, or hand control to the platform's native size. This is usually the first graphical statement in a program — it's what opens the window and gives LINE / PSET / PRINT / sprites somewhere to draw.
Classic screen modes
Pass a single integer for the QBasic-style preset:
| Mode | Resolution | Notes |
|---|---|---|
0 |
text | character grid, not pixel-based |
1 |
CGA | classic 320×200 4-color |
7 |
EGA | 320×200 16-color |
12 |
VGA | 640×480 |
13 |
VGA | 320×200 (the famous 256-color mode) |
Custom resolutions
Pass two integers for an arbitrary pixel size (1–4096 per axis):
SCREEN 320, 240 ' retro
SCREEN 1920, 1080 ' Full HDAdd two more for explicit scale factors (0.1–10, default 1). Pixel-perfect upscaling for retro looks is exactly this:
SCREEN 160, 120, 2, 2 ' 160×120 art, doubled to 320×240 on-screen`SCREEN NATIVE`
SCREEN NATIVE
SCREEN NATIVE, "My Game"Use the platform's actual screen size. Especially useful on mobile, where you don't know the resolution ahead of time and SCREENWIDTH / SCREENHEIGHT are how you find out after the call.
Idempotent on repeat
Calling SCREEN with the same logical dimensions as the current screen is a no-op for layer state: the window itself updates (title, scale, fullscreen) and the viewport is recomputed, but existing layers, tilemaps, animations, and shader transitions all survive. This protects multiplayer / remote-client setups where the local CrashPlayer runs SCREEN concurrently with server-pushed CreateLayer / CreateTilemap frames — pre-1.29.0 the local SCREEN could race-nuke a map the server had already painted.
A SCREEN call with different dimensions still resets every layer (the prior render targets are the wrong size), same as before.
If you want "clear the screen, start fresh" mid-program, use BEGIN SCENE / END SCENE for transactional layer state, or CLS + DESTROY LAYER for explicit teardown. Don't lean on a second SCREEN to do it.
Syntax
SCREEN mode
SCREEN width, height
SCREEN width, height, scaleX, scaleY
SCREEN NATIVE [, "title"]Parameters
- Classic Screen Modes:
- mode - Screen mode (0=text, 1=CGA, 7=EGA, 12=VGA 640x480, 13=VGA 320x200)
- Custom Resolution:
- width - Screen width in pixels (1-4096)
- height - Screen height in pixels (1-4096)
- scaleX - Horizontal scale factor (0.1-10 recommended; the value is passed through to the renderer and is not clamped; default: 1)
- scaleY - Vertical scale factor (0.1-10 recommended; the value is passed through to the renderer and is not clamped; default: 1)
- Native Mode:
- NATIVE - Use the platform's native resolution (mobile-friendly)
- title - Optional window title (string)
Examples
SCREEN 13 ' Classic VGA 320x200SCREEN 12 ' Classic VGA 640x480SCREEN 0 ' Text modeSCREEN 800, 600 ' Custom 800x600 resolutionSCREEN 320, 240 ' Custom 320x240 resolutionSCREEN 160, 120, 2, 2 ' 160x120 scaled 2x (pixel-perfect)SCREEN 640, 480, 0.5, 0.5 ' 640x480 scaled down 50%SCREEN 1920, 1080 ' Full HD resolutionSCREEN NATIVE ' Use native resolution (great for mobile)SCREEN NATIVE, "My Game" ' Native resolution with custom title' Adaptive layout using native resolution
SCREEN NATIVE
PRINT "Screen size:"; SCREENWIDTH; "x"; SCREENHEIGHTSee also: CLS, PSET, SCREENWIDTH, SCREENHEIGHT
SCREEN TEXT
Graphics · since 0.23.0
Creates a text-only screen mode optimized for character display. Uses 8x8 character cells and supports both ASCII and modern Unicode characters including emojis.
Syntax
SCREEN TEXT columns, rows [, scaleX, scaleY] [WITH ASCII | WITH MODERN]Parameters
- columns - Number of character columns (1-200)
- rows - Number of character rows (1-100)
- scaleX - Horizontal scale factor (1-10, default: 1)
- scaleY - Vertical scale factor (1-10, default: 1)
- ASCII - Use classic ASCII character set (default)
- MODERN - Enable modern Unicode characters and emojis
Examples
SCREEN TEXT 80, 25 'Classic 80x25 text modeSCREEN TEXT 40, 20, 2, 2 WITH ASCII '40x20 with 2x scalingSCREEN TEXT 60, 30 WITH MODERN '60x30 with emoji supportPRINT "Hello World!" 'Text displays normallyPRINT "👾 🎮 CrashBasic! 🚀" 'Emojis work in MODERN modeSee also: SCREEN, PRINT, LOCATE, COLOR
SCREENHEIGHT
Graphics · since 1.7.0
Returns the current screen height in pixels. Useful for adaptive layouts, especially when using SCREEN NATIVE on mobile devices.
Syntax
SCREENHEIGHTExamples
SCREEN 320, 200
PRINT SCREENHEIGHT ' Prints 200SCREEN NATIVE
PRINT "Height:"; SCREENHEIGHT' Position something at bottom of screen
y = SCREENHEIGHT - objectHeight - 10See also: SCREENWIDTH, SCREEN
SCREENWIDTH
Graphics · since 1.7.0
Returns the current screen width in pixels. Useful for adaptive layouts, especially when using SCREEN NATIVE on mobile devices.
Syntax
SCREENWIDTHExamples
SCREEN 320, 200
PRINT SCREENWIDTH ' Prints 320SCREEN NATIVE
PRINT "Width:"; SCREENWIDTH' Center something on screen
x = (SCREENWIDTH - objectWidth) / 2See also: SCREENHEIGHT, SCREEN
SET CURSOR
Graphics · since 0.23.0
Sets a custom mouse cursor using a previously loaded image, or restores the default cursor. The image should be loaded first using LOADIMG. Browser limits cursor size to 128x128 pixels.
Syntax
SET CURSOR image_name [, hotspot_x, hotspot_y] | SET CURSOR OFFParameters
- image_name - Name of the loaded image to use as cursor
- hotspot_x - Optional X coordinate of cursor hotspot (default: center)
- hotspot_y - Optional Y coordinate of cursor hotspot (default: center)
- OFF - Keyword to restore the default cursor
Examples
LOADIMG "pointer.png", "customPointer"SET CURSOR "customPointer" ' Use custom cursor with center hotspotSET CURSOR "customPointer", 0, 0 ' Hotspot at top-left cornerSET CURSOR OFF ' Restore default cursor
SET IMAGE
Graphics · since 0.23.0
Switches the drawing target to a named image buffer created with NEWIMAGE or captured with GETIMAGE. All graphics commands (PSET, LINE, CIRCLE, etc.) will draw to the image instead of the screen until SET LAYER is used to return to layer drawing.
Syntax
SET IMAGE imageName$Parameters
- imageName$ - Name of image buffer to draw to (must exist)
Examples
REM Draw to offscreen buffer
NEWIMAGE 100, 100, 0, "buffer"
SET IMAGE "buffer"
CIRCLE (50, 50), 40, 15
LINE (10, 10)-(90, 90), 14
REM Return to screen and display buffer
SET LAYER 0
PUTIMAGE (100, 50), "buffer"REM Prepare multiple sprites
NEWIMAGE 64, 64, 0, "player"
SET IMAGE "player"
CIRCLE (32, 32), 28, 14, 14
CIRCLE (24, 24), 4, 15, 15
CIRCLE (40, 24), 4, 15, 15
NEWIMAGE 64, 64, 0, "enemy"
SET IMAGE "enemy"
CIRCLE (32, 32), 28, 4, 4
SET LAYER 0
PUTIMAGE (50, 50), "player"
PUTIMAGE (200, 50), "enemy"REM Complex compositing
CREATE LAYER 0, 320, 200
NEWIMAGE 320, 200, 0, "effect"
REM Draw base to layer
SET LAYER 0
CIRCLE (160, 100), 80, 12, 12
REM Draw effect to buffer
SET IMAGE "effect"
FOR r = 10 TO 100 STEP 5
CIRCLE (160, 100), r, 14
NEXT
REM Composite effect over base
SET LAYER 0
PUTIMAGE (0, 0), "effect", ALPHA(0.5)REM Switch between buffers
WHILE running
SET IMAGE "backbuffer"
CLS
' Draw game state to backbuffer
CIRCLE (playerX, playerY), 10, 15
SET LAYER 0
PUTIMAGE (0, 0), "backbuffer"
WENDSee also: NEWIMAGE, SET LAYER, GETIMAGE, PUTIMAGE
SET LAYER
Graphics · since 0.23.0
Sets the active layer for subsequent graphics operations. All drawing commands (PSET, LINE, CIRCLE, etc.) will target the specified layer until changed. Optional ALPHA parameter sets the transparency of the entire layer (0.0=fully transparent, 1.0=fully opaque). Optional WRAPX/WRAPY parameters enable infinite scrolling (wrap-around) on the horizontal/vertical axis respectively - when enabled, content that scrolls off one edge reappears on the opposite edge. Optional SHADER parameter applies a post-processing shader to the entire layer for effects like CRT filters, vignettes, color grading, etc. NOTE: Use BEGIN FRAME/END FRAME for layer composition to work properly.
In multiplayer SET LAYER is dual-form: the bare SET LAYER n form (just switching draw target) is render-only and fatal-errors on a server-side interpreter — drive client-side draw via EVENT CLIENTS "RefreshHud" instead. Forms that carry layer attributes — SET LAYER n WRAPX/WRAPY, … ALPHA(v), … SHADER "x" — are layer configuration that runs everywhere; the SHADER form additionally broadcasts so all clients pick up the same post-processing.
Plane layers — AFFINE / PERSPECTIVE / FLAT
Reproject a layer (a drawn surface, a loaded image, or a tilemap) as a rotated/scaled or Mode 7-style receding plane. The camera is driven with SET SCROLL x, y ANGLE(deg) — x, y is the camera's world position and ANGLE is its heading in degrees (consistent with SIN/COS).
AFFINE [ZOOM(z)]— flat rotate/scale of the whole layer about the screen centre, no perspective.ZOOM(default 1.0): > 1 zooms out, < 1 zooms in. Ideal for spinning/scaling a tilemap or layer in pure 2D.PERSPECTIVE [HORIZON(y)] [HEIGHT(h)]— a receding floor.HORIZONis the horizon line's screen-Y in pixels; everything above it is transparent.HEIGHTis the camera's height above the plane (larger = floor seen from higher up).FLAT— turn plane mode off; the layer renders as normal 2D again.
Clipping a layer to a sub-region — CLIP
By default a layer (and a map layer in particular) fills the whole screen. CLIP (x1,y1)-(x2,y2) restricts that layer's rendering to a screen rectangle (a scissor rect), so a tilemap/map can occupy a sub-region and leave HUD/menu areas uncovered — compartmentalizing the screen. Coordinates are logical screen pixels; the rect is normalized (corners ordered, clamped to the screen) and mapped through any letterbox. The clip applies to the layer's final output, so it composes correctly with a layer SHADER and with AFFINE/PERSPECTIVE plane modes (the shaded/projected floor is clipped, not an intermediate). Sprites assigned to the layer clip with it. CLIP NONE removes the clip (full screen again). The clip persists across frames until changed — set it once at setup. CLIP does NOT apply to video playback (PLAYVIDEO): video is decoded through each platform's native pipeline and composites outside the layer system on some platforms, so a clip rectangle will not mask a playing video.
WRAPX/WRAPY make the plane tile infinitely toward the horizon. Sprites assigned to a plane layer are projected onto it (scaled by depth, painter-sorted far→near, culled at the camera/horizon). User shaders compose with plane layers: on a plain (drawn/image) plane layer the shader runs in map space and the result is then projected (shade-then-project); on a tilemap plane layer the shader runs in screen space on the projected floor (project-then-shade). SET LAYER ALPHA composes with a shader on any layer — layer alpha can fade a shadered layer all the way to transparent.
Syntax
SET LAYER layer_id [ALPHA(alpha)] [WRAPX [ON|OFF]] [WRAPY [ON|OFF]] [CLIP (x1,y1)-(x2,y2) | CLIP NONE] [AFFINE [ZOOM(z)] | PERSPECTIVE [HORIZON(y)] [HEIGHT(h)] | FLAT] [SHADER "name" [, @param=value, ...] | SHADER NONE]Parameters
- layer_id - Layer number to make active (0-99). Layers 100+ are reserved for internal use.
- ALPHA(alpha) - Optional layer transparency (0.0-1.0). 0.0=invisible, 1.0=fully opaque
- WRAPX [ON|OFF] - Enable or disable horizontal wrap-around scrolling. WRAPX alone enables it, WRAPX ON explicitly enables, WRAPX OFF disables
- WRAPY [ON|OFF] - Enable or disable vertical wrap-around scrolling. WRAPY alone enables it, WRAPY ON explicitly enables, WRAPY OFF disables
- AFFINE [ZOOM(z)] - Reproject the layer as a flat rotated/scaled plane about the screen centre. ZOOM (default 1.0): >1 zooms out, <1 zooms in. Camera driven by SET SCROLL ... ANGLE.
- PERSPECTIVE [HORIZON(y)] [HEIGHT(h)] - Reproject the layer as a Mode 7-style receding floor. HORIZON = horizon line's screen-Y in pixels (above it is transparent); HEIGHT = camera height above the plane.
- FLAT - Disable plane mode; the layer renders as normal 2D.
- CLIP (x1,y1)-(x2,y2) - Restrict the layer's rendering to this screen rectangle (a scissor rect), so a map layer fills a sub-region and leaves HUD/menu areas uncovered. Composes with the layer shader and plane modes; sprites on the layer clip too. Does NOT affect video (PLAYVIDEO), which renders via the platform's native pipeline. CLIP NONE removes it (full screen).
- SHADER "name" - Apply a shader to the entire layer (must be defined with SHADER block)
- @param=value - Optional shader parameter overrides
- SHADER NONE - Remove any shader from the layer
Examples
SET LAYER 0 ' Draw to main layerCIRCLE (160, 100), 50, 15SET LAYER 1 ' Switch to background layerLINE (0, 0)-(320, 200), 12SET LAYER 2 ALPHA(0.5) ' Set UI layer to 50% transparentSET LAYER 1 ALPHA(1.0) ' Set sprites fully opaqueSET LAYER 0 ALPHA(0.0) ' Make background invisibleSET LAYER 0 SHADER "CRT" ' Apply CRT effect to main layerSET LAYER 1 SHADER "Vignette", @intensity=0.8 ' Vignette with custom intensitySET LAYER 0 SHADER NONE ' Remove shader from layerSET LAYER 1 WRAPX ' Enable horizontal infinite scrollingSET LAYER 1 WRAPX WRAPY ' Enable both horizontal and vertical wrapping' Compartmentalize a map: it fills the left region, the right is a HUD area
CREATE MAP FROM map TILESET "tiles" TILESIZE 32 AS LAYER 0
SET LAYER 0 CLIP (0,0)-(212,240) ' map only renders in the left 212px
SET LAYER 0 CLIP NONE ' later: back to full screenSET LAYER 2 WRAPX ON WRAPY OFF ' Explicit wrap controlSET LAYER 0 ALPHA(0.8) WRAPX ' Combine transparency with wrappingREM Mode 7 receding floor (drive the camera with SET SCROLL ... ANGLE)
SET LAYER 0 PERSPECTIVE HORIZON(110) HEIGHT(90) WRAPX WRAPY
SET SCROLL 256, cameraY ANGLE(heading)SET LAYER 0 AFFINE ZOOM(1.0) ' Flat rotate/scale plane (pure 2D)SET LAYER 0 FLAT ' Turn plane mode off — back to normal 2DSee also: CREATE LAYER, CREATE MAP, SET SCROLL, ALPHA, SHADER, SET SPRITE
SET PALETTE
Graphics · since 1.39.0
Switches between palette (indexed) and direct-color modes. ON (the default) means color values passed to drawing commands are palette indices. OFF (direct-color mode) means a color value is a literal 0xAARRGGBB color (alpha, red, green, blue), so colors can be specified directly without defining palette entries. In direct mode, transparency is expressed with alpha = 0 (the -1 transparent sentinel applies only in palette mode). Hex literals (&H...) pair naturally with this mode. The mode applies to all color arguments (PSET, LINE, CIRCLE, POLYGON, COLOR, text, fills, etc.).
Syntax
SET PALETTE ON | OFFParameters
- ON - Color values are palette indices (default)
- OFF - Color values are literal 0xAARRGGBB colors (direct-color mode)
Examples
SET PALETTE OFF
COLOR &HFFFF8800 ' opaque orange (A=FF R=FF G=88 B=00)
PSET (10, 10), &H8000FF00 ' 50% greenPSET (20, 20), &H00000000 ' transparent (alpha 0) in direct modeSET PALETTE ON ' back to palette indices
SET POLYGON
Graphics · since 1.43.0
Creates a retained, mutation-bound polygon — a polygon whose vertices are a MUTATION. Unlike POLYGON, which rasterizes into a layer every time you call it, a bound polygon lives above its assigned layer in the sprite z-order and is re-composited on the GPU every frame from the bound mutation's current coordinates. Because the geometry rides the same per-frame update path sprites use, a behavior-driven (deforming) polygon costs about a sprite draw per frame instead of a re-rasterize, and it never dirties the layer beneath it.
The id is a 0-based polygon handle in its own space, independent of sprite IDs. BIND mutationVar names the MUTATION whose points are the polygon's vertices — apply a behavior to that mutation with SET MUTATION ... BEHAVIOR and the polygon deforms automatically.
Modifiers: FILL sets the solid fill colour (palette index); COLOR sets an outline colour; TEXTURE names a loaded image for a textured polygon; UV binds the per-vertex texture coordinates (0..1) to a second MUTATION so the texture animates — apply a behavior to that UV mutation and the texture warps/scrolls on the polygon while its shape can move independently; LAYER chooses the layer it composites above; ZORDER sets draw order within that layer (shared with sprites). Without UV, a textured polygon auto-maps the image across its bounding box. Fills are correct for concave shapes (ear-clipping). Remove a bound polygon with DESTROY POLYGON id.
Multiplayer: bound polygons are client-only. The polygon's mutation and its behavior tick on each client's own engine and render there, so a polygon is drawn independently on every client with no per-frame network traffic. Calling SET POLYGON (or DESTROY POLYGON) from server-side code in a CrashNet game is a fatal error — wrap it in IF ME.ISCLIENT THEN ... END IF. To share a polygon's shape across clients, replicate the data that drives its mutation (for example via PLAYER DATA / HOST DATA) and let each client draw it locally.
Pooling. The mutation may be an array slot — rocks(i) — and the index is an expression, so one loop can create and drive a whole field of shapes. Without it every shape needed its own literally-named variable, which is why bound polygons were only ever used one at a time.
Collision. A bound polygon is geometry and does not collide until asked. SOLID gives it a collider that tracks the shape's live extent, which makes every existing collision facility work on it unchanged — SOLID resolution, ONCOLLISION/AFTERCOLLISION, FOR COLLISIONS, tags, boundaries, rooms and layers. GHOST removes the collider again, leaving the geometry.
SOLID on its own collides with boundaries. Sprite-to-sprite resolution is opt-in by tag, exactly as it is for a sprite: SOLID SPRITES("ship", "bullet") names what this shape treats as an obstacle, and without that list the shape and any sprite pass through each other. Collision reporting — ONCOLLISION, FOR COLLISIONS, tag queries — does not need the list.
The shape defaults to CIRCLE, inscribed in the extent. A circle suits the round-ish silhouettes vector games are built from, costs one distance check, and — unlike a box — does not breathe as the shape rotates: a spinning rock's AABB swells by up to 41% and back every revolution, which plays as a hitbox that pulses. BOX opts into the axis-aligned box a sprite uses. POLY tests the shape's actual outline — every edge, concave shapes included — which is what an irregular silhouette needs: a circle inscribed in the extent misses everything in the shape's bulges, and a box catches its empty corners. It costs a handful of segment tests, and only on pairs whose bounding boxes already overlap, so the shapes that were never going to touch cost nothing.
TAG names the collider for queries and handlers, exactly as on a sprite. BBOX takes the same relative 0-1 quad or Range a sprite takes, but measured against the shape's current extent and resolved every tick — because that extent moves, and a box baked to pixels once would be left behind.
An AABB overlap always gates the shape test, so a circle costs nothing on pairs that were never going to touch. Resolution still separates on an axis whatever the shape.
A bound polygon is world state, so BEGIN SCENE covers it: a shape moved inside a scene is restored at END SCENE, and a polygon created inside one is destroyed with it. Re-issuing SET POLYGON on a slot that predates the scene is a restyle, not a creation, and leaves the polygon standing.
A bound polygon is a sprite too
The slot a polygon occupies is a sprite slot. Its geometry comes from the bound MUTATION; everything else about it — where it is, whether it is visible, how solid it is — is ordinary sprite state.
Two ways to reach that state, and the difference matters. SET POLYGON id, ... takes the polygon's own 0-based handle and is what you write in ordinary code. SET SPRITE takes a raw sprite id, and a polygon's half does not live at the handle you gave it — so SET SPRITE 3 is sprite 3, not polygon 3. Where you have the SLOT itself, SET SPRITE works on it like any other sprite; a collision handler is where that happens, because index_a is that slot.
You will most often reach it from a collision handler, where index_a is that slot:
BEHAVIOR "asteroid"
BVELOCITY @vx = 0, @vy = 0
ONCOLLISION
IF tag_b$ = "edge" THEN
' Move the sprite and the shape goes with it.
SET SPRITE index_a LOCATION(1, SPRITEY(index_a))
END IF
END ONCOLLISION
END BEHAVIOR| What you write | What it does to a bound polygon |
|---|---|
LOCATION(x, y), MOVE SPRITE |
moves the SHAPE — its points travel with it, and the behaviour driving it carries on undisturbed |
SCALE, ROTATE, FLIPX, FLIPY |
poses the shape: the transform is written into its points, so what is drawn, what collides and what the MUTATION reads back are the same thing |
ORIGIN(u, v) |
the TRANSLATIONAL anchor — the point LOCATION places. The pivot a pose turns about is separate, and is named in the rotation call: ROTATE(deg, ox, oy) |
ALPHA, LAYER, ZORDER, SHOW/HIDE SPRITE |
exactly what they do to any sprite — given the SLOT. From ordinary code use SET POLYGON id, ALPHA(a) / SHOW / HIDE, which take the handle |
SOLID, GHOST, TAG, BBOX, SPRITES(...) |
ordinary collider settings, measured against the shape's live extent |
DISABLE SPRITE |
destroys the polygon — geometry, binding and collider together |
Writing the shape's points by hand is the thing to avoid here. Moving the shape — SET POLYGON id, LOCATION(x, y), or SET SPRITE index_a LOCATION(...) from a handler — lets the engine keep the shape, its collider and its behaviour in step. Writing the points instead ends the behaviour: the shape it was attached to is no longer the shape you asserted, so it is removed rather than left running against geometry it no longer describes, and you would have to re-apply it.
A shape's behaviour handlers fire on that slot. ONCOLLISION and AFTERCOLLISION inside a behaviour applied with SET MUTATION ... BEHAVIOR run when the polygon collides, because a collision is looked up from the sprite that collided and this is that sprite.
Modifier-only form. SET POLYGON id, ... with no BIND updates a shape that is already bound, exactly as SET SPRITE n, ... updates an existing sprite. Every modifier works in either form — appearance, pose, motion and the collider set alike, so a shape bound once at setup can be made SOLID, re-TAGged or GHOSTed later in response to what is happening. A modifier you leave out keeps its current value, so SET POLYGON 0, LAYER 1 moves the shape and keeps its colour and texture. BIND is the other way round — it rebuilds the polygon, so a modifier you leave out of a re-bind goes back to its default. The drawing modifiers (COLOR, FILL, TEXTURE, LAYER, ZORDER) need something to draw, so using one on an id that was never bound is an error; the pose, motion and collider modifiers work on an unbound id.
Address a polygon by its handle, with SET POLYGON. SET SPRITE 3, SHOW SPRITE 3 and HIDE SPRITE 3 address sprite 3 — they do not reach polygon 3, and they do not report an error about it. Use SET POLYGON 3, SHOW / HIDE and the modifiers above. (The sprite statements do work on the SLOT, which is what a collision handler's index_a holds — see above.)
Pose lives in the points. A shape has no quad for the renderer to transform, so ROTATE / SCALE / SCALETO / FLIPX / FLIPY are baked into the bound mutation's coordinates — which means a program reading that mutation back sees the posed shape, and the collider measures the posed extent. Each pose is computed from the shape as authored, not from the last pose, so scaling out and back returns the coordinates you wrote.
ORIGIN is the TRANSLATIONAL anchor, in relative 0-1 coordinates across the shape's extent: the point LOCATION places, a parent link measures from, and the collider rect subtracts. The point a shape TURNS about is a DIFFERENT anchor, named inside the rotation call because that is the only context a pivot means anything — ROTATE(deg, ox, oy) for a pose, BROTATE ... ORIGIN(u, v) for a behavior. Omit it and the pivot is (0, 0), the top-left of the shape as authored. Keeping them apart is what lets a shape turn about one point and be placed by another. The default ORIGIN(0, 0) places a shape by the top-left of its extent; ORIGIN(0.5, 0.5) places it by its centre. Give it before LOCATION in the same statement — it is what LOCATION means.
Motion comes from the sprite half. BEHAVIOR, PATTERN and PARENT move and turn the shape by writing its points, and MAXVELOCITY / MAXACCELERATION cap that motion the same way they cap a sprite's. Reading the bound mutation while a behavior drives it is free; WRITING it ends the behavior and hands control back to the program (see MUTATION), which is why a shape that must keep its behavior is positioned with LOCATION rather than by authoring points.
Syntax
SET POLYGON id BIND mutationVar | mutationArray(index) [modifiers]
SET POLYGON id [modifiers]
(appearance) TEXTURE name | UV uvMutation | FILL fillColor | COLOR outlineColor |
SHADER "name" | LAYER n | ZORDER z | ALPHA(a) | SHOW | HIDE
(pose) LOCATION(x, y) | ORIGIN(ox, oy) | ROTATE(angle [, ox, oy]) | SCALE(sx, sy) |
SCALETO(w, h) | FLIPX | FLIPY
(motion) BEHAVIOR "name" [, @param=value ...] | PATTERN "name" | PARENT(n) | PARENT POLYGON(n) |
MAXVELOCITY(x, y) | MAXACCELERATION(x, y) | RESET
(collision) SOLID [CIRCLE|BOX|POLY] [SPRITES("tag", ...)] | GHOST | TAG "name" |
BBOX(x1,y1,x2,y2) | BBOX range
(network) SYNC [("room")] | NOSYNCParameters
- id - 0-based polygon handle (its own space, not a sprite ID)
- mutationVar - name of the MUTATION holding the polygon's vertices
- uvMutationVar - name of a MUTATION whose coords are the per-vertex UVs (0..1); animates the texture when a behavior drives it (UV modifier)
- fillColor - solid fill palette index (FILL modifier)
- outlineColor - outline palette index (COLOR modifier)
- name - texture image name for a textured polygon (TEXTURE modifier)
- n - layer to composite above (LAYER modifier)
- z - z-order within the layer (ZORDER modifier)
- SPRITES("tag", ...) - Optional after SOLID. Tags this collider treats as solid obstacles. Omit it and the shape resolves against boundaries only.
- a - ALPHA opacity 0-1, applied to fill and outline alike
- x, y - LOCATION target for the shape's anchor (accepts a Coordinate)
- ORIGIN(ox, oy) - the TRANSLATIONAL anchor, relative 0-1 across the shape's extent: the point the shape is PLACED by (what LOCATION positions, what a parent link measures from, and what the collider rect subtracts). It is NOT the point the shape turns about — that is a separate anchor, named inside the rotation call itself as ROTATE(deg, ox, oy) or BROTATE ... ORIGIN(u, v). Two anchors, two jobs; a shape may want them at different points.
- angle - ROTATE angle in degrees, turned about the shape's ROTATIONAL pivot — the optional ox, oy of this same ROTATE, NOT the translational ORIGIN. Omitted, the pivot is (0, 0): the top-left of the shape as authored.
- sx, sy - SCALE factors (accepts a Coordinate); SCALETO takes a target size in pixels instead
- "name" - BEHAVIOR or PATTERN to attach to the shape's sprite half
- PARENT(n) [RIGID] - the sprite the shape rides. A shape has no draw-time transform stage, so the parent's motion is folded into its points once per tick. Without RIGID the link is translation-only: the child follows the parent's position and keeps its own rotation. With RIGID the parent's ROTATION comes too — the shape orbits the parent and inherits its spin, which is what makes it a bone rather than a tagalong.
- PARENT POLYGON(polygonHandle) [RIGID] - Parent a BOUND POLYGON, named by its own 0-based handle rather than by a raw sprite id. A polygon's slot IS a sprite, so the link behaves exactly as PARENT(spriteNum) does — but the two number spaces are different and neither can be guessed from the value:
PARENT(0)is sprite 0,PARENT POLYGON(0)is polygon 0. This is the only way to name another shape's slot from ordinary code; inside a collision handlerindex_aalready IS the slot. RIGID and NONE work the same as for a sprite parent. - MAXVELOCITY / MAXACCELERATION - motion ceilings, same meaning as on a sprite (accept a Coordinate)
- RESET - clear the shape's motion and detach its behaviors
- SYNC / NOSYNC - replication control; SYNC("room") scopes it to one room
Examples
REM A deforming blob: wobble a mutation, bind a polygon to it
DIM blob AS MUTATION
blob = MUTATION((160, 70), (215, 120), (190, 190), (130, 190), (105, 120))
BEHAVIOR "Wobble"
BOSCILLATE AMPLITUDE 14, 14 SPEED 3 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIOR
SET MUTATION blob BEHAVIOR "Wobble"
SET POLYGON 0 BIND blob FILL 4 COLOR 15REM Sprite-controlled placement: the shape keeps its behavior
DIM flame AS MUTATION
flame = MUTATION((-4, -4.5), (0, 4.5), (4, -4.5))
BEHAVIOR "flicker"
BALPHA 1.0 TO 0.35 DURATION 8 LOOP
END BEHAVIOR
SET POLYGON 1 BIND flame COLOR 4 FILL 12
SET MUTATION flame BEHAVIOR "flicker"
REM Never write the points: that would end the flicker. Move it instead.
SET POLYGON 1, ORIGIN(0.5, 0.5) LOCATION(shipX, shipY) ROTATE(heading)See also: DESTROY POLYGON, POLYGON, MUTATION, SET MUTATION, SET SPRITE
SET SCROLL
Graphics · since 0.23.0
Sets the scroll offset for the current layer. Enables camera movement and parallax scrolling effects for advanced 2D graphics. IMPORTANT: Requires BEGIN FRAME/END FRAME for layer composition to display properly.
The optional ANGLE(degrees) clause sets the camera heading for plane layers (SET LAYER ... AFFINE / PERSPECTIVE). On a plane layer, x, y is the camera's world position and ANGLE rotates the plane around it — degrees, consistent with SIN/COS. ANGLE has no visible effect on a normal (non-plane) layer.
Syntax
SET SCROLL x, y [ANGLE(degrees)] (also accepts the coordinate form: SET SCROLL(x, y) [ANGLE(degrees)])Parameters
- x - Horizontal scroll offset in pixels (camera world X on a plane layer)
- y - Vertical scroll offset in pixels (camera world Y on a plane layer)
- ANGLE(degrees) - Optional camera heading in degrees for plane (AFFINE/PERSPECTIVE) layers; rotates the plane around the camera position. Degrees, like SIN/COS.
Examples
SET SCROLL(0, 0) ' Reset scroll positionSET SCROLL(cameraX, cameraY) ' Follow playerSET SCROLL(backgroundX / 2, 0) ' Parallax effectSET SCROLL 256, cameraY ANGLE(heading) ' Plane camera: pan + rotate (degrees)See also: GETSCROLL, CREATE LAYER, CREATE MAP, SET LAYER
SETFONT
Graphics · since 0.23.0
Synonym for FONT. Sets the font family and size for text rendering. The CURRENT sentinel keeps whatever font is already active and only changes the size. See FONT for full details, including FONT RESET (which has no SETFONT spelling) and the layer-scoped font behavior.
In multiplayer, SETFONT is client-only and halts with a runtime error if called from a server-side interpreter.
Syntax
SETFONT fontname$, size
SETFONT CURRENT, sizeParameters
- fontname$ - Font family name (built-in: "IBM BIOS", "IBM EGA 8x8", or custom loaded font)
- CURRENT - Sentinel meaning "keep the current font, only change size"
- size - Font size in pixels
Examples
SETFONT "IBM EGA 8x8", 16
PRINT "Retro pixel font"SETFONT CURRENT, 24 ' Resize without changing font faceSee also: FONT, LOADFONT, COLOR, PRINT, LOCATE
SHADER
Graphics · since 1.3.2
Defines a custom fragment shader using a simplified DSL that compiles to WGSL for the wgpu renderer. The shader executes for every pixel of the sprite or layer it's applied to. All values in shader expressions use 0.0-1.0 range for colors (not 0-255). Shaders are globally scoped and can be applied to sprites with SET SPRITE SHADER or to layers with SET LAYER SHADER.
Syntax
SHADER "name"
[USE NUMBER paramName [DEFAULT value]]
[USE COLOR paramName [DEFAULT RGBA(r, g, b, a)]]
[USE IMAGE paramName]
WHEN DRAWING
[variable = expression]
SET COLOR = expression
END WHEN
END SHADERParameters
- name - String name for the shader (used with SET SPRITE SHADER or SET LAYER SHADER)
- USE NUMBER paramName [DEFAULT value] - Declares a numeric parameter (f32). Default is 0.0 if not specified.
- USE COLOR paramName [DEFAULT RGBA(r,g,b,a)] - Declares a color parameter (vec4). Default is RGBA(0,0,0,1) if not specified.
- USE IMAGE paramName - Declares an additional texture input. When applying the shader, pass the image name as a string: @paramName="imageName". The image must be loaded with LOADIMG first. Use SAMPLE(paramName, uv) in the shader body to read from it.
- WHEN DRAWING ... END WHEN - The shader body. Executes once per pixel.
- variable = expression - Local variable assignment.
- SET COLOR = expression - Sets the final output color. Must be a vec4.
Examples
' Greyscale with intensity control
SHADER "Greyscale"
USE NUMBER intensity DEFAULT 1.0
WHEN DRAWING
c = IMAGE
grey = (c.r + c.g + c.b) / 3.0
result = MIX(c, RGBA(grey, grey, grey, c.a), intensity)
SET COLOR = result
END WHEN
END SHADER' Rainbow cycle based on time
SHADER "Rainbow"
USE NUMBER speed DEFAULT 1.0
WHEN DRAWING
c = IMAGE
hue = FRACT(TIME * speed + UV.x)
r = ABS(hue * 6.0 - 3.0) - 1.0
g = 2.0 - ABS(hue * 6.0 - 2.0)
b = 2.0 - ABS(hue * 6.0 - 4.0)
rainbow = CLAMP(RGB(r, g, b), 0.0, 1.0)
SET COLOR = RGBA(rainbow.r * c.r, rainbow.g * c.g, rainbow.b * c.b, c.a)
END WHEN
END SHADER' Damage flash (white pulse)
SHADER "Flash"
WHEN DRAWING
c = IMAGE
flash = ABS(SIN(TICK * 0.5))
SET COLOR = MIX(c, RGBA(1.0, 1.0, 1.0, c.a), flash)
END WHEN
END SHADER' Vignette (darken edges)
SHADER "Vignette"
USE NUMBER strength DEFAULT 0.5
WHEN DRAWING
c = IMAGE
center = VEC2(0.5, 0.5)
dist = LENGTH(UV - center)
vignette = 1.0 - SMOOTHSTEP(0.3, 0.7, dist) * strength
SET COLOR = RGBA(c.r * vignette, c.g * vignette, c.b * vignette, c.a)
END WHEN
END SHADER' Pixelation effect
SHADER "Pixelate"
USE NUMBER pixels DEFAULT 32.0
WHEN DRAWING
pixelUV = FLOOR(UV * pixels) / pixels
c = OWNTEXTURE(pixelUV)
SET COLOR = c
END WHEN
END SHADER' Color tint overlay
SHADER "Tint"
USE NUMBER tintR DEFAULT 1.0
USE NUMBER tintG DEFAULT 0.8
USE NUMBER tintB DEFAULT 0.6
USE NUMBER amount DEFAULT 0.3
WHEN DRAWING
c = IMAGE
tint = RGB(tintR, tintG, tintB)
SET COLOR = MIX(c, RGBA(tint.r, tint.g, tint.b, c.a), amount)
END WHEN
END SHADER' Dissolve transition (use with BSHADER for animation)
SHADER "Dissolve"
USE NUMBER progress DEFAULT 0.0
USE IMAGE noise
WHEN DRAWING
c = IMAGE
n = SAMPLE(noise, UV)
alpha = STEP(progress, n.r) * c.a
SET COLOR = RGBA(c.r, c.g, c.b, alpha)
END WHEN
END SHADERNotes
- Built-in variables
- IMAGE (vec4) - Sprite texture color at current UV. Components: .r, .g, .b, .a (0.0-1.0).
- UV (vec2) - Texture coordinates. .x/.y range 0.0 (top-left) to 1.0 (bottom-right).
- TIME (f32) - Elapsed seconds since program started.
- TICK (f32) - Frames since shader was assigned to sprite.
- SCREEN (vec2) - Fragment position in screen pixels (like gl_FragCoord.xy). Useful for screen-space effects.
- SCREEN_SIZE (vec2) - Screen dimensions in pixels (width, height). Use SCREEN / SCREEN_SIZE for normalized screen coords.
- Color functions
- RGBA(r, g, b, a) -> vec4 - Create color (0.0-1.0 range).
- RGB(r, g, b) -> vec4 - Create opaque color (alpha=1.0).
- Vector functions
- VEC2(x, y), VEC3(x, y, z), VEC4(x, y, z, w) - Create vectors.
- LENGTH(v) -> f32 - Vector magnitude.
- NORMALIZE(v) -> vec - Unit vector.
- DOT(a, b) -> f32 - Dot product.
- Math functions
- SIN(x), COS(x) -> f32 - Trig (radians).
- ABS(x), FLOOR(x), FRACT(x), POW(x,y), SQRT(x) -> f32/vec
- MIN(a,b), MAX(a,b), CLAMP(x,min,max) -> f32/vec
- RND(x, y) or RND(seed) -> f32 - Pseudo-random 0-1 from seed values. Deterministic (same input = same output).
- Interpolation
- MIX(a, b, t) -> f32/vec - Linear interpolation (t=0 returns a, t=1 returns b).
- STEP(edge, x) -> f32/vec - 0.0 if x < edge, else 1.0.
- SMOOTHSTEP(edge0, edge1, x) -> f32/vec - Smooth interpolation.
- Texture sampling
- SAMPLE(textureName, uv) -> vec4 - Sample USE IMAGE texture.
- OWNTEXTURE(uv) -> vec4 - Sample sprite's texture at arbitrary UV.
- PX(x, y) -> vec2 - Pixel offset to UV offset.
- OPERATORS: +, -, *, / (scalar and component-wise)
- SWIZZLING: .r/.g/.b/.a or .x/.y/.z/.w - Access components. .rgb, .xy, .bgr etc.
- All color values 0.0-1.0 (not 0-255). Up to 16 vec4 params (64 floats). Up to 4 USE IMAGE textures. Comments (') allowed. Case-insensitive variable names.
- Limitations
- Variable reassignment is NOT supported in shaders. Each variable name can only be assigned once because assignments compile to WGSL 'var' declarations. Use unique names for each step: instead of 'x = 1' then 'x = x + 1', use 'x = 1' then 'x2 = x + 1'.
See also: SET LAYER, BEHAVIOR, SET SPRITE
TEXTTOIMAGE
Graphics · since 1.0.3
Renders text (including emoji) to a new image that can be used as a sprite. The image is automatically sized to fit the text with some padding. Useful for creating emoji-based sprites, dynamic text labels, or any text that needs to be treated as a sprite for transformations like rotation or scaling.
Server-side dimension placeholder
In CrashNet multiplayer, TEXTTOIMAGE runs on every interpreter so the resulting image's dimensions register on each one's BehaviorEngine — the host's collision system needs them just like it needs LOADIMG / NEWIMAGE dims. Actual text rendering only happens on the user's CrashPlayer client (the host's headless gfx client drops the draw command; the player-server's render call is intercepted by the client's WS receiver, which acks without rendering since the client's LOCAL interpreter renders the same TEXTTOIMAGE line separately).
Because the server side has no font renderer, the dimensions it registers are a best-effort placeholder derived from the call arguments:
width = char_count × font_sizeheight = font_size
For monospace fonts and single-character emoji (the common cases), this lines up closely with what the local renderer actually produces. For proportional fonts, kerning, or text with significant character-width variation, the placeholder will be off — the visible rendered glyph differs from the bbox the server uses for SOLID sprite collision and for builtins like SPRITEWIDTH / SPRITEHEIGHT.
For collision-bearing text sprites where the placeholder hitbox is off, tighten or shift the collision region with SET SPRITE n BBOX(x1, y1, x2, y2) (relative coordinates, 0-1, where (0,0,1,1) is the full bounds) after the SET SPRITE call, so server-authoritative collision uses a hitbox you control rather than the placeholder bounds. Sprite-based PNG / LOADIMG textures don't have this issue — those dims are read directly from the source file on every interpreter.
Syntax
TEXTTOIMAGE text$, imageName$ [, fontSize] [, fontName$]Parameters
- text$ - The text to render (can include emoji characters like 🎮 👾 🚀)
- imageName$ - Name to identify the created image
- fontSize - Optional font size in pixels (default: 32)
- fontName$ - Optional font family name. When omitted, the current font is used (default: "IBM EGA 8x8").
Examples
REM Create emoji sprite
TEXTTOIMAGE "🎮", "gamepad"
PUTIMAGE (100, 100), "gamepad"REM Create with custom font size
TEXTTOIMAGE "💯", "score", 64
PUTIMAGE (160, 100), "score"REM Create with custom font
TEXTTOIMAGE "GAME OVER", "gameover", 48, "Arial"
PUTIMAGE (80, 90), "gameover"REM Use as hardware sprite
TEXTTOIMAGE "👾", "alien", 32
ENABLE SPRITE 0
SET SPRITE 0, 160, 100, "alien"REM Animate emoji
TEXTTOIMAGE "⭐", "star", 48
FOR angle = 0 TO 360 STEP 5
BEGIN FRAME
CLS
PUTIMAGE (160, 100), "star", ROTATE(angle, 0.5, 0.5)
END FRAME
DELAY 16
NEXTREM Multiple emoji characters
TEXTTOIMAGE "🚀🌟✨", "effects", 32
PUTIMAGE (100, 50), "effects"REM Family emoji (multi-character)
TEXTTOIMAGE "👨👩👧👦", "family", 64
PUTIMAGE (100, 100), "family"See also: PUTIMAGE, NEWIMAGE, SET SPRITE, LOADIMG
TILEMAP
Graphics · since 1.27.0
Defines a named tilemap whose tile + metadata data live in your globally-scoped BASIC arrays. The TILEMAP block itself is bookkeeping — it tells the renderer which arrays compose this map. The arrays remain the single source of truth: pathfinding, AI, line-of-sight, anything that needs to read or write tile data goes straight at the array, and UPDATE MAP pushes the result to the GPU.
Bindings
TILES idx (FROM | INTO) ident— declare a tile layer at z-order indexidx(0 = bottom).FROMbinds an existing globally-scopedINTEGER2D array. The block doesn't allocate; it just registers the binding.INTOauto-DIMs a new global of the right size and populates it from BASEMAP's tile layer at the same index.- Either way you end up with a user-mutable global after exec.
AS "name"gives the layer a readable handle forCREATE MAP "x" LAYER "name". When the block has 2+TILESclauses,AS "name"is required on each.
META "name" (FROM | INTO) ident— same shape for metadata layers.STRING2D arrays. No z-order (metadata isn't rendered, it's queryable viaMAPMETA$andFOR MAPDATA).BASEMAP "file.map.json"— load an editor-drawn.map.jsonas the starting state. Block declarations override BASEMAP layers by index (TILES) or name (META). BASEMAP-supplied layers your block doesn't redeclare or expose viaINTOstay as opaque cached data — still rendered, just not addressable as user arrays.TILESET / TILESIZE / SIZE— block-level versions override whatever BASEMAP supplied. With no BASEMAP, all three are required.
Global scope (always, regardless of where the block lives)
Every binding produced by a TILEMAP block goes to global scope, regardless of where the block executes. INTO arrays auto-DIM as globals; FROM bindings require their referenced array to already be global (declared outside any SUB). This means TILEMAP "current" inside a SUB called from anywhere produces the same global namespace artifacts — the level-loader pattern works cleanly without you thinking about scope.
Re-execution replaces
Running TILEMAP "x" a second time replaces the registry entry, regenerates INTO globals from the new BASEMAP, and lets the next CREATE MAP / UPDATE MAP push fresh data. This is the explicit reload path — CALL LoadLevel(2) after CALL LoadLevel(1) overwrites cleanly.
Slicing into higher-D arrays
FROM accepts any expression that resolves to a 2D array — so you can use the general partial-indexing primitive to feed a slice of a higher-D global into the tilemap. A 3D rooms(R, X, Y) with TILES 0 FROM rooms(level) binds the 2D view at the chosen room.
DIM rooms(0 TO 9, 0 TO 99, 0 TO 59) AS INTEGER
FOR i = 0 TO 9
WITH ROOM "level" + STR$(i)
TILEMAP "lvl" + STR$(i)
TILESET "grass.png"
TILESIZE 32, 32
SIZE 100, 60
TILES 0 FROM rooms(i)
END TILEMAP
CREATE MAP "lvl" + STR$(i) AS LAYER 1
END WITH
NEXT iOne global array, ten levels, no bazillion-tile-arrays bookkeeping. The slice is a live view — rooms(3, 5, 5) = WALL_TILE followed by UPDATE MAP "lvl3" does the right thing because the view shares storage with rooms. See the ARRAY type docs for the full story on partial indexing.
What works as a FROM expression:
- Bare identifier —
FROM tiles(the legacy form, re-resolved by name each tick). - Partial index of a higher-D array —
FROM rooms(level). - Member access —
FROM player.tiles(whenplayeris an Object). - Anything else that evaluates to an array.
Limits + caveats:
- Expression-form
FROMis evaluated once at TILEMAP DEF registration time, then the resulting array reference is snapshotted. Mutations to the underlying storage stay live; reassigning the root variable does not update the binding — re-execute the TILEMAP DEF when you switch arrays. - Out-of-range partial indices fail loudly at registration.
INTOaccepts the same two forms asFROM: bare identifier auto-DIMs a fresh global; a slice expression likeINTO Rooms(0)writes BASEMAP data INTO the named slice of an existing higher-D array. The slice must already exist and match the TILEMAPSIZE(height × width). Writes go through the partial-index view so they land on the parent's storage;UPDATE MAPlater reads from the same view.
Validation
These run at parse time + exec time. Violations halt with a runtime error pointing at the offending clause — we'd rather you see a clear failure than silently get a black-screen tilemap.
- Tile-layer indexes must be unique, contiguous from 0, and non-negative.
- With no BASEMAP, the lowest declared
TILESindex must be 0.TILES 1 ...without aTILES 0and no BASEMAP is rejected — there's no source for layer 0, soCREATE MAPwould silently render the empty default. Renumber, add aTILES 0, or add a BASEMAP that supplies the lower layers. - Tile-layer names must be unique within the block (and required when 2+
TILESclauses are present). - Metadata-layer names must be unique within the block.
INTOrequiresBASEMAPin the same block.FROMarrays must exist in global scope and match the declared (or BASEMAP-supplied)SIZE.- With no BASEMAP,
TILESET/TILESIZE/SIZEmust all be supplied.
Syntax
TILEMAP "name"
[BASEMAP "file.map.json"]
[TILESET "image"]
[TILESIZE w [, h]]
[SIZE w, h]
[TILES idx [AS "layer_name"] (FROM expression | INTO expression)]*
[META "name" (FROM expression | INTO expression)]*
END TILEMAPParameters
- name - String literal naming this tilemap. Used by CREATE MAP "name" / UPDATE MAP "name" to look it up.
- BASEMAP "file" - Optional starting state from a Map Editor .map.json. Block declarations layer on top.
- TILESET "image" - Tileset image (loaded with LOADIMG or carried over from BASEMAP).
- TILESIZE w[, h] - Per-tile pixel size. Single value means square tiles.
- SIZE w, h - Map size in tiles. Inherited from BASEMAP when absent.
- TILES idx [AS "name"] FROM expression - Bind any expression that resolves to a 2D INTEGER array as tile layer
idx. Bare identifier (re-resolved each tick) or a partial-index expression likerooms(level)(snapshotted once, view-aliased) both work. - TILES idx [AS "name"] INTO expression - Two forms:
INTO identauto-DIMs a new global of the right shape and populates it from BASEMAP's tile layeridx.INTO Rooms(0)(slice expression) writes BASEMAP data into an existing higher-D array's slice via the partial-indexing primitive — the slice must already exist in scope and match the TILEMAP SIZE. - META "name" FROM expression - Same expression-form FROM semantics for STRING 2D metadata arrays.
- META "name" INTO expression - Same expression-form INTO semantics for STRING 2D metadata arrays. Bare ident = auto-DIM; slice expression = write-into-existing-slice.
Examples
REM Procedural single-layer tilemap
DIM tiles(0 TO 99, 0 TO 59) AS INTEGER
TILEMAP "level1"
TILESET "grass.png"
TILESIZE 32, 32
SIZE 100, 60
TILES 0 FROM tiles
END TILEMAP
CREATE MAP "level1" AS LAYER 0REM Multi-layer with mixed FROM/INTO + metadata
TILEMAP "shop"
BASEMAP "shop-base.map.json"
TILES 0 AS "floor" INTO floorTiles ' loaded from BASEMAP, exposed as a global
TILES 1 AS "walls" INTO wallTiles
TILES 2 AS "decor" FROM proceduralDecor ' user-supplied procedural top layer
META "shopkeepers" INTO npcGrid$ ' base meta exposed as a global
END TILEMAP
CREATE MAP "shop" LAYER "floor" AS LAYER 0
CREATE MAP "shop" LAYER "walls" AS LAYER 1
CREATE MAP "shop" LAYER "decor" AS LAYER 2REM Level-loader pattern — TILEMAP DEF inside a SUB still produces globals
SUB LoadLevel(n)
TILEMAP "current"
BASEMAP "level" + STR$(n) + ".map.json"
TILES 0 AS "bg" INTO levelBg
META "doors" INTO doorGrid$
END TILEMAP
CREATE MAP "current" AS LAYER 0
END SUB
CALL LoadLevel(1)
CALL LoadLevel(2) ' replaces "current" + the levelBg + doorGrid$ globalsREM Room-aware level switching (1.29.0)
IF ME.ISHOST THEN
FOR i = 1 TO 4
WITH ROOM "level" + STR$(i)
TILEMAP "lvl" + STR$(i)
TILESET "grass.png"
TILESIZE 32, 32
SIZE 100, 60
TILES 0 FROM tiles
END TILEMAP
CREATE MAP "lvl" + STR$(i) AS LAYER 1
END WITH
NEXT i
END IFSee also: CREATE MAP, UPDATE MAP, MAPMETA$, FOR MAPDATA, WITH
TOSPRITESHEET
Graphics · since 1.0.3
Combines multiple existing images into a single sprite sheet. All source images must have identical dimensions. The resulting sprite sheet arranges frames horizontally in a single row. Useful for creating animation sequences from individual frame images or combining captured regions.
The output sheet's per-frame dimensions are derived from the first source image — that source must already be loaded (via LOADIMG / NEWIMAGE / GETIMAGE) when TOSPRITESHEET runs, otherwise the call halts with a runtime error pointing at the missing handle. Frame dimensions are registered on every interpreter (host, player-server, client) so server-authoritative collision sees the right hitbox for any sprite that uses the resulting sheet.
Syntax
TOSPRITESHEET outputName$, sourceImage1$, sourceImage2$ [, ...]Parameters
- outputName$ - Name for the new combined sprite sheet
- sourceImage1$, sourceImage2$, ... - Names of images to combine (minimum 2)
- All source images must have the same width and height
- Frames are arranged left-to-right in a single row
- Output width = sourceWidth * numberOfFrames
- Output height = sourceHeight
Examples
REM Combine captured frames into animation
GETIMAGE (0, 0)-(31, 31), "frame0"
GETIMAGE (32, 0)-(63, 31), "frame1"
GETIMAGE (64, 0)-(95, 31), "frame2"
TOSPRITESHEET "walk", "frame0", "frame1", "frame2"
PUTIMAGE (100, 100), "walk", currentFrameREM Create procedural animation frames
NEWIMAGE 32, 32, 0, "f0"
SET IMAGE "f0"
CIRCLE (16, 16), 8, 15
SET LAYER 0
NEWIMAGE 32, 32, 0, "f1"
SET IMAGE "f1"
CIRCLE (16, 16), 12, 15
SET LAYER 0
TOSPRITESHEET "pulse", "f0", "f1"REM Combine multiple enemy states
LOADIMG "enemy_idle.png", "idle"
LOADIMG "enemy_attack.png", "attack"
LOADIMG "enemy_hurt.png", "hurt"
TOSPRITESHEET "enemy_states", "idle", "attack", "hurt"See also: NEWIMAGE, GETIMAGE, LOADIMG, PUTIMAGE, FRAMECOUNT
TRANSITION
Graphics · since 1.4.3
Returns the name of the currently active screen transition effect on a layer, or an empty string if no transition is running. Use this to wait for CLS EFFECT transitions to complete. Each layer can have its own independent transition.
Syntax
TRANSITION([layerId])Parameters
- layerId - Optional layer number to check (default: current layer)
Examples
' Check current layer transition:
IF TRANSITION() <> "" THEN PRINT "Transition in progress"' Wait for fade to complete:
CLS EFFECT(FADE)
WHILE TRANSITION() <> ""
' Program continues running
WEND
PRINT "Fade complete!"' Check specific layer:
result$ = TRANSITION(2) ' Check layer 2' Multiple layer transitions:
SET LAYER 0
CLS EFFECT(MELT)
SET LAYER 1
CLS EFFECT(FADE)
' Both run simultaneously
TRANSITION$
Graphics · since 1.57.0
Returns the name of the transition effect currently running on a layer — "FADE", "MELT", "BLOCK", "DISSOLVE", "WIPE" — or an empty string if none is running.
Use it to wait for a CLS EFFECT(...) to finish before moving on. Both spellings exist and behave identically: TRANSITION$ follows the string-function convention, and TRANSITION is accepted for convenience. With no argument it reports the current layer.
Syntax
TRANSITION$([layer])
TRANSITION([layer])Parameters
- layer - Optional layer ID. Defaults to the current layer.
Examples
WITH LAYER 0
CLS EFFECT(FADE) SPEED 120
END WITH
WHILE TRANSITION()
YIELD
WEND
PRINT "faded out"IF TRANSITION$(1) = "" THEN PRINT "layer 1 is idle"
TYPING
Graphics · since 1.4.3
Returns 1 if a PRINT RATE typewriter effect is currently in progress, 0 if complete. Use this to wait for typewriter text to finish before continuing or to allow the user to skip the effect.
Syntax
TYPING()Examples
' Check if typing is in progress:
IF TYPING() = 1 THEN PRINT "Still typing..."' Wait for typing to complete:
PRINT RATE(50) "Loading game data..."
WHILE TYPING() = 1
' Can do other things here
WEND
PRINT ""
PRINT "Done!"' Allow user to skip:
PRINT WORDS RATE(30) "Long dialog text..."
WHILE TYPING() = 1
IF KEYDOWN(SPACE) THEN
' Could implement skip here
END IF
WEND
UPDATE MAP
Graphics · since 1.27.0
Mutate your tile array. Call UPDATE MAP. The GPU catches up. The mental model: your BASIC arrays are always the truth; the renderer's tilemap texture is a stale-until-told cache, and UPDATE MAP is the cache flush.
Heads up: `LAYER` here is TILE LAYER, not screen layer
The LAYER selector references tile layers declared INSIDE the TILEMAP block (TILES 0, TILES 1, ...), not the screen layers the tilemap got mounted onto via AS LAYER .... So given:
TILEMAP "level1"
TILES 0 AS "bg" FROM bgTiles ' tile layer 0
TILES 1 AS "fg" FROM fgTiles ' tile layer 1
END TILEMAP
CREATE MAP "level1" AS LAYER 1 TO 2 ' tile 0 → screen 1, tile 1 → screen 2…then UPDATE MAP "level1" LAYER 0 refreshes bg (tile layer 0), regardless of which screen layer it ended up on. LAYER "fg" refreshes by name. LAYER 0 TO 1 refreshes both. Mismatching this in your head is the #1 way to write an UPDATE MAP that does nothing visible.
Selector shapes
Mirrors CREATE MAP's named-form selector:
LAYER "name"— match the AS-name from theTILEMAPblock.LAYER N— match the tile layer at indexN(0-based).LAYER N TO M— match every mounted tile-layer index in that inclusive range.- No
LAYERclause — refresh every mounted tile layer.
If the selector matches zero mounted tile layers, UPDATE MAP halts loudly. We don't silently no-op on bad selectors — that's how black-screen bugs hide.
Performance: don't call this per frame
Each call triggers a tilemap GPU reload. It's not a draw call — it's a full texture re-bake of the affected layers. Batch your mutations and call UPDATE MAP once at the end of the batch. For per-frame mutations (water animation, scrolling effects), you almost certainly want a shader or a TILESET animation, not UPDATE MAP.
What `UPDATE MAP` works on
Tilemaps registered via TILEMAP DEF (or the named CREATE MAP form). For file-loaded maps, just CREATE MAP the file again — the file IS the source of truth in that path, not an array.
Room-aware broadcast
The refresh broadcast uses the same room filter as CREATE MAP. Inside a WITH ROOM "name" block the update reaches only players in that room; outside any WITH ROOM block it broadcasts unfiltered.
Known foot-gun: when CREATE MAP runs inside WITH ROOM "lobby" but UPDATE MAP runs outside any block, the update broadcasts unfiltered while the mount was lobby-scoped. Wrap the UPDATE MAP in the same WITH ROOM as the original CREATE MAP to keep them aligned.
Syntax
UPDATE MAP <name> [LAYER <tile_layer_selector>]Parameters
- name - String naming a registered TILEMAP (must have been CREATE MAP'd at least once).
- tile_layer_selector - Optional: filter the refresh to a subset of TILE layers (the layers declared inside the TILEMAP block — NOT screen layers). Three shapes: a string AS-name (LAYER "foreground"), a numeric tile-layer index (LAYER 1), or a numeric tile-layer range (LAYER 0 TO 2). Absent => refresh every mounted tile layer.
Examples
REM Mutate the array, then push to GPU
tiles(x, y) = newTileIdx
UPDATE MAP "level1"REM Refresh only the foreground after a region update
FOR i = 0 TO 9
decorTiles(spawnX + i, spawnY) = MUSHROOM_TILE
NEXT i
UPDATE MAP "shop" LAYER "decor"REM Numeric-index selector — handy when layers don't carry AS-names
UPDATE MAP "level1" LAYER 1REM Range selector — refresh tile layers 0..=2 in one statement
UPDATE MAP "level1" LAYER 0 TO 2REM Open every door in the level (pathfinding-friendly: same array)
FOR MAPDATA (0, 0)-(mapW, mapH) AS x, y, m$
IF m$ = "D" THEN tiles(x, y) = OPEN_DOOR_TILE
NEXT MAPDATA
UPDATE MAP "level1"REM Room-aware refresh — wrap UPDATE MAP in the same WITH ROOM as the CREATE MAP
WITH ROOM "lobby"
bg(0, 0) = 7
UPDATE MAP "lobbyMap" ' refresh broadcasts to lobby only
END WITHSee also: TILEMAP, CREATE MAP, MAPMETA$, FOR MAPDATA, WITH
WINDOW
Graphics · since 1.4.3
Defines a rectangular text window region in pixel coordinates. Text output will start at the window's top-left corner and word wrap at the right edge. The text cursor is reset to position (0, 0) within the window. The coordinate hierarchy is: WINDOW (base) → ORIGIN (offset) → LOCATE (cursor position).
Syntax
WINDOW (x1, y1) - (x2, y2)Parameters
- x1, y1 - Top-left corner of the text window in pixels
- x2, y2 - Bottom-right corner of the text window in pixels
Examples
WINDOW (50, 50) - (270, 150) ' Create centered text windowPRINT WORDS "Text wraps within the window boundaries"' Dialog box with typewriter effect:
WINDOW (20, 140) - (300, 190)
PRINT WORDS RATE(30) "Welcome traveler! The adventure begins now."' RPG-style text box:
LINE (18, 138)-(302, 192), 15, B ' Draw border
WINDOW (20, 140) - (300, 190)
PRINT WORDS RATE(25) "You found a treasure chest! Inside you discover 100 gold coins and a mysterious key."See also: PRINT, LOCATE, TYPING
Sprites
DISABLE SPRITE
Sprites · since 1.0.3
Disables a hardware sprite slot and destroys its associated GPU sprite. The sprite slot is reset to default state and can be reused.
Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.
Any PATTERN running on the sprite is removed with it. A pattern instance outliving its sprite keeps ticking against an id that no longer exists — and the slot is claimable again, so the next sprite to take that id would inherit a dead pattern mid-step.
Syntax
DISABLE SPRITE spriteNumParameters
- spriteNum - Sprite slot number (0-99,999)
Examples
DISABLE SPRITE 0 ' Disable and clean up sprite 0' Disable all enemy sprites:
FOR i = 0 TO 9
DISABLE SPRITE i
NEXT' Clean up sprites at game end:
FOR i = 0 TO maxSprites
IF SPRITEENABLED(i) THEN DISABLE SPRITE i
NEXTSee also: ENABLE SPRITE, SET SPRITE, SPRITEENABLED
EMITPARTICLE
Sprites · since 1.6.0
Creates a system-managed particle sprite that automatically expires after a specified number of ticks. Particles are allocated from their own band of sprite IDs (100000+, separate from user sprites) and leverage the existing pattern/behavior system. When a particle expires, it is automatically disabled and removed. Particles integrate with all existing sprite operations including collisions, tags, and patterns. If no particle slots are available, the command silently does nothing. Supports two syntaxes: coordinate form (using a coordinate literal or variable) and separate x, y form for convenience. Parameters can be passed to the pattern using @paramName=value syntax, allowing each particle to have unique behavior (e.g., different target positions, speeds, or directions). If LAYER is not specified, the particle is created on the current layer set by SET LAYER.
Polygon particles
Pass a MUTATION where the image name goes and the particle is a shape rather than a sprite: a copy of that mutation, centred on the spawn point, drawn as a polygon and expiring on the same tick count. It is an ordinary particle underneath — same ID band, same lifetime, same reaping, same tag — with the shape bound to its slot, so nothing about pooling or expiry behaves differently.
It takes the polygon appearance modifiers, from the same grammar rule as SET POLYGON: COLOR, FILL, TEXTURE, UV, SHADER, LAYER, ZORDER, SOLID [CIRCLE|BOX|POLY] [SPRITES(...)], BBOX.
DIM shard AS MUTATION
shard = MUTATION((0, -6), (5, 4), (-5, 4))
EMITPARTICLE (x, y), shard, "debris", "tumble", 90, @spin = 240 COLOR 5 FILL 6 SOLIDOne difference from the sprite form worth knowing: the polygon-only modifiers are a loud error on a particle emitted from an image name, rather than being quietly ignored — COLOR on a sprite particle is a mistake about which kind of particle you are emitting.
GHOST and TAG are rejected on either form. A particle is born rather than edited, so there is no collider to remove, and its tag is already the third argument.
Multiplayer: a polygon particle is a bound polygon, so it follows the same rule SET POLYGON does — client-only. Emitting one from server-side code in a CrashNet game is a fatal error; wrap it in IF ME.ISCLIENT THEN ... END IF. Sprite particles are unaffected.
What moves a particle
The fourth argument names the motion, and it may be either a BEHAVIOR or a PATTERN — for either kind of particle. The name says which it is. A name matching neither is a fatal error: a particle that was told to move and does not is a bug you would otherwise find by staring at a motionless particle.
BEHAVIOR "name" and PATTERN "name" are also accepted as modifiers, which is how you attach the kind the fourth argument did not name, or a second one alongside it.
Modifiers that shape a particle at birth
ALPHA(a), ROTATE(angle), SCALE(sx, sy), SCALETO(w, h), FLIPX, FLIPY and ORIGIN(ox, oy) apply to the particle as it is created, and MAXVELOCITY / MAXACCELERATION cap what its motion can build up to. This is what gives a burst per-particle variety without a behavior per variation:
EMITPARTICLE (x, y), rock, "debris", "tumble", 90 SCALE(0.4, 0.4) ROTATE(RND * 360) ALPHA(0.8)What a particle refuses, loudly, because it has no answer for them: LOCATION (position is the first argument), SHOW / HIDE (a particle is visible for its whole life — end it sooner with a shorter ticksToLive), RESET (nothing to reset at birth), PARENT (a particle outlives the statement that emitted it and is reaped by the engine), and SYNC / NOSYNC (particle replication is a room setting — PARTICLES in the CRASHNET block).
Syntax
EMITPARTICLE coord, "image", "tag", "pattern", ticksToLive [, @param=value, ...] [SOLID [SPRITES("tag", ...)]] [LAYER n]
EMITPARTICLE x, y, "image", "tag", "pattern", ticksToLive [, @param=value, ...] [SOLID [SPRITES("tag", ...)]] [LAYER n]
EMITPARTICLE coord, mutationVar, "tag", "behavior", ticksToLive [, @param=value, ...] [SET POLYGON modifiers]Parameters
- coord - Initial position as a coordinate literal (x, y) or coordinate variable (first syntax)
- x, y - Initial X and Y position in pixels (second syntax)
- "image" | mutationVar - Name of a loaded image for a sprite particle, or a MUTATION for a polygon particle
- "tag" - Tag string for collision detection and sprite queries
- "pattern" - Name of a defined PATTERN to apply (empty string "" for no pattern). For a polygon particle this names a BEHAVIOR instead.
- ticksToLive - Number of ticks before automatic expiration (60 ticks ≈ 1 second)
- @param=value - Optional parameters passed to the pattern or behavior, which flow down to its commands. Values are evaluated at emission time.
- SOLID - Optional modifier giving the particle a collider. On its own it resolves against boundaries; add SPRITES("tag", ...) to name the sprite tags it also treats as solid obstacles. On a polygon particle it also accepts CIRCLE (default), BOX or POLY.
- LAYER n - Optional layer number. If omitted, uses the current layer from SET LAYER.
- SET POLYGON modifiers - A polygon particle also accepts COLOR, FILL, TEXTURE, UV, SHADER, ZORDER and BBOX, with the same meanings they have on SET POLYGON.
Examples
' Simple particle with separate x, y
EMITPARTICLE 100, 100, "spark", "particle", "", 30' Simple particle with coordinate literal
EMITPARTICLE (100, 100), "spark", "particle", "", 30' Particle with fade-out pattern using coordinate variable
BEHAVIOR "fadeOut"
BALPHA 1 TO 0 DURATION 60
END BEHAVIOR
PATTERN "sparkle"
"fadeOut"
END PATTERN
DIM pos AS COORDINATE
pos = (playerX, playerY)
EMITPARTICLE pos, "spark", "effect", "sparkle", 60' Emit particles on mouse click (using x, y syntax)
DO
IF MOUSEBUTTON(0) THEN
EMITPARTICLE MOUSEX, MOUSEY, "star", "particle", "fadeOut", 45
END IF
YIELD
LOOP' Bullet casings with physics
BEHAVIOR "ejectCasing"
BVELOCITY -2, -4
BACCELERATION 0, 0.3
BROTATE 360 DURATION 30
END BEHAVIOR
PATTERN "casing"
"ejectCasing"
END PATTERN
EMITPARTICLE gunX, gunY, "casing", "debris", "casing", 90' Solid particle that collides with boundaries
EMITPARTICLE (160, 100), "debris", "solid_particle", "fallAndFade", 120 SOLID' Explosion effect spawning multiple particles
SUB Explosion(x, y)
FOR i = 1 TO 20
angle = RND * 360
EMITPARTICLE x, y, "spark", "explosion", "expand", 30 + RND * 30
NEXT i
END SUB' Parameterized particle with unique target per emission
BEHAVIOR "moveToTarget"
BMOVE @targetX=0, @targetY=0
END BEHAVIOR
PATTERN "homing"
"moveToTarget" DURATION 60
END PATTERN
' Each particle moves toward a different target
EMITPARTICLE (100, 100), "missile", "projectile", "homing", 90, @targetX=enemyX, @targetY=enemyY' Radial explosion with unique direction per particle
BEHAVIOR "radialMove"
BMOVE @dx=0, @dy=0
BALPHA 1 TO 0 DURATION 30
END BEHAVIOR
PATTERN "explode"
"radialMove"
END PATTERN
SUB Explode(x, y)
FOR i = 0 TO 7
angle = i * 45 * 3.14159 / 180
dx = COS(angle) * 3
dy = SIN(angle) * 3
EMITPARTICLE x, y, "spark", "debris", "explode", 30, @dx=dx, @dy=dy
NEXT i
END SUB' Emit particle on a specific layer (for foreground effects)
EMITPARTICLE (160, 100), "glow", "fx", "fadeOut", 60 LAYER 3' Emit solid particle on layer 2
EMITPARTICLE playerX, playerY, "debris", "collision", "fall", 90 SOLID LAYER 2See also: PATTERN, BEHAVIOR, SET SPRITE, ENABLE SPRITE, DISABLE SPRITE, FOR SPRITES
ENABLE SPRITE
Sprites · since 1.0.3
Enables a hardware sprite slot. Hardware sprites are GPU-native sprites that render between layers without being baked into layer textures. They are more efficient than PUTIMAGE for moving objects. Up to 100,000 sprite slots (0-99,999) are available.
Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.
Syntax
ENABLE SPRITE spriteNumParameters
- spriteNum - Sprite slot number (0-99,999)
Examples
ENABLE SPRITE 0 ' Enable first sprite slotENABLE SPRITE player ' Enable using variable' Enable multiple sprites for enemies:
FOR i = 0 TO 9
ENABLE SPRITE i
NEXTSee also: DISABLE SPRITE, SET SPRITE, MOVE SPRITE, PUTIMAGE
FOR COLLISIONS
Sprites · since 1.0.4
Iterates over all collision pairs. The two-variable form detects sprite-sprite collisions only. The three-variable form also detects sprite-boundary collisions: when a sprite collides with a boundary, spriteB is set to -1 and boundaryTag$ contains the boundary's tag name. Uses bounding box collision detection (honouring a sprite's BBOX and scale), and only SOLID sprites take part.
A pair is reported when either of two things is true:
- They overlap right now. Adjacency is not a collision — two sprites that came to rest edge-to-edge were never pushed apart by anything and do not appear here.
- The solid resolver corrected a collision between them on this tick. Resolution separates the pair, so a geometry-only test would miss an impact that provably happened. Such a collision is reportable for exactly the tick it occurred on and no longer.
A sprite pressing continuously into a wall is corrected every tick, so it produces a fresh collision every tick — that is not the same as one collision lingering.
To react to the moment of impact rather than poll for it, use a behavior's ONCOLLISION / AFTERCOLLISION handlers, which fire on collision edges and carry the contact details (including the pre-resolution impact velocity).
Syntax
FOR COLLISIONS a, b
statements...
NEXT COLLISION
FOR COLLISIONS spriteA, spriteB, boundaryTag$
statements...
NEXT COLLISIONParameters
- a/spriteA - Variable to receive first sprite index in collision pair
- b/spriteB - Variable to receive second sprite index, or -1 for boundary collisions (three-variable form)
- boundaryTag$ - (Optional, three-variable form) String variable to receive the tag of the boundary hit (empty for sprite-sprite collisions)
Examples
' Basic collision loop (sprite-sprite only)
FOR COLLISIONS a, b
PRINT "Sprites"; a; "and"; b; "collided"
NEXT COLLISION' Handle player-enemy collisions
FOR COLLISIONS a, b
IF SPRITETAG$(a) = "player" AND SPRITETAG$(b) = "enemy" THEN
playerHealth = playerHealth - 10
DISABLE SPRITE b
ELSEIF SPRITETAG$(b) = "player" AND SPRITETAG$(a) = "enemy" THEN
playerHealth = playerHealth - 10
DISABLE SPRITE a
END IF
NEXT COLLISION' Bullet collision detection
FOR COLLISIONS i, j
IF SPRITETAG$(i) = "bullet" THEN
IF SPRITETAG$(j) = "enemy" THEN
score = score + 100
DISABLE SPRITE i
DISABLE SPRITE j
END IF
END IF
NEXT COLLISION' Check all collisions including boundaries
FOR COLLISIONS a, b, tag$
IF b = -1 THEN
PRINT "Sprite"; a; "hit boundary:"; tag$
ELSE
PRINT "Sprite"; a; "hit sprite"; b
END IF
NEXT COLLISION' Handle player hitting walls
FOR COLLISIONS player, other, boundary$
IF other = -1 THEN
SELECT CASE boundary$
CASE "floor"
playerOnGround = 1
CASE "spikes"
playerHealth = playerHealth - 1
END SELECT
END IF
NEXT COLLISIONSee also: ONCOLLISION, AFTERCOLLISION, SPRITETAG$, SET SPRITE (TAG), ENABLE SPRITE, DISABLE SPRITE, FOR SPRITES, BOUNDARY ADD
FOR SPRITES
Sprites · since 1.0.3
Iterates over all enabled sprites that have the specified tag. Each iteration sets the loop variable to the sprite index of a matching sprite. This provides an efficient way to process groups of related sprites without manually tracking sprite indices.
Syntax
FOR SPRITES spriteVar, tagExpr$
statements...
NEXT SPRITEParameters
- spriteVar - Variable to receive sprite index on each iteration
- tagExpr$ - String expression for the tag to filter by (e.g., "enemy", "bullet")
Examples
' Move all enemies down
FOR SPRITES s, "enemy"
MOVE SPRITE s, SPRITEX(s), SPRITEY(s) + 1
NEXT SPRITE' Disable all bullets that went off screen
FOR SPRITES b, "bullet"
IF SPRITEY(b) < 0 THEN
DISABLE SPRITE b
END IF
NEXT SPRITE' Count active enemies
enemyCount = 0
FOR SPRITES e, "enemy"
enemyCount = enemyCount + 1
NEXT SPRITE
PRINT "Enemies remaining:"; enemyCount' Apply damage to all enemies in blast radius
FOR SPRITES e, "enemy"
dx = SPRITEX(e) - blastX
dy = SPRITEY(e) - blastY
dist = SQR(dx * dx + dy * dy)
IF dist < blastRadius THEN
DISABLE SPRITE e
score = score + 100
END IF
NEXT SPRITESee also: SET SPRITE (TAG), SPRITETAG$, FOR COLLISIONS, ENABLE SPRITE, DISABLE SPRITE, NEXTFREESPRITE
FRAMECOUNT
Sprites · since 0.23.0
Returns the number of frames in a sprite sheet loaded with LOADIMG AS SPRITESHEET.
Syntax
FRAMECOUNT(image_name)Parameters
- image_name - Name of sprite sheet image resource
Examples
LOADIMG "player.png", "player" AS SPRITESHEET(32, 32)
frames = FRAMECOUNT("player")
PRINT "Player has"; frames; "animation frames"REM Use CALL TIMER for sprite animation instead of FOR loops
DECLARE SUB AnimateSprite()
CALL TIMER AnimateSprite() EVERY 100 MILLISECONDS AS "spriteAnim"
SUB AnimateSprite()
frame = (frame + 1) MOD FRAMECOUNT("sprites")
PUTIMAGE (x, y), "sprites", frame
END SUB
GETSPRITEMETA
Sprites · since 1.0.3
Retrieves metadata previously stored on a sprite using SET SPRITE META. Supports both single-value and key-value retrieval. Returns the stored value (number, string, or object), or 0 if no metadata has been set for that key. Crash BASIC has no null: a missing key and a stored 0 are indistinguishable, so if 0 is a meaningful value for your metadata, store a sentinel (-1) or a separate "has" key rather than testing the value itself.
Syntax
GETSPRITEMETA(spriteNum)
GETSPRITEMETA(spriteNum, "key")Parameters
- spriteNum - Sprite slot number (0-99,999)
- key - Optional string key to retrieve specific value (e.g., "health", "lives")
Examples
' Get key-value metadata
health = GETSPRITEMETA(0, "health")
lives = GETSPRITEMETA(0, "lives")
state$ = GETSPRITEMETA(0, "state")
PRINT "Health:"; health; " Lives:"; lives' Damage calculation with key-value metadata
FOR SPRITES e, "enemy"
health = GETSPRITEMETA(e, "health")
health = health - playerDamage
SET SPRITE e META "health", health
IF health <= 0 THEN
DISABLE SPRITE e
score = score + 100
END IF
NEXT SPRITE' Check sprite type
FOR SPRITES s, "powerup"
type$ = GETSPRITEMETA(s, "type")
IF type$ = "powerup" THEN
CALL CollectPowerup(s)
END IF
NEXT SPRITE' Retrieve object metadata (single-value mode)
DIM enemyStats AS EnemyData
enemyStats = GETSPRITEMETA(0)
IF enemyStats.health <= 0 THEN
DISABLE SPRITE 0
END IF' Check for metadata before using
DIM meta AS VARIANT
meta = GETSPRITEMETA(spriteNum, "health")
IF meta <> 0 THEN
PRINT "Health:"; meta
END IFSee also: SET SPRITE (META), SPRITETAG$, FOR SPRITES
HIDE SPRITE
Sprites · since 1.0.3
Makes a hardware sprite invisible without disabling it. The sprite retains its configuration and can be shown again with SHOW SPRITE. Useful for temporary visibility toggling like invincibility flashing or pooled sprites.
Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.
Syntax
HIDE SPRITE spriteNumParameters
- spriteNum - Sprite slot number (0-99,999)
Examples
HIDE SPRITE 0 ' Make sprite invisible' Invincibility flashing effect:
FOR i = 0 TO 20
IF i MOD 2 = 0 THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0
DELAY 100
NEXT
SHOW SPRITE 0' Hide sprite when off-screen:
IF spriteX < 0 OR spriteX > 320 THEN HIDE SPRITE 0See also: SHOW SPRITE, DISABLE SPRITE, SPRITEVISIBLE
IMAGEBBOX
Sprites · since 1.45.0
Returns a named Aseprite slice's bounding box on a given frame of a loaded .ase image, as a Range (x1,y1)-(x2,y2) in RELATIVE 0-1 coordinates. There's no sprite context, so no world position — combine the relative box with a position yourself, feed it straight into SET SPRITE id BBOX <range>, or use SPRITEBBOX when you want world coordinates for a placed sprite.
The slice and its per-frame keys come from the Aseprite file (slice keys are expanded per frame). Naming a slice the image doesn't have — or a frame no key covers — is a loud runtime error listing the available slice names. The image name is case-insensitive.
Syntax
IMAGEBBOX(image$, sliceName$, frame)Parameters
- image$ - Name an .ase image was loaded under with LOADIMG (case-insensitive)
- sliceName$ - Aseprite slice name (case-insensitive)
- frame - Absolute frame index (0-based)
Example
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 64)
' Frame 3's "sword" slice, relative 0-1
b = IMAGEBBOX("HERO", "sword", 3)
SET SPRITE 0 BBOX bSee also: SPRITEBBOX, BEHAVIOR, SET SPRITE, LOADIMG
MOVE SPRITE
Sprites · since 1.0.3
Quickly updates only the position of a hardware sprite without recreating it. More efficient than SET SPRITE when only position changes. Use this for smooth sprite movement in game loops.
Syntax
MOVE SPRITE spriteNum, x, y
MOVE SPRITE spriteNum, coord (coord = (x,y) literal or a coordinate variable/constant)Parameters
- spriteNum - Sprite slot number (0-99,999)
- x, y - New position coordinates
Examples
MOVE SPRITE 0, newX, newY ' Update position' Game loop with sprite movement:
WHILE running
IF KEYDOWN("LEFT") THEN playerX = playerX - 2
IF KEYDOWN("RIGHT") THEN playerX = playerX + 2
MOVE SPRITE 0, playerX, playerY
DELAY 16
WEND' Move multiple sprites:
FOR i = 0 TO numEnemies - 1
enemyX(i) = enemyX(i) + enemyDX(i)
MOVE SPRITE i + 10, enemyX(i), enemyY(i)
NEXTSee also: SET SPRITE, ENABLE SPRITE, SPRITEX, SPRITEY
NEXTFREESPRITE
Sprites · since 1.0.3
Returns the index of the next available (not enabled) sprite slot. Use this function to get a free sprite from the pool without manually tracking which sprites are in use. When a sprite is disabled, its slot becomes available again and can be returned by this function.
Not race-safe: on the server, two player threads can both get the same ID before either enables it. For multiplayer, use RESERVE SPRITE instead — it atomically reserves the slot so concurrent callers can never collide.
Syntax
NEXTFREESPRITE()Examples
' Get a free sprite for a new bullet
bulletSprite = NEXTFREESPRITE()
IF bulletSprite >= 0 THEN
ENABLE SPRITE bulletSprite
SET SPRITE bulletSprite, playerX, playerY, "bullet" TAG "bullet"
END IF' Spawn enemies using free slots
FOR i = 1 TO 5
s = NEXTFREESPRITE()
IF s >= 0 THEN
ENABLE SPRITE s
SET SPRITE s, RND * 600, 0, "enemy" TAG "enemy"
END IF
NEXT' Check if sprite pool is exhausted
IF NEXTFREESPRITE() = -1 THEN
PRINT "No free sprites available!"
END IFSee also: ENABLE SPRITE, DISABLE SPRITE, RESERVE SPRITE, SPRITEENABLED, FOR SPRITES, SPRITECOUNT
RESERVE SPRITE
Sprites · since 1.21.0
Atomically reserve the next free sprite slot and write the reserved ID (or -1 if the pool is exhausted) into the named variable. Unlike NEXTFREESPRITE, the slot is immediately marked in use so concurrent callers on the CrashNet server can never race onto the same ID. The caller is expected to follow up with an ENABLE SPRITE / LOADIMG / SET SPRITE call to populate the slot; otherwise it stays reserved until explicitly disabled.
In CrashNet, the reserved sprite is tagged with the calling player's slot as its creator — on disconnect, every sprite that player created is automatically swept back into the free pool.
Syntax
RESERVE SPRITE varParameters
- var - Integer variable that receives the reserved sprite ID (or -1)
Examples
' Race-safe version of NEXTFREESPRITE for multiplayer
RESERVE SPRITE newId
IF newId >= 0 THEN
SET SPRITE newId, 100, 100, "bullet" TAG "bullet" SYNC
END IF' Host-side enemy spawn — safe even with many concurrent player threads
SUB SpawnEnemy(x, y)
RESERVE SPRITE e
IF e >= 0 THEN
SET SPRITE e, x, y, "enemy" TAG "enemy" SYNC
END IF
END SUBSee also: NEXTFREESPRITE, ENABLE SPRITE, DISABLE SPRITE, SET SPRITE, CRASHNET
SET SPRITE
Sprites · since 1.0.3
Configures a hardware sprite with position, image, and optional modifiers. Supports two forms: (1) Full form with position and texture for initial setup, and (2) Modifier-only form for updating existing sprite properties. Modifiers can be combined freely and include visual transforms (ALPHA, SCALE, ROTATE, FLIP), positioning (LOCATION, PARENT, ORIGIN, LAYER, ZORDER), collision (SOLID, BBOX), physics caps (MAXVELOCITY, MAXACCELERATION), behaviors and patterns (BEHAVIOR, PATTERN), rigging constraints (IK, PIN, LIMIT), tags and metadata (TAG, META), and shaders (SHADER).
ORIGIN(ox, oy) sets the sprite's anchor in relative 0-1 coordinates — the single point the sprite is positioned at, parented at, and rotated around. Default is (0, 0) = top-left; (0.5, 0.5) = center; (1, 1) = bottom-right. It accepts an (ox, oy) pair or a Coordinate. The rotation pivot follows the origin by default, so the sprite rotates around its anchor; an explicit ROTATE(angle, px, py) pivot overrides only the rotation centre, leaving the positioning anchor intact. The collision box follows the origin too: the sprite's rect is placed at position - origin x size, the same place it is drawn, and BBOX fractions are measured from that drawn corner. So ORIGIN(.5, 1) anchors a sprite by the ground under its feet without shifting where it collides.
BEHAVIOR "name" [, @param=value ...] attaches a behavior. Re-issuing a behavior that is ALREADY active with the same parameters is deliberately a no-op, so a DO loop can re-assert it every frame without restarting BMOVE timing, BOSCILLATE phase or re-rolling VARIANCE. Re-issuing it with DIFFERENT parameters rebuilds the running instance with the new values — that is how a sprite reverses itself, e.g. SET SPRITE 0 BEHAVIOR "Move", @vx=-@vx from inside an AFTERCOLLISION handler.
Rigging constraints (IK / PIN / LIMIT) build articulated, constraint-driven sprites on top of the PARENT hierarchy and resolve per-tick (single-player and CrashNet alike). IK applies inverse kinematics to a chain ending at this sprite (the tip): SET SPRITE tip IK TARGET(x, y) LENGTH n rotates the tip plus its n PARENT ancestors so the tip reaches the target, using a FABRIK solver. Optional BEND CW|CCW picks which way interior joints bend, MIX m blends 0..1 between the FK pose and the solved pose, and ITERATIONS k caps solver passes. A bare TARGET on an already-configured tip just re-aims it (other settings persist); IK NONE removes the constraint. Declaring IK auto-sets RIGID on the chain links; give each chain member ORIGIN at its joint so it rotates around the correct point. PIN blends a sprite's resolved position toward a target each tick (SET SPRITE id PIN TARGET(x, y) [MIX m] / PIN NONE) — independent of the rotation chain. LIMIT clamps a child's local offset to a range or locks it to one axis for mechanical rigs (SET SPRITE id LIMIT [X min, max] [Y min, max] [AXIS X|Y] / LIMIT NONE).
Not for a bound polygon. A polygon created with SET POLYGON has a sprite half — it is where its visibility, alpha, layer, pose, behavior and collider live — but it sits in the polygon band of the sprite ID space, not at the raw id. SET SPRITE 3 addresses sprite 3 and does not reach polygon 3, silently. Every modifier here that means something for a shape is accepted by SET POLYGON itself; use that.
Modifiers are separated by spaces, not commas. The comma before the FIRST modifier is optional — SET SPRITE 0, x, y, "hero", 3, ALPHA(0.5) and ... 3 ALPHA(0.5) are both fine — but there is no comma between modifiers: ALPHA(0.5) SCALE(2, 2) ROTATE(45). Commas inside a modifier's own parentheses are its arguments and are unrelated.
Syntax
SET SPRITE n, x, y, imageName$ [, frame] [modifiers]
SET SPRITE n [modifiers]
(LOCATION, SCALE, MAXVELOCITY and MAXACCELERATION each accept a coordinate: e.g. LOCATION(x,y), LOCATION(coord))
ORIGIN(ox, oy) ' anchor in relative 0-1 coords (also accepts a Coordinate)
IK TARGET(x, y) LENGTH n [BEND CW|CCW] [MIX m] [ITERATIONS k] | IK NONE
PIN TARGET(x, y) [MIX m] | PIN NONE
LIMIT [X min, max] [Y min, max] [AXIS X|Y] | LIMIT NONEParameters
- n - Sprite slot number (0-99,999)
- x, y - Position coordinates (relative to parent if PARENT is set)
- imageName$ - Texture/image name (loaded with LOADIMG)
- frame - Optional sprite sheet frame number (0-based)
- Visual modifiers
- ALPHA(value) - Transparency (0.0-1.0)
- SCALE(sx, sy) - Scale factors (1.0 = original, 2.0 = double)
- SCALETO(w, h) - Scale to target pixel dimensions
- ROTATE(angle [, originX, originY]) - Rotation in degrees with optional origin (0-1, default 0.5,0.5)
- FLIPX - Horizontal flip
- FLIPY - Vertical flip
- FRAME n - Show spritesheet cell n (0-based) without re-specifying position or texture. This is the canonical way to flip a sprite's frame:
SET SPRITE 0 FRAME 2. Equivalent to the positional frame in the full formSET SPRITE n, x, y, image$, n, but usable on its own to animate an existing sprite. - RESET - Reset to defaults (alpha=1, scale=1,1, rotation=0, no flip), clears behaviors/patterns. Preserves location, frame, texture.
- Positioning modifiers
- LOCATION(x, y) - Update sprite position (modifier-only form). A 3-component coordinate LOCATION(x, y, z) also sets elevation (z = height above a plane layer); a 2D coordinate leaves elevation unchanged.
- ELEVATION(z) - Height above the plane layer the sprite sits on (AFFINE/PERSPECTIVE), in world units; 0 = on the floor. Lifts the sprite's drawn position toward the camera (foreshortened by distance) while keeping its footprint and size; no visible effect on a flat layer. Animate with the BELEVATE behavior; query with SPRITEELEVATION(n). Cleared to 0 by RESET.
- PARENT(spriteNum) [RIGID] - Parent sprite for relative positioning. Child moves with parent. By default the link is translation-only: the child follows the parent's position but keeps its own rotation (does NOT revolve around the parent when the parent rotates). Add RIGID to make it a rigid attachment — the child orbits the parent on the parent's rotation and inherits the parent's spin (a true bone/joint). Use PARENT NONE to detach. Scale is not inherited in either mode.
- PARENT POLYGON(polygonHandle) [RIGID] - Parent a BOUND POLYGON, named by its own 0-based handle rather than by a raw sprite id. A polygon's slot IS a sprite, so the link behaves exactly as PARENT(spriteNum) does — but the two number spaces are different and neither can be guessed from the value:
PARENT(0)is sprite 0,PARENT POLYGON(0)is polygon 0. This is the only way to name another shape's slot from ordinary code; inside a collision handlerindex_aalready IS the slot. RIGID and NONE work the same as for a sprite parent. - PARENT NONE - Clear parent, make sprite independent
- LAYER layerNum - Which layer the sprite renders above
- ZORDER n - Z-order within same layer (higher = on top)
- Collision modifiers
- SOLID - Makes sprite interact with solid boundaries (pushback on collision)
- SOLID SPRITES("tag1", "tag2", ...) - Boundaries + automatic pushback against sprites with matching tags. One-sided: this sprite stops, tagged sprites are immovable.
- SOLID SPRITES() - Clear sprite-to-sprite collision tags (keep boundary solid)
- GHOST - Clear all solid collision (boundary and sprite-to-sprite)
- BBOX(x1, y1, x2, y2) - Custom collision box using relative coordinates (0-1). Default (0,0,1,1) = full bounds. Affects all collision: boundary pushback, sprite-to-sprite pushback, SPRITECOLLISION(), and SPRITETOUCHING$().
- BBOX range - Same custom collision box supplied as a single relative-0–1 Range value instead of four numbers, e.g.
BBOX boxes(curFrame)using a Range fromLOADIMG ... SLICES. Equivalent to the four-argument form. - Physics cap modifiers
- MAXVELOCITY(x, y) - Caps velocity to ±x and ±y per tick. Use MAXVELOCITY NONE to remove.
- MAXACCELERATION(x, y) - Caps acceleration to ±x and ±y per tick. Use MAXACCELERATION NONE to remove.
- Behavior modifiers
- BEHAVIOR "name" [, @param=value, ...] - Add a behavior with optional parameter overrides
- BEHAVIOR DELETE "name" - Remove a specific behavior by name
- BEHAVIOR NONE - Clear all behaviors from sprite
- Pattern modifiers
- PATTERN "name" [, @param=value, ...] - Set a pattern (replaces any existing pattern)
- PATTERN DELETE - Remove active pattern
- PATTERN NONE - Same as DELETE
- Tag & metadata modifiers
- TAG "tagName" - Set sprite tag for identification (used in FOR SPRITES, FOR COLLISIONS)
- META value - Store single metadata value
- META "key", value - Store key-value metadata
- Shader modifiers
- SHADER "name" [, @param=value, ...] - Apply shader with optional parameters
- SHADER NONE - Remove shader from sprite
- CrashNet sync modifiers (multiplayer only, no-op in single-player)
- SYNC - Mark sprite for cross-client broadcast (global, visible to every player and every room). The server sends this sprite's state updates to all connected clients every tick.
- SYNC("RoomName") - Same as SYNC but room-scoped: only players whose ME.ROOM$ equals "RoomName" receive broadcasts for this sprite, AND the sprite only collides / interacts (BFOLLOW, BORBIT, sprite-vs-sprite SOLID) with other sprites in the same room (or with global, unscoped sprites). Use this for MMORPG-style multi-room servers where Room A's NPCs shouldn't drift into Room B.
- NOSYNC - Stop broadcasting this sprite. The sprite remains on the server/creator; clients hold whatever state they last received. Leaves the sprite's room scope unchanged so a later SYNC re-uses it.
Examples
' Basic sprite setup
ENABLE SPRITE 0
SET SPRITE 0, 100, 100, "player"' With frame and modifiers
SET SPRITE 0, x, y, "player", 3, ALPHA(0.5) SCALE(2, 2)' Modifier-only updates
SET SPRITE 0 LOCATION(150, 200)
SET SPRITE 0 ALPHA(0.5) FLIPX
SET SPRITE 0 ROTATE(45)' Flip an existing sprite's spritesheet cell (e.g. animating mouth shapes)
SET SPRITE 0 FRAME 2' Parent-child hierarchy
SET SPRITE 1, 16, 0, "sword", PARENT(0) ' Sword follows player (translation only)
SET SPRITE 1 PARENT NONE ' Detach' RIGID attachment: a forearm that swings around the shoulder as the
' upper arm rotates (orbit + inherited spin), the basis for articulated rigs.
SET SPRITE 2, 20, 0, "forearm"
SET SPRITE 2 PARENT(upperArm) RIGID' Collision with boundaries
SET SPRITE 0 SOLID
SET SPRITE 0, x, y, "player" SOLID BBOX(0.2, 0.2, 0.8, 0.8)' BBOX from an Aseprite slice Range (per-frame hitbox)
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32) SLICES boxes()
SET SPRITE 0, x, y, "HERO"
SET SPRITE 0 BBOX boxes(curFrame)' Collision with sprites by tag (automatic pushback)
SET SPRITE player SOLID SPRITES("wall", "npc", "rock")
SET SPRITE npc TAG "npc" ' This sprite acts as an obstacle' Clear sprite collision but keep boundary collision
SET SPRITE player SOLID SPRITES()' Clear all collision
SET SPRITE player GHOST' Physics caps
SET SPRITE 0 MAXVELOCITY(5, 5) MAXACCELERATION(0.2, 0.5)
SET SPRITE 0 MAXVELOCITY NONE ' Remove cap' Behaviors
SET SPRITE 0 BEHAVIOR "walkRight"
SET SPRITE 0 BEHAVIOR "moveToPoint", @targetX=100, @targetY=200
SET SPRITE 0 BEHAVIOR DELETE "walkRight"
SET SPRITE 0 BEHAVIOR NONE ' Clear all' Patterns
SET SPRITE 0 PATTERN "walkCycle"
SET SPRITE 0 PATTERN "chase", @targetX=enemyX
SET SPRITE 0 PATTERN NONE' Tags for collision handling
SET SPRITE 0, 100, 100, "hero" TAG "player"
SET SPRITE 1, 200, 100, "monster" TAG "enemy"' Metadata for game stats
SET SPRITE 0 META "health", 100
SET SPRITE 0 META "lives", 3' Shaders
SET SPRITE 0 SHADER "Greyscale", @intensity=0.8
SET SPRITE 0 SHADER NONE' Full example combining many modifiers
SET SPRITE 0, 160, 100, "player", 1, ALPHA(0.9) SCALE(2, 2) ROTATE(angle) SOLID LAYER 1 TAG "player"' ORIGIN: anchor the sprite at its center so it positions and rotates
' around its middle (instead of the default top-left corner).
SET SPRITE 0, 160, 120, "ball" ORIGIN(0.5, 0.5)
SET SPRITE 0 ROTATE(45) ' spins around the center now
SET SPRITE 0 ORIGIN(centerCoord) ' ORIGIN also accepts a Coordinate value' IK chain: a tip ball driven to the mouse, the rest of the chain following.
SET SPRITE 0, 320, 240, "ball" ORIGIN(0.5, 0.5) ' root anchor
FOR i = 1 TO 5
SET SPRITE i, 320 + i * 40, 240, "ball" ORIGIN(0.5, 0.5)
SET SPRITE i PARENT(i - 1) RIGID
NEXT i
SET SPRITE 5 IK TARGET(MOUSEX, MOUSEY) LENGTH 5 BEND CCW
' ...then each frame just re-aim the tip:
SET SPRITE 5 IK TARGET(MOUSEX, MOUSEY)' PIN: an eye sprite that eases toward the cursor each tick.
SET SPRITE 7 PIN TARGET(MOUSEX, MOUSEY) MIX 0.3
SET SPRITE 7 PIN NONE ' stop tracking
' LIMIT: a piston that slides only along its X axis between 0 and 20.
SET SPRITE 8 PARENT(0) RIGID
SET SPRITE 8 LIMIT X 0, 20 AXIS XSee also: ENABLE SPRITE, MOVE SPRITE, HIDE SPRITE, SHOW SPRITE, BEHAVIOR, PATTERN, SHADER, SPRITETAG$, GETSPRITEMETA, HASBEHAVIOR, HASPATTERN
SHOW SPRITE
Sprites · since 1.0.3
Makes a previously hidden hardware sprite visible again. Only works if the sprite is enabled.
Not for a bound polygon. A polygon's sprite half lives in the polygon band of the sprite ID space, so this statement's raw sprite id does not reach it — quietly, with no error. Use SET POLYGON id, SHOW / HIDE instead; binding a polygon creates and enables its half, so there is nothing to enable by hand.
Syntax
SHOW SPRITE spriteNumParameters
- spriteNum - Sprite slot number (0-99,999)
Examples
SHOW SPRITE 0 ' Make sprite visible' Spawn enemy at position:
ENABLE SPRITE enemySlot
SET SPRITE enemySlot, spawnX, spawnY, "enemy"
SHOW SPRITE enemySlot' Toggle visibility:
IF SPRITEVISIBLE(0) THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0See also: HIDE SPRITE, ENABLE SPRITE, SPRITEVISIBLE
SPRITE RESET
Sprites · since 1.2.7
Resets a sprite's physics state and orientation, and clears all behaviors and patterns. Zeroes velocity and acceleration, removes velocity/acceleration caps, resets rotation to 0 and flip to false, and detaches all behaviors and patterns. Does NOT affect the sprite's position, texture, frame, alpha, scale, or visibility. Use this to stop a sprite's movement and clear automated behaviors while keeping it where it is.
Syntax
SPRITE spriteNum RESETParameters
- spriteNum - Sprite slot number (0-99,999)
Examples
SPRITE 0 RESET ' Stop all movement and clear behaviors' Stop player when they die:
SPRITE playerSprite RESET
SET SPRITE playerSprite, SPRITEX(playerSprite), SPRITEY(playerSprite), "player_dead"' Reset enemy before assigning new patrol:
SPRITE enemyId RESET
SET SPRITE enemyId BEHAVIOR "patrol"See also: SET SPRITE, MAXVELOCITY, MAXACCELERATION, SPRITEVELOCITYX, SPRITEVELOCITYY
SPRITEBBOX
Sprites · since 1.45.0
Returns a sprite's bounding box in WORLD pixels as a Range (x1,y1)-(x2,y2), ready for manual hit-detection. With no slice name it returns the sprite's active collision box — its custom BBOX, or the full frame if none is set. With a slice name it returns that Aseprite slice's box at the sprite's CURRENT animation frame. World coordinates account for the sprite's position (including any parent), its scale and its ORIGIN. A bbox is relative to the IMAGE and ORIGIN does not reshape it — but ORIGIN moves the image, and the box travels with it, so the rectangle returned here is exactly the one collision tests.
The slice geometry comes from the Aseprite (.ase) file the sprite's image was loaded from. Naming a slice that doesn't exist on the image — or a frame that no slice key covers — is a loud runtime error listing the available slice names. Use this for secondary hit checks (a hurtbox vs. a hitbox) beyond the single automatic collision box that AUTOBBOX drives.
Syntax
SPRITEBBOX(id [, sliceName$])Parameters
- id - Sprite id
- sliceName$ - Optional Aseprite slice name (case-insensitive). Omit for the sprite's active collision box.
Examples
' Active collision box of sprite 0, in world pixels
body = SPRITEBBOX(0)' A named slice's box at the sprite's current frame (world space)
' for a secondary, manual hit check
hurt = SPRITEBBOX(player, "hurtbox")See also: IMAGEBBOX, BEHAVIOR, SET SPRITE, LOADIMG
SPRITECOLLISIONID
Sprites · since 1.57.0
Returns the sprite ID of the first sprite tagged tag$ that spriteNum is colliding with, or -1 if it is colliding with none.
The difference from SPRITECOLLISION, which answers only yes or no: this tells you WHICH sprite was hit, so a handler can act on it — score it, destroy it, read its tag. Test against -1 rather than 0, because 0 is a valid sprite slot.
Syntax
SPRITECOLLISIONID(spriteNum, tag$)Parameters
- spriteNum - Sprite slot number to test
- tag$ - Tag to match against. Matched case-insensitively.
Example
hitId = SPRITECOLLISIONID(playerId, "coin")
IF hitId <> -1 THEN
DISABLE SPRITE hitId
score = score + 10
END IFSee also: SPRITECOLLISION, SPRITETAG$, SET SPRITE
SPRITECOUNT
Sprites · since 1.4.5
Returns the number of currently enabled sprites that have the specified tag. This is useful for checking how many enemies remain, counting collectibles, or managing spawn limits. Only enabled sprites are counted.
Syntax
SPRITECOUNT("tagName")Parameters
- tagName - String expression for the tag to count (case-insensitive)
Examples
enemyCount = SPRITECOUNT("enemy")
PRINT "Enemies remaining:"; enemyCount' Check for level completion
IF SPRITECOUNT("enemy") = 0 THEN
PRINT "Level complete!"
CALL NextLevel()
END IF' Limit enemy spawn count
IF SPRITECOUNT("enemy") < 10 THEN
CALL SpawnEnemy()
END IF' Count multiple types
totalBaddies = SPRITECOUNT("enemy") + SPRITECOUNT("boss")
PRINT "Total threats:"; totalBaddies' Game over check
IF SPRITECOUNT("player") = 0 THEN
CALL GameOver()
END IFSee also: FOR SPRITES, SPRITETAG$, SET SPRITE (TAG), NEXTFREESPRITE
SPRITEDIRECTION
Sprites · since 1.0.3
Returns the last movement direction of a sprite in degrees. 0 = right, 90 = down, 180 = left, 270 = up. Direction is updated whenever the sprite moves via behaviors. Returns 0 if sprite has not moved.
Syntax
SPRITEDIRECTION(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
dir = SPRITEDIRECTION(0) ' Get player's facing direction' Choose animation based on direction:
IF dir < 45 OR dir >= 315 THEN
' Facing right
SET SPRITE 0, x, y, "player_right"
ELSEIF dir >= 45 AND dir < 135 THEN
' Facing down
SET SPRITE 0, x, y, "player_down"
ELSEIF dir >= 135 AND dir < 225 THEN
' Facing left
SET SPRITE 0, x, y, "player_left"
ELSE
' Facing up
SET SPRITE 0, x, y, "player_up"
END IF' Point sprite in direction of movement:
SET SPRITE 0, x, y, "arrow", ROTATE(SPRITEDIRECTION(0))See also: SPRITEVELOCITYX, SPRITEVELOCITYY, BEHAVIOR
SPRITEDIST
Sprites · since 1.57.0
Returns the straight-line distance in pixels between two sprites' positions, as a Float.
Measured between the sprites' anchors — the same point SPRITEX / SPRITEY report and LOCATION places — so ORIGIN moves what is measured. Returns 0 if either sprite does not exist.
Syntax
SPRITEDIST(spriteA, spriteB)Parameters
- spriteA - First sprite slot number
- spriteB - Second sprite slot number
Examples
IF SPRITEDIST(playerId, enemyId) < 32 THEN
PRINT "too close"
END IF' Chase only when in range
IF SPRITEDIST(enemyId, playerId) < 200 THEN
SET SPRITE enemyId BEHAVIOR "chase"
END IFSee also: SPRITEX, SPRITEY, SPRITECOLLISION, DISTANCE
SPRITEELEVATION
Sprites · since 1.40.0
Returns a sprite's current ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units; 0 means on the floor. Useful in behavior/pattern WHEN conditions, e.g. landing detection. Elevation is set with SET SPRITE id ELEVATION(z) or animated with the BELEVATE behavior.
Syntax
SPRITEELEVATION(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
z = SPRITEELEVATION(0) ' How high is the player off the floor?REM Landing detection inside a pattern (THISSPRITE = the running sprite)
PATTERN "Jumper" LOOP
"Land" WHEN SPRITEELEVATION(THISSPRITE) <= 0
END PATTERNSee also: SET SPRITE, BEHAVIOR, SET LAYER, SPRITEX, SPRITEY
SPRITEENABLED
Sprites · since 1.0.3
Returns 1 if the sprite slot is enabled, 0 otherwise. An enabled sprite can be configured and rendered. Use this to check sprite slot availability.
Syntax
SPRITEENABLED(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
IF SPRITEENABLED(0) THEN PRINT "Sprite 0 is enabled"' Find first available sprite slot:
FOR i = 0 TO 999
IF NOT SPRITEENABLED(i) THEN
freeSlot = i
EXIT FOR
END IF
NEXT' Count active sprites:
active = 0
FOR i = 0 TO 999
IF SPRITEENABLED(i) THEN active = active + 1
NEXT
PRINT "Active sprites:" + STR$(active)' Safe cleanup:
FOR i = 0 TO 999
IF SPRITEENABLED(i) THEN DISABLE SPRITE i
NEXTSee also: SPRITEVISIBLE, ENABLE SPRITE, DISABLE SPRITE
SPRITEHEIGHT
Sprites · since 1.0.3
Returns the current height of a hardware sprite in pixels. The returned value accounts for scaling (base texture height multiplied by the sprite's Y scale factor). Returns 0 if the sprite does not exist.
Syntax
SPRITEHEIGHT(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
h = SPRITEHEIGHT(0) ' Get player sprite height' Position sprite so its bottom touches the floor:
SET SPRITE 0, x, floorY - SPRITEHEIGHT(0), "player"' Bottom edge of a sprite (equivalent to SPRITEY(id, 1)):
bottomEdge = SPRITEY(0) + SPRITEHEIGHT(0)See also: SPRITEWIDTH, SPRITEX, SPRITEY, SET SPRITE
SPRITETAG$
Sprites · since 1.0.3
Returns the tag string associated with the specified sprite. Returns an empty string if no tag has been set.
Syntax
SPRITETAG$(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
PRINT SPRITETAG$(0) ' Might print "player"IF SPRITETAG$(a) = "enemy" THEN
PRINT "Hit an enemy!"
END IF' Collision handling by tag
FOR COLLISIONS a, b
IF SPRITETAG$(a) = "player" AND SPRITETAG$(b) = "enemy" THEN
PRINT "Player hit enemy!"
END IF
NEXT COLLISIONSee also: SET SPRITE (TAG), FOR COLLISIONS, FOR SPRITES
SPRITEVISIBLE
Sprites · since 1.0.3
Returns 1 if the sprite is both enabled AND visible, 0 otherwise. A sprite is not visible if it has been hidden with HIDE SPRITE or if it is not enabled.
Syntax
SPRITEVISIBLE(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
IF SPRITEVISIBLE(0) THEN PRINT "Sprite is visible"' Count visible sprites:
visible = 0
FOR i = 0 TO 999
IF SPRITEVISIBLE(i) THEN visible = visible + 1
NEXT
PRINT "Visible sprites:" + STR$(visible)' Toggle visibility:
IF SPRITEVISIBLE(0) THEN HIDE SPRITE 0 ELSE SHOW SPRITE 0See also: SPRITEENABLED, HIDE SPRITE, SHOW SPRITE
SPRITEWIDTH
Sprites · since 1.0.3
Returns the current width of a hardware sprite in pixels. The returned value accounts for scaling (base texture width multiplied by the sprite's X scale factor). Returns 0 if the sprite does not exist.
Syntax
SPRITEWIDTH(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
w = SPRITEWIDTH(0) ' Get player sprite width' Center a sprite horizontally on screen:
SET SPRITE 0, (SCREENWIDTH - SPRITEWIDTH(0)) / 2, y, "player"' Check if sprite fits within a gap:
IF SPRITEWIDTH(0) < gapSize THEN canPass = 1' Right edge of a sprite (equivalent to SPRITEX(id, 1)):
rightEdge = SPRITEX(0) + SPRITEWIDTH(0)See also: SPRITEHEIGHT, SPRITEX, SPRITEY, SET SPRITE
SPRITEX
Sprites · since 1.0.3
Returns the X position of a hardware sprite. By default returns the left edge (x position). When relativeX is provided (0-1 range), returns the translated X coordinate at that relative position across the sprite's width. For example, 0.5 returns the horizontal center.
Bound polygons take a slot id, not a polygon id. This function reads the raw sprite id you pass, and a polygon's sprite half sits in the polygon band of that space — so SPRITEX(3) reports sprite 3, not polygon 3. Where you DO have the slot, this reports the shape's anchor: its extent's left edge offset by ORIGIN, the same point SET POLYGON n, LOCATION(...) places. A collision handler is the usual place that happens — index_a is the slot the shape occupies, so SPRITEX(index_a) reads the shape that was hit. Note that a shape's extent comes from its live points, so a ROTATING shape's bounding box grows and shrinks as it turns; anchor it at ORIGIN(0.5, 0.5) for a stable point.
Syntax
SPRITEX(spriteNum [, relativeX])Parameters
- spriteNum - Sprite slot number (0-999 for user sprites; emitted particles use 100,000+)
- relativeX - Optional. Relative position (0-1) across sprite width. 0 = left edge (default), 0.5 = center, 1 = right edge
Examples
x = SPRITEX(0) ' Get player sprite left edge X positioncenterX = SPRITEX(0, 0.5) ' Get sprite horizontal centerrightX = SPRITEX(0, 1) ' Get sprite right edge' Check if sprite center is on screen:
IF SPRITEX(0, 0.5) >= 0 AND SPRITEX(0, 0.5) < 320 THEN onScreen = 1' Distance between sprite centers:
dx = SPRITEX(0, 0.5) - SPRITEX(1, 0.5)
dy = SPRITEY(0, 0.5) - SPRITEY(1, 0.5)
distance = SQR(dx * dx + dy * dy)' Display sprite center position:
LOCATE 1, 1: PRINT "Center: " + STR$(SPRITEX(0, 0.5)) + "," + STR$(SPRITEY(0, 0.5))Notes
Works for any live sprite, including emitted particles (IDs 100,000+). Querying a sprite that doesn't exist is a loud runtime error, not a silent 0 — a missing sprite would otherwise read as a valid coordinate at the origin. Guard with IF SPRITEENABLED(id) THEN ... first. A common cause is reading an emitted particle's position after its TTL has expired (e.g. from an async BEVENT handler); give the particle a longer TTL or fire the event with BEVENT "..." SYNC so the handler runs before cleanup.
See also: SPRITEY, SPRITEWIDTH, SPRITEHEIGHT, SPRITEVISIBLE, SPRITEENABLED, MOVE SPRITE
SPRITEY
Sprites · since 1.0.3
Returns the Y position of a hardware sprite. By default returns the top edge (y position). When relativeY is provided (0-1 range), returns the translated Y coordinate at that relative position across the sprite's height. For example, 0.5 returns the vertical center.
Bound polygons take a slot id, not a polygon id. This function reads the raw sprite id you pass, and a polygon's sprite half sits in the polygon band of that space — so SPRITEX(3) reports sprite 3, not polygon 3. Where you DO have the slot, this reports the shape's anchor: its extent's top edge offset by ORIGIN, the same point SET POLYGON n, LOCATION(...) places. A collision handler is the usual place that happens — index_a is the slot the shape occupies, so SPRITEX(index_a) reads the shape that was hit. Note that a shape's extent comes from its live points, so a ROTATING shape's bounding box grows and shrinks as it turns; anchor it at ORIGIN(0.5, 0.5) for a stable point.
Syntax
SPRITEY(spriteNum [, relativeY])Parameters
- spriteNum - Sprite slot number (0-99,999)
- relativeY - Optional. Relative position (0-1) across sprite height. 0 = top edge (default), 0.5 = center, 1 = bottom edge
Examples
y = SPRITEY(0) ' Get player sprite top edge Y positioncenterY = SPRITEY(0, 0.5) ' Get sprite vertical centerbottomY = SPRITEY(0, 1) ' Get sprite bottom edge' Check if sprite bottom hit floor:
IF SPRITEY(0, 1) > 180 THEN gameOver = 1' Vertical collision check using centers:
IF ABS(SPRITEY(0, 0.5) - SPRITEY(1, 0.5)) < 16 THEN verticalOverlap = 1' Check if sprite is fully on screen:
IF SPRITEY(0) >= 0 AND SPRITEY(0, 1) < 200 THEN fullyVisible = 1Notes
Works for any live sprite, including emitted particles (IDs 100,000+). Querying a sprite that doesn't exist is a loud runtime error, not a silent 0 — a missing sprite would otherwise read as a valid coordinate at the origin. Guard with IF SPRITEENABLED(id) THEN ... first. A common cause is reading an emitted particle's position after its TTL has expired (e.g. from an async BEVENT handler); give the particle a longer TTL or fire the event with BEVENT "..." SYNC so the handler runs before cleanup.
See also: SPRITEX, SPRITEWIDTH, SPRITEHEIGHT, SPRITEVISIBLE, SPRITEENABLED, MOVE SPRITE
Behaviors
AFTERCOLLISION
Behaviors · since 1.53.0
Inside a BEHAVIOR block, runs inline BASIC after collision resolution — the closing half of ONCOLLISION. The block is hoisted to an anonymous handler and fired by the behavior engine.
A tick goes: contact, then resolution, then the next tick. ONCOLLISION runs on contact, before resolution, so impact_vx / impact_vy hold the velocity the sprite struck at. AFTERCOLLISION runs between resolution and the next tick, once the engine has stopped or reflected the sprite — which makes it the place to decide what to do about the hit, such as reversing direction by re-issuing the behavior.
Solid contacts always resolve, so both edges land on the tick of the hit and AFTERCOLLISION does NOT wait for the sprite to move away. That matters because a sprite driven by BVELOCITY never stops pressing — BVELOCITY re-applies every tick — so a sprite held against a wall would never separate at all.
A ghost contact has no resolution to follow, so its collision stays in effect for as long as the pair overlaps: a sprite can wander around inside an otherwise-solid block indefinitely. Its AFTERCOLLISION fires on the tick the overlap finally ends. Calling GHOST from inside ONCOLLISION switches the contact to this behavior, which is what makes phase-through work.
Edge-triggered either way, and it fires exactly once per contact — never per tick.
The scoped values describe the contact as it began — the same edge$, tags and impact velocity ONCOLLISION saw.
The same restrictions apply as ONCOLLISION: no blocking statements, changes take effect next tick, handler bodies scope like a SUB, and INCLUDES stacks handlers.
Syntax
AFTERCOLLISION
statements
END AFTERCOLLISIONParameters
- index_a - (in scope) This sprite's ID. Always the sprite the behavior is running on.
- index_b - (in scope) The other sprite's ID, or -1 when the contact was with a boundary.
- tag_a$ - (in scope) This sprite's tag.
- tag_b$ - (in scope) The other sprite's tag, or the boundary's tag when index_b is -1.
- edge$ - (in scope) The face struck when the contact began.
- impact_vx - (in scope) Horizontal velocity recorded when the contact began.
- impact_vy - (in scope) Vertical velocity recorded when the contact began.
- THISSPRITE is also set to index_a.
Examples
' Walking off a ledge: gravity comes back
BEHAVIOR "grounded"
AFTERCOLLISION
IF tag_b$ = "ground" THEN SET SPRITE index_a BEHAVIOR "falling"
END AFTERCOLLISION
END BEHAVIOR' Leaving a lava boundary stops the burn
BEHAVIOR "burnable"
AFTERCOLLISION
IF tag_b$ = "lava" THEN STOP TIMER "burnTick"
END AFTERCOLLISION
END BEHAVIOR' Become solid again once clear of whatever we phased through.
' SOLID SPRITES(...) rather than bare SOLID: GHOST cleared the sprite
' barrier tags, and plain SOLID restores only boundary solidity.
BEHAVIOR "phaser"
ONCOLLISION
SET SPRITE index_a GHOST
END ONCOLLISION
AFTERCOLLISION
SET SPRITE index_a SOLID SPRITES("wall")
END AFTERCOLLISION
END BEHAVIORSee also: ONCOLLISION, BEHAVIOR, BOUNDARY ADD, SET SPRITE, FOR COLLISIONS
BEHAVIOR
Behaviors · since 1.0.5
Defines declarative sprite animation and movement behaviors. A single BEHAVIOR block can define one named behavior, or multiple named behaviors separated by labels. Behaviors are applied to hardware sprites using SET SPRITE n BEHAVIOR. Behavior commands can use parameterized values with @paramName=defaultValue syntax, allowing the same behavior to be reused with different values. Use INCLUDES to compose behaviors from other defined behaviors - included commands come first, followed by this behavior's commands.
Declaration-time control flow. A behavior body may contain IF/ELSEIF/ELSE/END IF and SELECT CASE/CASE/CASE ELSE/END SELECT to choose which commands are baked into the behavior. These are evaluated once, when the BEHAVIOR statement runs (not per tick) — the matching branch's commands are included in the registered behavior, the rest are skipped. Re-declaring the behavior with different variables re-evaluates the conditions and overwrites the definition. This is distinct from a pattern's runtime WHEN clause, which re-checks each tick. Both block and single-line IF cond THEN command forms are supported, and SELECT CASE supports the same CASE value, CASE a, b, CASE a TO b, CASE IS <op> v, and CASE ELSE forms as the statement-level SELECT.
A behavior can also carry collision handlers — ONCOLLISION and AFTERCOLLISION blocks of ordinary BASIC that the engine fires when this sprite starts and stops touching something. They are edge-triggered, so a character landing on the ground runs its handler once rather than every tick it stands there, and they receive the contact details (what was hit, which face, and how fast it was going at the moment of impact). Because they are ordinary blocks, declaration-time IF / SELECT CASE can choose between them.
Behavior names are case-insensitive. SET SPRITE n BEHAVIOR "Walk" finds the behavior defined as "walk", and the same holds everywhere one is named — INCLUDES, a PATTERN step, EMITPARTICLE, HASBEHAVIOR.
The commands
Every command below lives INSIDE a BEHAVIOR ... END BEHAVIOR block. None of them is a statement you can write on its own — that is why they are documented here rather than as separate keywords.
Motion
BVELOCITY
BVELOCITY vx, vy [, vz]Inside a BEHAVIOR block, contributes a continuous velocity to the sprite. This is a standing movement intent, not a one-shot kick: the engine zeroes the BVELOCITY contribution at the start of every tick and each active BVELOCITY command adds its value back, so several behaviors carrying BVELOCITY are additive rather than last-one-wins. Combine with BACCELERATION (gravity, thrust) for arcs.
Because the contribution is rebuilt every tick, it has no inertia: BFRICTION does not decay it (there is nothing accumulated to decay), and a solid collision does not zero it — the sprite keeps pushing against whatever blocked it for as long as the behavior is active. For the same reason BBOUNCE cannot rebound it: the reflection is applied, then overwritten by the constant on the next tick, so a BVELOCITY-driven sprite presses against a wall rather than bouncing off it.
For a discrete kick — a jump, dash, recoil, or knockback that decays and can be blocked — use BIMPULSE, which seeds the accumulated velocity once and is subject to friction and collision zeroing.
The optional third component vz sets the sprite's vertical (elevation) velocity — height above a plane layer; pairs with BACCELERATION z (gravity) and the ground clamp for jump/fall physics.
- vx - Horizontal velocity in pixels per tick, re-applied every tick while the behavior is active
- vy - Vertical velocity in pixels per tick, re-applied every tick while the behavior is active (negative = up)
- vz - (optional) Vertical (elevation) velocity in world units per tick. Height above a plane layer; gravity (BACCELERATION z) and the ground (elevation 0) act on it.
BMOVE
BMOVE [ABSOLUTEX] dx, [ABSOLUTEY] dy [, [ABSOLUTEZ] dz]Inside a BEHAVIOR block, defines constant movement per tick. By default, all arguments are per-tick deltas applied to the sprite's current position. Multiple BMOVE behaviors stack additively per tick.
The optional ABSOLUTEX / ABSOLUTEY / ABSOLUTEZ modifiers (per-axis, applied independently) flip the corresponding argument's interpretation from delta to absolute target — instead of dx adding to the sprite's X each tick, the sprite's X is set to dx. This lets you mix idioms freely: snap one axis while leaving the others to drift, set a column on entry but allow free vertical motion, etc. Axes can be made absolute simultaneously to teleport-snap on every tick.
The optional third component dz moves the sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units. Only the resulting elevation affects rendering on a plane layer; on a flat layer it has no visible effect. Omit it for plain 2D movement.
Values can use @paramName=default syntax for parameterized behaviors. All axes accept VARIANCE for per-instance jitter.
- dx - Horizontal movement in pixels per tick. With
ABSOLUTEX, treated as the target X coordinate instead. Supports @param=default and VARIANCE. - dy - Vertical movement in pixels per tick. With
ABSOLUTEY, treated as the target Y coordinate instead. Supports @param=default and VARIANCE. - dz - (optional) Elevation movement in world units per tick. With
ABSOLUTEZ, treated as the target elevation instead. Supports @param=default and VARIANCE. - ABSOLUTEX (modifier) - Optional, prefixes the X argument. Treats
dxas an absolute target X position rather than a per-tick delta. - ABSOLUTEY (modifier) - Optional, prefixes the Y argument. Treats
dyas an absolute target Y position rather than a per-tick delta. - ABSOLUTEZ (modifier) - Optional, prefixes the Z argument. Treats
dzas an absolute target elevation rather than a per-tick delta.
BACCELERATION
BACCELERATION dx, dy [, dz]Inside a BEHAVIOR block, applies continuous acceleration to the sprite. Unlike BMOVE which applies constant velocity, BACCELERATION adds to velocity each tick, creating effects like gravity, thrust, or deceleration. Velocity accumulates over time and persists until the behavior is removed. Use with BVELOCITY for proper jump physics.
The optional third component dz accelerates the sprite's ELEVATION (height above a plane layer) — this is how gravity for the vertical axis works: BACCELERATION 0, 0, -1 pulls a sprite back down to the floor after a BIMPULSE 0, 0, z jump. The z axis integrates into elevation each tick and lands at the ground (elevation 0).
- dx - Horizontal acceleration in pixels per tick per tick
- dy - Vertical (screen-Y) acceleration in pixels per tick per tick (positive = down for in-plane gravity)
- dz - (optional) Elevation acceleration in world units per tick per tick. Negative pulls toward the floor (gravity off a plane layer); pairs with BIMPULSE z for jumps.
BIMPULSE
BIMPULSE ix, iy [, iz]Inside a BEHAVIOR block, applies a one-shot impulse: adds (ix, iy[, iz]) to the sprite's accumulated accel_velocity exactly once per behavior activation, then becomes a no-op for the rest of that activation. The seeded velocity persists in accel_velocity and decays through BFRICTION (if any) the same way BACCELERATION contributions decay. Honours max_accel_x/max_accel_y so an oversized impulse can't blow past a sprite's configured speed ceiling.
The optional third component iz kicks the sprite's ELEVATION — a true jump off a plane layer: BIMPULSE 0, 0, 16 paired with gravity (BACCELERATION 0, 0, -1) arcs up and lands back on the floor (elevation 0). The vertical (z) axis is a real physics axis: z-velocity integrates into elevation each tick, decelerated by gravity, clamped at the ground.
BIMPULSE is the right primitive for discrete kicks of momentum: jumps, dashes, recoil, knockback. Compare with the related primitives:
- BACCELERATION is applied every tick (continuous force — gravity, thrust).
- BVELOCITY overwrites
bvelocityevery tick (no inertia, ignored by friction — useful for direct movement intent). - BMOVE translates the sprite directly with no velocity at all.
To re-fire the impulse, the behavior must be deactivated and re-activated — typically driven from a pattern step or a WHEN trigger. On reactivation, a fresh internal state is built (tick = 0) and the impulse fires again. All axes accept VARIANCE for per-instance jitter, and support @paramName=default parameter references for pattern-driven values.
- ix - Horizontal impulse in pixels per tick. Added to accel_velocity_x once on the activation tick.
- iy - Vertical impulse in pixels per tick. Same semantics on the Y axis (positive = down).
- iz - (optional) Elevation impulse in world units — a vertical jump kick off a plane layer. Added to the z-velocity once; gravity (BACCELERATION z) pulls it back to the ground.
- All values support VARIANCE(low, high), VARIANCEINT(...), and @paramName=default parameter references.
BFRICTION
BFRICTION x_friction, y_friction [, z_friction]Inside a BEHAVIOR block, applies universal per-axis friction every tick — independent of collisions. Multiplies the sprite's accumulated accel_velocity by (1 - friction) on each axis. 0 = frictionless (no decay), 1 = instant stop, in between = exponential decay (e.g. 0.1 = lose 10% of accumulated velocity per tick).
BFRICTION is the natural counterpart to BACCELERATION. They reach equilibrium at the terminal velocity ≈ acceleration / friction — useful for modelling air resistance, surface drag, or maximum-speed clamps via gentle decay rather than a hard cap.
The optional third coefficient z_friction decays vertical (ELEVATION) velocity each tick the same way — air resistance on jumps and falls, or a terminal fall speed when paired with BGRAVITY (terminal ≈ gravity / z_friction).
Does not affect BVELOCITY (which is reset and re-derived each tick from the BVELOCITY commands themselves, so friction on it would be a no-op) or BMOVE (constant-motion intent). All axes accept VARIANCE for per-instance jitter — useful when you want a swarm of objects to settle at slightly different speeds.
- x_friction - Friction coefficient on the X axis. 0 = no decay, 1 = instant stop, between = exponential decay per tick.
- y_friction - Friction coefficient on the Y axis. Same scale.
- z_friction - (optional) Friction on the elevation axis — air resistance on vertical motion; pairs with BGRAVITY for a terminal fall speed.
- All values support VARIANCE(low, high) and VARIANCEINT(...) for per-instance randomness.
BGRAVITY
BGRAVITY strengthInside a BEHAVIOR block, applies a constant downward acceleration to the sprite's ELEVATION every tick — convenience sugar for the vertical component of BACCELERATION (0, 0, -strength). Use a positive strength; the sprite accelerates toward the ground (elevation 0). This is the everyday way to give plane-layer sprites weight: pair it with BIMPULSE z for jumps and BBOUNCE z for bouncing. strength supports VARIANCE and @paramName=default. Only meaningful on a PERSPECTIVE/AFFINE plane layer (where elevation is rendered); with no elevation it has no visible effect.
- strength - Downward acceleration in elevation units per tick per tick. Positive pulls the sprite toward the ground. Supports VARIANCE(low, high) and @paramName=default.
BELEVATE
BELEVATE from TO to DURATION ticks [LOOP]Inside a BEHAVIOR block, tweens a sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE), in world units — from from to to over ticks. Mirrors BALPHA/BSCALE/BROTATE. Sequence two BELEVATE behaviors in a looping PATTERN (rise then fall) for a continuous hop, or use a single LOOP for a repeating lift. Elevation only changes how the sprite is drawn on a plane layer (it rides toward the camera eye-height, foreshortened by distance); on a flat layer it has no visible effect. Query the live value with SPRITEELEVATION(id), e.g. in a WHEN condition for landing detection.
- from - Starting elevation (world units; 0 = on the floor)
- to - Ending elevation (world units)
- ticks - Duration in ticks/frames
- LOOP - Repeat the tween indefinitely
BBOUNCE
BBOUNCE x_restitution, y_restitution [, z_restitution]Inside a BEHAVIOR block, applies bounce physics to the sprite when it collides with a solid boundary. Each axis takes a restitution coefficient: 0 = absorb (sprite stops, identical to the default zero-on-block behaviour), 1 = perfect elastic reflection, between 0 and 1 = partial energy loss per bounce.
BBOUNCE reflects the sprite's accumulated velocity — the momentum built up by BIMPULSE, BACCELERATION and BGRAVITY. That is the motion that carries across ticks, so it is what a bounce can meaningfully reverse: BIMPULSE 8, 0 + BBOUNCE 0.9, 0.9 sends a ball off a wall and back, losing 10% per hit.
BVELOCITY is not a useful bounce source. It is a constant re-applied every tick by design, so although the reflection is applied, the next tick rebuilds the original value and overwrites it — a sprite whose only motion is BVELOCITY does not rebound, it presses steadily against whatever it hit. Build bouncing things out of BIMPULSE (a one-shot kick) or BACCELERATION / BGRAVITY (continuous force).
For SLOPE boundaries, the velocity is reflected along the slope's normal vector and scaled by the average of x_restitution and y_restitution — so a ball hitting a 45° SE slope from above will bounce up-and-to-the-right cleanly. AABB walls reflect axis-by-axis: LEFT/RIGHT edges use x_restitution, TOP/BOTTOM edges use y_restitution. Both restitutions support VARIANCE for per-instance jitter (good for natural-feeling particle systems).
The optional third coefficient z_restitution bounces the ELEVATION axis off the ground (elevation 0) — a ball dropping under BGRAVITY (or BACCELERATION z) on a plane layer rebounds upward scaled by z_restitution, and with z < 1 loses energy each hit and settles. z_restitution 0 (or omitting it) = land and stop, the prior 2D behaviour.
When BBOUNCE is not active, sprites use the default behaviour: accel_velocity in the blocked direction is zeroed and BVELOCITY persists. Adding BBOUNCE 0, 0 explicitly is equivalent to the default for the blocked-axis component, and additionally zeroes the BVELOCITY component for the remainder of that tick (so SPRITEVELOCITYX / SPRITEVELOCITYY read zero) — though, as above, the BVELOCITY constant is rebuilt on the next tick. Durable absorption applies to accumulated velocity.
- x_restitution - Bounce coefficient on the X axis (LEFT/RIGHT walls). 0 = absorb, 1 = perfect elastic.
- y_restitution - Bounce coefficient on the Y axis (TOP/BOTTOM walls). 0 = absorb, 1 = perfect elastic.
- z_restitution - (optional) Bounce coefficient for the ground (elevation 0) on a plane layer. 0 = land/stop, 1 = perfect elastic, between = damped bounce that settles. Needs vertical motion (BGRAVITY / BIMPULSE z) to matter.
- All values support VARIANCE(low, high) and VARIANCEINT(...) for per-instance randomness.
- SLOPE hits use (x_restitution + y_restitution) / 2 as the restitution along the slope normal.
BSTOP
BSTOPInside a BEHAVIOR block, performs a one-shot motion halt. On activation, zeros the sprite's bvelocity, accumulated accel_velocity, and the cached total velocity on ALL axes — including the vertical z (elevation) axis — exactly once, then becomes a no-op for the rest of that activation.
BSTOP fills a real gap in the motion model: pattern transitions (SET SPRITE n PATTERN ... and GOTOPATTERN) do not reset sprite motion. Contributions to accel_velocity from BACCELERATION and BIMPULSE survive across pattern changes unless something explicitly zeros them, and BFRICTION only decays them gradually. Without BSTOP there's no clean primitive for 'stop here, regardless of what the previous pattern was doing.'
Lifecycle matches BIMPULSE: fires on the activation tick, no-op after, re-fires on reactivation (pattern step entered again, WHEN trigger transitions back to true). Subsequent ticks of the same activation don't keep wrestling motion to zero — a 'stop' pattern that holds for several ticks halts once and then idles, leaving room for other behaviors in the same activation slot to operate.
- (no parameters)
Paths and orbits
BORBIT
BORBIT (x, y[, z]) DISTANCE radius [SPEED degreesPerTick] [CLOCKWISE|COUNTERCLOCKWISE] [DECAY pixelsPerTick] [STARTANGLE angle] [AXIS (ax, ay, az)] [SEEKFIRST [SPEED seekSpeed]]
BORBIT SPRITE targetId DISTANCE radius [SPEED degreesPerTick] [CLOCKWISE|COUNTERCLOCKWISE] [DECAY pixelsPerTick] [STARTANGLE angle] [AXIS (ax, ay, az)] [SEEKFIRST [SPEED seekSpeed]]
(the (x, y[, z]) center also accepts a coordinate variable/constant)Inside a BEHAVIOR block, makes the sprite orbit around a fixed coordinate or another sprite. The sprite maintains a constant distance (radius) from the center point while rotating around it. Without STARTANGLE, the starting angle is derived from the sprite's actual position relative to the orbit center. Use STARTANGLE to specify an explicit starting angle in degrees (supports VARIANCE for randomness). Two modes: coordinate-based (x, y) for fixed orbit center, or sprite-based for orbiting around a moving target. Use DECAY to create a spiral effect where the orbit radius shrinks over time. AXIS (ax, ay, az) makes the orbit 3D: it is the axis of rotation, i.e. the normal of the plane the sprite circles in. The default axis (0, 0, 1) is the flat floor orbit (x/y circle, constant elevation). AXIS (1, 0, 0) stands the ring up into a vertical wheel; any tilted vector gives a tilted ring, foreshortened by the plane projection. The center coordinate may itself be 3D — (x, y, z) — to lift the whole ring off the floor; a ring of sprites at staggered STARTANGLEs then traces a rotating 3D ring. AXIS only has a visible elevation effect on a PERSPECTIVE/AFFINE plane layer. A 3D orbit around a SPRITE center rides at that sprite's own elevation, so a ring around a jumping sprite rises and falls with it. Use SEEKFIRST to make the sprite smoothly approach its computed orbit start position before beginning to orbit.
DECAY requires SPEED. DECAY pulls the orbit inward and SPEED is what carries the sprite around it, so a decaying orbit with no speed is a sprite sliding along a fixed bearing. Writing DECAY without SPEED is a runtime error.
- (x, y) - Fixed coordinate to orbit around
- SPRITE targetId - Sprite slot number (0-99,999) to orbit around
- DISTANCE radius - Orbital distance in pixels from center
- SPEED n - Optional angular rate. The orbit advances
n * 0.01radians per tick (default: 1 — about 0.57 degrees per tick, a full revolution in roughly 628 ticks). This is a rate multiplier, not an angle in degrees. - CLOCKWISE - Orbit in clockwise direction (default)
- COUNTERCLOCKWISE - Orbit in counterclockwise direction
- DECAY pixelsPerTick - Optional decay rate to reduce orbital radius each tick (creates spiral inward)
- STARTANGLE angle - Optional starting orbit angle in degrees (supports VARIANCE for randomness). Without STARTANGLE, the angle is derived from the sprite's actual position relative to the orbit center.
- AXIS (ax, ay, az) - Optional 3D rotation axis (normal of the orbit plane). Default (0, 0, 1) = flat floor orbit. (1, 0, 0) = vertical wheel; tilted vectors = tilted rings. Requires a PERSPECTIVE/AFFINE plane layer for the elevation to be visible.
- (x, y, z) center - The center coordinate may include a 3rd component to lift the orbit ring to that elevation.
- SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach its computed orbit start position before beginning to orbit. Default seek speed is 2.0 pixels per tick.
BOSCILLATE
BOSCILLATE AMPLITUDE amplitudeX, amplitudeY [, amplitudeZ] [SPEED degreesPerTick] [DECAY pixelsPerTick] [PHASE radians] [PERSISTENT]Inside a BEHAVIOR block, makes the sprite oscillate (move back and forth) around its starting position using a sine wave pattern. The sprite moves smoothly between -amplitude and +amplitude on each axis. Useful for floating, hovering, or wobbling effects.
The optional third amplitude amplitudeZ oscillates the sprite's ELEVATION — its height above the plane layer it sits on (AFFINE/PERSPECTIVE) — for hover/bob. It swings symmetrically (±amplitudeZ) around whatever elevation the sprite had when the behavior activated, so set a base height first (e.g. SET SPRITE n ELEVATION(30)) for a hover that stays above the floor. Only affects rendering on a plane layer.
Use DECAY to gradually reduce the oscillation amplitude over time until the sprite settles. Use PERSISTENT to keep the sprite at its current oscillation offset when the behavior ends (instead of snapping back to the starting position).
Use PHASE to set the initial sine angle (in radians). The default initial phase is 0 — every instance starts synchronized at the wave origin. Combine PHASE with VARIANCE(low, high) to scatter starting phases per-instance (each sprite or mutation point gets a different random phase, useful for swarms of bobbing objects so they don't all move in lockstep).
When transitioning between two behaviors that both use BOSCILLATE, the new behavior automatically inherits the previous behavior's current phase so there is no visual jerk at the join. The carryover applies for one transition only — any non-oscillating behavior between two oscillating ones breaks the chain. An explicit PHASE override on the new behavior wins over the inherited value.
- amplitudeX - Maximum horizontal displacement in pixels from starting position
- amplitudeY - Maximum vertical displacement in pixels from starting position
- amplitudeZ - (optional) Maximum elevation displacement in world units; oscillates height above a plane layer around the base elevation at activation (hover/bob).
- SPEED degreesPerTick - Optional oscillation speed in degrees per tick (default: 6, full cycle in ~60 ticks)
- DECAY pixelsPerTick - Optional decay rate to reduce amplitude each tick (sprite settles to starting position)
- PHASE radians - Optional initial sine angle in radians (default 0). Supports VARIANCE(low, high) for per-instance scatter — e.g. PHASE 0 VARIANCE(0, 6.28) gives each instance a random phase across a full cycle.
- PERSISTENT - Optional modifier to keep the current offset when the behavior ends (sprite does not snap back)
BSHAKE
BSHAKE AMPLITUDE amplitudeInside a BEHAVIOR block, makes the sprite shake by applying random position offsets each tick. The sprite jitters randomly within the amplitude range on both X and Y axes, creating a vibration or trembling effect. Useful for damage feedback, screen shake effects, or unstable objects.
- amplitude - Maximum random offset in pixels (applies to both X and Y axes)
BFOLLOW
BFOLLOW targetSpriteId DISTANCE minDistance [SPEED pixelsPerTick] [ELEVATION offset] [SEEKFIRST [SPEED seekSpeed]]Inside a BEHAVIOR block, makes the sprite automatically follow another sprite. The sprite will move toward the target sprite only when the distance between them exceeds the specified minimum distance. The follower stops when it reaches the minimum distance from the target. Use SEEKFIRST to make the sprite smoothly approach the target before beginning normal follow behavior.
The optional ELEVATION clause makes BFOLLOW 3D: the follower also tracks the target's elevation, offset by the given amount, easing toward target_elevation + offset at the follow speed each tick. ELEVATION 0 pins the follower to the target's height (a drone matching a jumping player); ELEVATION 20 hovers 20 units above; ELEVATION -elevationOfTarget can pin a shadow to the ground. Without the ELEVATION clause, BFOLLOW only matches x/y (and on non-plane games all elevations are 0, so it is a no-op).
- targetSpriteId - The sprite slot number (0-99,999) of the sprite to follow
- minDistance - Minimum distance to maintain from target sprite (in pixels)
- SPEED pixelsPerTick - Optional movement speed in pixels per tick (default: 2)
- ELEVATION offset - (optional) Track the target's elevation plus this offset (3D follow). Eases at the follow speed; requires a PERSPECTIVE/AFFINE plane layer to be visible.
- SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach the target before beginning to follow. Default seek speed is 2.0 pixels per tick.
PATH
PATH [ABSOLUTE] (x1,y1[,z1]), (x2,y2[,z2]), ... [SPEED n] [LOOP] [PINGPONG] [SEEKFIRST [SPEED seekSpeed]] (each waypoint may also be a coordinate variable: PATH (0,0), home, (100,100))Inside a BEHAVIOR block, defines a waypoint path for sprite movement. By default, coordinates are relative to sprite's starting position. Use ABSOLUTE for screen coordinates. Absolute PATH without SEEKFIRST snaps the sprite to the first waypoint immediately. Use SEEKFIRST to make the sprite smoothly approach the first waypoint before following the path (this was the previous default behavior for absolute paths). Relative PATH is unaffected by this change.
Waypoints may include an optional third coordinate component = ELEVATION, the sprite's height above the plane layer it sits on (AFFINE/PERSPECTIVE). Elevation is interpolated along the path in lockstep with x/y, so a sprite can arc up and over a Mode-7 floor. Elevation is always absolute (no relative-origin offset). If every waypoint is 2D, the path leaves elevation untouched — fully backward compatible.
- (x,y[,z]) - Waypoint coordinates. The optional third component z is elevation (height above a plane layer), interpolated along the path.
- ABSOLUTE - Use absolute screen coordinates instead of relative (x/y only; elevation is always absolute)
- SPEED n - Movement speed in pixels per tick (default: 2)
- LOOP - Repeat path indefinitely
- PINGPONG - Reverse direction at endpoints instead of jumping back
- SEEKFIRST [SPEED seekSpeed] - Optional modifier that causes the sprite to smoothly approach the first waypoint before following the path. Default seek speed is 2.0 pixels per tick.
Appearance
BROTATE
BROTATE from TO to DURATION ticks [ORIGIN(x, y)] [LOOP]
BROTATE offset DURATION ticks [ORIGIN(x, y)] [LOOP]Inside a BEHAVIOR block, defines rotation transition over time. Multiple BROTATE behaviors are additive. Use ORIGIN to specify the rotation pivot point. Two modes available: absolute (FROM to TO) and relative (single offset value rotates from current angle).
The pivot defaults to the sprite's top-left corner, not its centre. A behavior always sends the pivot it is configured with, and an unset ORIGIN is (0, 0) — write ORIGIN(0.5, 0.5) to spin about the centre.
- Absolute mode (with TO):
- from - Starting rotation in degrees
- to - Ending rotation in degrees
- Relative mode (without TO):
- offset - Amount to rotate in degrees from current angle
- ticks - Duration in ticks/frames
- ORIGIN(x, y) - Rotation pivot point using normalized values (0-1), relative to the sprite's top-left corner. This is independent of BORIGIN (anchor point). Default is (0, 0) = the top-left corner.
- (0, 0) = top-left corner (default)
- (0.5, 0.5) = center
- (1, 1) = bottom-right corner
- LOOP - Repeat transition indefinitely
BSCALE
BSCALE fromX, fromY TO toX, toY DURATION ticks [LOOP]Inside a BEHAVIOR block, defines scale transition over time.
- fromX, fromY - Starting scale factors
- toX, toY - Ending scale factors
- ticks - Duration in ticks/frames
- LOOP - Repeat transition indefinitely
BALPHA
BALPHA from TO to DURATION ticks [LOOP]Inside a BEHAVIOR block, defines alpha (transparency) transition over time.
- from - Starting alpha value (0-1)
- to - Ending alpha value (0-1)
- ticks - Duration in ticks/frames
- LOOP - Repeat transition indefinitely
BBLINK
BBLINK rateInside a BEHAVIOR block, makes the sprite blink by toggling visibility at the specified rate. Use rate of 0 to disable blinking.
- rate - Ticks between visibility toggles (0 = disable blinking)
BFLIPX
BFLIPX ON
BFLIPX OFFInside a BEHAVIOR block, sets horizontal flip state.
- ON - Enable horizontal flip
- OFF - Disable horizontal flip
BFLIPY
BFLIPY ON
BFLIPY OFFInside a BEHAVIOR block, sets vertical flip state.
- ON - Enable vertical flip
- OFF - Disable vertical flip
BORIGIN
BORIGIN ox, oyInside a BEHAVIOR block, sets the sprite's anchor in relative 0-1 coordinates — the single point the sprite is positioned at, parented at, and rotated around. The same anchor SET SPRITE ... ORIGIN(ox, oy) sets, moved from a behavior instead of a statement.
0, 0 is the top-left of the sprite, 0.5, 0.5 its centre, 1, 1 its bottom-right. Applied once, on the behavior's first tick.
Not the same as BROTATE ... ORIGIN(x, y). The anchor is where the sprite is; BROTATE's ORIGIN overrides the pivot for that one rotation. Setting an anchor does not start a rotation, so on its own BORIGIN has no visible effect — it changes where later positioning, parenting and rotation measure from.
Both values support VARIANCE and @paramName=default.
- ox - Horizontal anchor, relative 0-1 across the sprite (0 = left, 1 = right)
- oy - Vertical anchor, relative 0-1 down the sprite (0 = top, 1 = bottom)
BIMAGE
BIMAGE imageName [, frame]Inside a BEHAVIOR block, changes the sprite's texture to the specified image when the behavior becomes active. The image is only changed if it differs from the current image (avoids redundant texture switches). This is useful for state-based sprite changes like different character states or transformations.
- imageName - Name of a loaded image (via LOADIMG, LOADIMAGE, or TEXTTOIMAGE)
- frame - Optional frame number for sprite sheets (0-based)
ANIMATE
ANIMATE imageName [, frame1, frame2, ...] [EVENTSON(frame1, frame2, ...)] [AUTOBBOX] FRAMERATE fps
ANIMATE imageName, startFrame TO endFrame [EVENTSON(...)] [AUTOBBOX] FRAMERATE OFSPRITEInside a BEHAVIOR block, defines animation frames to cycle through. If frame indices are omitted, all frames from the sprite sheet are used automatically. Frame indices can be listed individually or as a range using startFrame TO endFrame (e.g., 0 TO 18 expands to frames 0, 1, 2, ..., 18). Only one ANIMATE command can be active at a time per sprite (last wins). EVENTSON triggers events when specified frames become visible - these events run asynchronously and do not block the animation or tick loop. Multiple sprites can trigger the same event concurrently. EVENT NAMING: The event name is the animation image name UPPERCASED followed by '_FRAME'. For example, ANIMATE "player" fires events named 'PLAYER_FRAME', and ANIMATE "walkSprite" fires 'WALKSPRITE_FRAME'. This means multiple behaviors sharing the same animation image fire the same event. Register handlers with empty parens to receive dynamic args (sprite_id, frame_number). Use STOP THREAD "PLAYER_FRAME" to stop all running handlers.
FRAMERATE OFSPRITE drives the timing from the image's own Aseprite-authored per-frame durations instead of a single fps — each frame can be on screen for a different length of time, exactly as set on the Aseprite timeline. It requires an image loaded from an .ase/.aseprite file; using it on a plain PNG/sprite sheet (which has no per-frame durations) is a loud runtime error.
AUTOBBOX updates the sprite's bounding box every frame from the image's Aseprite per-frame slice geometry, so a hitbox that was animated in Aseprite tracks the animation automatically. Bare AUTOBBOX uses the file's first slice; AUTOBBOX "sliceName" selects a specific slice by name (case-insensitive) — useful when the file has several slices and the first isn't the collision box you want. It needs an .ase image with at least one slice (or a matching named slice); otherwise it is a loud runtime error. The sprite has a single collision box, so AUTOBBOX drives one slice; query other slices for manual checks with SPRITEBBOX(id, name$) / IMAGEBBOX(image$, name$, frame). AUTOBBOX is independent of OFSPRITE — combine it with either OFSPRITE or an explicit fps. Both options resolve identically on server and client in CrashNet, since the per-frame metadata registers on every interpreter at LOADIMG time.
- imageName - Name of loaded sprite sheet
- frame1, frame2, ... - Optional frame indices to cycle through (if omitted, uses all frames)
- startFrame TO endFrame - Optional shorthand for a contiguous range of frames (e.g., 0 TO 18 expands to 0, 1, 2, ..., 18). If start > end, frames are generated in reverse order.
- EVENTSON(frames) - Optional list of frame numbers that trigger events when displayed. Event name = UPPERCASED animation image name + '_FRAME' (e.g., ANIMATE "player" → event 'PLAYER_FRAME')
- AUTOBBOX ["sliceName"] - Optional (Aseprite only): set the sprite's bounding box each frame from a slice's per-frame geometry. Bare = first slice; with a name = that slice.
- FRAMERATE fps - Frames per second for the animation
- FRAMERATE OFSPRITE - Use the .ase file's authored per-frame durations instead of a fixed fps (Aseprite only)
BSHADER
BSHADER "shaderName" [@param=from TO to DURATION ticks [LOOP]]Behavior command that attaches a shader to a sprite and optionally animates one of its parameters over time. Use within a BEHAVIOR block to create animated shader effects like dissolves, fades, or pulsing. With the parameter clause, the value is linearly interpolated from 'from' to 'to' over the specified duration. The clause is optional — BSHADER "name" on its own just attaches the shader.
- shaderName - Name of the shader (must be defined with SHADER block)
- @param - The shader parameter to animate
- from - Starting value for the parameter
- to - Ending value for the parameter
- ticks - Duration in behavior ticks (60 ticks/second)
- LOOP - Optional: repeat the animation continuously
Syntax
BEHAVIOR "name" [INCLUDES("other1", "other2", ...)]
commands...
[ONCOLLISION
statements...
END ONCOLLISION]
[AFTERCOLLISION
statements...
END AFTERCOLLISION]
END BEHAVIOR
BEHAVIOR
"name1":
commands...
"name2":
commands...
END BEHAVIORParameters
- name - String expression for behavior name (case-insensitive)
- INCLUDES("name1", "name2", ...) - Optional. Include commands from other behaviors. Included commands come first, then this behavior's commands. Included behaviors must be defined before this one. Parameter defaults are also inherited (later includes and this behavior's params override earlier ones).
- IF cond THEN / ELSEIF / ELSE / END IF, and SELECT CASE sel / CASE ... / CASE ELSE / END SELECT - Declaration-time control flow: chooses which commands are baked into the behavior when it is declared (evaluated once, not per tick). Single-line IF cond THEN command is also allowed.
- Commands available inside BEHAVIOR blocks:
- ONCOLLISION ... END ONCOLLISION - Inline BASIC run when this sprite BEGINS touching something. Edge-triggered: fires once when the contact starts, not every tick it lasts. In scope: index_a/tag_a$ (this sprite), index_b/tag_b$ (the other sprite, or -1 and the boundary tag), edge$, and impact_vx/impact_vy — the velocity at impact, before resolution zeroes it. Blocking statements (PAUSE, DELAY, DELAY UNTIL, YIELD) are rejected: handlers run inline on the behavior tick loop, so waiting there would freeze every sprite.
- AFTERCOLLISION ... END AFTERCOLLISION - Inline BASIC run when a contact ENDS. Same scoped values, describing the contact as it began. A ghost overlap stays live until the sprites separate, so GHOST from ONCOLLISION defers this until the sprite is clear.
- Collision handler bodies scope like a SUB: assigning a name never declared at program scope creates a local that dies with the handler. Declare variables you want to persist before the BEHAVIOR block. INCLUDES runs every inherited handler, in composition order.
- Parameter references: Any numeric value in behavior commands can use @paramName=defaultValue syntax. The default value is used if no override is provided when the behavior is activated. Parameter values are evaluated once at assignment time.
- Attribute persistence on deactivation
- When a behavior is deactivated (via DELETE or NONE), some attributes revert while others persist:
- REVERT: Visibility (BBLINK restores visible=true), Velocity (BVELOCITY zeros controlled axes if no other behavior controls them), Shaders (BSHADER removes the shader), Bounce (BBOUNCE clears the sprite's bounce profile if no other active behavior provides one)
- PERSIST: Position (BMOVE, PATH, BFOLLOW, BORBIT, BOSCILLATE, BSHAKE changes remain), Alpha (BALPHA final value), Scale (BSCALE final values), Rotation (BROTATE final angle), Flip state (BFLIPX/BFLIPY), Image/Frame (ANIMATE, BIMAGE), Anchor point (BORIGIN)
- SPECIAL: Acceleration velocity (BACCELERATION) persists when a single behavior is deactivated but clears when ALL behaviors are cleared (BEHAVIOR NONE)
Examples
BEHAVIOR "walkRight"
ANIMATE "player" FRAMERATE 10 ' Uses all frames
BMOVE 2, 0
END BEHAVIORBEHAVIOR "walkRight"
ANIMATE "player", 0, 1, 2, 3 FRAMERATE 10 ' Explicit frames
BMOVE 2, 0
END BEHAVIOR' Declaration-time IF: bake in different commands per difficulty.
' Re-declare with a changed variable to swap the behavior.
BEHAVIOR "enemy"
IF hardMode THEN
BMOVE 4, 0
BGRAVITY 0.5
ELSE
BMOVE 2, 0
END IF
IF hardMode THEN BSHAKE AMPLITUDE 2
END BEHAVIOR' Declaration-time SELECT CASE on a level variable.
BEHAVIOR "enemy"
SELECT CASE level
CASE 1
BMOVE 1, 0
CASE 2, 3
BMOVE 2, 0
CASE IS > 5
BMOVE 6, 0
CASE ELSE
BMOVE 3, 0
END SELECT
END BEHAVIORBEHAVIOR "fadeIn"
BALPHA 0 TO 1 DURATION 30
END BEHAVIORBEHAVIOR "bounce"
PATH (0,0), (0,-20), (0,0) SPEED 3 LOOP
END BEHAVIORBEHAVIOR
"idle":
ANIMATE "player", 0, 1 FRAMERATE 5
"walk":
ANIMATE "player" FRAMERATE 12 ' Uses all frames
BMOVE 3, 0
END BEHAVIOR' EVENTSON triggers events when specific frames are displayed
BEHAVIOR "walk"
ANIMATE "player", 0, 1, 2, 3 EVENTSON(1, 3) FRAMERATE 10
END BEHAVIOR
SUB OnWalkFrame(sprite_id, frame)
IF frame = 1 THEN PLAYWAV "step1"
IF frame = 3 THEN PLAYWAV "step2"
END SUB
CALL EVENT OnWalkFrame() AS "PLAYER_FRAME"' Parameterized behavior with defaults
BEHAVIOR "moveToPoint"
BMOVE @targetX=0, @targetY=0
END BEHAVIOR
' Use with overrides - values evaluated at assignment time
SET SPRITE 0 BEHAVIOR "moveToPoint", @targetX=100, @targetY=playerY
' Use with defaults (moves to 0,0)
SET SPRITE 1 BEHAVIOR "moveToPoint"' Sprite positioned by center (BORIGIN) and rotates around center (BROTATE ORIGIN)
' Note: These are independent - BORIGIN affects positioning, BROTATE ORIGIN affects rotation pivot
BEHAVIOR "spinCenter"
BORIGIN 0.5, 0.5
BROTATE 0 TO 360 DURATION 60 ORIGIN(0.5, 0.5) LOOP
END BEHAVIOR' Compose behaviors using INCLUDES
BEHAVIOR "moveRight"
BVELOCITY 2, 0
END BEHAVIOR
BEHAVIOR "wobble"
BOSCILLATE AMPLITUDE 3, 3 SPEED 0.1
END BEHAVIOR
' Combined behavior - gets moveRight + wobble commands, then adds fade
BEHAVIOR "wobblyMove" INCLUDES("moveRight", "wobble")
BALPHA 1 TO 0.8 DURATION 30 LOOP
END BEHAVIORBEHAVIOR "driftRight"
BVELOCITY 5, 0 ' Steady drift to the right
END BEHAVIORBEHAVIOR "thrust"
BVELOCITY 0, -8 ' Steady upward thrust
END BEHAVIORBEHAVIOR "driftDiagonal"
BVELOCITY 3, -5 ' Steady diagonal drift
END BEHAVIOR' Rising arc: the standing -8 thrust is overcome by accumulating gravity
BEHAVIOR "lob"
BVELOCITY 0, -8 ' Re-applied every tick, not a one-shot
BACCELERATION 0, 0.5 ' Accumulates each tick until it wins
END BEHAVIOR' Projectile with arc
BEHAVIOR "cannonball"
BVELOCITY 5, -3 ' Constant thrust
BACCELERATION 0, 0.2 ' Gravity arc
END BEHAVIOR' Knockback belongs to BIMPULSE, not BVELOCITY: it must decay and
' stop when the sprite hits something.
BEHAVIOR "knockedBack"
BIMPULSE -4, -2 ' One-shot kick into accumulated velocity
BFRICTION 0.1, 0.1 ' Decays back to rest
END BEHAVIORBEHAVIOR "rise"
BVELOCITY 0, 0, 4 ' Rise off a plane floor at constant vertical speed
END BEHAVIORBEHAVIOR "moveRight"
BMOVE 2, 0 ' Move right 2 pixels per tick
END BEHAVIORBEHAVIOR "moveDiagonal"
BMOVE -1, 1 ' Move diagonally
END BEHAVIORBEHAVIOR "moveBy"
BMOVE @dx=0, @dy=0 ' Parameterized movement — pass values at activation
END BEHAVIORBEHAVIOR "snapX"
BMOVE ABSOLUTEX 100, 0 ' Snap X to 100 (absolute), Y unchanged each tick
END BEHAVIORBEHAVIOR "snapY"
BMOVE 0, ABSOLUTEY 50 ' Y snapped to 50 (absolute), X drifts freely
END BEHAVIORBEHAVIOR "snapTo"
BMOVE ABSOLUTEX 160, ABSOLUTEY 120 ' Teleport-snap to (160, 120) each tick
END BEHAVIORBEHAVIOR "climb"
BMOVE 0, 0, 0.7 ' Rise off a plane layer (elevation +0.7/tick)
END BEHAVIOR' Lane-locked horizontal mover — snap X, drift Y
BEHAVIOR "laneRight"
BMOVE ABSOLUTEX 240, 1
END BEHAVIORBEHAVIOR "gravity"
BACCELERATION 0, 0.5 ' Simulate gravity (accelerate downward)
END BEHAVIORBEHAVIOR "rocket"
BACCELERATION 0, -0.3 ' Thrust upward (rocket)
END BEHAVIORBEHAVIOR "dragLeft"
BACCELERATION -0.1, 0 ' Decelerate leftward (friction-like)
END BEHAVIORBEHAVIOR "fall"
BACCELERATION 0, 0, -1 ' Gravity on the elevation axis (plane layers)
END BEHAVIOR' Jumping with proper physics
BEHAVIOR "jump"
BVELOCITY 0, -8 ' Initial upward velocity
BACCELERATION 0, 0.5 ' Gravity pulls back down
END BEHAVIOR' True 3D jump off a Mode-7 floor — vertical kick + elevation gravity
BEHAVIOR "hop"
BIMPULSE 0, 0, 16
BACCELERATION 0, 0, -1
END BEHAVIORBEHAVIOR "jump"
BIMPULSE 0, -8 ' Classic jump — instant upward kick
END BEHAVIORBEHAVIOR "dash"
BIMPULSE 4, 0 ' Dash to the right
END BEHAVIOR' True jump off a plane floor — vertical kick + gravity, lands on its own
BEHAVIOR "jump"
BIMPULSE 0, 0, 16
BACCELERATION 0, 0, -1
END BEHAVIORBEHAVIOR "knockback"
BIMPULSE -3 VARIANCE(-1, 1), -2 ' Slight horizontal scatter on knockback
END BEHAVIOR' Jump with gravity — impulse fires once, gravity pulls back
BEHAVIOR "jump"
BIMPULSE 0, -8
BACCELERATION 0, 0.5
END BEHAVIOR' Dash with friction-based decay — impulse, then ice-style settle
BEHAVIOR "dashRight"
BIMPULSE 6, 0
BFRICTION 0.1, 0
END BEHAVIOR' Pattern-driven recoil — re-fires on every reactivation
PATTERN "shoot"
"recoil" DURATION 8
"idle" DURATION 12
END PATTERN
BEHAVIOR "recoil"
BIMPULSE -2, 0
BFRICTION 0.2, 0
END BEHAVIOR' Parameterised — pattern can pass per-shot impulse
BEHAVIOR "kick"
BIMPULSE @ix=0, @iy=-6
END BEHAVIORBEHAVIOR "ice"
BFRICTION 0.05, 0.05 ' Ice — slow, even decay on both axes
END BEHAVIORBEHAVIOR "airDrag"
BFRICTION 0, 0, 0.05 ' Air resistance on a jump/fall (elevation axis only)
END BEHAVIORBEHAVIOR "groundDrag"
BFRICTION 0.2, 0 ' Ground drag (X only). Sprite slides but slows; falls untouched.
END BEHAVIORBEHAVIOR "fallDrag"
BFRICTION 0, 0.05 ' Air resistance (Y only). Useful for falling objects with terminal velocity.
END BEHAVIOR' Air-resistance + gravity → terminal velocity ≈ 1.0 / 0.1 = 10 px/tick
BEHAVIOR "airDrop"
BACCELERATION 0, 1.0
BFRICTION 0, 0.1
END BEHAVIOR' Slippery ice with slight randomness per object
BEHAVIOR "icePuck"
BFRICTION 0.02 VARIANCE(0, 0.03), 0.02 VARIANCE(0, 0.03)
END BEHAVIOR' Hockey-style: friction decays accel from impacts
BEHAVIOR "slidingPuck"
BFRICTION 0.04, 0.04
BBOUNCE 0.85, 0.85
END BEHAVIORBEHAVIOR "weight"
BGRAVITY 0.5 ' Standard weight on a plane layer
END BEHAVIOR' Jump: kick up, gravity brings it back down
BEHAVIOR "jump"
BIMPULSE 0, 0, 16
BGRAVITY 1
END BEHAVIOR' Bouncing ball on the floor
BEHAVIOR "ball"
BGRAVITY 0.4
BBOUNCE 0, 0, 0.7
END BEHAVIOR' Capped fall speed (terminal velocity ~= 0.5 / 0.05 = 10)
BEHAVIOR "feather"
BGRAVITY 0.5
BFRICTION 0, 0, 0.05
END BEHAVIORBEHAVIOR "liftOff"
BELEVATE 0 TO 60 DURATION 14 ' Rise 60 units over 14 ticks
END BEHAVIORREM A continuous hop: sequence rise + fall in a looping pattern
BEHAVIOR "RiseUp"
BELEVATE 0 TO 60 DURATION 14
END BEHAVIOR
BEHAVIOR "FallDown"
BELEVATE 60 TO 0 DURATION 14
END BEHAVIOR
PATTERN "Hop" LOOP
"RiseUp" DURATION 14
"FallDown" DURATION 14
END PATTERNBEHAVIOR "rubberBall"
BBOUNCE 0.9, 0.9 ' Rubber ball — almost-perfect bounce on both axes
END BEHAVIORBEHAVIOR "ballOnGround"
BBOUNCE 0.9, 0.5 ' Bouncy side-to-side, dampened up-down (ball on grass)
END BEHAVIORBEHAVIOR "dribble"
BBOUNCE 0.7, 0 ' Rolls along floor, no Y bounce (basketball dribble)
END BEHAVIORBEHAVIOR "superBall"
BBOUNCE 1.2, 1.2 ' Energetic — gains speed each hit (rare; for power-ups)
END BEHAVIOR' Bouncing ball on a plane floor (3D): drops, rebounds, settles
BEHAVIOR "ball3d"
BGRAVITY 0.4
BBOUNCE 0, 0, 0.7
END BEHAVIOR' Particle system with natural jitter — BIMPULSE, so each spark's
' sideways motion survives a bounce
BEHAVIOR "sparkBounce"
BBOUNCE 0.85 VARIANCE(-0.1, 0.05), 0.85 VARIANCE(-0.1, 0.05)
BIMPULSE 4 VARIANCE(-3, 3), -6 VARIANCE(-2, 2)
BACCELERATION 0, 0.3
END BEHAVIOR' Classic bouncing ball with gravity
BEHAVIOR "ball"
BBOUNCE 0.85, 0.85
BIMPULSE 5, 0 ' one-shot kick; BVELOCITY would not rebound
BACCELERATION 0, 0.4 ' gravity, bounces off the floor
END BEHAVIOR' Projectile that ricochets off walls then dies
BEHAVIOR "ricochet"
BBOUNCE 0.95, 0.95
BIMPULSE 8, 0 ' accumulated momentum is what reflects
BALPHA 1 TO 0 DURATION 120
END BEHAVIORBEHAVIOR "halt"
BSTOP ' Hard halt — zeroes all velocity and acceleration state
END BEHAVIOR' Stop pattern — typically transitioned into from another pattern
BEHAVIOR "halt"
BSTOP
END BEHAVIOR
PATTERN "stop" LOOP
"halt" WHEN 1=1
END PATTERN' WHEN-triggered stop — kill all motion the moment a flag goes true
PATTERN "chase" LOOP
"chasePlayer" WHEN player_visible = 1
"halt" WHEN player_visible = 0
END PATTERN' Use after a knockback impulse to clamp the recovery period
PATTERN "hit"
"knockback" DURATION 8
"freeze" DURATION 12
END PATTERN
BEHAVIOR "knockback"
BIMPULSE -3, -2
END BEHAVIOR
BEHAVIOR "freeze"
BSTOP
END BEHAVIORBEHAVIOR "orbitCentre"
BORBIT (160, 120) DISTANCE 50 ' Orbit around screen center
END BEHAVIORBEHAVIOR "slowOrbit"
BORBIT (160, 120) DISTANCE 40 SPEED 3 ' Slower orbit at 3 degrees/tick
END BEHAVIORBEHAVIOR "orbitPlayer"
BORBIT SPRITE 0 DISTANCE 30 ' Orbit around sprite 0
END BEHAVIORBEHAVIOR "orbitBackwards"
BORBIT SPRITE 0 DISTANCE 40 COUNTERCLOCKWISE ' Orbit counterclockwise
END BEHAVIORBEHAVIOR "spiralIn"
BORBIT (160, 120) DISTANCE 80 DECAY 0.5 ' Spiral inward
END BEHAVIOR' Satellite orbiting planet
BEHAVIOR "satellite"
ANIMATE "moon", 0, 1 FRAMERATE 5
BORBIT SPRITE 0 DISTANCE 50 SPEED 2
END BEHAVIOR' Shield effect orbiting player
BEHAVIOR "shield"
BORBIT SPRITE 0 DISTANCE 32 SPEED 8 CLOCKWISE
BROTATE 360 DURATION 60 LOOP
END BEHAVIOR' Decaying orbit (spiraling into target)
BEHAVIOR "spiralIn"
BORBIT SPRITE 0 DISTANCE 60 SPEED 6 DECAY 0.3
END BEHAVIOR' Vertical 3D ring (wheel) on a plane layer (8 balls)
BEHAVIOR "Ring"
BORBIT (256, 280, 60) DISTANCE 70 STARTANGLE @a=0 SPEED 3 AXIS (1, 0, 0)
END BEHAVIOR
FOR i = 0 TO 7
SET SPRITE i, 256, 280, "guy", 0 LAYER 0
SET SPRITE i BEHAVIOR "Ring", @a=(i * 45)
NEXT i' Tilted ring (30deg off the floor)
BEHAVIOR "TiltRing"
BORBIT (256, 280, 40) DISTANCE 70 SPEED 3 AXIS (0.4, 0, 1)
END BEHAVIORBEHAVIOR "wobble"
BOSCILLATE AMPLITUDE 10, 5 ' Wobble horizontally and vertically
END BEHAVIORBEHAVIOR "hover"
BOSCILLATE AMPLITUDE 0, 10 ' Vertical bob only (hovering)
END BEHAVIOR' Hover above a plane floor: lift to a base height, then bob the elevation
SET SPRITE 0 ELEVATION(30)
BEHAVIOR "hover"
BOSCILLATE AMPLITUDE 0, 0, 15 SPEED 4
END BEHAVIORBEHAVIOR "sway"
BOSCILLATE AMPLITUDE 15, 0 SPEED 3 ' Slow horizontal sway
END BEHAVIORBEHAVIOR "settle"
BOSCILLATE AMPLITUDE 20, 10 DECAY 0.2 ' Wobble that fades out
END BEHAVIORBEHAVIOR "bobAndStay"
BOSCILLATE AMPLITUDE 0, 20 PERSISTENT ' Keep position when behavior ends
END BEHAVIORBEHAVIOR "bobOffset"
BOSCILLATE AMPLITUDE 0, 8 PHASE 1.5708 ' Start a quarter-cycle into the wave
END BEHAVIOR' Scatter starting phases across a swarm of mutation points
BEHAVIOR "swarmBob"
BOSCILLATE AMPLITUDE 0, 8 SPEED 4 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIOR' Floating coin effect
BEHAVIOR "float"
ANIMATE "coin", 0, 1, 2, 3 FRAMERATE 8
BOSCILLATE AMPLITUDE 0, 8
END BEHAVIOR' Wobbling ghost enemy
BEHAVIOR "ghostWobble"
BOSCILLATE AMPLITUDE 5, 3 SPEED 8
BALPHA 0.7 TO 1 DURATION 30 LOOP
END BEHAVIOR' Platform that moves up and down
BEHAVIOR "movingPlatform"
BOSCILLATE AMPLITUDE 0, 50 SPEED 2
END BEHAVIOR' Landing effect - bounce that settles
BEHAVIOR "landing"
BOSCILLATE AMPLITUDE 0, 15 SPEED 10 DECAY 0.4
END BEHAVIOR' Smooth transition between two oscillating idles — phase carries over
BEHAVIOR "idleSlow"
BOSCILLATE AMPLITUDE 0, 6 SPEED 3
END BEHAVIOR
BEHAVIOR "idleFast"
BOSCILLATE AMPLITUDE 0, 6 SPEED 8
END BEHAVIOR
' Switching idleSlow → idleFast resumes the wave from where idleSlow left itBEHAVIOR "shiver"
BSHAKE AMPLITUDE 3 ' Subtle shake
END BEHAVIORBEHAVIOR "quake"
BSHAKE AMPLITUDE 8 ' Intense shake
END BEHAVIOR' Damage feedback behavior
BEHAVIOR "damaged"
BSHAKE AMPLITUDE 4
BBLINK 5
END BEHAVIOR' Unstable platform
BEHAVIOR "unstable"
BSHAKE AMPLITUDE 2
END BEHAVIOR' Explosion tremor
BEHAVIOR "tremor"
BSHAKE AMPLITUDE 6
BALPHA 1 TO 0.5 DURATION 30 LOOP
END BEHAVIORBEHAVIOR "followPlayer"
BFOLLOW 0 DISTANCE 32 ' Follow sprite 0, stay 32 pixels away
END BEHAVIORBEHAVIOR "chase"
BFOLLOW 0 DISTANCE 50 SPEED 3 ' Follow faster at 3 pixels per tick
END BEHAVIORBEHAVIOR "droneEscort"
BFOLLOW 0 DISTANCE 24 SPEED 3 ELEVATION 30 ' Drone hovering 30 units above the target
END BEHAVIOR' Enemy following player behavior
BEHAVIOR "followPlayer"
ANIMATE "enemy", 0, 1, 2 FRAMERATE 8
BFOLLOW 0 DISTANCE 40 SPEED 2
END BEHAVIOR' Companion behavior - follows and animates
BEHAVIOR "companion"
BFOLLOW 0 DISTANCE 24 SPEED 1.5
ANIMATE "pet", 0, 1, 2, 3 FRAMERATE 10
END BEHAVIORBEHAVIOR "hop"
PATH (0,0), (0,-20), (0,0) SPEED 3 LOOP ' Bounce effect
END BEHAVIORBEHAVIOR "patrol"
PATH ABSOLUTE (100,100), (200,100) SPEED 2 LOOP PINGPONG ' Patrol
END BEHAVIORBEHAVIOR "square"
PATH (0,0), (50,0), (50,50), (0,50) LOOP ' Square path
END BEHAVIORBEHAVIOR "arc"
PATH ABSOLUTE (180,260,0), (256,260,70), (332,260,0) SPEED 3 PINGPONG ' Arc up over a plane floor (3rd component = elevation)
END BEHAVIORBEHAVIOR "spin"
BROTATE 0 TO 360 DURATION 60 LOOP ' Absolute: spin from 0 to 360
END BEHAVIORBEHAVIOR "spinRelative"
BROTATE 360 DURATION 60 LOOP ' Relative: spin 360 degrees from current
END BEHAVIORBEHAVIOR "quarterTurn"
BROTATE 90 DURATION 30 ' Relative: rotate 90 degrees from current
END BEHAVIORBEHAVIOR "spinCentred"
BROTATE 0 TO 360 DURATION 60 ORIGIN(0.5, 0.5) LOOP ' Spin around center
END BEHAVIORBEHAVIOR "tipBack"
BROTATE -45 DURATION 15 ORIGIN(0, 1) ' Rotate backwards around bottom-left
END BEHAVIORBEHAVIOR "grow"
BSCALE 1, 1 TO 2, 2 DURATION 30 ' Grow to 2x over 30 ticks
END BEHAVIORBEHAVIOR "pulse"
BSCALE 1, 1 TO 1.2, 1.2 DURATION 10 LOOP ' Pulsing effect
END BEHAVIORBEHAVIOR "fadeIn"
BALPHA 0 TO 1 DURATION 30 ' Fade in over 30 ticks
END BEHAVIORBEHAVIOR "pulseAlpha"
BALPHA 1 TO 0.3 DURATION 15 LOOP ' Pulsing blink effect
END BEHAVIORBEHAVIOR "blink"
BBLINK 10 ' Toggle visibility every 10 ticks
END BEHAVIORBEHAVIOR "fastBlink"
BBLINK 5 ' Fast blink
END BEHAVIORBEHAVIOR "stopBlink"
BBLINK 0 ' Disable blinking
END BEHAVIOR' Invincibility effect
BEHAVIOR "invincible"
BBLINK 4
END BEHAVIOR' Damaged enemy blinks
BEHAVIOR "damaged"
BBLINK 8
BALPHA 0.7 TO 1 DURATION 16 LOOP
END BEHAVIORBEHAVIOR "faceLeft"
BFLIPX ON ' Flip sprite horizontally
END BEHAVIORBEHAVIOR "faceRight"
BFLIPX OFF ' Normal orientation
END BEHAVIORBEHAVIOR "upsideDown"
BFLIPY ON ' Flip sprite vertically
END BEHAVIORBEHAVIOR "upright"
BFLIPY OFF ' Normal orientation
END BEHAVIORREM Swing from the top of the sprite rather than its middle.
BEHAVIOR "pendulum"
BORIGIN 0.5, 0
BROTATE -30 TO 30 DURATION 60 LOOP
END BEHAVIORREM Anchor a chain link at its joint so it turns about the right point.
BEHAVIOR "upperArm"
BORIGIN 0, 0.5
END BEHAVIORBEHAVIOR "run"
BIMAGE "playerRun" ' Change to running sprite
END BEHAVIORBEHAVIOR "swapToEnemy"
BIMAGE "enemy", 2 ' Change to enemy sprite, frame 2
END BEHAVIOR' Walking behavior with different sprite
BEHAVIOR "walkRight"
BIMAGE "playerWalk"
ANIMATE "playerWalk", 0, 1, 2, 3 FRAMERATE 10
BMOVE 2, 0
END BEHAVIOR' State-based appearance
BEHAVIOR "powered"
BIMAGE "playerPowered"
BSCALE 1, 1 TO 1.2, 1.2 DURATION 30 LOOP
END BEHAVIOR' Enemy transformation
BEHAVIOR "enraged"
BIMAGE "enemyAngry"
BALPHA 1 TO 0.8 DURATION 10 LOOP
END BEHAVIORBEHAVIOR "walkCycle"
ANIMATE "player" FRAMERATE 10 ' Use all frames from sprite sheet
END BEHAVIORBEHAVIOR "walkFrames"
ANIMATE "player", 0, 1, 2, 3 FRAMERATE 10 ' Explicit frames
END BEHAVIORBEHAVIOR "longWalk"
ANIMATE "player", 0 TO 18 FRAMERATE 10 ' Frame range (expands to 0, 1, 2, ..., 18)
END BEHAVIORBEHAVIOR "enemyIdle"
ANIMATE "enemy", 0, 1 FRAMERATE 5 ' Specific frames only
END BEHAVIORBEHAVIOR "attackWithCues"
ANIMATE "player", 0 TO 8 EVENTSON(3, 6) FRAMERATE 10 ' Range with event triggers
END BEHAVIOR' Aseprite: honor the file's own per-frame timing and per-frame hitboxes
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32)
BEHAVIOR "walk"
ANIMATE "HERO", 0 TO 7 AUTOBBOX FRAMERATE OFSPRITE
END BEHAVIOR' Frame events for sound effects
BEHAVIOR "walk"
ANIMATE "player", 0, 1, 2, 3 EVENTSON(1, 3) FRAMERATE 10
END BEHAVIOR
SUB OnWalkFrame(sprite_id, frame)
IF frame = 1 THEN PLAYWAV "step1"
IF frame = 3 THEN PLAYWAV "step2"
END SUB
CALL EVENT OnWalkFrame() AS "PLAYER_FRAME"' Define a dissolve behavior
BEHAVIOR "dissolve"
BSHADER "Dissolve" @progress=0 TO 1 DURATION 60
END BEHAVIOR
' Apply to sprite
SET SPRITE 1 BEHAVIOR "dissolve"' Looping pulse effect
BEHAVIOR "pulse"
BSHADER "Pulse" @intensity=0.5 TO 1.0 DURATION 30 LOOP
END BEHAVIOR' Combine with other behavior commands
BEHAVIOR "fadeAndDissolve"
BALPHA 1.0 TO 0.5 DURATION 60
BSHADER "Dissolve" @progress=0 TO 1 DURATION 60
END BEHAVIORSee also: ONCOLLISION, AFTERCOLLISION, SET SPRITE, ENABLE SPRITE, HASBEHAVIOR, SPRITEBEHAVIORS$, BEHAVIOR, CALL EVENT
HASBEHAVIOR
Behaviors · since 1.0.3
Returns 1 if the specified behavior is currently active on the sprite, 0 otherwise. Behavior name matching is case-insensitive.
Syntax
HASBEHAVIOR(spriteNum, "behaviorName")Parameters
- spriteNum - Sprite slot number (0-99,999)
- behaviorName - Name of behavior to check
Examples
IF HASBEHAVIOR(0, "walkRight") THEN PRINT "Walking"IF NOT HASBEHAVIOR(0, "idle") THEN
SET SPRITE 0 BEHAVIOR "idle"
END IFSee also: BEHAVIOR, SPRITEBEHAVIORS$, SET SPRITE
HASPATTERN
Behaviors · since 1.0.3
Returns 1 (TRUE) if the specified sprite has an active pattern, 0 (FALSE) otherwise. With a pattern name, returns 1 only if that is the pattern currently running.
Use the name form to test which pattern is running, rather than comparing SPRITEPATTERN$. Pattern names match case-insensitively everywhere they are used to find something, but = on strings is case-sensitive — so IF SPRITEPATTERN$(n) = "walk" fails against a PATTERN "Walk" block, while IF HASPATTERN(n, "walk") does what you meant. This mirrors HASBEHAVIOR.
Added in 1.57.0: the optional pattern name. HASPATTERN(n) is unchanged.
Syntax
HASPATTERN(spriteNum)
HASPATTERN(spriteNum, patternName$)Parameters
- spriteNum - Sprite slot number (0-99,999)
- patternName$ - Optional. Ask about ONE pattern: returns 1 only if that is the pattern running. Matched case-insensitively, like every other pattern name.
Examples
IF HASPATTERN(0) THEN PRINT "Pattern active"IF NOT HASPATTERN(0) THEN
SET SPRITE 0 PATTERN "defaultPattern"
END IFIF HASPATTERN(playerId, "walk") THEN
PRINT "still walking"
END IFREM Case does not matter — this finds PATTERN "Walk"
IF HASPATTERN(playerId, "WALK") THEN PRINT "yes"See also: PATTERN, SPRITEPATTERN$, SET SPRITE (PATTERN), HASBEHAVIOR
ONCOLLISION
Behaviors · since 1.53.0
Inside a BEHAVIOR block, runs inline BASIC when a sprite carrying this behavior begins touching something. The block is hoisted to an anonymous handler and fired by the behavior engine.
Edge-triggered. It fires on the tick a contact starts, not every tick the contact lasts. A character landing on the ground fires ONCOLLISION once; standing there fires nothing further. The contact stays "active" for as long as the engine keeps resolving it (or, for a ghost, while the overlap persists), and only a fresh contact fires again.
ONCOLLISION observes the world before collision resolution, which is the point of it: impact_vx / impact_vy hold the velocity the sprite was travelling at when it hit, the value resolution is about to zero or reflect away. Use AFTERCOLLISION to see the world once resolution has run.
Both sprites in a sprite-to-sprite hit fire their own handler, each seeing itself as index_a — including a stationary sprite that something ran into. A sprite touching a boundary reports index_b = -1 with the boundary's tag in tag_b$; because a tag names a boundary TYPE ("wall", "lava", "water"), a wall built from many same-tagged segments is one surface and sliding across the seams does not re-fire.
Handlers run inline on the behavior tick loop, so blocking statements are rejected at definition time: PAUSE, DELAY, DELAY UNTIL and YIELD would freeze every sprite in the game, not just this one. Anything the handler changes — a behavior swap, GHOST, SOLID — takes effect from the next tick.
A handler body follows the same scoping rules as a SUB: assigning a name that was never declared at program scope creates a handler-local that vanishes when the handler returns. Declare the variables you want to persist before the BEHAVIOR block.
When a behavior is composed with INCLUDES, every inherited ONCOLLISION runs, in composition order.
Bound polygons fire these too. A behaviour attached with SET MUTATION ... BEHAVIOR is carried by the shape's sprite half, so a SET POLYGON shape reports its collisions exactly as a sprite does, and index_a is the slot it occupies.
A handler may destroy its own sprite. DISABLE SPRITE index_a is what a bullet does when it strikes, and it is safe: any handler still queued for a sprite that no longer exists is skipped rather than run against nothing. That covers the two ordinary ways it happens — a sprite carrying more than one ONCOLLISION (two hits in a tick, or a behaviour composed with INCLUDES), and a particle whose lifetime ends on the same tick its contact was recorded.
Syntax
ONCOLLISION
statements
END ONCOLLISIONParameters
- index_a - (in scope) This sprite's ID. Always the sprite the behavior is running on.
- index_b - (in scope) The other sprite's ID, or -1 when the contact is with a boundary.
- tag_a$ - (in scope) This sprite's tag.
- tag_b$ - (in scope) The other sprite's tag, or the boundary's tag when index_b is -1.
- edge$ - (in scope) Which face of the other party was struck: LEFT, RIGHT, TOP, BOTTOM or SLOPE. The two sides of one contact report opposite edges.
- impact_vx - (in scope) Horizontal velocity at the moment of impact, captured before resolution alters it.
- impact_vy - (in scope) Vertical velocity at the moment of impact, captured before resolution alters it.
- THISSPRITE is also set to index_a, matching the convention used by PATTERN steps.
Examples
' Score when the ball strikes a brick
score = 0
BEHAVIOR "ball"
BIMPULSE 6, -4
ONCOLLISION
IF tag_b$ = "brick" THEN score = score + 10
END ONCOLLISION
END BEHAVIOR' Damage scaled by how hard the landing was — impact_vy is the
' pre-resolution speed, which resolution would otherwise destroy
health = 100
BEHAVIOR "faller"
BGRAVITY 0.5
ONCOLLISION
IF edge$ = "TOP" AND impact_vy > 12 THEN
health = health - (impact_vy - 12)
END IF
END ONCOLLISION
END BEHAVIOR' Landing on the ground: swap to a behavior without gravity
BEHAVIOR "falling"
BGRAVITY 0.5
ONCOLLISION
IF tag_b$ = "ground" THEN SET SPRITE index_a BEHAVIOR "grounded"
END ONCOLLISION
END BEHAVIOR' Phase through on contact; AFTERCOLLISION fires once clear.
' Note SOLID SPRITES(...), not bare SOLID: GHOST cleared the sprite
' barrier tags, and plain SOLID only restores boundary solidity.
BEHAVIOR "phaser"
ONCOLLISION
SET SPRITE index_a GHOST
END ONCOLLISION
AFTERCOLLISION
SET SPRITE index_a SOLID SPRITES("wall")
END AFTERCOLLISION
END BEHAVIOR' A stationary hazard reacting to whatever ran into it
BEHAVIOR "spikes"
ONCOLLISION
IF tag_b$ = "player" THEN EVENT "playerHurt"
END ONCOLLISION
END BEHAVIORSee also: AFTERCOLLISION, BEHAVIOR, BOUNDARY ADD, SET SPRITE, FOR COLLISIONS, SPRITETOUCHING$
PATTERN
Behaviors · since 1.0.4
Defines a sequence of behaviors and commands to chain together over time. Patterns provide automatic behavior sequencing without program logic. Three step modes: DURATION (run for fixed ticks), completion mode (run until behavior completes), and WHEN (conditional). Special commands control visibility (SHOW, HIDE, DISABLE, FADE), collision (SOLID, GHOST), tagging (RETAG), pattern switching (GOTOPATTERN), and event firing (BEVENT). WAITFOR parks the pattern on the current step until a named event fires — other sprites' patterns keep running normally. Commands can also have WHEN clauses for conditional execution. Use LOOP to repeat. When a pattern is activated with parameters (via SET SPRITE n PATTERN "name", @param=value), those parameters are automatically passed to any behaviors the pattern activates.
Declaration-time control flow. A pattern body may contain IF/ELSEIF/ELSE/END IF and SELECT CASE/CASE/CASE ELSE/END SELECT to choose which steps are baked into the pattern. Like the BEHAVIOR equivalent, these are evaluated once, when the PATTERN statement runs (not per tick), and the matching branch's steps are included in the registered pattern. This is distinct from a step's runtime WHEN clause, which re-checks each tick: use IF/SELECT to decide what the pattern contains at declaration, and WHEN to decide when an included step fires at runtime. An included step may still carry its own WHEN.
Pattern names are case-insensitive, like behavior names. SET SPRITE n PATTERN "Walk" finds the pattern defined as "walk", and so do GOTOPATTERN, INCLUDES and EMITPARTICLE.
The steps
A pattern is a list of steps, run in order. Everything below lives INSIDE a PATTERN ... END PATTERN block — none of it is a statement you can write on its own, which is why it is documented here rather than as separate keywords.
A step is one of three shapes:
[STEP] <behaviorName> [DURATION ticks] [WHEN condition] ' run a behavior
[STEP] <command> [WHEN condition] ' do something once
[STEP] WAITFOR <eventName> [TIMEOUT ticks] ' block until an event firesEvery name here is an expression. GOTOPATTERN "attack" is the literal form of one; GOTOPATTERN "phase" + STR$(n) works exactly as well, and the same goes for a behavior name, a RETAG tag and a BEVENT event name.
WHEN steps run IN PARALLEL with the sequential steps, not after them — a WHEN can fire on the very first tick while step 0 is still running.
Running a behavior
DURATION
"behaviorName" DURATION ticksInside a PATTERN block, specifies how long a behavior step should run before transitioning to the next step. Duration is measured in ticks (1/60th of a second at 60 FPS). This is one of three step modes; omit DURATION for completion mode.
- behaviorName - Name of a defined BEHAVIOR
- ticks - Number of ticks to run this step (60 ticks = 1 second at 60 FPS)
WHEN
"behaviorName" WHEN conditionInside a PATTERN block, creates a conditional step that activates when the condition is true. WHEN steps only become active after all sequential steps (DURATION and completion mode) have finished. Multiple WHEN steps can be active simultaneously when their conditions are true.
- behaviorName - Name of a defined BEHAVIOR
- condition - Boolean expression to check each tick
Commands
SHOW
[STEP] SHOW [WHEN condition]Inside a PATTERN block, shows the sprite by setting its visibility to true. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. Useful for creating visibility sequences or conditional display.
- WHEN condition - Optional: only execute when condition becomes true
HIDE
[STEP] HIDE [WHEN condition]Inside a PATTERN block, hides the sprite by setting its visibility to false. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. The sprite still exists and can be shown again later.
- WHEN condition - Optional: only execute when condition becomes true
DISABLE
[STEP] DISABLE [WHEN condition]Inside a PATTERN block, disables and destroys the sprite. Without WHEN, this is a command step that executes immediately. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. The pattern instance is removed along with the sprite, so the step runs exactly once. Useful for conditional destruction based on game state.
- WHEN condition - Optional: only execute when condition becomes true
SOLID
[STEP] SOLID [WHEN condition]Inside a PATTERN block, marks the sprite as solid for collision detection. Solid sprites will interact with boundaries and stop at solid barriers. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running.
- WHEN condition - Optional: only execute when condition becomes true
GHOST
[STEP] GHOST [WHEN condition]Inside a PATTERN block, marks the sprite as not solid (ghost mode). Ghost sprites pass through other sprites and boundaries without collision. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running.
- WHEN condition - Optional: only execute when condition becomes true
FADE
[STEP] FADE [SPEED speedMultiplier] [WHEN condition]Inside a PATTERN block, animates the sprite's alpha from 1 to 0 over time. Unlike instant commands, FADE is a transition step that takes time to complete before advancing to the next step. The sprite fades out gradually, creating a smooth disappearance effect. Often followed by DISABLE or HIDE. Note: WHEN clause is parsed but conditional FADE is not yet fully supported at runtime.
- SPEED speedMultiplier - Optional speed multiplier (default: 1)
- SPEED 1: Fade completes in 60 ticks (~1 second)
- SPEED 2: Fade completes in 30 ticks (~0.5 seconds)
- SPEED 0.5: Fade completes in 120 ticks (~2 seconds)
- WHEN condition - Optional: parsed but not yet supported at runtime
RETAG
[STEP] RETAG "newTag" [WHEN condition]Inside a PATTERN block, changes the sprite's tag to the specified value. Without WHEN, this is a command step that executes immediately and advances to the next step. With WHEN, the command executes once when the condition becomes true. WHEN steps are evaluated IN PARALLEL with the pattern's sequential steps, not after them — a WHEN can fire on the very first tick, while step 0 is still running. Useful for changing sprite type during gameplay based on conditions.
- "newTag" - New tag string to assign to the sprite
- WHEN condition - Optional: only execute when condition becomes true
GOTOPATTERN
[STEP] GOTOPATTERN "patternName" [WHEN condition]Inside a PATTERN block, switches the sprite to a different pattern. This allows patterns to chain into other patterns based on conditions or at specific points in the sequence. When executed, the current pattern stops and the new pattern begins from its first step. Commonly used with WHEN for conditional pattern transitions like switching from patrol to attack when a player is detected.
- "patternName" - Name of the pattern to switch to (case-insensitive)
- WHEN condition - Optional: only switch patterns when condition is true
BEVENT
[STEP] BEVENT "eventName" [SYNC] [WHEN condition]Inside a PATTERN block, fires a one-time event passing the current sprite ID as an argument. The event is fired once per pattern execution (or per WHEN trigger if conditional). Any event handler registered with CALL EVENT will receive the sprite ID as its first argument. By default, the handler runs asynchronously in a new thread. The SYNC modifier makes the handler run synchronously, blocking the pattern tick until it completes. This is useful when the handler sets variables or state that subsequent pattern steps depend on.
- "eventName" - Name of the event to fire (case-insensitive, matches CALL EVENT registration)
- SYNC - Optional: run the event handler synchronously (blocks pattern tick until handler returns)
- WHEN condition - Optional: only fire event when condition is true
WAITFOR
[STEP] WAITFOR eventName [TIMEOUT ticks]Blocks this sprite's pattern on the current step until the named event fires (via EVENT or BEVENT). Other sprites' patterns are unaffected — only this one waits.
It waits for a NEW firing. The event's fire count is snapshotted when the step is reached, and the wait ends when that count changes — so an event that already fired before the pattern got here does not satisfy it. The step also parks for at least one tick on arrival, and forgets it was waiting whenever the pattern leaves the step (advance, GOTOPATTERN, loop), so coming back starts a fresh wait rather than resuming a stale one.
TIMEOUT ticks advances the pattern anyway after that many ticks, so a pattern cannot be stranded by an event that never arrives. Without it, the step waits indefinitely.
Both the event name and the timeout are expressions.
- eventName - Event to wait for. Any expression.
- TIMEOUT ticks - Optional. Advance after this many ticks even if the event never fires.
Syntax
PATTERN "name" [INCLUDES("other1", "other2", ...)] [LOOP]
[STEP] "behavior1" [DURATION ticks]
[STEP] "behavior2" [WHEN condition]
[STEP] SHOW | HIDE | DISABLE | SOLID | GHOST | FADE | RETAG | GOTOPATTERN | BEVENT [WHEN condition]
[STEP] WAITFOR "eventName" [TIMEOUT ticks]
...
END PATTERNParameters
- name - String expression for pattern name (case-insensitive)
- INCLUDES("name1", "name2", ...) - Optional. Include steps from other patterns. Included steps come first, then this pattern's steps. Included patterns must be defined before this one.
- LOOP - Optional modifier to repeat pattern after completion
- Inside PATTERN blocks:
- [STEP] "behaviorName" DURATION ticks - Run behavior for fixed duration
- [STEP] "behaviorName" - Completion mode: run until behavior completes (path ends, animation cycles, transition finishes)
- [STEP] "behaviorName" WHEN condition - Conditional: active while condition is true
- [STEP] SHOW [WHEN condition] - Show the sprite (optionally conditional)
- [STEP] HIDE [WHEN condition] - Hide the sprite (optionally conditional)
- [STEP] DISABLE [WHEN condition] - Disable/destroy the sprite (optionally conditional)
- [STEP] SOLID [WHEN condition] - Mark sprite as solid for collision (optionally conditional)
- [STEP] GHOST [WHEN condition] - Mark sprite as not solid (optionally conditional)
- [STEP] FADE [SPEED n] - Animate alpha to 0 over time (transition step)
- [STEP] RETAG "tag" [WHEN condition] - Change sprite tag (optionally conditional)
- [STEP] GOTOPATTERN "name" [WHEN condition] - Switch to a different pattern (optionally conditional)
- [STEP] BEVENT "eventName" [SYNC] [WHEN condition] - Fire a one-time event with the current sprite as argument. SYNC makes it blocking. (optionally conditional)
- [STEP] WAITFOR "eventName" [TIMEOUT ticks] - Park this sprite's pattern on the current step until the named event fires (broadcast from EVENT / BEVENT). Only firings that occur after the step starts count. Optional TIMEOUT (in pattern ticks) forces the pattern to advance after that many ticks even if the event never fires. Other sprites' patterns are unaffected. WAITFOR does not support WHEN.
- STEP keyword is optional
- Commands with WHEN execute once when condition becomes true, and can re-execute if condition cycles back to true
Examples
'Fixed duration steps
PATTERN "walkCycle" LOOP
"walkRight" DURATION 120
"idle" DURATION 60
END PATTERN'Completion mode - waits for behavior to complete
PATTERN "fadeAndDie"
"fadeOut" ' Runs until alpha transition completes
DISABLE ' Then disable the sprite
END PATTERN'Conditional steps (WHEN mode)
PATTERN "playerControl" LOOP
"idle" ' Default behavior
"walkRight" WHEN KEYDOWN("RIGHT")
"walkLeft" WHEN KEYDOWN("LEFT")
END PATTERN'Declaration-time IF / SELECT choose which steps the pattern contains
'(decided once when the PATTERN runs; WHEN still works on included steps).
PATTERN "ai" LOOP
IF boss THEN
"charge" DURATION 60
ELSE
"walk" DURATION 30
END IF
SELECT CASE mode
CASE 1
"patrol" DURATION 90
CASE ELSE
"idle" DURATION 30
END SELECT
END PATTERN'Commands with WHEN conditions
PATTERN "enemy" LOOP
"patrol"
RETAG "dead" WHEN health <= 0
DISABLE WHEN health <= 0
END PATTERN'Show/hide for blinking effect
PATTERN "spawning" LOOP
SHOW
"visible" DURATION 10
HIDE
"hidden" DURATION 10
END PATTERN'Death sequence with ghost mode
PATTERN "death"
GHOST ' Stop colliding
"hit" DURATION 10
"fall" ' Completion mode
"fadeOut" ' Completion mode
DISABLE ' Remove sprite
END PATTERN' Parameters flow from pattern to behaviors
' Define behavior with parameters:
BEHAVIOR "moveToPoint"
BMOVE @targetX=0, @targetY=0
END BEHAVIOR
' Pattern uses the parameterized behavior:
PATTERN "chase" LOOP
"moveToPoint" DURATION 60
END PATTERN
' Activate with parameters (flow to behaviors):
SET SPRITE 0 PATTERN "chase", @targetX=enemyX, @targetY=enemyY' Compose patterns using INCLUDES
PATTERN "intro"
"fadeIn" DURATION 30
"flash" DURATION 10
END PATTERN
' Combined pattern - gets intro steps first, then adds gameplay loop
PATTERN "fullGame" INCLUDES("intro") LOOP
"idle"
"walk" WHEN KEYDOWN("RIGHT")
END PATTERN' Sync pattern steps to an external event
PATTERN "bossAttack" LOOP
"windup" DURATION 30
WAITFOR "drumHit" ' Wait for the next drumHit before swinging
"swing" DURATION 20
WAITFOR "drumHit" TIMEOUT 120 ' Wait for next drumHit, but cap at 2 seconds
END PATTERNPATTERN "walk"
"walkRight" DURATION 120 ' Run walkRight for 2 seconds
END PATTERNPATTERN "rest"
"idle" DURATION 30 ' Run idle for 0.5 seconds
END PATTERNPATTERN "combo"
STEP "attack" DURATION 15 ' Optional STEP keyword
END PATTERNPATTERN "moveOnKey"
"moveLeft" WHEN KEYDOWN("LEFT") ' Move when left key is pressed
END PATTERNPATTERN "jumpOnKey"
"jump" WHEN KEYDOWN("SPACE") ' Jump when space is pressed
END PATTERN' Player control pattern
PATTERN "player" LOOP
"idle"
"walkRight" WHEN KEYDOWN("RIGHT")
"walkLeft" WHEN KEYDOWN("LEFT")
"jump" WHEN KEYDOWN("SPACE")
END PATTERNPATTERN "reveal"
SHOW ' Make sprite visible immediately
END PATTERNPATTERN "revealWhenNear"
SHOW WHEN playerNearby ' Show when player is nearby
END PATTERN' Blinking spawn effect
PATTERN "spawning" LOOP
SHOW
"visible" DURATION 10
HIDE
"pause" DURATION 10
END PATTERN' Conditional visibility
PATTERN "hiddenTrap" LOOP
"idle"
SHOW WHEN SPRITEDIST(THISSPRITE, 0) < 50
END PATTERNPATTERN "vanish"
HIDE ' Make sprite invisible immediately
END PATTERNPATTERN "hideInStealth"
HIDE WHEN stealthMode ' Hide when stealth is activated
END PATTERN' Disappear effect
PATTERN "vanish"
"fadeOut" ' Completion mode
HIDE
END PATTERN' Conditional hiding
PATTERN "shyEnemy" LOOP
"patrol"
HIDE WHEN SPRITEDIST(THISSPRITE, 0) < 30
END PATTERNPATTERN "destroy"
DISABLE ' Disable sprite immediately
END PATTERNPATTERN "destroyWhenDead"
DISABLE WHEN health <= 0 ' Destroy when health depleted
END PATTERN' Death sequence
PATTERN "death"
"hit" DURATION 10
"fall" ' Completion mode
"fadeOut" ' Completion mode
DISABLE ' Remove sprite
END PATTERN' Conditional destruction
PATTERN "enemy" LOOP
"patrol"
DISABLE WHEN GETSPRITEMETA(THISSPRITE, "health") <= 0
END PATTERN' Explosion effect
PATTERN "explode"
"explosion" ' Play animation until done
DISABLE ' Then remove
END PATTERNPATTERN "makeSolid"
SOLID ' Mark sprite as solid immediately
END PATTERNPATTERN "solidOnGround"
SOLID WHEN grounded ' Become solid when touching ground
END PATTERN' Landing sequence - become solid when landing
PATTERN "landing"
"falling" ' Fall animation
SOLID ' Become solid on landing
"idle"
END PATTERN' Conditional solidity
PATTERN "platformer" LOOP
"idle"
SOLID WHEN SPRITEY(THISSPRITE) > 100
END PATTERNPATTERN "makeGhost"
GHOST ' Mark sprite as not solid immediately
END PATTERNPATTERN "ghostWhenInvincible"
GHOST WHEN invincible ' Become ghost when invincible
END PATTERN' Invincibility power-up effect
PATTERN "invincible"
GHOST ' Pass through enemies
"flashing" DURATION 300 ' Flash for 5 seconds
SOLID ' Return to normal
END PATTERN' Conditional ghost mode
PATTERN "phaseAbility" LOOP
"idle"
GHOST WHEN KEYDOWN("SHIFT")
END PATTERN' Death sequence - become ghost before falling
PATTERN "dying"
GHOST ' Stop colliding
"deathSpin" DURATION 60
"fall"
DISABLE
END PATTERNPATTERN "fadeOut"
FADE ' Fade out over 1 second
END PATTERNPATTERN "quickFade"
FADE SPEED 2 ' Fast fade (0.5 seconds)
END PATTERNPATTERN "slowFade"
FADE SPEED 0.5 ' Slow fade (2 seconds)
END PATTERN' Death sequence with fade
PATTERN "death"
GHOST
"hit" DURATION 15
FADE SPEED 1.5
DISABLE
END PATTERN' Collectible pickup effect
PATTERN "collected"
"sparkle" DURATION 20
FADE SPEED 3
DISABLE
END PATTERN' Ghost enemy fade out
PATTERN "vanish"
"spin" DURATION 30
FADE
HIDE
END PATTERN' Slow dramatic fade
PATTERN "farewell"
"wave" DURATION 60
FADE SPEED 0.3
DISABLE
END PATTERNPATTERN "markDead"
RETAG "dead" ' Change sprite tag immediately
END PATTERNPATTERN "retagWhenDead"
RETAG "enemy" WHEN health <= 0 ' Retag when health depleted
END PATTERN' Enemy transforms when defeated
PATTERN "defeat"
GHOST
RETAG "collectible"
"spin" DURATION 60
DISABLE
END PATTERN' Conditional retagging based on state
PATTERN "enemy" LOOP
"patrol"
RETAG "enraged" WHEN GETSPRITEMETA(THISSPRITE, "health") < 20
END PATTERN' Boss phase transition
PATTERN "bossPhase2"
RETAG "boss_phase2"
"transform" DURATION 60
SOLID
END PATTERNPATTERN "toAttack"
GOTOPATTERN "attack" ' Switch to attack pattern
END PATTERNPATTERN "toIdleWhenStill"
GOTOPATTERN "idle" WHEN NOT moving
END PATTERN' Enemy AI with pattern switching
PATTERN "patrol" LOOP
"walkLeft" DURATION 60
"walkRight" DURATION 60
GOTOPATTERN "chase" WHEN playerNearby
END PATTERN' Chase pattern returns to patrol
PATTERN "chase"
"runToPlayer" DURATION 120
GOTOPATTERN "attack" WHEN playerInRange
GOTOPATTERN "patrol" ' Return to patrol if no attack
END PATTERN' State machine with patterns
PATTERN "idle" LOOP
"stand"
GOTOPATTERN "walk" WHEN KEYDOWN("RIGHT") OR KEYDOWN("LEFT")
GOTOPATTERN "jump" WHEN KEYDOWN("SPACE")
END PATTERNPATTERN "explode"
BEVENT "explosion" ' Fire explosion event asynchronously
END PATTERNPATTERN "attackSync"
BEVENT "attack" SYNC ' Fire and wait for handler to complete
END PATTERNPATTERN "attackWhenReady"
BEVENT "attack" WHEN attacking
END PATTERNPATTERN "damageWhenAttacking"
BEVENT "calcDamage" SYNC WHEN attacking ' Sync + conditional
END PATTERN' Pattern fires event when animation reaches a key frame
PATTERN "attack" LOOP
"windup" DURATION 15
BEVENT "attackHit" ' Fire hit event at attack frame
"followThrough" DURATION 15
END PATTERN' Synchronous event to set state before next step
PATTERN "boss" LOOP
BEVENT "chooseAttack" SYNC ' Handler sets ATTACKTYPE variable
"windupA" WHEN ATTACKTYPE = 1
"windupB" WHEN ATTACKTYPE = 2
END PATTERNSee also: BEHAVIOR, SET SPRITE (PATTERN), HASPATTERN, SPRITEPATTERN$, PATTERN, WHEN, EVENT, DELAY
SAVE BEHAVIOR
Behaviors · since 1.46.0
Serialize a registered behavior or pattern to a JSON file, and load it back. The unit of the foundation for a sprite rigger: author behaviors/patterns once, save them, and reuse across projects.
SAVE BEHAVIOR "name" TO file$ writes the behavior in its resolved form — declaration-time IF/SELECT is already flattened away, while @param defaults and VARIANCE are preserved. SAVE PATTERN "name" TO file$ writes the pattern's steps (keeping runtime WHEN conditions and durations intact) and its LOOP flag.
LOAD BEHAVIOR file$ / LOAD PATTERN file$ register every behavior/pattern in the file by its saved name; re-loading an existing name overwrites it, exactly like re-declaring the block. Saving an undefined name, or loading a missing/mismatched file, is a loud runtime error. The file is a single versioned JSON schema shared with rig files.
Syntax
SAVE BEHAVIOR "name" TO file$
LOAD BEHAVIOR file$
SAVE PATTERN "name" TO file$
LOAD PATTERN file$Parameters
- name - The registered behavior/pattern name to save
- file$ - JSON file path (saved via the same VFS as SAVETEXT)
Examples
BEHAVIOR "walk"
BMOVE @speed=2, 0
END BEHAVIOR
SAVE BEHAVIOR "walk" TO "walk.json"
' ...later, or in another project:
LOAD BEHAVIOR "walk.json" ' re-registers "walk", @speed default intactPATTERN "ai" LOOP
"walk" DURATION 30
"walk" WHEN KEYDOWN("RIGHT")
END PATTERN
SAVE PATTERN "ai" TO "ai.json"
LOAD PATTERN "ai.json"See also: LOAD BEHAVIOR, SAVE RIG, BEHAVIOR, PATTERN
SAVE RIG
Behaviors · since 1.46.0
Save and load a sprite rig — a tree of parent/child sprites plus the behaviors and patterns they use — as a single JSON file. This is the unit a sprite rigger produces: a torso with a head, arms, and legs attached at offsets, each part carrying its own image, hitbox, and behavior.
SAVE RIG FROM SPRITE id TO file$ walks the parent/child tree rooted at id (any depth) and captures each node: image, relative offset from its parent, whether the attachment is RIGID (orbit + inherited spin), frame, bbox, scale, flip, visibility, solid flag, tag, alpha, rotation, origin, elevation, layer, and its active behavior(s) or pattern. Every behavior/pattern referenced anywhere in the tree is collected into the file once. The root's absolute position is NOT saved — placement is per-instance; child offsets ARE saved, since they are the rig's structure.
LOAD RIG file$ registers the file's behaviors and patterns only. LOAD RIG file$ ONTO SPRITE id additionally rebuilds the tree: the root's bindings are applied to id (kept at its current position), and each child is created as a fresh sprite at its relative offset, parented, and given its bindings + behavior/pattern — recursively. Loading a behavior/pattern-only file ONTO a sprite (no tree in it) is a loud error.
Syntax
SAVE RIG FROM SPRITE id TO file$
LOAD RIG file$
LOAD RIG file$ ONTO SPRITE idParameters
- id (SAVE) - The root sprite of the rig to capture (its children are walked automatically)
- id (LOAD ... ONTO) - The sprite to use as the rebuilt rig's root; children are allocated fresh ids
- file$ - JSON rig file path
Examples
' Rig a two-part sprite, then save the whole thing.
SET SPRITE 0, 100, 100, "torso"
SET SPRITE 1, 100, 80, "head"
SET SPRITE 1 PARENT(0)
SET SPRITE 1 BEHAVIOR "bob"
SAVE RIG FROM SPRITE 0 TO "hero.rig.json"' Spawn a fresh copy of the rig onto a new root sprite.
rootId = NEXTFREESPRITE()
SET SPRITE rootId, 200, 150, "torso"
LOAD RIG "hero.rig.json" ONTO SPRITE rootId ' head re-attaches at its offset' Register a rig's behaviors/patterns without spawning sprites.
LOAD RIG "hero.rig.json"See also: SAVE BEHAVIOR, SET SPRITE, NEXTFREESPRITE, BEHAVIOR, PATTERN
SET BEHAVIOR RATE
Behaviors · since 1.57.0
Sets how many times per second the behavior engine ticks. The default is 60. Takes effect on the next tick, so a program can change pace between game states — a calm title screen, brisker gameplay — without restarting anything.
What scales and what doesn't. Anything authored in ticks scales with the rate; that is the point of the statement. Anything authored in real units is re-derived so it keeps meaning the same thing:
| Authored in | Examples | On SET BEHAVIOR RATE 120 |
|---|---|---|
| ticks | BMOVE/BIMPULSE velocity (per tick), BFRICTION decay, BOSCILLATE speed, EMITPARTICLE ticksToLive, DURATION in ticks |
runs twice as fast |
| real time | ANIMATE … FRAMERATE (frames per second), Aseprite millisecond frame durations, pattern SPEED (steps per second) |
unchanged |
So doubling the rate makes sprites move twice as quickly while their animations still play at the authored frame rate.
Other timing systems keep their own clocks and are unaffected: CALL TIMER, DELAY, COOLDOWN and shader TIME are wall-clock, not tick-driven. The behavior engine is not the program's master clock.
Multiplayer: every interpreter runs the same program, so set the rate from code they all reach — not inside an IF ME.ISHOST branch — or the host and clients will simulate motion at different speeds.
Accepts 1 to 240. Outside that range is a runtime error rather than a silent clamp: 0 has no meaning (the tick interval divides by it) and an absurd rate is a bug worth hearing about.
Syntax
SET BEHAVIOR RATE ticksPerSecondParameters
- ticksPerSecond - Behavior engine ticks per second (1-240, default 60)
Examples
REM Slow, drifting motion on the title screen; normal speed in play.
SET BEHAVIOR RATE 30
CALL ShowTitle()
SET BEHAVIOR RATE 60
CALL PlayGame()REM A bullet-time effect: everything authored in ticks slows down,
REM while animations keep their authored frame rate.
SET BEHAVIOR RATE 20
DELAY 2000 ' DELAY is wall-clock, so this is still 2 real seconds
SET BEHAVIOR RATE 60REM Rates can come from an expression, e.g. a settings value.
DIM smooth AS INTEGER
smooth = 120
SET BEHAVIOR RATE smoothSee also: BEHAVIOR, SET SPRITE, EMITPARTICLE, SET YIELD, COOLDOWN
SET MUTATION BEHAVIOR
Behaviors · since 1.4.3
Applies, removes, or clears behaviors on a MUTATION variable. When a behavior is applied, all points in the mutation move together as a unit. Multiple behaviors can be stacked. Behaviors are automatically cleared when the mutation goes out of scope.
Parameters. @param=value overrides are resolved exactly as they are for SET SPRITE ... BEHAVIOR: the definition's defaults merged with the overrides, baked into the commands at apply time. This is what lets one definition drive a pool — each slot gets its own speed, direction and spin from the same BEHAVIOR block.
Pooling. The target may be an array slot (rocks(i)), so a loop can bind a whole field of shapes to one behavior.
What a behavior can do to a mutation. Everything that describes motion or geometry: position (BMOVE, PATH, BFOLLOW, BORBIT, BOSCILLATE, BSHAKE), velocity and momentum (BVELOCITY, BACCELERATION, BIMPULSE, BFRICTION, BSTOP), and transforms (BROTATE, BSCALE, BFLIPX, BFLIPY) which are baked into the points about the shape's own extent. Appearance splits in two. BALPHA and BBLINK work on a shape bound to a polygon — they drive its sprite half, so a shape fades and blinks like anything else on screen (bind it to nothing and there is naturally nothing to fade). ANIMATE and BIMAGE do not apply at all: both select frames of an image, and a shape has no image — its colour, fill and texture come from SET POLYGON.
Poses do not drift. BROTATE, BSCALE and the flips are computed from the shape as you authored it, not from the shape as it was last posed, so scaling out and back returns the coordinates you wrote and a shape turned one degree at a time comes home after 360 of them. Writing the shape's points yourself (SET MUTATIONPOINT, re-assignment) makes that the new baseline and ends the behavior: a mutation is readable while a behavior drives it — that is how a program watches where a shape has got to — but writing it means the shape the behavior was attached to no longer exists, so the behavior is removed rather than left running against geometry it no longer describes. Re-apply it if you still want it. Behavior motion is not authoring and does not end anything.
A shape bound to a polygon is driven by its sprite half too. SET POLYGON n, BEHAVIOR "name" attaches to the same shape, and binding a polygon to a mutation that already carries a behavior copies it onto that half so its collision handlers have something to fire against. Position a shape that must KEEP its behavior with SET POLYGON n, LOCATION(x, y) rather than by writing its points.
Syntax
SET MUTATION varName BEHAVIOR "behaviorName" [, @param=value, ...]
SET MUTATION mutationArray(slot) BEHAVIOR "behaviorName" [, @param=value, ...]
SET MUTATION varName BEHAVIOR DELETE "behaviorName"
SET MUTATION varName BEHAVIOR NONEParameters
- varName - Name of the MUTATION variable
- behaviorName - Name of a defined BEHAVIOR BLOCK
- DELETE - Removes a specific behavior from the mutation
- NONE - Clears all behaviors from the mutation
Examples
'Define a movement behavior
BEHAVIOR "moveRight"
BMOVE 2, 0
END BEHAVIOR
'Apply to mutation
DIM path AS MUTATION
path = MUTATION((0, 0), (10, 10), (20, 0))
SET MUTATION path BEHAVIOR "moveRight"'Remove specific behavior
SET MUTATION path BEHAVIOR DELETE "moveRight"'Clear all behaviors
SET MUTATION path BEHAVIOR NONE'Stack multiple behaviors
BEHAVIOR "oscillate"
BOSCILLATE AMPLITUDE 0, 10
END BEHAVIOR
SET MUTATION path BEHAVIOR "moveRight"
SET MUTATION path BEHAVIOR "oscillate"See also: MUTATION, BEHAVIOR BLOCK, BEHAVIOR
SPRITEBEHAVIORS$
Behaviors · since 1.0.3
Returns a comma-separated string of all behavior names currently active on the specified sprite. Returns empty string if no behaviors are active.
Names come back spelled the way they were written when the behavior was attached. Attaching the same behavior again in different case does not list it twice — it is one behavior.
Syntax
SPRITEBEHAVIORS$(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
PRINT SPRITEBEHAVIORS$(0) ' Might print "walkRight,fadeIn"behaviors$ = SPRITEBEHAVIORS$(0)
IF behaviors$ = "" THEN
PRINT "No active behaviors"
END IFSee also: BEHAVIOR, HASBEHAVIOR, SET SPRITE
SPRITEPATTERN$
Behaviors · since 1.0.3
Returns the name of the currently active pattern on the specified sprite, or an empty string if no pattern is active.
The name comes back spelled the way the PATTERN block declared it, not the way the statement that attached it happened to be capitalised — so comparing this against your own literal keeps working now that the lookup itself ignores case.
To test WHICH pattern is running, use HASPATTERN(n, name$) instead of comparing this string. Pattern names match case-insensitively, but = on strings does not, so SPRITEPATTERN$(n) = "walk" is false for a PATTERN "Walk" block. This function is for reporting the name — showing it, logging it, saving it — not for deciding on it.
Syntax
SPRITEPATTERN$(spriteNum)Parameters
- spriteNum - Sprite slot number (0-99,999)
Examples
PRINT SPRITEPATTERN$(0) ' Might print "walkCycle"currentPattern$ = SPRITEPATTERN$(0)
IF currentPattern$ <> "attackSequence" THEN
SET SPRITE 0 PATTERN "attackSequence"
END IFSee also: PATTERN, HASPATTERN, SET SPRITE (PATTERN)
SPRITEVELOCITYX
Behaviors · since 1.0.3
Returns the sprite's current total X velocity (includes BVELOCITY, BACCELERATION, and any other velocity-affecting behaviors). Returns 0 if the sprite has no active velocity. Pass the optional literal "direction" as a second argument to get the sign of the velocity (-1, 0, or 1) instead of the raw value.
Syntax
SPRITEVELOCITYX(spriteNum [, "direction"])Parameters
- spriteNum - Sprite slot number (0-99,999)
- "direction" - Optional literal string; returns the sign of the velocity (-1, 0, or 1) instead of the raw value.
Examples
vx = SPRITEVELOCITYX(0) ' Get player's current X velocity' Check if moving right:
IF SPRITEVELOCITYX(0) > 0 THEN movingRight = 1' Cap maximum velocity:
IF ABS(SPRITEVELOCITYX(0)) > 10 THEN
PRINT "Too fast!"
END IF' Display velocity:
PRINT "VX:" + STR$(SPRITEVELOCITYX(0)) + " VY:" + STR$(SPRITEVELOCITYY(0))See also: SPRITEVELOCITYY, SPRITEDIRECTION, BEHAVIOR
SPRITEVELOCITYY
Behaviors · since 1.0.3
Returns the sprite's current total Y velocity (includes BVELOCITY, BACCELERATION, and any other velocity-affecting behaviors). Returns 0 if the sprite has no active velocity. Positive values indicate downward movement. Pass the optional literal "direction" as a second argument to get the sign of the velocity (-1, 0, or 1) instead of the raw value.
Syntax
SPRITEVELOCITYY(spriteNum [, "direction"])Parameters
- spriteNum - Sprite slot number (0-99,999)
- "direction" - Optional literal string; returns the sign of the velocity (-1, 0, or 1) instead of the raw value.
Examples
vy = SPRITEVELOCITYY(0) ' Get player's current Y velocity' Check if falling (positive Y = downward):
IF SPRITEVELOCITYY(0) > 0 THEN falling = 1' Detect landing (was falling, now stopped):
IF wasFalling AND SPRITEVELOCITYY(0) <= 0 THEN
PRINT "Landed!"
END IF' Calculate total speed:
speed = SQR(SPRITEVELOCITYX(0)^2 + SPRITEVELOCITYY(0)^2)See also: SPRITEVELOCITYX, SPRITEDIRECTION, BEHAVIOR
THISSPRITE
Behaviors · since 1.12.1
A special variable that returns the sprite ID of the sprite currently being evaluated in a PATTERN WHEN condition. Only valid inside WHEN condition expressions - allows patterns to reference metadata or properties of the specific sprite they are attached to. This enables writing generic patterns that work with any sprite.
Syntax
THISSPRITEParameters
- Returns the sprite ID (0-99,999) of the sprite currently running the pattern
Examples
' Generic enemy pattern that checks its own metadata
PATTERN "enemyAI" LOOP
"patrol" WHEN GETSPRITEMETA(THISSPRITE, "state") = "patrol"
"chase" WHEN GETSPRITEMETA(THISSPRITE, "state") = "chase"
"attack" WHEN GETSPRITEMETA(THISSPRITE, "state") = "attack"
END PATTERN' Check collision with player using sprite's own ID
PATTERN "enemy" LOOP
"idle"
"attack" WHEN SPRITECOLLISION(THISSPRITE, 0)
END PATTERN' Pattern that responds to sprite's own tag
PATTERN "tagBehavior" LOOP
"normalMove" WHEN SPRITETAG$(THISSPRITE) = "normal"
"fastMove" WHEN SPRITETAG$(THISSPRITE) = "powered"
END PATTERN' Check if this sprite is near a specific position
PATTERN "zoneAware" LOOP
"idle"
"activate" WHEN SPRITEX(THISSPRITE) > 100 AND SPRITEX(THISSPRITE) < 200
END PATTERNSee also: PATTERN, GETSPRITEMETA, SPRITETAG$, SPRITEX, SPRITEY
VARIANCE
Behaviors · since 1.18.1
Adds randomness to numeric parameters in BEHAVIOR commands. When a behavior is assigned to a sprite, variance values are resolved once to create per-sprite variation. VARIANCE produces floating-point results, VARIANCEINT produces integer results. The NONZERO modifier ensures the result is never zero (useful for velocities where zero would mean no movement).
Syntax
value VARIANCE(low, high)
value VARIANCEINT(low, high)
value VARIANCEINT(low, high, NONZERO)Parameters
- value - Base value to add variance to
- low - Minimum variance offset (can be negative)
- high - Maximum variance offset
- Result = value + random(low, high)
- VARIANCE - Floating-point result
- VARIANCEINT - Integer result (floored)
- NONZERO - Only for VARIANCEINT: guarantees result is not zero
Examples
' Float variance on velocity
BEHAVIOR "spread"
BVELOCITY 0 VARIANCE(-3, 3), -5 VARIANCE(-1, 1)
END BEHAVIOR' Integer variance (useful for frame rates, pixel values)
BEHAVIOR "randomAnim"
ANIMATE "spark" FRAMERATE 8 VARIANCEINT(-2, 4)
END BEHAVIOR' NONZERO ensures movement (never zero velocity)
BEHAVIOR "scatter"
BVELOCITY 0 VARIANCEINT(-5, 5, NONZERO), 0 VARIANCEINT(-5, 5, NONZERO)
END BEHAVIOR' Variance on multiple parameters
BEHAVIOR "randomOrbit"
BORBIT SPRITE 0 DISTANCE 50 VARIANCE(-10, 10) SPEED 3 VARIANCE(-1, 2)
END BEHAVIOR' Particle spread effect
BEHAVIOR "explosion"
BVELOCITY 0 VARIANCEINT(-8, 8, NONZERO), 0 VARIANCEINT(-8, 8, NONZERO)
BACCELERATION 0, 0.2
BALPHA 1 TO 0 DURATION 30 VARIANCEINT(-5, 10)
END BEHAVIOR' Scatter oscillation phases so a swarm of bobbers doesn't move in lockstep
BEHAVIOR "swarmBob"
BOSCILLATE AMPLITUDE 0, 6 SPEED 4 PHASE 0 VARIANCE(0, 6.28)
END BEHAVIORSee also: BEHAVIOR
Collisions
BOUNDARY ADD
Collisions · since 1.24.0
Creates a named boundary region for collision detection. Boundaries are invisible regions that sprites can collide with, useful for walls, floors, trigger zones, hills, ramps, and level boundaries. Use FOR COLLISIONS with a boundary tag variable to detect sprite-boundary collisions. Normal boundaries trigger collisions when sprites ENTER the region. ENCLOSURE boundaries trigger collisions when sprites try to LEAVE the region (inverted boundaries).
By default the boundary is a rectangular AABB. Add the SLOPE modifier to turn it into a half-plane: the bounding rect is split along its diagonal and only the triangle anchored at the named compass corner is solid — the other half is empty. This lets you build hills, ramps, and corner cuts using the same tag-based collision API. Sprites colliding with a SOLID slope slide along the diagonal surface (velocity-projection physics) instead of being shoved perpendicular off it. Slope hits report "SLOPE" as the edge string in FOR COLLISIONS.
Syntax
BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] [SOLID] [ENCLOSURE] [SLOPE direction]Parameters
- "tag" - String expression for boundary name (e.g., "left-wall", "floor", "hill")
- (x1,y1) - Top-left corner coordinates of the boundary rectangle
- (x2,y2) - Bottom-right corner coordinates of the boundary rectangle
- layer - Optional layer number. By default, boundaries apply to ALL layers. Specify a layer number to limit collision to sprites on that specific layer only.
- SOLID - Optional keyword. When specified, sprites with behaviors cannot pass through this boundary. Their movement will be stopped at the boundary edge (or slid along it for SLOPE boundaries).
- ENCLOSURE - Optional keyword. Inverts the boundary behavior: collision triggers when sprites try to LEAVE the region instead of entering it. Useful for keeping sprites inside a play area.
- SLOPE direction - Optional keyword. Turns the bounding rect into a half-plane where only the triangle anchored at the named compass corner is solid. The diagonal of the rect is the collision surface. Direction is one of NE / NW / SE / SW (or long forms NORTHEAST / NORTHWEST / SOUTHEAST / SOUTHWEST). Compass directions map to VISUAL screen direction in CrashGFX's Y-down coordinate system: NE = upper-right corner = (xmax, ymin). NE/SW share one diagonal (visually ""); NW/SE share the other (visually "/"). The named corner is the SOLID anchor, so picking the right direction depends on whether you want a hill (sprite walks UP onto it — solid is below the diagonal, so use SE for climbs-right and SW for climbs-left) or a ceiling/overhang (sprite hits the diagonal from below — solid is above, so use NE/NW). Example: SLOPE SE on a 32×32 rect makes the lower-right triangle solid; a sprite walking right enters at the rect's left edge (where the diagonal meets the floor) and rises up the slope until it exits the right edge at the rect's full height.
Examples
BOUNDARY ADD "left-wall", (0,0)-(10,480)BOUNDARY ADD "floor", (0,470)-(640,480)BOUNDARY ADD "platform", (100,300)-(200,310), 1BOUNDARY ADD "solid-wall", (0,0)-(10,480) SOLIDBOUNDARY ADD "floor", (0,470)-(640,480), 0 SOLIDBOUNDARY ADD "play-area", (0,0)-(320,200) SOLID ENCLOSURE' Create walls around a game area
BOUNDARY ADD "left", (0,0)-(10,480) SOLID
BOUNDARY ADD "right", (630,0)-(640,480) SOLID
BOUNDARY ADD "top", (0,0)-(640,10) SOLID
BOUNDARY ADD "bottom", (0,470)-(640,480) SOLID' Hill the player climbs walking RIGHT — solid is the lower-right wedge
BOUNDARY ADD "hill-right", (200,400)-(264,432) SOLID SLOPE SE' Hill the player climbs walking LEFT — solid is the lower-left wedge
BOUNDARY ADD "hill-left", (264,400)-(328,432) SOLID SLOPE SOUTHWEST' Two ramps forming a peak — climb up the right-rising hill, then down the left-rising hill on the other side
BOUNDARY ADD "left-ramp", (100,400)-(164,432) SOLID SLOPE SE
BOUNDARY ADD "right-ramp", (164,400)-(228,432) SOLID SLOPE SW' Ceiling overhang — solid mass anchored at upper-left, sloping down to the right (sprites underneath bonk and slide along the diagonal)
BOUNDARY ADD "overhang", (50,0)-(150,80) SOLID SLOPE NWSee also: BOUNDARY DELETE, BOUNDARY CLEAR, FOR COLLISIONS, ENCLOSURE
BOUNDARY ADD FROM
Collisions · since 1.41.0
Loads boundaries from a JSON map file exported by the Crash BASIC IDE's map editor and registers each one exactly as a literal BOUNDARY ADD would. This lets you draw collision regions visually and consume them at runtime instead of hand-writing every BOUNDARY ADD.
The file is read from the game directory. Each entry carries a tag, a pixel rect, and the SOLID / ENCLOSURE / SLOPE / layer attributes, and is added under the current WITH ROOM scope (if any), matching BOUNDARY ADD.
The statement fails loudly: a missing file, malformed JSON, an invalid slope, or an entry that is both ENCLOSURE and SLOPE halts with an error rather than registering a partial set.
File format
{
"boundaries": [
{ "tag": "floor", "rect": [0, 480, 640, 500], "solid": true, "enclosure": false, "slope": null, "layer": null },
{ "tag": "ramp", "rect": [100, 400, 200, 480], "solid": true, "slope": "NE", "layer": 1 }
]
}rect is [x1, y1, x2, y2] in pixels. slope is one of NE / NW / SE / SW or null. layer is a layer number or null (all layers). Extra fields such as version, source, and generated are ignored.
Syntax
BOUNDARY ADD FROM("file.json")Parameters
- "file.json" - String expression naming the boundaries map file, relative to the game directory.
Examples
BOUNDARY ADD FROM("level1.boundaries.json")' Load per-level collision drawn in the IDE map editor
level$ = "forest"
BOUNDARY ADD FROM(level$ + ".boundaries.json")See also: BOUNDARY ADD, BOUNDARY DELETE, BOUNDARY CLEAR, FOR COLLISIONS
BOUNDARY CLEAR
Collisions · since 1.0.3
Removes all boundary regions. This is useful for level transitions or resetting the game state.
Syntax
BOUNDARY CLEARExamples
BOUNDARY CLEAR' Level transition
BOUNDARY CLEAR
CALL LoadLevel(nextLevel)See also: BOUNDARY ADD, BOUNDARY DELETE
BOUNDARY DELETE
Collisions · since 1.0.3
Removes a boundary region by its tag name. After deletion, the boundary will no longer be detected in collision checks.
Syntax
BOUNDARY DELETE "tag"Parameters
- "tag" - String expression for the boundary name to delete
Examples
BOUNDARY DELETE "left-wall"BOUNDARY DELETE boundaryName$See also: BOUNDARY ADD, BOUNDARY CLEAR
COLLISION
Collisions · since 0.23.0
Professional 2D collision detection with multiple algorithms. Returns 1 if collision detected, 0 if no collision. Each collision type uses only the parameters it logically needs. An unrecognized type, or too few arguments for the chosen type, is a runtime error (it does not silently return 0).
Syntax
COLLISION(type, ...) - Parameters vary by typeParameters
- BOX: COLLISION("BOX", x1, y1, w1, h1, x2, y2, w2, h2) - Rectangle vs rectangle
- CIRCLE: COLLISION("CIRCLE", cx1, cy1, r1, cx2, cy2, r2) - Circle vs circle (centers and radii)
- POINT: COLLISION("POINT", px, py, rx, ry, rw, rh) - Point in rectangle
Examples
REM Player vs enemy collision (fastest)
IF COLLISION("BOX", playerX, playerY, 32, 32, enemyX, enemyY, 24, 24) THEN
PRINT "Player hit enemy!"
END IFREM Smooth ball collision
DIM ball1X AS SINGLE, ball1Y AS SINGLE, ball1Radius AS SINGLE
DIM ball2X AS SINGLE, ball2Y AS SINGLE, ball2Radius AS SINGLE
ball1Radius = 8: ball2Radius = 10
IF COLLISION("CIRCLE", ball1X, ball1Y, ball1Radius, ball2X, ball2Y, ball2Radius) THEN
ballSpeedY = -ballSpeedY ' Bounce
END IFREM Button click detection
IF MOUSEDOWN() THEN
IF COLLISION("POINT", MOUSEX(), MOUSEY(), buttonX, buttonY, buttonW, buttonH) THEN
CALL HandleButtonClick()
END IF
END IFREM Performance optimization pattern
IF COLLISION("BOX", x1, y1, w1, h1, x2, y2, w2, h2) THEN ' Fast check first
IF COLLISION("CIRCLE", x1, y1, w1/2, x2, y2, w2/2) THEN ' Then precise circle check
REM Handle collision
END IF
END IFSee also: KEYDOWN, MOUSEX, MOUSEY, PUTIMAGE, LOADIMG, SPRITECOLLISION
ENCLOSURE (boundary modifier)
Collisions · since 1.4.6
The ENCLOSURE keyword creates an inverted boundary that keeps sprites INSIDE the defined region. Unlike normal boundaries that trigger when sprites enter them, enclosures trigger when sprites try to LEAVE them. This is perfect for defining play areas, arenas, or any region where sprites should be contained. When combined with SOLID, sprites are physically prevented from leaving the enclosure. Without SOLID, the collision is detected but sprites can still pass through (useful for triggering events when sprites leave an area).
Syntax
BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] [SOLID] ENCLOSUREParameters
- ENCLOSURE - Add at the end of BOUNDARY ADD to invert the collision behavior
- SOLID ENCLOSURE - Combine both to create an impassable container that sprites cannot escape
Examples
' Keep player inside the game screen
BOUNDARY ADD "screen", (0,0)-(320,200) SOLID ENCLOSURE' Detect when enemy leaves patrol zone (but allow it)
BOUNDARY ADD "patrol-zone", (100,50)-(250,150) ENCLOSURE' Arena boundary - sprites cannot escape
BOUNDARY ADD "arena", (20,20)-(300,180) SOLID ENCLOSURE' Trigger event when player leaves safe zone
BOUNDARY ADD "safe-zone", (50,50)-(270,150) ENCLOSURE
FOR COLLISIONS a, b, tag$
IF b = -1 AND tag$ = "safe-zone" THEN
PRINT "Warning: Leaving safe zone!"
END IF
NEXT COLLISIONSee also: BOUNDARY ADD, SOLID, FOR COLLISIONS
SOLID (boundary modifier)
Collisions · since 1.0.3
The SOLID keyword marks a boundary as impassable. When a sprite with an active behavior (movement via BMOVE, PATH, BFOLLOW, etc.) attempts to move through a SOLID boundary, its movement is stopped at the boundary edge. This allows "sliding" along walls - if movement is blocked in one direction, movement in the perpendicular direction is still allowed. Non-solid boundaries (the default) can be used as trigger zones that sprites can pass through.
Syntax
BOUNDARY ADD "tag", (x1,y1)-(x2,y2) [, layer] SOLIDParameters
- SOLID - Add at the end of BOUNDARY ADD to make the boundary solid
Examples
' Solid wall that sprites cannot pass through
BOUNDARY ADD "wall", (0,0)-(10,480) SOLID' Trigger zone that sprites pass through (non-solid, default)
BOUNDARY ADD "checkpoint", (100,100)-(150,150)' Create a platformer level with solid platforms
BOUNDARY ADD "ground", (0,470)-(640,480), 0 SOLID
BOUNDARY ADD "platform1", (100,350)-(200,360), 0 SOLID
BOUNDARY ADD "platform2", (300,250)-(400,260), 0 SOLIDSee also: BOUNDARY ADD, BEHAVIOR
SPRITECOLLISION
Collisions · since 1.15.0
Checks if a sprite is colliding with another sprite or any sprite with a specific tag. The second parameter can be a sprite index (number) or a tag string. Returns 1 (TRUE) if colliding, 0 (FALSE) if not. Uses custom BBOX if set via SET SPRITE, otherwise uses full sprite bounds. Only enabled sprites are considered for tag-based collision.
Syntax
SPRITECOLLISION(sprite, spriteOrTag)Parameters
- sprite - Sprite slot number (0-99,999)
- spriteOrTag - Either a sprite index (number) or a tag string to match against
Examples
REM Basic collision check between two sprites
IF SPRITECOLLISION(playerSprite, enemySprite) THEN
PRINT "Hit!"
END IFREM Check player against all sprites tagged "enemy"
IF SPRITECOLLISION(0, "enemy") THEN
lives = lives - 1
GOTO GameOver
END IFREM Tag-based collision for bullets hitting enemies
FOR i = 0 TO numBullets - 1
IF SPRITECOLLISION(bulletSprite(i), "enemy") THEN
CALL DestroyBullet(i)
score = score + 100
END IF
NEXTREM With custom bounding boxes for precise collision
SET SPRITE 0, playerX, playerY, "player", BBOX(0.2, 0.3, 0.8, 0.9) TAG "player"
SET SPRITE 1, enemyX, enemyY, "enemy", BBOX(0.1, 0.1, 0.9, 0.9) TAG "enemy"
IF SPRITECOLLISION(0, "enemy") THEN
REM Collided with any enemy-tagged sprite
END IFSee also: SET SPRITE, COLLISION, ENABLE SPRITE
SPRITETOUCHING$
Collisions · since 1.18.3
Returns the tag of the solid boundary or solid sprite that a sprite is touching at the specified edge. Checks boundaries first, then sprites registered via SOLID SPRITES(...). Returns an empty string if nothing is touching that edge. Use "ANY" to check all four edges at once. When the optional boundaryTag$ is provided, only checks boundaries/sprites with that tag. IMPORTANT: The sprite must have a movement behavior (BMOVE, BVELOCITY, etc.) applied for collision detection to be active.
Syntax
SPRITETOUCHING$(spriteId, edge$ [, boundaryTag$])Parameters
- spriteId - The sprite index to check
- edge$ - Which edge of the BOUNDARY to check: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY" (checks all edges, returns first match)
- boundaryTag$ - (Optional) Only check the boundary with this tag name
Examples
' Platformer ground detection — sprite standing on boundary's TOP edge
IF SPRITETOUCHING$(player, "TOP") <> "" THEN
onGround = 1
canJump = 1
ELSE
onGround = 0
END IF' Check if sprite hit a wall to the right — touching boundary's LEFT edge
IF SPRITETOUCHING$(player, "LEFT", "walls") <> "" THEN
PRINT "Hit wall!"
END IF' Check all four boundary edges
topEdge$ = SPRITETOUCHING$(0, "TOP") ' standing on it
bottomEdge$ = SPRITETOUCHING$(0, "BOTTOM") ' bumped head
leftEdge$ = SPRITETOUCHING$(0, "LEFT") ' hit wall moving right
rightEdge$ = SPRITETOUCHING$(0, "RIGHT") ' hit wall moving leftSee also: BOUNDARY ADD, SOLID, FOR COLLISIONS, SPRITEX, SPRITEY
SPRITETOUCHINGX
Collisions · since 1.12.0
Returns the X coordinate the sprite should be at to align with the boundary it's touching on the given edge. Useful for snapping sprites to walls. If not touching a boundary, returns the sprite's current X position. Supports optional boundary tag filter. Requires the sprite to have a movement behavior active for collision detection to work.
Syntax
SPRITETOUCHINGX(spriteId, edge$ [, boundaryTag$])Parameters
- spriteId - The sprite index to check
- edge$ - Which edge of the BOUNDARY: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY"
- boundaryTag$ - (Optional) Only check the boundary with this tag
Example
' Snap to wall — sprite hit boundary's LEFT edge (moving right)
IF SPRITETOUCHING$(player, "LEFT", "walls") <> "" THEN
SET SPRITE player, LOCATION(SPRITETOUCHINGX(player, "LEFT", "walls"), SPRITEY(player))
velocityX = 0
END IFSee also: SPRITETOUCHINGY, SPRITETOUCHING$, SPRITEX
SPRITETOUCHINGY
Collisions · since 1.12.0
Returns the Y coordinate the sprite should be at to align with the boundary it's touching on the given edge. Useful for snapping sprites to floors/ceilings and disabling gravity when grounded. If not touching a boundary, returns the sprite's current Y position. Supports optional boundary tag filter. Requires the sprite to have a movement behavior active for collision detection to work.
Syntax
SPRITETOUCHINGY(spriteId, edge$ [, boundaryTag$])Parameters
- spriteId - The sprite index to check
- edge$ - Which edge of the BOUNDARY: "TOP", "BOTTOM", "LEFT", "RIGHT", or "ANY"
- boundaryTag$ - (Optional) Only check the boundary with this tag
Example
' Snap to floor (boundary's TOP edge) and disable gravity
IF SPRITETOUCHING$(player, "TOP") <> "" THEN
SET SPRITE player, LOCATION(SPRITEX(player), SPRITETOUCHINGY(player, "TOP"))
velocityY = 0
onGround = 1
END IFSee also: SPRITETOUCHINGX, SPRITETOUCHING$, SPRITEY
Maps
FOR MAPDATA
Maps · since 1.4.3
Iterates over all non-empty metadata cells in the specified region of a map layer. Each iteration sets the loop variables to the coordinates, metadata character, and optionally the tile index of the next non-empty cell. Metadata is placed in the Map Editor using the Metadata layer. This is useful for finding spawn points, item locations, triggers, and other marked positions in a tile map.
Syntax
FOR MAPDATA [LAYER n] (x1, y1)-(x2, y2) AS xVar, yVar, metaVar$ [, indexVar]
statements...
NEXT MAPDATAParameters
- LAYER n - Optional. Specifies which map layer to scan (0-99). If omitted, uses the current layer.
- x1, y1 - Top-left corner of the region to scan (in tile coordinates)
- x2, y2 - Bottom-right corner of the region to scan (in tile coordinates)
- xVar - Variable to receive the X tile coordinate of each metadata cell
- yVar - Variable to receive the Y tile coordinate of each metadata cell
- metaVar$ - String variable to receive the metadata character (single char or emoji)
- indexVar - Optional. Variable to receive the tile index at each cell (0-based, -1 for empty)
- Only non-empty cells are returned; empty cells are skipped automatically
Examples
' Find player spawn point (marked with "P" in map editor)
FOR MAPDATA (0, 0)-(mapWidth, mapHeight) AS x, y, m$
IF m$ = "P" THEN
playerX = x * 16
playerY = y * 16
END IF
NEXT MAPDATA' Spawn entities based on metadata markers
FOR MAPDATA (0, 0)-(20, 15) AS x, y, m$
IF m$ = "E" THEN
CALL SpawnEnemy(x * 16, y * 16)
ELSEIF m$ = "C" THEN
CALL SpawnCoin(x * 16, y * 16)
ELSEIF m$ = "💎" THEN
CALL SpawnGem(x * 16, y * 16)
END IF
NEXT MAPDATA' Scan a specific layer with tile index
FOR MAPDATA LAYER 2 (0, 0)-(mapWidth, mapHeight) AS x, y, m$, tileIdx
IF m$ = "T" THEN
PRINT "Trigger at"; x; y; "tile:"; tileIdx
END IF
NEXT MAPDATA' Initialize game objects from map metadata
CREATE MAP("level1.map.json") AS LAYER 0
FOR MAPDATA (0, 0)-(100, 100) AS tx, ty, marker$
SELECT CASE marker$
CASE "P"
playerStartX = tx: playerStartY = ty
CASE "E"
CALL AddEnemy(tx, ty)
CASE "🚪"
CALL AddDoor(tx, ty)
CASE "🔑"
CALL AddKey(tx, ty)
END SELECT
NEXT MAPDATASee also: MAPMETA$, CREATE MAP, FOR SPRITES, FOR COLLISIONS
MAPHEIGHT
Maps · since 1.12.0
Returns the height of a tilemap layer in tiles. Pair it with MAPWIDTH for full map dimensions — iteration bounds, boundary checks, centering math. Returns 0 if the specified layer isn't a map layer or doesn't exist.
Server-side lookup
Same snapshot-fallback semantics as MAPTILE and MAPWIDTH. When the renderer-local cache is empty — which happens on player-server interpreters that never get their own CreateTilemap broadcast — MAPHEIGHT consults the per-room tilemap snapshot on NetState. Room resolution: WITH ROOM if active, else ME.ROOM, else the _global sentinel.
Syntax
MAPHEIGHT(layer_id)Parameters
- layer_id - The layer number to query (must be a tilemap layer created with CREATE MAP)
- Returns the height of the map in tiles, or 0 if not a map layer
Examples
' Get map dimensions
mapW = MAPWIDTH(0)
mapH = MAPHEIGHT(0)
PRINT "Map size:"; mapW; "x"; mapH; "tiles"' Scan bottom row of map
y = MAPHEIGHT(1) - 1
FOR x = 0 TO MAPWIDTH(1) - 1
IF MAPMETA$(x, y) = "E" THEN
exitFound = 1
END IF
NEXT x' Check if position is within map bounds
IF y >= 0 AND y < MAPHEIGHT(0) THEN
' Safe to access map at this Y coordinate
END IF' Center camera on map
cameraX = (MAPWIDTH(1) * 16) / 2 - screenWidth / 2
cameraY = (MAPHEIGHT(1) * 16) / 2 - screenHeight / 2See also: MAPWIDTH, MAPMETA$, FOR MAPDATA, CREATE MAP
MAPMETA$
Maps · since 1.4.3
Returns the metadata character at the specified tile coordinates in the current map layer. Returns an empty string if no metadata exists at that position or if coordinates are out of bounds. Metadata is placed in the Map Editor using the Metadata layer. Use this to check specific tile positions for markers like spawn points, triggers, or item locations.
Syntax
MAPMETA$(x, y)
MAPMETA$(coord) (also accepts a single coordinate value)Parameters
- x - X tile coordinate (0-based)
- y - Y tile coordinate (0-based)
- Returns a string containing the metadata character (single char or emoji), or empty string if none
Examples
' Check what's at a specific tile
marker$ = MAPMETA$(10, 5)
IF marker$ = "P" THEN
PRINT "Player spawn at tile 10,5"
END IF' Check tile under player
tileX = playerX \ 16
tileY = playerY \ 16
IF MAPMETA$(tileX, tileY) = "💀" THEN
playerHealth = playerHealth - 1
PRINT "Stepped on spikes!"
END IF' Check for trigger zones
IF MAPMETA$(doorTileX, doorTileY) = "🚪" THEN
IF hasKey THEN
CALL OpenDoor()
END IF
END IF' Validate movement destination
newTileX = (playerX + dx) \ 16
newTileY = (playerY + dy) \ 16
IF MAPMETA$(newTileX, newTileY) <> "X" THEN
' "X" marks blocked tiles
playerX = playerX + dx
playerY = playerY + dy
END IFSee also: FOR MAPDATA, CREATE MAP, MAPWIDTH, MAPHEIGHT
MAPWIDTH
Maps · since 1.12.0
Returns the width of a tilemap layer in tiles. Useful for iteration bounds, boundary checks, dynamic camera centering, anything that needs to know how big the map is. Returns 0 if the specified layer isn't a map layer or doesn't exist.
Server-side lookup
Same snapshot-fallback semantics as MAPTILE. When the renderer-local cache is empty — which happens on player-server interpreters that never get their own CreateTilemap broadcast — MAPWIDTH consults the per-room tilemap snapshot on NetState. Room resolution: WITH ROOM if active, else ME.ROOM, else the _global sentinel.
Syntax
MAPWIDTH(layer_id)Parameters
- layer_id - The layer number to query (must be a tilemap layer created with CREATE MAP)
- Returns the width of the map in tiles, or 0 if not a map layer
Examples
' Get map dimensions
mapW = MAPWIDTH(1)
mapH = MAPHEIGHT(1)
PRINT "Map size:"; mapW; "x"; mapH; "tiles"' Iterate entire map with dynamic bounds
FOR MAPDATA (0, 0)-(MAPWIDTH(1) - 1, MAPHEIGHT(1) - 1) AS x, y, m$
IF m$ = "P" THEN playerX = x * 16
NEXT MAPDATA' Bounds checking before accessing map
IF tileX >= 0 AND tileX < MAPWIDTH(0) THEN
marker$ = MAPMETA$(tileX, tileY)
END IF' Calculate total tile count
totalTiles = MAPWIDTH(1) * MAPHEIGHT(1)
PRINT "Total tiles:"; totalTilesSee also: MAPHEIGHT, MAPMETA$, FOR MAPDATA, CREATE MAP
Sound
ADD ARRANGEMENT
Sound · 📱 Client-only · since 1.51.0
Appends one step to the end of a song's arrangement.
REPEAT(n) plays the pattern n times in a row (default 1). WITH(...) layers other patterns on top of it, playing simultaneously — that is how you stack a drum loop under a melody without merging them into one pattern.
Both WITH("a", "b") and WITH "a", "b" are accepted.
Syntax
ADD ARRANGEMENT song, pattern [REPEAT(n)] [WITH(other, ...)]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Pattern to play
- REPEAT(n) - Number of consecutive plays (default 1)
- WITH(other, ...) - Patterns layered on top, played at the same time
Examples
ADD ARRANGEMENT "battle", "chorus" REPEAT(2)ADD ARRANGEMENT "battle", "verse" REPEAT(4) WITH("drums", "pad")See also: SET ARRANGEMENT, INSERT ARRANGEMENT, DELETE ARRANGEMENT, MUSIC
APPLY MUSIC
Sound · 📱 Client-only · since 1.51.0
Pushes a song's pending edits to the audio engine now, and runs its pattern generators, without playing it. Use it to warm the render up ahead of the moment the song is actually needed — during a loading screen or scene transition — so the eventual PLAYMUSIC has nothing left to do.
You never have to call this: PLAYMUSIC, FADEMUSIC and MUSICLENGTH all push pending edits themselves. It only moves the work earlier.
Syntax
APPLY MUSIC songParameters
- song - Name of the song in the music bank (matched ignoring case)
Example
MUSIC "boss"
TEMPO 168
END MUSIC
APPLY MUSIC "boss" ' render during the loading screen
' ...
PLAYMUSIC "boss" ' instantSee also: PLAYMUSIC, MUSIC, GENERATOR
ARRANGEMENTCOUNT
Sound · since 1.51.0
Number of steps in a song's arrangement.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
ARRANGEMENTCOUNT(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
FOR i = 0 TO ARRANGEMENTCOUNT("battle") - 1
PRINT ARRANGEMENTPATTERN$("battle", i); " x"; ARRANGEMENTREPEAT("battle", i)
NEXTSee also: ADD ARRANGEMENT, ARRANGEMENTPATTERN$
ARRANGEMENTLAYER$
Sound · since 1.51.0
One of the patterns an arrangement step layers on top of its main pattern.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
ARRANGEMENTLAYER$(song, step, layer)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- step - Zero-based arrangement step index
- layer - Zero-based index into that step's WITH list
Example
name$ = ARRANGEMENTLAYER$("battle", 0, 0)See also: ARRANGEMENTLAYERCOUNT, ADD ARRANGEMENT
ARRANGEMENTLAYERCOUNT
Sound · since 1.51.0
How many patterns one arrangement step layers on top of its main pattern (its WITH list).
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
ARRANGEMENTLAYERCOUNT(song, step)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- step - Zero-based arrangement step index
Example
FOR L = 0 TO ARRANGEMENTLAYERCOUNT("battle", 0) - 1
PRINT ARRANGEMENTLAYER$("battle", 0, L)
NEXTSee also: ARRANGEMENTLAYER$, ADD ARRANGEMENT
ARRANGEMENTPATTERN$
Sound · since 1.51.0
The pattern played by one arrangement step.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
ARRANGEMENTPATTERN$(song, step)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- step - Zero-based arrangement step index
Example
p$ = ARRANGEMENTPATTERN$("battle", 0)See also: ARRANGEMENTCOUNT, ARRANGEMENTREPEAT
ARRANGEMENTREPEAT
Sound · since 1.51.0
How many consecutive times one arrangement step plays its pattern.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
ARRANGEMENTREPEAT(song, step)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- step - Zero-based arrangement step index
Example
PRINT ARRANGEMENTREPEAT("battle", 1)See also: ARRANGEMENTCOUNT, ADD ARRANGEMENT
BEEP
Sound · 📱 Client-only · since 0.23.0
Produces a system beep sound.
Syntax
BEEPExamples
BEEPIF error THEN BEEP: PRINT "Error occurred"
CREATE MUSIC PATTERN
Sound · 📱 Client-only · since 1.51.0
Adds an empty pattern to a song. Add tracks to it with CREATE TRACK.
LENGTH is the pattern's length in beats; omit it to size the pattern automatically from its longest track. Creating a pattern that already exists is an error — use DELETE MUSIC PATTERN first, or a MUSIC block, which replaces.
Spelled MUSIC PATTERN rather than PATTERN because a bare PATTERN block already means a sprite behavior pattern.
Syntax
CREATE MUSIC PATTERN song, pattern [LENGTH(beats)]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- LENGTH(beats) - Pattern length in beats (optional)
Example
CREATE MUSIC PATTERN "battle", "bridge" LENGTH(16)
CREATE TRACK "battle", "bridge" CHANNEL(1) NOTES "o3 e g b"See also: DELETE MUSIC PATTERN, SET MUSIC PATTERN, CREATE TRACK, MUSIC
CREATE TRACK
Sound · 📱 Client-only · since 1.51.0
Appends a track to a pattern and returns nothing; the new track's index is the previous TRACKCOUNT. Unmentioned properties start at their defaults: channel 1, instrument 0, volume 80, centred, no notes.
Track modifiers, all optional and in any order. Anything you don't mention keeps its current value.
CHANNEL(n)— MIDI channel 1 to 16 (10 is conventionally drums)INSTRUMENT(n)— instrument number (built-ins are 0 to 66;.cbiinstruments may use any number)VOLUME(n)— 0 to 100PAN(x)— -1.0 (left) to 1.0 (right)NAME(s$)— a label for the track, shown in the Music EditorNOTES(s$)— the track's MMLGENERATOR(s$)— name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR)REPEAT/NOREPEAT— whether the track tiles to fill the pattern's length
Every value modifier accepts both VOLUME(80) and VOLUME 80.
Syntax
CREATE TRACK song, pattern [modifiers...]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- modifiers - See SET TRACK
Examples
CREATE TRACK "battle", "verse" CHANNEL(3) INSTRUMENT(5) VOLUME(70) NOTES "o4 g a b"' Add a drum track that tiles to fill the pattern
CREATE TRACK "battle", "verse" CHANNEL(10) REPEAT NOTES "c16 r16 c16 r16"See also: SET TRACK, DELETE TRACK, TRACKCOUNT, MUSIC
DELETE ARRANGEMENT
Sound · 📱 Client-only · since 1.51.0
Removes one step from a song's arrangement. Later steps shift down one index, so delete from the highest index first when removing several.
Syntax
DELETE ARRANGEMENT song, indexParameters
- song - Name of the song in the music bank (matched ignoring case)
- index - Zero-based step index
Example
DELETE ARRANGEMENT "battle", 3See also: ADD ARRANGEMENT, SET ARRANGEMENT, ARRANGEMENTCOUNT
DELETE MUSIC
Sound · 📱 Client-only · since 1.51.0
Removes a song from the interpreter's music bank. Queries and edit statements can no longer reach it.
The audio engine keeps whatever copy it already has, so a song that was played before being deleted can still be playing; STOPMUSIC to end it.
Syntax
DELETE MUSIC songParameters
- song - Name of the song in the music bank (matched ignoring case)
Example
DELETE MUSIC "scratch"See also: MUSIC, MUSICEXISTS, STOPMUSIC
DELETE MUSIC PATTERN
Sound · 📱 Client-only · since 1.51.0
Removes a pattern from a song, along with every arrangement step that played it and every WITH layer that referenced it. Leaving those behind would render as silence with no diagnostic, so they go with the pattern.
Syntax
DELETE MUSIC PATTERN song, patternParameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
Example
DELETE MUSIC PATTERN "battle", "bridge"See also: CREATE MUSIC PATTERN, DELETE ARRANGEMENT, MUSIC
DELETE TRACK
Sound · 📱 Client-only · since 1.51.0
Removes a track from a pattern. Tracks after it shift down one index, so delete from the highest index first when removing several.
Syntax
DELETE TRACK song, pattern, indexParameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
DELETE TRACK "battle", "verse", 2See also: CREATE TRACK, MUTE TRACK, TRACKCOUNT
FADEMUSIC
Sound · 📱 Client-only · since 1.6.1
Fades synthesizer music in or out over the specified duration. FADEMUSIC IN starts the specified song (if not playing) and ramps volume from 0 to full. FADEMUSIC OUT ramps volume from current to 0 and stops the music when complete. Use this for smooth transitions between songs or for dramatic effect. In multiplayer FADEMUSIC is client-only — wrap in IF ME.ISCLIENT THEN ... END IF or trigger from the server with EVENT CLIENT "name". Fire-and-forget: FADEMUSIC does not block; engine errors (song not in bank, FADEMUSIC OUT on a song that isn't playing) surface in the log as [native] AUDIO ERROR: ... but do not halt. Pre-validate with BOOM at the call site if you want loud failure on a typo.
Syntax
FADEMUSIC IN|OUT song_name DURATION secondsParameters
- IN|OUT - Fade direction (IN = fade in, OUT = fade out)
- song_name - Name of the song to fade (from loaded music bank)
- seconds - Duration of the fade in seconds
Examples
FADEMUSIC IN "Battle Theme" DURATION 2FADEMUSIC OUT "Battle Theme" DURATION 5' Start a song with fade-in (no PLAYMUSIC needed)
FADEMUSIC IN "Battle Theme" DURATION 3' Crossfade between songs
FADEMUSIC OUT "Overworld" DURATION 2
FADEMUSIC IN "Battle Theme" DURATION 2See also: PLAYMUSIC, STOPMUSIC, PAUSEMUSIC, FADEMP3
GENERATOR
Sound · 📱 Client-only · since 1.51.0
Binds a BASIC FUNCTION to a track so the track's notes are computed instead of written out — algorithmic music. The function returns a string of MML, exactly what you would otherwise put in NOTES.
The generator runs once per arrangement repeat, every time the song is played. That is the point: a song with a generator track produces a fresh take on each PLAYMUSIC, and a step written PLAY "chorus" REPEAT 4 gets four different passes rather than the same bar four times.
The function may declare 0, 1, 2 or 3 parameters and receives a prefix of (pattern$, track, repeat) — so FUNCTION Bass$() and FUNCTION Bass$(pattern$, track, repeat) are both valid. repeat is the zero-based pass number within the current arrangement step, which is what you vary on to build a fill on the last time round. Declaring more than three parameters is an error.
Because a generator returns text, it is normally declared with BASIC's string suffix. Both GENERATOR "MakeBass" and GENERATOR "MakeBass$" find FUNCTION MakeBass$.
GENERATOR "" clears the binding and turns the track back into an ordinary one.
Failures are loud: a generator that names a function that doesn't exist, returns a non-string, or returns an empty string (usually a missing RETURN) halts with an error rather than rendering a silent track.
Note that generators are resolved by the interpreter before the song reaches the audio engine, so they cost nothing at playback time — but a generative song is re-rendered on every play, where an ordinary song is rendered once and cached.
Syntax
TRACK ... GENERATOR function_name
SET TRACK song, pattern, index GENERATOR function_name
FUNCTION function_name$([pattern$[, track[, repeat]]])Parameters
- function_name - Name of a BASIC FUNCTION returning a string of MML
- pattern$ - Name of the pattern being generated (first parameter, optional)
- track - Zero-based index of the track being generated (second parameter, optional)
- repeat - Zero-based pass number within the arrangement step (third parameter, optional)
Examples
MUSIC "ambient"
PATTERN "drift"
TRACK CHANNEL(1) INSTRUMENT(5) GENERATOR "MakeArp"
END PATTERN
ARRANGEMENT
PLAY "drift" REPEAT 8
END ARRANGEMENT
END MUSIC
PLAYMUSIC "ambient"
FUNCTION MakeArp$(pattern$, track, repeat)
DIM notes(4) AS STRING
notes(0) = "c"
notes(1) = "e"
notes(2) = "g"
notes(3) = "b"
seq$ = "o4 "
FOR i = 0 TO 15
seq$ = seq$ + notes(INT(RND * 4)) + "16"
NEXT
RETURN seq$
END FUNCTION' Vary by pass: a fill on the last time round
FUNCTION MakeDrums$(pattern$, track, repeat)
IF repeat = 3 THEN
RETURN "c16 c16 c16 c16 c16 c16 c16 c16"
END IF
RETURN "c8 r8 c8 r8"
END FUNCTION' Bind and unbind at run time
SET TRACK "ambient", "drift", 0 GENERATOR "MakeArp"
SET TRACK "ambient", "drift", 0 GENERATOR "" NOTES "o4 c e g"See also: MUSIC, SET TRACK, TRACKGENERATOR$, PLAY, APPLY MUSIC
INSERT ARRANGEMENT
Sound · 📱 Client-only · since 1.51.0
Inserts a step at a position in the arrangement, shifting later steps along. index may equal ARRANGEMENTCOUNT to append; anything past that is an error.
Syntax
INSERT ARRANGEMENT song, index, pattern [REPEAT(n)] [WITH(other, ...)]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- index - Zero-based position to insert at (0 to ARRANGEMENTCOUNT)
- pattern - Pattern to play
- REPEAT(n) - Number of consecutive plays (default 1)
- WITH(other, ...) - Patterns layered on top, played at the same time
Examples
INSERT ARRANGEMENT "battle", 0, "intro"INSERT ARRANGEMENT "battle", 2, "bridge" REPEAT(2)See also: ADD ARRANGEMENT, DELETE ARRANGEMENT, ARRANGEMENTCOUNT
INSTRUMENTMODE
Sound · 📱 Client-only · since 1.23.0
Engine-wide instrument playback mode. Modern (the default) uses linear sample interpolation, the full 32-voice polyphony pool, and no output filtering — clean, modern DSP. Retro switches to a SNES-style voicing: 4-tap Gaussian sample interpolation, an 8-voice global polyphony cap with oldest-released voice-stealing, and a one-pole low-pass at 15 kHz on the output mix. The cap and the Gaussian filter together produce the soft, compressed character associated with mid-90s sampled-instrument soundtracks. Only sampled instruments (bound via SETINSTRUMENTWAV) change character audibly; synthesized waveforms are mostly unaffected except for the output low-pass. Mode can be switched at any time.
Syntax
INSTRUMENTMODE "modern" | "retro"Parameters
- mode - "modern" (default) or "retro"
Examples
INSTRUMENTMODE "retro" ' everything else plays SNES-ish from now on' Opening title uses retro, gameplay uses modern
INSTRUMENTMODE "retro"
PLAYMUSIC "Title"
' ...after title screen...
INSTRUMENTMODE "modern"
PLAYMUSIC "Overworld"See also: SETINSTRUMENTWAV, LOADWAV, PLAY
LIPSYNC
Sound · 🧪 Experimental · since 1.8.5
Analyzes a WAV or MP3 audio file and returns an array of timestamped mouth shapes (visemes) for lip sync animation. Each element is an object with TIME (absolute seconds from the start of the audio file, as a float) and SHAPE$ (viseme code). The optional sensitivity parameter (0.0 to 1.0, default 0.5) controls how much audio is detected: 1.0 picks up the quietest sounds, 0.0 picks up almost nothing. Increase sensitivity for quiet recordings; decrease it to ignore background noise. Uses FFT-based formant analysis to classify vowel and consonant mouth shapes.
Viseme codes:
- X — closed / silence
- A — open jaw ('ah')
- B — rounded lips ('oh')
- C — wide / flat ('ee' / 'eh')
- D — lip tuck ('f' / 'v' / sibilants)
- E — slightly open (neutral / 'th' / 'l')
- F — tight pucker ('w' / 'oo')
IMPORTANT: For best results, the audio file should contain an isolated voice track (speech/dialogue only). Background music, drums, and sound effects will produce inaccurate mouth shapes. If your audio has music, use external tools (e.g. Audacity vocal isolation, AI stem separators like Demucs/UVR) to extract a clean voice track, then pass that to LIPSYNC while playing the original mix for audio. TIME values are absolute timestamps, not delays. To animate in sync with playback, compute the delay between consecutive entries (see examples).
Syntax
LIPSYNC(path$ [, sensitivity])Parameters
- path$ - Path to a WAV or MP3 audio file
- sensitivity - Optional. Detection sensitivity from 0.0 (picks up nothing) to 1.0 (picks up everything). Default is 0.5; a missing or non-numeric value falls back to 0.5
Examples
lip = LIPSYNC("dialogue.wav")
FOR i = 1 TO UBOUND(lip)
PRINT lip(i).TIME; " "; lip(i).SHAPE$
NEXT iREM High sensitivity for quiet dialogue
lip = LIPSYNC("whisper.mp3", 0.9)REM Animate mouth sprite based on audio.
REM Lay out the mouth spritesheet so the cells match this viseme order:
REM cell 0 = X (closed), 1 = A, 2 = B, 3 = C, 4 = D, 5 = E, 6 = F
lip = LIPSYNC("speech.mp3")
PLAYWAV "speech.mp3", "voice"
prevTime = 0
FOR i = 1 TO UBOUND(lip)
REM TIME is absolute seconds from start of file
REM Delay by the delta between consecutive entries
DELAY lip(i).TIME - prevTime
prevTime = lip(i).TIME
shape$ = lip(i).SHAPE$
REM Map the viseme letter to its 0-based cell (X->0 closed, A->1 ... F->6)
SET SPRITE 1 FRAME INSTR("XABCDEF", shape$) - 1
NEXT iSee also: LOADWAV, LOADMP3, PLAYWAV, PLAYMP3
LOADMUSIC
Sound · 📱 Client-only · since 1.51.0
Loads a .cbm music bank and renders it, so a later PLAYMUSIC starts instantly.
Rendering is the expensive half of multi-track music. A dense song — many tracks, sampled or wavetable instruments — takes seconds to render, and that has to happen somewhere. LOADMUSIC is how you choose where: put it behind a loading screen or a scene transition instead of letting it land on the first PLAYMUSIC.
It blocks until the render is finished. That is what makes it useful — when it returns, the music is ready. To keep drawing while it works, call it from an ASYNC function and RESOLVE the promise when you need it (see the examples).
All four forms do something different:
LOADMUSIC— the defaultmusic.cbm, every song in itLOADMUSIC "levels/boss.cbm"— that bank, every song in itLOADMUSIC SONG "battle"— one song from the bank already loadedLOADMUSIC "levels/boss.cbm" SONG "battle"— that bank, one song from it
Banks merge. Loading a per-level bank adds to what is already there rather than replacing it, so the songs from music.cbm stay available. Re-stating a filename you have already loaded (the usual way to pre-render a second song from the same bank) does not re-read it, so it cannot undo SET MUSIC / SET TRACK edits you made since. A bank file whose contents actually changed does replace its songs.
Songs you built in code with a MUSIC block need no file: LOADMUSIC SONG "name" renders them the same way.
Calling it twice on the same song is cheap — an already-rendered song is not rendered again — so a loading screen can re-enter without penalty.
LOADMUSIC is an optimisation, never a prerequisite. A song you never pre-render still plays; PLAYMUSIC renders it first. Naming a song or a file that doesn't exist is a fatal error rather than a silent no-op that leaves a loading screen waiting forever.
Multiplayer: client-only, like every other resource load. Wrap in IF ME.ISCLIENT THEN ... END IF.
Syntax
LOADMUSIC [filename] [SONG song]Parameters
- filename - Path to a .cbm bank, relative to your game folder. Omit to use the bank already loaded (music.cbm is loaded for you at startup)
- SONG song - Render only this song. Omit to render every song in the bank
Examples
' Pre-render one song before the scene that needs it
LOADMUSIC SONG "battle"
PLAYMUSIC "battle" ' instant' Load a per-level bank and render all of it
LOADMUSIC "levels/boss.cbm"' Put the loading screen up, then render the music on a worker
' while the rest of the level loads.
CLS
PRINT "Loading..."
p = ASYNC PreloadMusic()
' ...other loading work happens here, overlapping the render...
ready = RESOLVE(p) ' block only where the music is actually needed
PLAYMUSIC "battle"
FUNCTION PreloadMusic()
LOADMUSIC "levels/boss.cbm" SONG "battle"
RETURN 1
END FUNCTION' Render a song that was built in code, not loaded from a file
MUSIC "victory"
TEMPO 140
PATTERN "p"
TRACK CHANNEL(1) INSTRUMENT(12) VOLUME(80) NOTES "o4 c e g o5 c"
END PATTERN
ARRANGEMENT
PLAY "p"
END ARRANGEMENT
END MUSIC
LOADMUSIC SONG "victory"See also: PLAYMUSIC, MUSIC, APPLY MUSIC, MUSICEXISTS, FADEMUSIC
MUSIC
Sound · 📱 Client-only · since 1.51.0
Declares or adjusts a multi-track song, laid out the way you would build it in the Music Editor. If the song already exists in the music bank the block edits it in place; if it doesn't, the block creates it.
Multi-track songs live in a music bank (a .cbm file, normally music.cbm, authored in the IDE's Music Editor). A song has a tempo, a loop flag, a time signature, a set of named patterns, and an arrangement that plays those patterns in order. A pattern holds tracks, and each track is one MML part on one channel with its own instrument, volume and pan. A pattern may also carry an AUDIO clip — a pre-recorded MP3 or WAV mixed in under whatever the tracks play.
Crash BASIC can build and adjust all of that at run time. Edits apply to the interpreter's copy of the bank and are sent to the audio engine automatically the next time the song is played, so you never have to think about when to push them. The .cbm file on disk is never rewritten — edits last for the run.
Only what you mention changes. A block that sets just TEMPO on an authored song keeps that song's patterns, arrangement and loop flag. A PATTERN block replaces that one pattern's track list outright, which makes re-running the same MUSIC block idempotent — a level restart won't double every track. An ARRANGEMENT block replaces the whole arrangement; an empty one clears it.
Patterns may be declared after the ARRANGEMENT block that names them; the arrangement is checked once the whole block has been applied. Arranging a pattern the song doesn't define is a fatal error rather than silent silence.
Mixing in a recording. AUDIO handle names a clip you already loaded with LOADMP3 or LOADWAV, and mixes it into the pattern alongside the tracks. The song refers to the handle, not the file — so loading a different file onto that handle changes what the song plays, without touching the song at all. Load the clip before LOADMUSIC or PLAYMUSIC; a handle with nothing on it is a fatal error naming the handle, not silence.
An audio pattern is as long as its clip, rounded up to a whole bar, so whatever plays next still lands on a downbeat. A pattern may hold tracks, an AUDIO clip, or both; layering a recording under a different pattern is what WITH in the arrangement is for. Like tracks, the clip is declared whole: a pattern block with no AUDIO line clears it.
The clip is baked into the song when it renders, which is what keeps it locked to the synth parts across a loop seam. Re-loading a handle re-renders the songs that use it, so a swap costs a render.
For per-property tweaks during play, use SET MUSIC and SET TRACK instead of re-declaring the song.
Syntax
MUSIC song
[TEMPO n]
[LOOP ON | LOOP OFF]
[TIMESIG beats, unit]
[PATTERN name [LENGTH beats]
TRACK [modifiers...]
[AUDIO handle [modifiers...]]
...
END PATTERN]
[ARRANGEMENT
PLAY pattern [REPEAT n] [WITH other, ...]
...
END ARRANGEMENT]
END MUSICParameters
- song - Name of the song to define or edit
- TEMPO n - Beats per minute (default 120)
- LOOP ON | OFF - Whether the song repeats when played
- TIMESIG beats, unit - Time signature, e.g. 3, 4 for 3/4 (default 4, 4)
- PATTERN name [LENGTH beats] - A named pattern; LENGTH is in beats, omit to auto-size from the longest track
- TRACK [modifiers] - One MML part. Track modifiers, all optional and in any order. Anything you don't mention keeps its current value. -
CHANNEL(n)— MIDI channel 1 to 16 (10 is conventionally drums) -INSTRUMENT(n)— instrument number (built-ins are 0 to 66;.cbiinstruments may use any number) -VOLUME(n)— 0 to 100 -PAN(x)— -1.0 (left) to 1.0 (right) -NAME(s$)— a label for the track, shown in the Music Editor -NOTES(s$)— the track's MML -GENERATOR(s$)— name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR) -REPEAT/NOREPEAT— whether the track tiles to fill the pattern's length Every value modifier accepts bothVOLUME(80)andVOLUME 80. - AUDIO handle [modifiers] - Mixes a pre-recorded clip, loaded with LOADMP3 or LOADWAV under that handle, into the pattern. Modifiers are all optional and in any order; an omitted one takes its default rather than keeping a previous value. -
VOLUME(n)— 0 to 100 (default 100) -PAN(x)— -1.0 (left) to 1.0 (right) -SECTION(start, end)— mix only this region of the clip, in seconds, so one long file can supply several patterns -REPEAT/NOREPEAT— tile the clip to fill the pattern (default NOREPEAT) -OFFSET(secs)— seconds into the pattern at which the clip starts, so a recording can enter late Seconds, not beats, for both SECTION and OFFSET — they describe positions in a recording. Every value modifier accepts bothVOLUME(80)andVOLUME 80. - PLAY pattern [REPEAT n] [WITH other, ...] - One arrangement step. WITH layers other patterns on top simultaneously
Examples
MUSIC "battle"
TEMPO 140
LOOP ON
PATTERN "verse"
TRACK CHANNEL(1) INSTRUMENT(12) VOLUME(80) NOTES "o4 c d e f"
TRACK CHANNEL(10) VOLUME(90) REPEAT NOTES "c8 c8 r8 c8"
END PATTERN
ARRANGEMENT
PLAY "verse" REPEAT 4
END ARRANGEMENT
END MUSIC
PLAYMUSIC "battle"' Adjust a song the Music Editor authored, without redefining it
MUSIC "overworld"
TEMPO 96
LOOP ON
END MUSIC' Layer two patterns in one step
MUSIC "boss"
PATTERN "riff"
TRACK CHANNEL(1) NOTES "o3 e e g e"
END PATTERN
PATTERN "drums"
TRACK CHANNEL(10) REPEAT NOTES "c16 c16"
END PATTERN
ARRANGEMENT
PLAY "riff" REPEAT 8 WITH "drums"
END ARRANGEMENT
END MUSIC' Mix a recorded track under the synth parts
LOADMP3 "vocals.mp3", "vox"
MUSIC "anthem"
TEMPO 120
PATTERN "chorus"
AUDIO "vox" VOLUME 85
TRACK CHANNEL(1) INSTRUMENT(12) NOTES "o4 c e g"
END PATTERN
ARRANGEMENT
PLAY "chorus"
END ARRANGEMENT
END MUSIC
LOADMUSIC SONG "anthem"
PLAYMUSIC "anthem"' One file, two patterns, and a drum loop tiled underneath
LOADMP3 "session.mp3", "take"
LOADWAV "breakbeat.wav", "break"
MUSIC "jam"
PATTERN "intro"
AUDIO "take" SECTION(0, 8)
END PATTERN
PATTERN "verse"
AUDIO "take" SECTION(8, 24) OFFSET 0.25
AUDIO "break" REPEAT VOLUME 60
END PATTERN
END MUSIC' Swap the backing track without touching the song
LOADMP3 "day_theme.mp3", "bed"
MUSIC "level"
PATTERN "main"
AUDIO "bed"
END PATTERN
END MUSIC
PLAYMUSIC "level"
' ...night falls
LOADMP3 "night_theme.mp3", "bed" ' same handle, different file
PLAYMUSIC "level" ' re-renders with the new recordingSee also: SET MUSIC, SET TRACK, GENERATOR, PLAYMUSIC, APPLY MUSIC
MUSICBEATS
Sound · since 1.51.0
Beats per measure — the top number of the song's time signature.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICBEATS(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
PRINT MUSICBEATS("waltz"); "/"; MUSICBEATUNIT("waltz")See also: MUSICBEATUNIT, SET MUSIC
MUSICBEATUNIT
Sound · since 1.51.0
Beat unit — the bottom number of the song's time signature (4 = quarter notes, 8 = eighth notes).
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICBEATUNIT(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
PRINT MUSICBEATUNIT("jig")See also: MUSICBEATS, SET MUSIC
MUSICEXISTS
Sound · since 1.51.0
Returns TRUE if the named song is in the music bank. The one query that never errors on an unknown name — use it to guard the others.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICEXISTS(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
IF MUSICEXISTS("battle") THEN PLAYMUSIC "battle"
MUSICLENGTH
Sound · since 1.7.2
Total length of a song in ticks (4 ticks per beat), measured from the last audible note of its fully expanded arrangement.
Works on any song in the bank, whether or not it has been pre-rendered or played. Pending SET MUSIC / SET TRACK edits are applied before measuring, so the number describes the song as your program has it now.
Returns 0 if the song has no notes.
Syntax
MUSICLENGTH(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Examples
ticks = MUSICLENGTH("battle")' Length in seconds, from ticks and the song's own tempo
secs = MUSICLENGTH("battle") / 4 * 60 / MUSICTEMPO("battle")See also: MUSICPLAYING, MUSICTEMPO, PLAYMUSIC
MUSICLOOP
Sound · since 1.51.0
TRUE if the song is set to loop when played.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICLOOP(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
IF MUSICLOOP("battle") THEN PRINT "loops"See also: SET MUSIC, PLAYMUSIC
MUSICPATTERNCOUNT
Sound · since 1.51.0
Number of patterns defined in a song.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICPATTERNCOUNT(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
FOR i = 0 TO MUSICPATTERNCOUNT("battle") - 1
PRINT MUSICPATTERNNAME$("battle", i)
NEXTSee also: MUSICPATTERNNAME$, CREATE MUSIC PATTERN
MUSICPATTERNLENGTH
Sound · since 1.51.0
A pattern's declared length in beats, or 0 if it sizes itself automatically from its longest track.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICPATTERNLENGTH(song, pattern)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
Example
PRINT MUSICPATTERNLENGTH("battle", "verse")See also: SET MUSIC PATTERN, CREATE MUSIC PATTERN
MUSICPATTERNNAME$
Sound · since 1.51.0
Name of one of a song's patterns. Patterns are ordered alphabetically ignoring case, so enumeration is stable from run to run.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICPATTERNNAME$(song, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- index - Zero-based pattern index
Example
p$ = MUSICPATTERNNAME$("battle", 0)See also: MUSICPATTERNCOUNT, TRACKCOUNT
MUSICPLAYING
Sound · since 1.7.2
Returns whether multi-track music is currently playing. Returns 1 (TRUE) if music is playing, 0 (FALSE) if not playing or paused. (A non-looping song that finishes on its own still reads as playing until you call STOPMUSIC — the engine sends no end-of-song notification.)
Syntax
MUSICPLAYING()Examples
IF MUSICPLAYING() THEN PRINT "Music is playing"REM Only start music if not already playing
IF NOT MUSICPLAYING() THEN
PLAYMUSIC "Background"
END IFREM Check music state
playing = MUSICPLAYING()
IF playing THEN
PRINT "Now playing..."
ELSE
PRINT "Silence"
END IFSee also: PLAYMUSIC, STOPMUSIC, PAUSEMUSIC
MUSICPOSITION
Sound · since 1.7.2
Current playback position of the multi-track song, in ticks — the same unit MUSICLENGTH reports, so the two divide into a progress fraction.
There are 4 ticks per beat, so a tick's real duration follows the song's tempo: at 120 BPM a tick is 125ms. Returns 0 when no song is playing.
A looping song's position wraps at its loop point rather than growing without bound, so it stays in step with what you can hear. A one-shot song's position runs past the end of its music and stays there, which is what you want when timing something to an outro. A paused song holds its position — a pause consumes no song time.
Use it to drive a progress bar, or to fire something at a musical point without hand-maintaining a second clock.
Syntax
MUSICPOSITION()Examples
pos = MUSICPOSITION()' How far through the song, 0.0 to 1.0
total = MUSICLENGTH("battle")
IF total > 0 THEN progress = MUSICPOSITION() / total' Fire once the song reaches the halfway mark
IF MUSICPOSITION() >= MUSICLENGTH("battle") / 2 AND NOT spawned THEN
spawned = TRUE
CALL SpawnBoss()
END IFSee also: MUSICPLAYING, MUSICLENGTH, PLAYMUSIC
MUSICTEMPO
Sound · since 1.51.0
A song's tempo in beats per minute.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
MUSICTEMPO(song)Parameters
- song - Name of the song in the music bank (matched ignoring case)
Example
SET MUSIC "battle" TEMPO(MUSICTEMPO("battle") + 10)See also: SET MUSIC, MUSICBEATS
MUTE TRACK
Sound · 📱 Client-only · since 1.51.0
Silences a single track without removing it, so it can be brought back later. Muted tracks are simply left out when the song is sent to the audio engine.
This is the natural way to build adaptive music: author every layer up front, then unmute them as the scene escalates.
Takes effect the next time the song is played. Read the state back with TRACKMUTED.
Syntax
MUTE TRACK song, pattern, index
UNMUTE TRACK song, pattern, indexParameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Examples
MUTE TRACK "battle", "verse", 2' Bring the drums in when combat starts
UNMUTE TRACK "battle", "verse", 1
PLAYMUSIC "battle"See also: SOLO TRACK, TRACKMUTED, SET TRACK
Music File Format
Sound · since 1.7.2
Multi-track music is stored in .cbm (CrashBasic Music) files, which are JSON files created in the Music Editor widget. Songs can contain multiple patterns with multiple tracks each, arranged into a complete composition. Tracks use MML notation for notes. Custom instruments can be created and edited using the Instrument Editor widget, which allows you to design synthesizer patches using waveforms, envelopes, and effects.
Syntax
.cbm JSON file formatParameters
- File Structure:
- songs - Object containing song definitions keyed by name
- Song Properties:
- name - Song name (string)
- tempo - Beats per minute (number, default 120)
- loop - Whether to loop the song (boolean)
- timeSignature - [beats, unit] like [4, 4] for 4/4 time
- patterns - Object of pattern definitions keyed by name
- arrangement - Array of arrangement steps
- Pattern Properties:
- tracks - Array of track definitions
- length - Optional length in beats (auto-calculated if omitted)
- Track Properties:
- channel - MIDI channel 1-16 (10 for drums)
- instrument - Instrument number (I0-I66, same as PLAY command)
- volume - Volume 0-100
- pan - Pan position -1.0 (left) to 1.0 (right)
- notes - MML notation string
- name - Optional track name
- Arrangement Step:
- pattern - Name of pattern to play
- repeat - Times to repeat (default 1)
- with - Additional patterns to layer
Example
REM Example .cbm file structure:
{
"songs": {
"My Song": {
"name": "My Song",
"tempo": 120,
"loop": true,
"patterns": {
"intro": {
"tracks": [
{
"channel": 1,
"instrument": 1,
"volume": 80,
"notes": "L4 C E G >C"
},
{
"channel": 2,
"instrument": 10,
"volume": 70,
"notes": "L2 C <G"
}
]
},
"verse": {
"tracks": [
{
"channel": 1,
"instrument": 1,
"volume": 80,
"notes": "L8 C D E F G A B >C"
}
]
}
},
"arrangement": [
{ "pattern": "intro" },
{ "pattern": "verse", "repeat": 2 }
]
}
}
}See also: PLAYMUSIC, PLAY, Instruments
PATTERNAUDIO$
Sound · since 1.52.0
The LOADMP3 / LOADWAV handle of the recording a pattern mixes, or an empty string if the pattern has no AUDIO clip — so IF PATTERNAUDIO$(s$, p$) <> "" THEN is the has-a-recording test.
This is the handle the song names, not a filename. What that handle currently holds is whatever was last loaded onto it, which is what lets one song play different recordings.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song or pattern that doesn't exist is a fatal error rather than a silent zero.
Syntax
PATTERNAUDIO$(song, pattern)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
Examples
PRINT PATTERNAUDIO$("anthem", "chorus")IF PATTERNAUDIO$("anthem", "chorus") <> "" THEN PRINT "chorus has a backing track"See also: MUSIC, PATTERNAUDIOVOLUME, LOADMP3, LOADWAV
PATTERNAUDIOVOLUME
Sound · since 1.52.0
The mix level of a pattern's AUDIO clip, 0 to 100.
Returns 0 for a pattern with no clip, which reads the same as a clip turned all the way down — pair it with PATTERNAUDIO$ when the difference matters.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song or pattern that doesn't exist is a fatal error rather than a silent zero.
Syntax
PATTERNAUDIOVOLUME(song, pattern)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
Example
PRINT PATTERNAUDIOVOLUME("anthem", "chorus")See also: MUSIC, PATTERNAUDIO$
PAUSEMUSIC
Sound · 📱 Client-only · since 1.7.2
Pauses the currently playing multi-track song. The song can be resumed from the same position with RESUMEMUSIC. Has no effect if no music is playing or if music is already paused.
Syntax
PAUSEMUSICExamples
PAUSEMUSICREM Pause music when game is paused
SUB PauseGame()
gamePaused = 1
PAUSEMUSIC
END SUBREM Toggle pause
IF KEYDOWN("P") THEN
IF gamePaused THEN
RESUMEMUSIC
ELSE
PAUSEMUSIC
END IF
gamePaused = NOT gamePaused
END IFSee also: RESUMEMUSIC, PLAYMUSIC, STOPMUSIC
PLAY
Sound · 📱 Client-only · since 0.23.0
Plays musical sequences using Music Macro Language (MML). Use PLAY BACKGROUND for non-blocking music that continues while program runs. Supports notes (A-G), sharps (#/+), flats (-), octaves (O0-O6, <, >), note lengths (L1-L64), tempo (T32-T255), rests (R1-R64, also P1-P64), modes (MN/ML/MS), volume (V0-V15), numeric notes (N0-N116), instruments (I0-I66 including Futuristic and Gothic NES-style patches), dotted notes (.), ties (^) that extend the previous note or rest without re-triggering, pitch bends (~ for a semitone bend within a note, & for portamento sliding to the next note), chords using [notes] syntax, and track-level effects via $NAME:p[:q] (echo, reverb, chorus, flanger, phaser, tremolo, vibrato, drive, bitcrush, filter, highpass, pan) applied as a per-track DSP chain.
Syntax
PLAY [BACKGROUND] command_stringParameters
- BACKGROUND (optional) - Play music in background without blocking program execution
- command_string - Musical commands in MML format
- Music Macro Language Commands:
- A, B, C, D, E, F, G - Musical notes
or + - Sharp (raise pitch by semitone)
- Flat (lower pitch by semitone)
- O0-O6 - Set octave (O0=lowest, O4=middle, O6=highest)
- < - Down one octave
- Up one octave
- L1-L64 - Set note length (L1=whole, L2=half, L4=quarter, L8=eighth)
- T32-T255 - Set tempo in beats per minute
- R1-R64 - Rest/pause with duration (P1-P64 is an accepted alias)
- MN - Normal mode (7/8 note length)
- ML - Legato mode (full note length)
- MS - Staccato mode (3/4 note length)
- V0-V15 - Set volume (V0=silent, V15=maximum)
- I0-I66 - Set instrument:
- Basic Waveforms (I0-I9):
- I0 - Basic Beep (pure sine tone)
- I1 - Square 50% (hollow classic square)
- I2 - Square 25% (brighter 25%-duty pulse)
- I3 - Square 12.5% (thin, nasal 12.5%-duty pulse)
- I4 - NES Triangle (mellow triangle with a near-instant attack; the classic chiptune bass)
- I5 - Sawtooth (buzzy, bright timbre)
- I6 - Square Vibrato (square with a 6 Hz pitch wobble)
- I7 - Triangle Pad (soft triangle, slow attack and long release)
- I8 - NES Noise (LFSR noise through a 3 kHz bandpass)
- I9 - PWM (square with a slow 0.8 Hz pitch drift; despite the name the duty cycle is fixed at 50%)
- Melodic Instruments (I10-I19):
- I10 - Piano (triangle plus three sine partials, long decay)
- I11 - Electric Piano (detuned sine stack, bell-like decay)
- I12 - Guitar (saw, triangle and an octave sine through a 2.5 kHz lowpass, fast decay)
- I13 - Bass Guitar (triangle plus octave sine through a 1.2 kHz lowpass)
- I14 - Strings (four detuned saws, slow attack, gentle vibrato)
- I15 - Brass (stacked saws and a square through a 3.5 kHz lowpass)
- I16 - Flute (sine with a faint breath-noise layer and 5.5 Hz vibrato)
- I17 - Organ (four-partial sine drawbar, instant attack)
- I18 - Synth Lead (saw plus a detuned 25% pulse through a 3 kHz lowpass)
- I19 - Synth Pad (detuned saws over a sub triangle, 0.5 s attack)
- Advanced Synthesis (I20-I29):
- I20 - FM Synth (sine plus octave sine with a deep 40-cent vibrato)
- I21 - Distorted Square (square through a 2.5 kHz lowpass)
- I22 - Brass Stab (two saws, short punchy decay)
- I23 - String Ensemble (four widely detuned saws, 0.3 s swell)
- I24 - Recorder (sine with a faint high partial, soft attack)
- I25 - Choir Pad (detuned triangles plus a sine, slow swell)
- I26 - Synth Bass (sub square plus triangle with a 40 ms pitch scoop up to pitch)
- I27 - Acid Bass (saw through a 700 Hz lowpass, short decay)
- I28 - PWM Synth (square with a very slow 0.3 Hz pitch drift; the duty cycle is fixed at 50%)
- I29 - Vocoder (saw plus octave square through a 1.2 kHz bandpass)
- Percussion & Drums (I30-I39):
- I30 - Kick Drum (sine and triangle swept 165 Hz down to 45 Hz)
- I31 - Snare Drum (noise plus a pitched triangle body, 800 Hz highpass)
- I32 - Closed Hi-Hat (short noise burst, 8 kHz highpass)
- I33 - Open Hi-Hat (sustained noise, 6.5 kHz highpass)
- I34 - Tom High (sine swept 320 Hz down to 200 Hz)
- I35 - Tom Low (sine swept 180 Hz down to 75 Hz)
- I36 - Crash Cymbal (long noise wash, 7 kHz bandpass)
- I37 - Ride Cymbal (medium noise decay, 5.5 kHz highpass)
- I38 - Clap (noise burst through a 1.8 kHz bandpass)
- I39 - Shaker (soft noise, 4.5 kHz highpass)
- Drum pitch: I30, I34 and I35 sweep an absolute frequency and I32, I33, I36-I39 are fixed-pitch noise, so the played note does not change any of them. I31 keeps a fixed triangle body but its noise layer does brighten with the played note.
- Special Effects (I40-I50):
- I40 - Laser Zap (square swept 2 kHz down to 100 Hz; fixed pitch)
- I41 - Explosion (noise and triangle swept 200 Hz down to 30 Hz; fixed pitch)
- I42 - Power-up (two squares swept up four octaves; follows the played note)
- I43 - Coin Pickup (square blip swept B5 up to E6; fixed pitch)
- I44 - Jump Sound (square swept 150 Hz up to 500 Hz; fixed pitch)
- I45 - Damage Sound (square and noise swept 500 Hz down to 80 Hz; fixed pitch)
- I46 - Door Creak (sub saw through a 600 Hz bandpass with a deep slow vibrato)
- I47 - Wind Sound (noise through a 400 Hz bandpass with a slow tremolo)
- I48 - Water Drop (sine swept 1.5 kHz down to 400 Hz; fixed pitch)
- I49 - Bubble Pop (sine swept 1.8 kHz down to 600 Hz; fixed pitch)
- I50 - Alien Voice (saw with a wide 10 Hz, 150-cent vibrato)
- Futuristic NES Patches (I51-I58):
- I51 - Futuristic Lead (12.5%-duty pulse melody voice)
- I52 - Futuristic Arpeggio (12.5%-duty pulse with a very fast decay, for arpeggio runs)
- I53 - Futuristic Bass (bare NES triangle)
- I54 - Futuristic Kick (triangle swept 185 Hz down to 52 Hz; fixed pitch)
- I55 - Futuristic Pulse Bass (25%-duty pulse)
- I56 - Futuristic Snare (noise plus a pitched triangle body, 900 Hz highpass)
- I57 - Futuristic Echo Lead (two 25%-duty pulses 8 cents apart)
- I58 - Futuristic Tom (triangle with a 1.5x-to-1x pitch drop; follows the played note)
- Gothic NES Patches (I59-I66):
- I59 - Gothic Lead (square with a 6.5 Hz vibrato)
- I60 - Gothic Strings (two 25%-duty pulses 12 cents apart, soft attack)
- I61 - Gothic Bass (triangle with an octave-up square)
- I62 - Gothic Church Organ (five-partial sine drawbar including a sub octave)
- I63 - Gothic Melody (25%-duty pulse with a gentle vibrato)
- I64 - Gothic Bell (three sines with an inharmonic top partial, one-second decay)
- I65 - Gothic Dark Pad (detuned 25% pulses over a sub triangle, 1.4 kHz lowpass)
- I66 - Gothic Staccato (square with a 60 ms decay to silence)
- N0-N116 - Play note by number (N0=rest, N1=C0, N84=B6, N116=G9). Covers the full engine pitch range, which is wider than the O0-O6 letter-note range. Values above N116 clamp to G9.
- . (dot) - Dotted note (extends duration by 50%)
- ^ - Tie (extend the previous note or rest by the following duration without re-attacking; e.g., C4^16 = one C attack held for quarter + sixteenth). Ties may chain (C^8^16) and tie default-length when no number follows.
- ~ - Pitch bend the previous note (or chord) by a signed number of semitones over its duration, then hold; e.g., C4
2 bends up 2 semitones, C-3 bends down 3, [CEG]2 bends the whole chord up 2. Add ,length to bend over a shorter span first: C42,8 reaches +2 over an eighth then holds. Fractional semitones allowed. - & - Portamento: glide the previous note (or chord) to the pitch of the following note over its duration; e.g., C&E slides C up to E, E&C slides down. For a chord the whole chord glides in parallel so its root reaches the target: [CEG]&F shifts the chord up 5 semitones. The target is pitch-only (any length on it is ignored) and uses the current octave — for wider slides use ~ with a semitone count. (A bend after a rest is a no-op.)
- [notes] - Chord (play multiple notes simultaneously, e.g., [CEG] for C major)
- Track effects - $NAME:p[:q] declares a DSP effect applied to the whole track (e.g. $ECHO:25, $REVERB:40:60). p and q are 0-100 intensity unless noted; p=0 bypasses (except $PAN). Declared order = signal-chain order; track-global, last value wins. Effects ride inside the MML string, so they work identically in PLAY, PLAY BACKGROUND, and .cbm music tracks.
- $ECHO:p[:q] - Tempo-synced delay. p = wet/feedback. q = note division (1/2/4/8/16/32, default 8)
- $REVERB:p[:q] - Room reverb. p = wet mix. q = room size/decay (default 50)
- $CHORUS:p[:q] - Detuned doubling (thickens). p = depth. q = LFO rate (default 30)
- $FLANGER:p[:q] - Sweeping comb (jet). p = depth. q = sweep rate (default 30)
- $PHASER:p[:q] - All-pass sweep. p = depth. q = sweep rate (default 30)
- $TREMOLO:p[:q] - Amplitude wobble. p = depth. q = LFO rate (default 40)
- $VIBRATO:p[:q] - Pitch wobble. p = depth. q = LFO rate (default 40)
- $DRIVE:p[:q] - Soft saturation/overdrive. p = drive. q = post tone/brightness (default 70)
- $BITCRUSH:p[:q] - Lo-fi crush. p = bit reduction. q = sample-rate reduction (default 0)
- $FILTER:p[:q] - Lowpass. p = cutoff. q = resonance (default 10)
- $HIGHPASS:p[:q] - Highpass. p = cutoff. q = resonance (default 10)
- $PAN:p - Static placement. p: 0 = hard left, 50 = center, 100 = hard right
Examples
PLAY "C D E F G A B"PLAY "T120 L4 C E G > C"PLAY "I5 O4 C D E P4 F G A"PLAY "I30 L4 C C C C" ' Kick drumPLAY "I43 C E G" ' Coin pickup soundPLAY "I22 O3 L2 C E G" ' Brass-like chordPLAY "L4 [CEG] [FAC] [GBD]" ' Chord progressionPLAY "O4 [C E G] P4 [D F A]" ' Chords with rest betweenPLAY "L4 C^16 D E" ' C held for quarter + sixteenth, then D EPLAY "L8 C^^ D" ' Two chained ties extend C by two more eighthsPLAY "L2 C~2 E~-1" ' C bends up 2 semitones, E bends down 1PLAY "L2 C&E G&C" ' Portamento: C glides to E, G glides to CPLAY "L4 C~12,8" ' Bend up an octave over an eighth, then holdPLAY "L2 [CEG]~2 [CEG]&F" ' Whole chord bends up 2, then glides to FPLAY "$ECHO:30:8 T120 L8 O5 C E G > C" ' Lead with a tempo-synced echoPLAY "$DRIVE:40 $REVERB:35 O3 L2 C E G" ' Saturated, reverberant chord (drive then reverb)PLAY BACKGROUND "$FILTER:55 $CHORUS:50 T110 L4 C G E G" ' Filtered, chorused background padSee also: SOUND
PLAYMUSIC
Sound · 📱 Client-only · since 1.24.0
Plays a multi-track song by name. Songs must be loaded first (typically from a .cbm music bank file included in your bundle, or created in the Music Editor). The song plays in the background, allowing your program to continue. Only one song can play at a time; calling PLAYMUSIC again stops the current song.
The optional LOOP / NOLOOP modifier overrides the loop flag declared for the song in its bank definition. Without the modifier, the song's declared default is used (typical for menu/overworld themes that loop and stinger/fanfare cues that don't). LOOP forces continuous looping regardless of the bank's setting; NOLOOP forces single playback.
Rendering: a song has to be rendered to audio before it can play, and for a dense song with sampled or wavetable instruments that takes seconds. PLAYMUSIC renders on demand, so it always works — but the render happens right then, and the song starts late.
Use LOADMUSIC to move that cost somewhere you control (a loading screen, a scene transition). After LOADMUSIC has pre-rendered a song, PLAYMUSIC on it is instant. Songs you never pre-render still play; they just pay the render at first play.
Changing instrument state (LOADINSTRUMENT, LOADINSTRUMENTBANK, SETINSTRUMENTWAV) invalidates every cached render, since a song rendered against the old instruments is now wrong. Songs you pre-rendered with LOADMUSIC are re-rendered immediately so they stay instant; everything else simply renders again on demand. Most programs only touch instrument state during init / scene setup, where this is invisible.
Prior to 1.51.0 every song in the bank was rendered eagerly when the bank loaded, which put the whole cost — sometimes many seconds — into startup whether the program played the music or not.
Multiplayer: PLAYMUSIC is client-only. Wrap in IF ME.ISCLIENT THEN ... END IF, or trigger from the server with EVENT CLIENT "name" so the client's handler runs PLAYMUSIC. Calling it on the host or a player-server raises a fatal runtime error.
Fire-and-forget: PLAYMUSIC does not block waiting for an engine acknowledgement (a looping song would never return). If the song name isn't in the music bank, the engine emits an error event that surfaces in the runtime log as [native] AUDIO ERROR: Song '...' not found in music bank but does not halt execution. To fail loud on a missing song, pre-validate with BOOM at the call site, e.g. BOOM NOT MUSICEXISTS("theme"), "theme not loaded".
Syntax
PLAYMUSIC song_name [LOOP | NOLOOP]Parameters
- song_name - String name of the song to play (as defined in the music file)
- LOOP - Optional. Force the song to loop continuously, ignoring the bank's declared loop flag.
- NOLOOP - Optional. Force single playback, ignoring the bank's declared loop flag.
Examples
PLAYMUSIC "Battle Theme" ' Use the song's declared loop defaultPLAYMUSIC "Battle Theme" LOOP ' Force loop regardless of bank settingPLAYMUSIC "Victory Fanfare" NOLOOP ' Stinger that plays oncePLAYMUSIC "Menu Music"REM Play different music based on game state
IF inBattle THEN
PLAYMUSIC "Battle Theme"
ELSE
PLAYMUSIC "Overworld"
END IFSee also: LOADMUSIC, STOPMUSIC, PAUSEMUSIC, RESUMEMUSIC, FADEMUSIC, MUSICPLAYING
RESUMEMUSIC
Sound · 📱 Client-only · since 1.7.2
Resumes a paused multi-track song from where it was paused. Has no effect if music is not paused.
Syntax
RESUMEMUSICExamples
RESUMEMUSICREM Resume music when game is unpaused
SUB UnpauseGame()
gamePaused = 0
RESUMEMUSIC
END SUBSee also: PAUSEMUSIC, PLAYMUSIC, STOPMUSIC
SET ARRANGEMENT
Sound · 📱 Client-only · since 1.51.0
Replaces a song's whole arrangement with the given patterns, each played once. With no patterns at all, clears the arrangement.
For per-step control (repeat counts and layering) use ADD ARRANGEMENT or a MUSIC block's ARRANGEMENT section. Naming a pattern the song doesn't define is an error.
Syntax
SET ARRANGEMENT song [, pattern1, pattern2, ...]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern1, pattern2, ... - Patterns to play in order
Examples
SET ARRANGEMENT "battle", "intro", "verse", "verse", "chorus"' Strip a song back to its loop
SET ARRANGEMENT "battle", "verse"See also: ADD ARRANGEMENT, INSERT ARRANGEMENT, ARRANGEMENTCOUNT, MUSIC
SET AUDIO
Sound · 📱 Client-only · since 1.46.0
Live-updates the volume and/or stereo pan of a currently-playing sound by name — a WAV (PLAYWAV), an MP3 (PLAYMP3), or a multi-track song (PLAYMUSIC). Use it to duck background music under a cutscene, pan a positional sound as its source moves, or ride a sound's level over time.
The VOLUME and PAN modifiers may appear in any order, and at least one must be present. A modifier you omit leaves that parameter unchanged — SET AUDIO "music" PAN(0.5) adjusts only the pan and keeps the current volume. Setting a parameter on a sound that isn't currently playing is a harmless no-op.
VOLUME(v) takes v in 0.0 (silent) to 1.0 (full). PAN(x [, y]) takes x in -1.0 (hard left) to 1.0 (hard right), 0.0 = center; the optional y is a vertical/depth axis reserved for future 3D audio and is not yet mixed. PAN also accepts a single coordinate value (PAN(pos)) whose x and y fill both axes.
For multi-track music this targets the whole song. Multiplayer: SET AUDIO is client-only — it adjusts local playback and raises a fatal runtime error if called on the host or a player-server. Wrap in IF ME.ISCLIENT THEN ... END IF.
Syntax
SET AUDIO name VOLUME(v) PAN(x [, y])Parameters
- name - String name of the playing WAV, MP3, or song to adjust
- VOLUME(v) - Optional. Set playback volume, v 0.0 (silent) to 1.0 (full)
- PAN(x [, y]) - Optional. Set stereo pan: x -1.0 (left) to 1.0 (right), 0.0 = center. Optional y (depth) is reserved. A single coordinate fills both axes.
Examples
SET AUDIO "music" VOLUME(0.4) ' Duck the background musicSET AUDIO "engine" PAN(0.6) ' Pan a looping engine sound rightSET AUDIO "alarm" VOLUME(0.8) PAN(-0.5) ' Set both at onceREM Pan a sound to follow an on-screen object
SET AUDIO "engine" PAN(enemyX / 160.0 - 1.0)See also: PLAYWAV, PLAYMP3, PLAYMUSIC, FADEMUSIC
SET MUSIC
Sound · 📱 Client-only · since 1.51.0
Adjusts a song's own properties without touching its patterns or arrangement. At least one modifier is required; they may appear in any order, and anything you don't mention is left alone.
The change takes effect the next time the song is played. To hear it immediately, call PLAYMUSIC again (music is pre-rendered, so a tempo change cannot be applied to a song already in flight).
Both TEMPO(140) and TEMPO 140 are accepted.
Syntax
SET MUSIC song [TEMPO(n)] [LOOP | NOLOOP] [TIMESIG(beats, unit)]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- TEMPO(n) - Beats per minute
- LOOP | NOLOOP - Whether the song repeats when played
- TIMESIG(beats, unit) - Time signature, e.g. TIMESIG(6, 8)
Examples
SET MUSIC "battle" TEMPO(160)' Speed the music up as the timer runs down
SET MUSIC "battle" TEMPO(120 + (60 - secondsLeft))
PLAYMUSIC "battle"SET MUSIC "fanfare" NOLOOP TIMESIG(3, 4)See also: MUSIC, SET TRACK, MUSICTEMPO, PLAYMUSIC
SET MUSIC PATTERN
Sound · 📱 Client-only · since 1.51.0
Sets a pattern's length in beats. A pattern shorter than its longest track is extended to fit; tracks marked REPEAT tile to fill the length.
Syntax
SET MUSIC PATTERN song, pattern LENGTH(beats)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- LENGTH(beats) - Pattern length in beats
Example
SET MUSIC PATTERN "battle", "verse" LENGTH(32)See also: CREATE MUSIC PATTERN, MUSICPATTERNLENGTH, SET TRACK
SET TRACK
Sound · 📱 Client-only · since 1.51.0
Changes one track's properties. At least one modifier is required, and this is a partial update: setting VOLUME leaves the notes, instrument and pan exactly as they were.
Track modifiers, all optional and in any order. Anything you don't mention keeps its current value.
CHANNEL(n)— MIDI channel 1 to 16 (10 is conventionally drums)INSTRUMENT(n)— instrument number (built-ins are 0 to 66;.cbiinstruments may use any number)VOLUME(n)— 0 to 100PAN(x)— -1.0 (left) to 1.0 (right)NAME(s$)— a label for the track, shown in the Music EditorNOTES(s$)— the track's MMLGENERATOR(s$)— name of a BASIC FUNCTION that supplies the MML instead (see GENERATOR)REPEAT/NOREPEAT— whether the track tiles to fill the pattern's length
Every value modifier accepts both VOLUME(80) and VOLUME 80.
The change takes effect the next time the song is played.
Syntax
SET TRACK song, pattern, index [modifiers...]Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
- modifiers - Any combination of the track modifiers above
Examples
SET TRACK "battle", "verse", 1 VOLUME(20) PAN(-0.4)' Swap a lead line for a different instrument mid-game
SET TRACK "battle", "chorus", 0 INSTRUMENT(24)
PLAYMUSIC "battle"' Replace a track's notes outright
SET TRACK "battle", "verse", 0 NOTES "o2 c4 g4 c4 g4"See also: CREATE TRACK, MUTE TRACK, GENERATOR, TRACKVOLUME, MUSIC
SETINSTRUMENTWAV
Sound · 📱 Client-only · since 1.23.0
Binds a WAV sample (previously loaded with LOADWAV) as the oscillator source for an instrument. The instrument's existing modulation — ADSR envelope, filter + filter envelope, vibrato, tremolo, FM — still applies; only the waveform source changes. This lets authors layer any .cbi instrument's modulation shell over a custom sampled waveform for sampled-instrument playback (similar in spirit to SNES sample-based instruments). Bindings persist across music bank (.cbm) loads — clear them by passing an empty handle. Without an explicit layer number, the binding applies to all layers of the instrument; specify a layer to bind only one. BASEPITCH sets the MIDI note the sample represents at its native rate (default A4); accepts either a note name ("C4", "A#5", "Bb3") or a raw MIDI number 0-127. LOOP sets forward loop points in sample frames; omitted means one-shot playback.
Syntax
SETINSTRUMENTWAV instrument, [layer,] handle [BASEPITCH note] [LOOP start, end]Parameters
- instrument - Instrument number (0-255) matching your PLAY/MML @N references
- layer - (optional) 0-indexed layer to bind; omitted = all layers of the instrument
- handle - String handle of a WAV loaded via LOADWAV; empty string clears the binding
- note - (optional BASEPITCH) MIDI note the sample represents at native rate; default A4 (an unrecognized note name or out-of-range number also falls back to A4)
- start, end - (optional LOOP) Forward loop points in sample frames
Examples
LOADWAV "samples/piano.wav", "piano"
SETINSTRUMENTWAV 1, "piano"' Bind only to layer 0, leaving other layers as the original .cbi waveform
SETINSTRUMENTWAV 1, 0, "piano"' Pitched + looped for sustained instruments
SETINSTRUMENTWAV 2, "strings" BASEPITCH "A4" LOOP 4800, 24000' Clear a binding and revert to the original .cbi waveform
SETINSTRUMENTWAV 1, ""' Typical workflow: set up once at game start, play indefinitely after
LOADWAV "samples/kick.wav", "kick"
LOADWAV "samples/snare.wav", "snare"
SETINSTRUMENTWAV 10, "kick" BASEPITCH 36
SETINSTRUMENTWAV 11, "snare" BASEPITCH 38
PLAY "I10 o3 l16 c c I11 e c"See also: LOADWAV, PLAY, INSTRUMENTMODE, PLAYMUSIC
SOLO TRACK
Sound · 📱 Client-only · since 1.51.0
Plays only the soloed tracks of a song. Solo beats mute, as on any mixer: while anything in the song is soloed, every track that isn't soloed is silent, including un-muted ones in other patterns. UNSOLO on the last soloed track returns the song to normal mute behaviour.
Takes effect the next time the song is played. Read the state back with TRACKSOLOED.
Syntax
SOLO TRACK song, pattern, index
UNSOLO TRACK song, pattern, indexParameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Examples
SOLO TRACK "battle", "verse", 0
PLAYMUSIC "battle"UNSOLO TRACK "battle", "verse", 0See also: MUTE TRACK, TRACKSOLOED, SET TRACK
SONGCOUNT
Sound · since 1.51.0
Number of songs in the music bank.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
SONGCOUNT()Example
FOR i = 0 TO SONGCOUNT() - 1
PRINT SONGNAME$(i)
NEXTSee also: SONGNAME$, MUSICEXISTS
SONGNAME$
Sound · since 1.51.0
Name of a song in the bank. Songs are ordered alphabetically ignoring case, so enumeration is stable from run to run.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
SONGNAME$(index)Parameters
- index - Zero-based song index
Example
name$ = SONGNAME$(0)See also: SONGCOUNT, MUSICEXISTS
SOUND
Sound · 📱 Client-only · since 0.23.0
Generates tones by frequency and duration. Non-blocking - program continues immediately while sound plays.
Syntax
SOUND frequency, durationParameters
- frequency - Sound frequency in Hz
- duration - Duration in ticks (18.2/sec)
Examples
SOUND 440, 18 ' A note for 1 second (non-blocking)FOR F = 200 TO 2000 STEP 100
SOUND F, 2 ' Quick beep sequence
NEXTSOUND 1000, 5 ' Sound effect
PRINT "Sound playing in background"
STOP PLAY
Sound · 📱 Client-only · since 0.23.0
Stops background music started with PLAY BACKGROUND. Does not affect foreground PLAY commands or SOUND effects.
Syntax
STOP PLAYExamples
PLAY BACKGROUND "T120 C D E F G"
PRINT "Music playing..."
STOP PLAY
PRINT "Music stopped!"REM Toggle background music
IF musicPlaying THEN
STOP PLAY
musicPlaying = 0
ELSE
PLAY BACKGROUND "C E G C"
musicPlaying = 1
END IFSee also: PLAY BACKGROUND, PLAY
STOPMUSIC
Sound · 📱 Client-only · since 1.7.2
Stops the currently playing multi-track song. Has no effect if no music is playing.
Syntax
STOPMUSICExamples
STOPMUSICREM Stop music when entering a cutscene
STOPMUSIC
PLAYMP3 "cutscene_audio"REM Clean stop when game ends
SUB GameOver()
STOPMUSIC
PLAYMUSIC "Game Over"
END SUBSee also: PLAYMUSIC, PAUSEMUSIC, MUSICPLAYING
TRACKCHANNEL
Sound · since 1.51.0
A track's MIDI channel, 1 to 16.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKCHANNEL(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
IF TRACKCHANNEL("battle", "verse", t) = 10 THEN PRINT "drums"See also: SET TRACK, TRACKINSTRUMENT
TRACKCOUNT
Sound · since 1.51.0
Number of tracks in a pattern. Valid track indices are 0 to TRACKCOUNT - 1.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKCOUNT(song, pattern)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
Example
FOR t = 0 TO TRACKCOUNT("battle", "verse") - 1
MUTE TRACK "battle", "verse", t
NEXTSee also: CREATE TRACK, SET TRACK
TRACKGENERATOR$
Sound · since 1.51.0
The name of the BASIC function bound to a track as its generator, or the empty string if it has none. Reads back exactly as it was written.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKGENERATOR$(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
IF TRACKGENERATOR$("ambient", "drift", 0) <> "" THEN PRINT "generative"See also: GENERATOR, SET TRACK
TRACKINSTRUMENT
Sound · since 1.51.0
A track's instrument number.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKINSTRUMENT(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
PRINT TRACKINSTRUMENT("battle", "verse", 0)See also: SET TRACK, TRACKCHANNEL
TRACKMUTED
Sound · since 1.51.0
TRUE if the track is muted. Lets a program read its own mixer state back instead of shadowing it in variables.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKMUTED(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
IF TRACKMUTED("battle", "verse", 1) THEN UNMUTE TRACK "battle", "verse", 1See also: MUTE TRACK, TRACKSOLOED
TRACKNAME$
Sound · since 1.51.0
A track's label, or the empty string if it has none.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKNAME$(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
PRINT TRACKNAME$("battle", "verse", 0)See also: SET TRACK, CREATE TRACK
TRACKNOTES$
Sound · since 1.51.0
A track's MML. For a generator track this is whatever the generator last produced, which is empty until the song has been played or applied.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKNOTES$(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
PRINT TRACKNOTES$("battle", "verse", 0)See also: SET TRACK, GENERATOR, PLAY
TRACKPAN
Sound · since 1.51.0
A track's stereo position, -1.0 (left) to 1.0 (right).
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKPAN(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
PRINT TRACKPAN("battle", "verse", 0)See also: SET TRACK, TRACKVOLUME
TRACKREPEAT
Sound · since 1.51.0
TRUE if the track tiles to fill its pattern's length.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKREPEAT(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
IF TRACKREPEAT("battle", "verse", 1) THEN PRINT "tiles"See also: SET TRACK, MUSICPATTERNLENGTH
TRACKSOLOED
Sound · since 1.51.0
TRUE if the track is soloed.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKSOLOED(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
IF TRACKSOLOED("battle", "verse", 0) THEN UNSOLO TRACK "battle", "verse", 0See also: SOLO TRACK, TRACKMUTED
TRACKVOLUME
Sound · since 1.51.0
A track's volume, 0 to 100.
Reads the interpreter's music bank, so it sees pending edits immediately and works with no audio device at all. Naming a song, pattern or index that doesn't exist is a fatal error rather than a silent zero.
Syntax
TRACKVOLUME(song, pattern, index)Parameters
- song - Name of the song in the music bank (matched ignoring case)
- pattern - Name of a pattern within that song (matched ignoring case)
- index - Zero-based track index within the pattern
Example
' Duck a track by half
v = TRACKVOLUME("battle", "verse", 0)
SET TRACK "battle", "verse", 0 VOLUME(v / 2)
Video
LOADVIDEO
Video · 🧪 Experimental · since 1.9.3
Loads a video file from local resources for playback. The video is decoded on a dedicated thread (native) or Web Worker (WASM). Supported formats are MP4 (H.264) and WebM (VP8/VP9). Loading is synchronous — execution pauses until the video metadata is read and the video is ready to play. Argument order matches LOADWAV/LOADMP3/LOADIMG: filename first, handle name second.
In CrashNet multiplayer, resource loads are client-only — calling LOADVIDEO from server-side code is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block.
Syntax
LOADVIDEO filename$, name$Parameters
- filename$ - Path to the video file relative to the game directory
- name$ - Reference name for the video (used in PLAYVIDEO, STOPVIDEO, etc.)
- Supported formats: MP4 (H.264), WebM (VP8/VP9)
Examples
LOADVIDEO "cutscenes/intro.mp4", "intro"LOADVIDEO "cutscenes/ending.webm", "ending"REM Load and play a cutscene
LOADVIDEO "intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"See also: PLAYVIDEO, STOPVIDEO, WAITVIDEO, LOADMP3, LOADWAV
PAUSEVIDEO
Video · 🧪 Experimental · since 1.9.3
Pauses playback of an active video. The current frame remains displayed on the layer. Use RESUMEVIDEO to continue playback from where it was paused.
Syntax
PAUSEVIDEO name$Parameters
- name$ - Reference name of the video to pause
Examples
PAUSEVIDEO "cutscene"REM Pause on user input
IF INKEY$ = " " THEN
PAUSEVIDEO "cutscene"
END IFSee also: RESUMEVIDEO, PLAYVIDEO, STOPVIDEO
PLAYVIDEO
Video · 🧪 Experimental · since 1.9.3
Plays a previously loaded video. The decoded frames are rendered to the specified graphics layer. Playback runs asynchronously — execution continues immediately unless WAITVIDEO is used. The LOOP modifier restarts the video when it finishes. The MUTE modifier suppresses the video's audio track.
Layer effects do NOT reliably apply to video playback. To support hardware mp4 decode, each platform renders video through its own native pipeline (for example a hardware-accelerated HTML <video> element on web), which composites outside the layer system on some platforms. Do not rely on layer CLIP, ALPHA, SHADER, or plane (AFFINE/PERSPECTIVE) modes to mask or transform a playing video — treat video as exempt from layer compositing. Use the video's own target dimensions and position instead.
Syntax
PLAYVIDEO name$ [, layer] [LOOP] [MUTE]Parameters
- name$ - Reference name of a loaded video
- layer - Optional graphics layer to render on (default 0). The video frames replace the layer's texture each frame.
- LOOP - Optional modifier that restarts the video when it finishes
- MUTE - Optional modifier that suppresses the video's audio track
Examples
PLAYVIDEO "intro"PLAYVIDEO "intro", 2PLAYVIDEO "intro" LOOPPLAYVIDEO "intro" MUTEPLAYVIDEO "background", 0 LOOP MUTEREM Play a cutscene on layer 1 and wait for it
CREATE LAYER 1, 320, 200
LOADVIDEO "scene1.mp4", "cutscene"
PLAYVIDEO "cutscene", 1
WAITVIDEO "cutscene"
FREE LAYER 1See also: LOADVIDEO, PAUSEVIDEO, RESUMEVIDEO, STOPVIDEO, WAITVIDEO, CREATE LAYER
RESUMEVIDEO
Video · 🧪 Experimental · since 1.9.3
Resumes playback of a paused video from where it was paused.
Syntax
RESUMEVIDEO name$Parameters
- name$ - Reference name of the video to resume
Examples
RESUMEVIDEO "cutscene"REM Toggle pause with spacebar
IF INKEY$ = " " THEN
IF videoPaused = 0 THEN
PAUSEVIDEO "cutscene"
videoPaused = 1
ELSE
RESUMEVIDEO "cutscene"
videoPaused = 0
END IF
END IFSee also: PAUSEVIDEO, PLAYVIDEO, STOPVIDEO
SEEKVIDEO
Video · 🧪 Experimental · since 1.9.3
Seeks to a specific time position in a loaded video. The position is specified in seconds (supports fractional values for sub-second precision).
Syntax
SEEKVIDEO name$, secondsParameters
- name$ - Reference name of the video to seek
- seconds - Target time position in seconds (e.g., 1.5 for one and a half seconds)
Examples
SEEKVIDEO "cutscene", 0SEEKVIDEO "cutscene", 10.5REM Skip ahead 5 seconds
SEEKVIDEO "intro", 5.0
PLAYVIDEO "intro"See also: PLAYVIDEO, LOADVIDEO, PAUSEVIDEO
STOPVIDEO
Video · 🧪 Experimental · since 1.9.3
Stops playback and unloads a video, freeing its resources. The video must be reloaded with LOADVIDEO before it can be played again.
Stopping a video releases any WAITVIDEO blocked on it. That is what makes a skippable cutscene work: the program waits on the video while an event handler calls STOPVIDEO on a keypress. A stopped video reports no "finished" event — it did not finish — so WAITVIDEO ends on the stop itself.
Syntax
STOPVIDEO name$Parameters
- name$ - Reference name of the video to stop and unload
Examples
STOPVIDEO "cutscene"REM Skip cutscene on key press
PLAYVIDEO "intro"
WHILE videoPlaying
IF INKEY$ <> "" THEN
STOPVIDEO "intro"
videoPlaying = 0
END IF
BEGIN FRAME
END FRAME
WENDSee also: PLAYVIDEO, LOADVIDEO, PAUSEVIDEO
WAITVIDEO
Video · 🧪 Experimental · since 1.9.3
Blocks execution until the specified video finishes playing or is stopped. This is the primary way to implement cutscenes that must complete before gameplay resumes. If the video is looping, WAITVIDEO will block indefinitely until the video is stopped by another thread or event handler.
Syntax
WAITVIDEO name$Parameters
- name$ - Reference name of the video to wait for
Examples
WAITVIDEO "intro"REM Full cutscene pattern
LOADVIDEO "cutscenes/intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"
PRINT "Cutscene finished!"REM Skippable cutscene with event handler
DECLARE SUB SkipCutscene()
CALL EVENT SkipCutscene() AS "keypress"
LOADVIDEO "intro.mp4", "intro"
PLAYVIDEO "intro"
WAITVIDEO "intro"
STOP THREAD "keypress"
SUB SkipCutscene()
STOPVIDEO "intro"
END SUBSee also: PLAYVIDEO, LOADVIDEO, STOPVIDEO, CALL EVENT
File I/O
CALL SQL
File I/O · since 1.56.0
Runs a declared SQL block against an open database. Arguments are passed exactly as a behavior's are — @name=value — and merge over the block's declared defaults.
A statement that returns rows binds INTO an array of objects, one per row, with the columns as named fields: rows(0).name, rows(0).score — object fields carry no $ suffix, whatever the column's type. A statement that changes the database instead reports through SQLROWSAFFECTED and SQLLASTID.
Syntax
CALL SQL "name" ON #n [(@param=value, ...)] [INTO variable]Parameters
- name - The SQL block to run.
- #n - An open database handle.
- @param=value - Values for the block's declared parameters. Anything the call site omits falls back to that parameter's DEFAULT.
- INTO variable - Receives an array of row objects. Omit it for a statement that returns no rows.
Examples
CALL SQL "topScores" ON #1 (@minScore=1000, @limit=5) INTO rows
DIM i AS INTEGER
FOR i = 0 TO UBOUND(rows)
PRINT rows(i).name; " "; rows(i).score
NEXT i' A write reports what it changed
CALL SQL "addRun" ON #2 (@who$="Dan", @score=1200)
PRINT "rows: "; SQLROWSAFFECTED; " id: "; SQLLASTIDNotes
- Results are objects.
UBOUND(rows)gives the last index androws(i).columnNamereads a field, using the column name the query produced — name a computed column withASto get a usable field. Object fields take no$suffix, so a text column isrows(i).name, notrows(i).name$. - After a write,
SQLROWSAFFECTEDholds the number of changed rows andSQLLASTIDthe last inserted row id. INTOon a statement that returns no rows is reported — it would otherwise look like an empty result rather than a mistake.- Blobs have no BASIC type; a blob column reads as its length in bytes.
See also: SQL, OPEN DATABASE, CLOSE
CLOSE
File I/O · since 1.0.0
Closes open file handles or DATA block handles. Always close handles when done to free resources. Can close multiple handles in one statement.
One handle, one thing. Files, DATA blocks and databases share the #n space so that CLOSE #n is unambiguous. Opening onto a number something else already holds is a runtime error naming what holds it — in either direction, for either kind. CLOSE frees the number for any of them again.
Syntax
CLOSE #filenumber [, #filenumber2, ...]Parameters
- filenumber - File or DATA block handle number to close
Examples
OPEN DATA "scores" AS #1
DIM score AS INTEGER
READ #1, score
CLOSE #1' Close multiple handles
OPEN DATA "block1" AS #1
OPEN DATA "block2" AS #2
CLOSE #1, #2' Close after reading all data
DATA "palette"
255, 0, 0
0, 255, 0
END DATA
OPEN DATA "palette" AS #1
WHILE NOT EOF(1)
DIM r AS INTEGER, g AS INTEGER, b AS INTEGER
READ #1, r, g, b
WEND
CLOSE #1See also: OPEN DATA, DATA, READ, EOF
DESERIALIZE$
File I/O · since 0.23.0
Parses a JSON string back into its original data structure. Used to restore game state, player data, settings, and other complex data from storage. Accepts any valid JSON, including data from external APIs or files.
A rectangular nested JSON array is restored as a true multi-dimensional array, indexable as a(row, col) — so a matrix from another tool round-trips as a matrix. A ragged or mixed nesting (rows of differing lengths) has no single shape, so it stays an array of arrays, indexed a(row)(col). LOADJSON reconstructs arrays the same way.
Syntax
DESERIALIZE$(serializedData$)Parameters
- serializedData$ - A JSON string, typically from SERIALIZE$ or LOADTEXT
Examples
savedData$ = LOADTEXT("player.dat")
DIM player AS Object
player = DESERIALIZE$(savedData$)
PRINT "Welcome back, " + player.namescoresData$ = LOADTEXT("highscores.dat")
DIM highScores(10) AS Number
highScores = DESERIALIZE$(scoresData$)
PRINT "High Score: " + STR$(highScores(0))TYPE GameState
level AS Number
lives AS Number
END TYPE
savedGame$ = LOADTEXT("savegame.dat")
DIM state AS GameState
state = DESERIALIZE$(savedGame$)
PRINT "Resuming level " + STR$(state.level)REM Load and apply game settings
configData$ = LOADTEXT("config.json")
DIM config AS Object
config = DESERIALIZE$(configData$)
IF config.sound THEN CALL enableSound()
CALL setGraphicsQuality(config.graphics)See also: SERIALIZE$, LOADTEXT, LOADJSON
EOF
File I/O · since 1.0.0
Returns TRUE (1) if the handle is exhausted, FALSE (0) otherwise. Works for both DATA block handles (exhausted when every value has been READ) and file-stream handles opened FOR INPUT (exhausted when every line has been read). For a closed or invalid handle, EOF returns TRUE — so a stray WHILE NOT EOF(n) on a never-opened handle terminates immediately rather than spinning.
Syntax
EOF(filenumber)Parameters
- filenumber - File handle number (without the # symbol)
Examples
DATA "numbers"
10, 20, 30, 40, 50
END DATA
OPEN DATA "numbers" AS #1
DIM num AS INTEGER
WHILE NOT EOF(1)
READ #1, num
PRINT STR$(num)
WEND
CLOSE #1' Load all enemies from DATA block
DATA "enemies"
"Goblin", 10
"Orc", 25
"Dragon", 100
END DATA
OPEN DATA "enemies" AS #1
DIM name AS STRING, hp AS INTEGER
DIM count AS INTEGER
count = 0
WHILE NOT EOF(1)
READ #1, name, hp
count = count + 1
PRINT STR$(count) + ": " + name
WEND
CLOSE #1' Read a config file line by line
OPEN "config.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
LINE INPUT #1, line$
PRINT line$
WEND
CLOSE #1See also: DATA, OPEN, OPEN DATA, READ, LINE INPUT, LOC, LOF, CLOSE
FETCH
File I/O · since 1.30.0
Synchronous HTTP client. Returns an Object with exactly two fields: STATUSCODE (Integer) and MESSAGE (String). On success, STATUSCODE is the HTTP status (200, 404, 500, ...) and MESSAGE is the response body as text. On failure to complete an HTTP exchange (DNS failure, connection refused, TLS error, invalid URL, invalid method, timeout), STATUSCODE is 0 and MESSAGE is a human-readable error description. Network errors do NOT halt the interpreter — the BASIC program detects them via STATUSCODE. A non-Object headers argument is handled the same way (STATUSCODE=0 with the error in MESSAGE; it does not halt). Only a wrong argument count halts.
Body types: String passes through verbatim. Object/Array auto-encode as JSON with Content-Type: application/json (unless overridden via headers). Anything else is stringified.
Headers: optional 4th arg is an Object of header names to values.
Native: blocking reqwest client with a 30-second timeout. WASM: synchronous XMLHttpRequest in the Worker the interpreter runs on. Cross-origin destinations on WASM are subject to normal browser CORS rules. Sync FETCH blocks the worker — for slow or parallel calls, wrap in ASYNC FETCH(...) and RESOLVE().
Syntax
result = FETCH(url$, method$ [, body [, headers]])Parameters
- url$ - Full URL of the resource
- method$ - HTTP method (case-insensitive: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, ...)
- body - Optional body. String sent as-is. Object/Array auto-encode as JSON with Content-Type: application/json. Other types stringified.
- headers - Optional 4th-arg Object of header names → values (e.g. {"Authorization": "Bearer xyz"})
Examples
REM Simple GET
r = FETCH("https://api.example.com/score", "GET")
IF r.STATUSCODE = 200 THEN
PRINT r.MESSAGE
END IFREM POST with auto-JSON body
DIM payload AS OBJECT
payload.player = "alice"
payload.score = 4200
r = FETCH("https://api.example.com/scores", "POST", payload)REM Headers (4th arg)
DIM headers AS OBJECT
headers.Authorization = "Bearer " + token$
r = FETCH(url$, "GET", "", headers)REM Parallel fetches via ASYNC
p1 = ASYNC FETCH("https://api.example.com/leaderboard", "GET")
p2 = ASYNC FETCH("https://api.example.com/news", "GET")
results = RESOLVE(p1, p2)
lb = results(0)
news = results(1)REM Detect network failure (STATUSCODE = 0) vs HTTP error (4xx/5xx)
r = FETCH(url$, "GET")
IF r.STATUSCODE = 0 THEN
PRINT "Network error: " + r.MESSAGE
ELSEIF r.STATUSCODE >= 400 THEN
PRINT "HTTP "; r.STATUSCODE; ": "; r.MESSAGE
ELSE
PRINT "Got: " + r.MESSAGE
END IFSee also: ASYNC, RESOLVE, BASE64TOIMG, BASE64TOWAV, BASE64TOMP3
FILEEXISTS
File I/O · since 1.0.0
Checks whether a file exists in the project's local storage (the same VFS that LOADTEXT, SAVETEXT, and OPEN use). Returns 1 if the file is present, 0 otherwise.
Distinct from EXISTS, which checks the in-memory resource manager (LOADIMG / LOADWAV / LOADMP3 / loaded text resources). FILEEXISTS is for raw filesystem paths — use it to gate LOADTEXT, LOADJSON, or OPEN against missing optional saves and configs without triggering a runtime error.
Syntax
result = FILEEXISTS(filename$)Parameters
- filename$ - Path to test, same form accepted by LOADTEXT / OPEN
Examples
REM Only restore if a save actually exists
IF FILEEXISTS("savegame.json") THEN
saved = LOADJSON("savegame.json")
END IFREM Decide between user settings and defaults
IF FILEEXISTS("settings.dat") THEN
s$ = LOADTEXT("settings.dat")
ELSE
s$ = LOADTEXT("defaults.dat")
END IFSee also: EXISTS, LOADTEXT, LOADJSON, SAVETEXT, OPEN
FILES
File I/O · since 1.36.4
Returns a 1D array of filenames in the bus root or in path$. Used to discover what's loadable / saved / available without hardcoding lists in BASIC.
Unified single- and multi-player behavior. FILES returns the union of cart-bundled assets AND user-saved files. The same call on the same logical content gives the same answer in solo play, on a host server, and on a player-server — a file written via SAVETEXT / SAVEJSON / OPEN FOR OUTPUT shows up in a subsequent FILES() call regardless of which interpreter wrote it. (Per-player saves on a server are scoped to that player; another player's saves are not visible.)
Shape:
- Regular files only. Subdirectories are silently skipped — they don't appear as entries. Pass the subdirectory as
path$to enumerate inside. - Basenames only. The returned strings are just the filename, not the full path. Concatenate with the queried directory if you need full paths.
- Sorted alphabetically (byte order). Deterministic across platforms and across native / WASM / multiplayer — important for replays and lockstep multiplayer.
- Dotfiles excluded (
.DS_Store, editor swap files, etc.).
Sandbox. path$ is resolved inside the bus's storage root. Attempts to escape (.., absolute paths to outside the sandbox) raise a fatal runtime error rather than silently returning an empty array — bugs in path construction fail loud.
No path$ (or empty "", ".", "/") lists the bus root.
Syntax
result = FILES([path$])Parameters
- path$ - Optional directory inside the bus root. Omit (or pass empty) for the root.
Examples
REM List every file in the project root
FOR EACH f IN FILES()
PRINT f
NEXTREM Enumerate save slots
saves = FILES("saves")
IF UBOUND(saves) >= 0 THEN
PRINT "You have " + STR$(UBOUND(saves) + 1) + " saved games"
ELSE
PRINT "No saves yet"
END IFREM Round-trip — write a save, then it shows up in FILES()
SAVETEXT "slot01.dat", SERIALIZE$(player)
allFiles = FILES()
FOR EACH f IN allFiles
IF RIGHT$(f, 4) = ".dat" THEN
PRINT "Save: " + f
END IF
NEXTREM Multiplayer: same call works solo and on a server
mySaves = FILES("saves")
FOR EACH s IN mySaves
PRINT ME.NAME$ + " has " + s
NEXTREM Path traversal is a fatal error, not silent empty
' FILES("../etc") ' would halt with a runtime errorSee also: FILEEXISTS, LOADTEXT, LOADJSON, SAVETEXT, SAVEJSON, OPEN, UBOUND
ISOPEN
File I/O · since 1.0.0
Returns 1 if the given file or DATA block handle is currently open, 0 otherwise. Useful for cleanup paths that may run without knowing whether OPEN succeeded, and for guarding a CLOSE against an unopened handle (which would otherwise be a silent no-op rather than an error, but the guard reads more clearly).
Syntax
result = ISOPEN(filenumber)Parameters
- filenumber - Handle to test (1-255)
Examples
OPEN "data.txt" FOR INPUT AS #1
IF ISOPEN(1) THEN
PRINT "Open"
END IF
CLOSE #1
IF ISOPEN(1) = 0 THEN
PRINT "Closed"
END IFREM Defensive close — only runs if OPEN succeeded
IF ISOPEN(1) THEN CLOSE #1See also: OPEN, OPEN DATA, CLOSE, EOF
JMES
File I/O · since 1.41.0
Runs a JMESPath query against an array or object (or any value) and returns the result. JMESPath is a compact query language for JSON-like data — it reaches into nested fields, slices and filters arrays, projects values, and applies built-in functions such as max, length, and sort_by. The data is matched as if it were JSON, so objects, arrays, numbers, strings, and booleans all work.
The result is converted back to a Crash value: an object becomes an Object, an array becomes an Array, numbers and strings come back as themselves, and a no-match (JSON null) returns 0 — the same convention as DESERIALIZE$. An invalid query halts with an error.
See https://jmespath.org for the full query syntax.
Syntax
JMES(data, "query")Parameters
- data - An array, object, or any value to query (commonly the result of DESERIALIZE$ or LOADJSON)
- "query" - A JMESPath query string
Examples
data = DESERIALIZE$(json$)
PRINT JMES(data, "players[0].name")REM Highest score across all players
best = JMES(roster, "max(players[*].score)")REM Filter, then take the first match
winner$ = JMES(roster, "players[?score > `100`].name | [0]")REM Reach a nested field
title$ = JMES(config, "window.title")See also: DESERIALIZE$, SERIALIZE$, LOADJSON
LINE INPUT
File I/O · since 1.0.0
Reads one full line from a file handle (opened with OPEN ... FOR INPUT) into a string variable. Unlike READ, LINE INPUT does not auto-parse — the entire line is stored verbatim, including embedded commas, quotes, and leading/trailing whitespace. When the file is exhausted, the variable is set to an empty string; check EOF before reading to distinguish a true empty line from end-of-file.
Syntax
LINE INPUT #filenumber, var$Parameters
- filenumber - File handle from OPEN ... FOR INPUT
- var$ - String variable to receive the line
Examples
OPEN "dialog.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
LINE INPUT #1, line$
PRINT line$
WEND
CLOSE #1REM Skip a CSV header row, then process the rest verbatim
OPEN "data.csv" FOR INPUT AS #1
DIM header$ AS STRING, row$ AS STRING
LINE INPUT #1, header$
WHILE NOT EOF(1)
LINE INPUT #1, row$
PRINT row$
WEND
CLOSE #1See also: OPEN, READ, EOF, CLOSE, INPUT
LOADJSON
File I/O · since 0.23.0
Loads and parses JSON data from files in the project bundle. Returns the parsed data as CrashBasic objects and arrays. Excellent for loading structured game data, level definitions, sprite configurations, and complex settings.
Syntax
result = LOADJSON(filename$)Parameters
- filename$ - Name of JSON file in project bundle (relative to project root)
Examples
DIM levelData AS Object
levelData = LOADJSON("levels/level1.json")
PRINT "Level: " + levelData.name
PRINT "Size: " + STR$(levelData.width) + "x" + STR$(levelData.height)DIM sprites AS Object
sprites = LOADJSON("sprites/characters.json")
PRINT "Player sprite: " + sprites.player.filename
PRINT "Frames: " + STR$(sprites.player.frames)REM Load game configuration
DIM config AS Object
config = LOADJSON("config.json")
SCREEN config.screen.width, config.screen.height
IF config.audio.enabled THEN CALL enableAudio()REM Load enemy data
DIM enemies AS Array
enemies = LOADJSON("data/enemies.json")
FOR i = 0 TO UBOUND(enemies)
PRINT enemies(i).name + " HP: " + STR$(enemies(i).health)
NEXTREM Load dialog system
DIM dialogs AS Object
dialogs = LOADJSON("dialog/game.json")
PRINT dialogs.intro.character + ": " + dialogs.intro.textSee also: SAVEJSON, LOADTEXT, DESERIALIZE$
LOADTEXT
File I/O · since 0.23.0
Loads text content from files in the project bundle. Returns the complete file content as a string. Ideal for loading game data, configuration files, level definitions, dialog text, and any text-based resources.
Syntax
result$ = LOADTEXT(filename$)Parameters
- filename$ - Name of text file in project bundle (relative to project root)
Examples
levelData$ = LOADTEXT("levels/level1.txt")
PRINT "Level 1 loaded: " + LEN(levelData$) + " characters"dialog$ = LOADTEXT("dialog/intro.txt")
PRINT dialog$REM Load game configuration
configText$ = LOADTEXT("config.txt")
lines = SPLIT(configText$, CHR$(10))
FOR i = 0 TO UBOUND(lines)
PRINT "Setting: " + lines(i)
NEXTREM Load serialized game state
savedGame$ = LOADTEXT("autosave.dat")
IF LEN(savedGame$) > 0 THEN
gameState = DESERIALIZE$(savedGame$)
PRINT "Game restored from autosave"
END IFREM Load high score names
scoreNames$ = LOADTEXT("scores.txt")
names = SPLIT(scoreNames$, ",")
FOR i = 0 TO UBOUND(names)
PRINT (i + 1) + ". " + names(i)
NEXTSee also: SAVETEXT, LOADJSON, DESERIALIZE$
LOC
File I/O · since 1.0.0
Returns the current read/write position of an open handle. For a DATA block handle, this is the 0-based index of the next value to be read. For a file-stream handle (OPEN ... FOR INPUT / OUTPUT / APPEND), this is the 0-based line number — how many lines have been read or written so far. Returns -1 if the handle is closed or invalid.
Syntax
result = LOC(filenumber)Parameters
- filenumber - Handle from OPEN or OPEN DATA
Examples
OPEN "log.txt" FOR INPUT AS #1
DIM line$ AS STRING
LINE INPUT #1, line$
PRINT "After 1 line, LOC = " + STR$(LOC(1)) ' 1
CLOSE #1DATA "nums"
10, 20, 30
END DATA
OPEN DATA "nums" AS #1
DIM n AS INTEGER
READ #1, n
PRINT "After 1 READ, LOC = " + STR$(LOC(1)) ' 1
CLOSE #1See also: LOF, EOF, ISOPEN, OPEN, OPEN DATA
LOF
File I/O · since 1.0.0
Returns the total size of the resource backing an open handle. For a DATA block handle, this is the total number of values in the block. For a file-stream handle, this is the total number of lines in the buffered file. Returns -1 if the handle is closed or invalid.
Combine with LOC to compute remaining content, or to drive a progress indicator over a long input file.
Syntax
result = LOF(filenumber)Parameters
- filenumber - Handle from OPEN or OPEN DATA
Examples
OPEN "level.txt" FOR INPUT AS #1
PRINT "File has " + STR$(LOF(1)) + " lines"
CLOSE #1REM Show a progress indicator while parsing
OPEN "big.csv" FOR INPUT AS #1
DIM total AS INTEGER, row$ AS STRING
total = LOF(1)
WHILE NOT EOF(1)
LINE INPUT #1, row$
PRINT "Progress: " + STR$(INT(LOC(1) * 100 / total)) + "%"
WEND
CLOSE #1See also: LOC, EOF, OPEN, OPEN DATA
OPEN
File I/O · since 1.0.0
Opens a text file for sequential line-by-line access. FOR INPUT reads from an existing file; FOR OUTPUT creates (or truncates) a file for writing; FOR APPEND opens a file for writing while preserving existing contents. Pair with LINE INPUT or READ to read, PRINT # to write, and CLOSE when done.
Files are buffered: FOR INPUT loads the whole file into memory at OPEN time, so subsequent reads do not hit the bundle/VFS. FOR OUTPUT and FOR APPEND accumulate writes in memory and flush to the bundle/VFS when CLOSE runs — drop the handle without CLOSE and writes are lost. EOF, LOC, LOF, and ISOPEN all work on file handles.
In multiplayer, FOR OUTPUT and FOR APPEND are server-side only — calling them from the local-client interpreter halts with a runtime error pointing at the EVENT HOST "Save" pattern (same policy as SAVETEXT/SAVEJSON). FOR INPUT is allowed on every interpreter because clients legitimately read level data, dialog, and configs from the cart.
This is the file-stream form. For named in-program data tables, use OPEN DATA instead.
One handle, one thing. Files, DATA blocks and databases share the #n space so that CLOSE #n is unambiguous. Opening onto a number something else already holds is a runtime error naming what holds it — in either direction, for either kind. CLOSE frees the number for any of them again.
Syntax
OPEN filename$ FOR INPUT|OUTPUT|APPEND AS #filenumberParameters
- filename$ - Path to the file (any string expression, resolved against the project bundle / local storage)
- FOR INPUT - Open for reading (file must exist)
- FOR OUTPUT - Open for writing (truncates any existing file)
- FOR APPEND - Open for writing, preserving existing contents
- filenumber - Numeric handle (1-255) used by subsequent READ / LINE INPUT / PRINT # / CLOSE
Examples
REM Read a configuration file line by line
OPEN "config.txt" FOR INPUT AS #1
DIM line$ AS STRING
WHILE NOT EOF(1)
LINE INPUT #1, line$
PRINT line$
WEND
CLOSE #1REM Write a high score table
OPEN "scores.txt" FOR OUTPUT AS #1
FOR i = 0 TO 9
PRINT #1, names$(i) + "," + STR$(scores(i))
NEXT
CLOSE #1REM Append a timestamped audit line
OPEN "events.log" FOR APPEND AS #1
PRINT #1, TIME$ + " player joined"
CLOSE #1REM Multiplayer — only the host writes (clients halt on FOR OUTPUT)
IF ME.ISHOST THEN
OPEN "world.dat" FOR OUTPUT AS #1
PRINT #1, SERIALIZE$(world)
CLOSE #1
END IFSee also: OPEN DATA, CLOSE, LINE INPUT, PRINT #, READ, EOF, LOC, LOF, ISOPEN, FILEEXISTS, SAVETEXT
OPEN DATABASE
File I/O · since 1.56.0
Opens a SQLite database on a file handle. A read resolves like any other read — a database bundled in the cart is found the same way an image is. FOR OUTPUT opens it for writing in whatever location the platform keeps save data, creating the database if it does not exist. Databases share the #n handle space with files and DATA blocks, so CLOSE covers all three and one handle cannot be two things at once — enforced on every OPEN, in both directions.
The database is held in memory while open and written back when the handle closes — the same point a buffered OPEN ... FOR OUTPUT file is flushed. A program that ends without CLOSE loses its changes, exactly as it would with a file.
Syntax
OPEN DATABASE path$ [FOR INPUT|OUTPUT] AS #nParameters
- path$ - Database file. A read resolves through the cart; FOR OUTPUT resolves to the platform's save location.
- FOR INPUT - Read-only, and the default when the clause is omitted. The file is never written back.
- FOR OUTPUT - Writable. Created if missing, flushed on CLOSE.
- #n - Handle number, shared with the file layer.
Example
' A database shipped inside the cart
OPEN DATABASE "content.db" AS #1
' The player's own save, in the platform's save location
OPEN DATABASE "player.db" FOR OUTPUT AS #2
' ... queries ...
CLOSE #1, #2Notes
- A missing database is an error for a read and a new empty database for a write — the same asymmetry
OPEN ... FOR INPUTandFOR OUTPUTalready have. - Read-only means read-only. A database opened without
FOR OUTPUTis never written back, so a cart cannot be modified by the program reading it. - Multiplayer: opening
FOR OUTPUTon a client is refused like any other client-side file write — the server owns the bytes. - Every platform. Desktop, iOS, tvOS, Android and the web player run the same SQLite through one implementation (
rusqlite::ffinatively,sqlite-wasm-rson wasm32 — the same C library compiled for each target). The file format is identical, so a database moves between them unchanged.
See also: SQL, CALL SQL, OPEN, CLOSE
File I/O · since 1.0.0
Writes one line of text to a file handle (opened with OPEN ... FOR OUTPUT or FOR APPEND). Each expression is stringified and joined with single spaces; a newline is appended automatically. Output is buffered in memory until CLOSE flushes the file to the bundle/VFS.
Unlike console PRINT, the file form has no WORDS or RATE modifiers — this is plain text output, not a typewriter effect.
In multiplayer, the underlying handle is server-side only. The policy fires at OPEN time (see OPEN), so a PRINT # statement that the interpreter actually reaches always has a valid server-side handle behind it. For client-visible text output, use console PRINT inside a GUARD(CLIENT) handler.
Syntax
PRINT #filenumber, expression [, expression ...]Parameters
- filenumber - File handle from OPEN ... FOR OUTPUT or FOR APPEND
- expression - One or more values; stringified and space-joined into a single line
Examples
OPEN "scores.txt" FOR OUTPUT AS #1
PRINT #1, "alice", 4200
PRINT #1, "bob", 3800
CLOSE #1REM Append a timestamped log line
OPEN "events.log" FOR APPEND AS #1
PRINT #1, TIME$ + " " + event$
CLOSE #1REM Round-trip — write then read
OPEN "player.txt" FOR OUTPUT AS #1
PRINT #1, playerName$
PRINT #1, STR$(playerLevel)
CLOSE #1
OPEN "player.txt" FOR INPUT AS #2
DIM name$ AS STRING, lvl$ AS STRING
LINE INPUT #2, name$
LINE INPUT #2, lvl$
CLOSE #2See also: OPEN, PRINT, CLOSE, SAVETEXT
SAVEJSON
File I/O · since 0.23.0
Saves data as JSON format to local storage. Automatically converts CrashBasic objects, arrays, and basic data types to properly formatted JSON. Ideal for saving structured game data, settings, and player information in a readable format.
In multiplayer, SAVEJSON is server-side only — same policy as SAVETEXT. A client-side call halts with a runtime error pointing at EVENT HOST "Save" as the right pattern. The server is the authoritative writer.
Syntax
SAVEJSON filename$, dataParameters
- filename$ - Name for the saved JSON file (will be stored in local storage)
- data - Any data structure (Object, Array, String, Number, Boolean, TYPE structure)
Examples
DIM player AS Object
player.name = "Hero"
player.level = 10
player.inventory = ["sword", "potion", "key"]
SAVEJSON "player.json", playerDIM gameSettings AS Object
gameSettings.difficulty = "hard"
gameSettings.graphics = {"quality": "high", "fullscreen": true}
gameSettings.audio = {"music": 0.8, "effects": 1.0}
SAVEJSON "settings.json", gameSettingsREM Save high scores with player data
DIM scores AS Array
FOR i = 0 TO 4
DIM entry AS Object
entry.name = playerNames(i)
entry.score = highScores(i)
entry.date = DATE$
scores(i) = entry
NEXT
SAVEJSON "highscores.json", scoresREM Save level progress
TYPE LevelProgress
completed AS Boolean
bestTime AS Number
starsEarned AS Number
END TYPE
DIM progress(20) AS LevelProgress
progress(0).completed = true
progress(0).bestTime = 45.2
progress(0).starsEarned = 3
SAVEJSON "progress.json", progressREM Save custom level data
DIM levelEditor AS Object
levelEditor.tiles = tileArray
levelEditor.enemies = enemyPositions
levelEditor.metadata = {"author": "Player", "version": 1}
SAVEJSON "custom_level.json", levelEditorSee also: LOADJSON, SAVETEXT, SERIALIZE$
SAVETEXT
File I/O · since 0.23.0
Saves text content to local storage with the specified filename. Data persists between game sessions and can be loaded with LOADTEXT. Perfect for saving player progress, game settings, high scores, and user-generated content.
In multiplayer, SAVETEXT is server-side only. The host or a player-server is the authoritative writer; a call from the local-client interpreter halts with a runtime error pointing at EVENT HOST "Save" as the right pattern. The server writes once; clients read replicated state. Single-player allows it unconditionally (one interpreter, one filesystem).
Syntax
SAVETEXT filename$, content$Parameters
- filename$ - Name for the saved file (will be stored in local storage)
- content$ - Text content to save
Examples
playerName$ = "Hero"
SAVETEXT "player.txt", playerName$REM Save high scores
scoreList$ = "1000,800,650,500,300"
SAVETEXT "highscores.txt", scoreList$REM Save game state
DIM gameState AS Object
gameState.level = 5
gameState.score = 2500
saveData$ = SERIALIZE$(gameState)
SAVETEXT "savegame.dat", saveData$REM Save player preferences
DIM settings AS Object
settings.difficulty = "normal"
settings.music = true
settings.controls = {"up": "W", "down": "S"}
settingsText$ = SERIALIZE$(settings)
SAVETEXT "settings.dat", settingsText$REM Autosave system
DECLARE SUB AutoSave()
CALL TIMER AutoSave() EVERY 30000 MILLISECONDS AS "autosave"
SUB AutoSave()
currentState$ = SERIALIZE$(gameState)
SAVETEXT "autosave.dat", currentState$
END SUBSee also: LOADTEXT, SAVEJSON, SERIALIZE$
SERIALIZE$
File I/O · since 0.23.0
Converts objects, arrays, or any data structure to a JSON string for storage or transmission. Essential for saving game state, player progress, settings, and complex data structures to persistent storage. The output is standard JSON, so it can be used with external tools or APIs that accept JSON.
Multi-dimensional arrays serialize as nested JSON arrays: a 2D array becomes [[1,2],[3,4]], a 3D array nests one level deeper, and so on. A matrix stays a matrix for tools that ingest the output. Objects and arrays that contain multi-dimensional arrays serialize their contents in full.
Syntax
SERIALIZE$(data)Parameters
- data - Any data type (Object, Array, String, Number, Boolean, TYPE structure)
Examples
DIM player AS Object
player.name = "Hero"
player.level = 5
player.score = 1000
saveData$ = SERIALIZE$(player)DIM highScores(10) AS Number
FOR i = 0 TO 9
highScores(i) = (10 - i) * 1000
NEXT
scoresData$ = SERIALIZE$(highScores)TYPE GameState
level AS Number
lives AS Number
powerups AS Object
END TYPE
DIM state AS GameState
state.level = 3
state.lives = 5
savedGame$ = SERIALIZE$(state)REM Save complex game configuration
DIM config AS Object
config.graphics = "high"
config.sound = true
config.controls = {"jump": "SPACE", "fire": "CTRL"}
configData$ = SERIALIZE$(config)
SAVETEXT "config.json", configData$See also: DESERIALIZE$, SAVETEXT, SAVEJSON
SQL
File I/O · since 1.56.0
Declares a named, parameterized SQL statement. Nothing runs until CALL SQL — the block registers the statement the way BEHAVIOR registers a behavior, and the name is case-insensitive.
The body is real SQL, kept verbatim. SQLite's named-parameter syntax is @name, the same sigil a behavior parameter uses, so the two notations coincide and nothing has to be rewritten or escaped. Every parameter binds as an actual SQLite parameter, which means a value can never turn into syntax — SQL injection is not filtered here, it is impossible.
One statement per block. A block is named, called, bound with one set of parameters and reported as one outcome, so it holds exactly one SQL statement. A second statement is a runtime error naming it — before 1.57.0 it was compiled away and silently never ran, which is how a table you thought you had created turned up missing. A trailing semicolon or comment is formatting and is fine.
Syntax
SQL "name"
[USE NUMBER paramName [DEFAULT value]]
[USE STRING paramName$ [DEFAULT "value"]]
[NULLS column = value [, column = value ...]]
<statement text>
END SQLParameters
- name - String name for the block, used by CALL SQL.
- USE NUMBER paramName [DEFAULT value] - Declares a numeric parameter. Without a DEFAULT the call site must supply it.
- USE STRING paramName$ [DEFAULT "value"] - Declares a string parameter.
- NULLS column = value - Declares what an absent value means for that column. Only columns named here may come back NULL.
Examples
SQL "topScores"
USE NUMBER minScore DEFAULT 0
USE NUMBER limit DEFAULT 10
SELECT name, score FROM runs
WHERE score >= @minScore
ORDER BY score DESC LIMIT @limit
END SQL' Declaring what an absent value means
SQL "allRuns"
NULLS score = 0, name = "(unknown)"
SELECT name, score FROM runs
END SQL' Keeping absence and zero apart, in SQL
SQL "runsWithGaps"
SELECT name,
IFNULL(score, 0) AS score,
score IS NULL AS scoreMissing
FROM runs
END SQLNotes
- NULL is never converted silently. Crash BASIC has no NULL, and mapping one to
0would make it indistinguishable from a real zero ever after. A column that comes back NULL with noNULLSmapping halts with a message naming the block, the column and the row. - Telling absence from zero is done in SQL, which already has the vocabulary:
SELECT IFNULL(score, 0) AS score, score IS NULL AS scoreMissinggives you both, andIS NULLarrives as an ordinary 1 or 0. - A parameter the statement never mentions is reported rather than ignored, as is an argument the block never declared — both are usually a typo.
- Only scalars bind. A number or a string; an array, object, coordinate or mutation has no SQL equivalent and is refused by name.
See also: CALL SQL, OPEN DATABASE, BEHAVIOR, SHADER
Resources
BASE64TOIMG
Resources · since 1.30.0
Function-form analog of LOADIMG that takes a base64-encoded PNG payload instead of a file path. Designed to compose with FETCH — load an image over the network, then register it as a named texture in one more line. Accepts either raw base64 or a full data:image/png;base64,... URI; the data URI prefix is stripped automatically. Same dim-parsing and sprite-sheet semantics as LOADIMG. Errors halt with a clear runtime error — gate the call on FETCH's STATUSCODE if the source might fail.
Like all image handles, the image_name is case-insensitive — BASE64TOIMG(data$, "Player") registers it under "PLAYER" and PUTIMAGE looks it up the same way.
It is a function, not a statement. It returns 1 when the image was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOIMG(data$, "name"). A bare BASE64TOIMG(...) on its own line is a syntax error.
Syntax
ok = BASE64TOIMG(base64$, image_name)
ok = BASE64TOIMG(base64$, image_name, sheet_w, sheet_h)Parameters
- base64$ - Base64-encoded PNG payload, or a
data:image/png;base64,...URI - image_name - Reference name for later use with PUTIMAGE (case-insensitive)
- sheet_w, sheet_h - Optional frame dimensions to load as a sprite sheet (matches LOADIMG ... AS SPRITESHEET(w, h))
Examples
REM Pair with FETCH to load a remote image
r = FETCH("https://cdn.example.com/player.png", "GET")
IF r.STATUSCODE = 200 THEN
ok = BASE64TOIMG(r.MESSAGE, "player")
PUTIMAGE (100, 100), "player"
END IFREM Sprite sheet variant — 16x16 frames
r = FETCH("https://cdn.example.com/tiles.png", "GET")
ok = BASE64TOIMG(r.MESSAGE, "tiles", 16, 16)
PUTIMAGE (0, 0), "tiles", 0REM Accept a full data URI (e.g. embedded asset)
ok = BASE64TOIMG("data:image/png;base64,iVBOR...", "icon")See also: LOADIMG, PUTIMAGE, FETCH, BASE64TOWAV, BASE64TOMP3
BASE64TOMP3
Resources · since 1.30.0
Function-form analog of LOADMP3 that takes a base64-encoded MP3 payload instead of a file path. Designed to compose with FETCH. Accepts either raw base64 or a full data:audio/mpeg;base64,... URI; the data URI prefix is stripped automatically. Client-only — calling from a server-side multiplayer interpreter is a fatal runtime error, matching LOADMP3's gating. The audio_name is case-insensitive — BASE64TOMP3(data$, "Theme") and PLAYMP3 "theme" agree.
It is a function, not a statement. It returns 1 when the MP3 was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOMP3(data$, "name"). A bare BASE64TOMP3(...) on its own line is a syntax error.
Syntax
ok = BASE64TOMP3(base64$, audio_name)Parameters
- base64$ - Base64-encoded MP3 payload, or a
data:audio/mpeg;base64,...URI - audio_name - Reference name for later playback (case-insensitive)
Example
REM Pair with FETCH
r = FETCH("https://cdn.example.com/theme.mp3", "GET")
IF r.STATUSCODE = 200 THEN
ok = BASE64TOMP3(r.MESSAGE, "theme")
PLAYMP3 "theme" LOOP
END IFSee also: LOADMP3, PLAYMP3, FETCH, BASE64TOWAV, BASE64TOIMG
BASE64TOWAV
Resources · since 1.30.0
Function-form analog of LOADWAV that takes a base64-encoded WAV payload instead of a file path. Designed to compose with FETCH. Accepts either raw base64 or a full data:audio/wav;base64,... URI; the data URI prefix is stripped automatically. Client-only — calling from a server-side multiplayer interpreter is a fatal runtime error, matching LOADWAV's gating. The audio_name is case-insensitive — BASE64TOWAV(data$, "Boom") and PLAYWAV "boom" agree.
It is a function, not a statement. It returns 1 when the WAV was decoded and registered and 0 when it was not, so call it in an expression and check the result — ok = BASE64TOWAV(data$, "name"). A bare BASE64TOWAV(...) on its own line is a syntax error.
Syntax
ok = BASE64TOWAV(base64$, audio_name)Parameters
- base64$ - Base64-encoded WAV payload, or a
data:audio/wav;base64,...URI - audio_name - Reference name for later playback (case-insensitive)
Example
REM Pair with FETCH
r = FETCH("https://cdn.example.com/explosion.wav", "GET")
IF r.STATUSCODE = 200 THEN
ok = BASE64TOWAV(r.MESSAGE, "boom")
PLAYWAV "boom"
END IFSee also: LOADWAV, PLAYWAV, FETCH, BASE64TOMP3, BASE64TOIMG
FADEMP3
Resources · 📱 Client-only · since 0.24.0
Fades an MP3 in or out over the specified duration. FADEMP3 IN starts the track (if not already playing) and ramps volume from 0 to master volume. FADEMP3 OUT ramps volume from the current volume to 0 and stops playback when complete. In multiplayer FADEMP3 is client-only — wrap in IF ME.ISCLIENT THEN ... END IF or trigger from the server with EVENT CLIENT "name".
Syntax
FADEMP3 IN|OUT audio_name DURATION secondsParameters
- IN|OUT - Fade direction (IN = fade in, OUT = fade out)
- audio_name - Reference name of a loaded MP3
- seconds - Duration of the fade in seconds
Examples
FADEMP3 IN "bgmusic" DURATION 3FADEMP3 OUT "bgmusic" DURATION 5' Start background music with fade-in
FADEMP3 IN "bgm" DURATION 2' Fade out background music over 30 seconds
FADEMP3 OUT "bgm" DURATION 30See also: PLAYMP3, STOPMP3, LOADMP3
FREE
Resources · since 1.4.3
Frees a resource from memory. For audio resources (MP3/WAV), stops playback and unloads the sound data. For images, removes the texture from GPU memory and disables any sprites using that image. For layers, destroys the layer and disables sprites assigned to it. Layer 0 (default screen layer) cannot be freed.
Syntax
FREE MP3 name | FREE WAV name | FREE IMAGE name | FREE LAYER idParameters
- MP3/WAV/IMAGE name - String expression identifying the resource to free
- LAYER id - Numeric expression for the layer ID to free (1 or higher)
Examples
FREE MP3 "bgmusic" ' Stop and unload MP3FREE WAV "explosion" ' Stop and unload WAVFREE IMAGE "player_sprite" ' Free image and disable sprites using itFREE IMAGE imgName$ ' Free using variable nameFREE LAYER 2 ' Free layer and disable sprites on itSee also: LOADWAV, LOADMP3, LOADIMG, CREATE LAYER, DISABLE SPRITE
GETIMAGE
Resources · since 0.23.0
Captures a rectangular area of the screen as an image resource that can be used with PUTIMAGE. Useful for creating dynamic sprites, saving screen regions, or implementing effects like trails.
Syntax
GETIMAGE (x1, y1)-(x2, y2), image_nameParameters
- x1, y1 - Top-left corner of capture area
- x2, y2 - Bottom-right corner of capture area
- image_name - Name to assign to captured image
- Captured image includes all graphics in the specified region
- Can capture across multiple layers if using layer system
Examples
GETIMAGE (0, 0)-(319, 199), "screenshot" ' Capture entire screenGETIMAGE (playerX-16, playerY-16)-(playerX+16, playerY+16), "player_backup" ' Capture around playerGETIMAGE (100, 100)-(200, 150), "ui_section" ' Capture UI area' Create trail effect:
GETIMAGE (shipX-5, shipY-5)-(shipX+5, shipY+5), "trail"
PUTIMAGE (shipX-10, shipY), "trail", ALPHA(0.5)' Save and restore screen area:
GETIMAGE (x, y)-(x+width, y+height), "backup"
' Draw temporary graphics...
PUTIMAGE (x, y), "backup" ' Restore originalSee also: PUTIMAGE, LOADIMG, Screen Capture
LOADIMG
Resources · since 0.23.0
Loads PNG, JPG, GIF, or Aseprite (.ase/.aseprite) images from bundle resources for use with PUTIMAGE. Supports transparency and can load images as sprite sheets for animation. Files must be uploaded to the project bundle via the Resource Manager. LOADPNG is a legacy alias for this command.
Both the filename and image_name are case-insensitive on native hosts. LOADIMG "HELLOWORLD.PNG", "player" finds HelloWorld.png on disk (the LocalBus does an exact-match pass then falls back to a case-insensitive component walk inside the sandbox). The image_name is case-insensitive at lookup time. WASM hosts SHOULD implement the same fallback in their VFS handler — see the IoBus trait docs in io.rs.
Aseprite files. A .ase/.aseprite file is flattened into a horizontal sprite sheet (one frame per timeline frame), so it loads exactly like a sprite sheet — the per-frame size is the canvas size unless you override it with AS SPRITESHEET(w, h). Aseprite files also carry two kinds of animation metadata that other formats can't, and LOADIMG reads both: the per-frame display durations authored on the timeline (used by ANIMATE ... FRAMERATE OFSPRITE) and the per-frame slice geometry (used by ANIMATE ... AUTOBBOX and by the SLICES clause).
SLICES arr() (Aseprite only) fills the array variable arr with one relative-0–1 bounding-box Range per frame, expanded from the file's first slice's keyframes (a slice key stays in effect until the next key, matching Aseprite). arr(frame) is (x1,y1)-(x2,y2) in 0–1 coordinates, ready to drop into SET SPRITE id BBOX arr(frame). Frames no slice covers fall back to the full frame (0,0)-(1,1), so the array is always dense. Using SLICES on a non-Aseprite file, or on an .ase with no slices, is a loud runtime error.
TAG("name") (Aseprite only) loads ONLY that animation tag's frames into the sheet, re-indexed from 0 — so ANIMATE image_name, 0 TO N plays exactly that tag, and the FRAMERATE OFSPRITE durations and AUTOBBOX/SLICES geometry are narrowed to the tag's frames to match. A .ase already implies a sprite sheet (each timeline frame is a cell), so no AS SPRITESHEET clause is needed. Tag names match case-insensitively; an unknown tag, or TAG on a non-Aseprite file, is a loud runtime error that lists the available tags. Want several tags from one file? Load it once per tag under different names.
LAYERS(...) (Aseprite only) composites ONLY the named or indexed source layers into the loaded image, dropping the rest. Each selector is a layer name (string, case-insensitive) or a 0-based layer index (number, where 0 is the bottom of the stack) — mix them freely: LAYERS("Turret", 0). Blend modes, opacity, and per-cel z-order are honored exactly as Aseprite renders them, and an explicitly-selected layer composites even if its eye icon is toggled off in the file. This is how you split one layered .ase into separate composited sprites: load it once per component under different names — LAYERS("Base", "Treads") for the hull, LAYERS("Turret") for the turret. Combines with TAG (that tag's frames, only these layers). An unknown layer name or an out-of-range index is a loud runtime error that lists the available layer names.
In CrashNet multiplayer, LOADIMG runs on every interpreter — including the host and player-servers — but the server-side execution is dimension-only. The image bytes are parsed (PNG/ASE headers give real width/height) and the dimensions register on each interpreter's BehaviorEngine so collision and pattern math work correctly. The Aseprite per-frame durations and slice bboxes register on every interpreter too, so OFSPRITE timing and AUTOBBOX hitboxes resolve identically on server and client. The actual texture data sent to the renderer is dropped by the headless host's GfxClient (it only forwards sprite commands), so there's no double-render or wire-bandwidth cost. Practically: just call LOADIMG once at top-level (no IF ME.ISCLIENT gate needed) and every interpreter will know the dims.
Syntax
LOADIMG filename, image_name [AS SPRITESHEET(width, height)] [TAG("name")] [SLICES arr()] [LAYERS(layer, ...)]Parameters
- filename - Image filename in project bundle (supports spaces in quotes)
- image_name - Reference name for later use with PUTIMAGE
- AS SPRITESHEET(width, height) - Optional: Load as sprite sheet with frame dimensions
- TAG("name") - Optional (Aseprite only): load only the named animation tag's frames into the sheet, re-indexed from 0
- SLICES arr() - Optional (Aseprite only): fill arr with one relative-0–1 bounding-box Range per frame, from the file's first slice
- LAYERS(layer, ...) - Optional (Aseprite only): composite only these source layers (each a name string or a 0-based index) into the image; call again with different layers to split a layered file into separate sprites
- Supported formats: PNG (with transparency), JPG, GIF, Aseprite (.ase/.aseprite)
- Local files: Upload via Resource Manager, then reference by filename
- Frame numbering: Sprite sheet frames start at 0 (top-left), increment left-to-right, top-to-bottom
Examples
LOADIMG "player.png", "player" ' Load single imageLOADIMG "Sprite Sheet.png", "sprites" AS SPRITESHEET(32, 32) ' Load as 32x32 sprite sheetLOADIMG "background.jpg", "bg" ' Load JPEG backgroundLOADIMG "icons.png", "ui" AS SPRITESHEET(16, 16) ' Load UI iconsLOADIMG "animated character.gif", "hero" ' GIF with spaces in filename' Sprite sheet example with 8x8 frames:
' If image is 64x32, creates 8 frames (64/8=8 wide, 32/8=4 tall)
LOADIMG "tiles.png", "tiles" AS SPRITESHEET(8, 8)' Aseprite: load with per-frame slice hitboxes into boxes()
LOADIMG "hero.ase", "HERO" AS SPRITESHEET(32, 32) SLICES boxes()
SET SPRITE 0, 100, 100, "HERO"
SET SPRITE 0 BBOX boxes(0) ' frame 0's slice as the bbox' Aseprite: load just the "run" animation tag's frames
LOADIMG "hero.ase", "HERO_RUN" TAG("run")
BEHAVIOR "running"
ANIMATE "HERO_RUN", 0 TO 5 FRAMERATE OFSPRITE
END BEHAVIOR' Aseprite: split a layered tank into separate composited sprites
LOADIMG "tank.ase", "TANK_BASE" LAYERS("Base", "Treads")
LOADIMG "tank.ase", "TANK_TURRET" LAYERS("Turret")
' now the turret can rotate independently of the hull' Note: Upload files via Resources button first
' Then reference by exact filename in LOADIMGSee also: PUTIMAGE, FRAMECOUNT, BEHAVIOR, SET SPRITE, Resource Manager
LOADMP3
Resources · since 0.23.0
Loads MP3/WAV audio files from local resources or URLs for playback. Supports filenames with spaces.
Audio handles are case-insensitive — LOADMP3 "theme.mp3", "Theme" and PLAYMP3 "theme" agree. File paths are also case-insensitive on native hosts, so LOADMP3 "HELLOWORLD.MP3", "x" finds HelloWorld.mp3 on disk via the sandbox's component walk on miss.
Re-loading the same file onto a handle you already loaded it into is a no-op, so a LOADMP3 sitting in a game loop costs nothing after the first call. Loading a different file onto that handle swaps the clip — the old audio is freed and the handle plays the new file from then on. To drop a clip without replacing it, use FREE MP3 name.
In CrashNet multiplayer, resource loads are client-only — calling LOADMP3 from server-side code (host or player-server interpreter) is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block; PLAYMP3 issued from the server (or host's broadcast) plays back on all clients using the resource each client already loaded.
Syntax
LOADMP3 filename_or_url, audio_nameParameters
- filename_or_url - Local filename or full URL to audio file
- audio_name - Reference name for later playback
- Supported formats: MP3, WAV, OGG
- Local files: Upload via Resource Manager, then reference by filename
- URLs: Full HTTP/HTTPS URLs for remote files
Examples
LOADMP3 "music.mp3", "bgmusic" ' From local resourcesLOADMP3 "Background Music.mp3", "bg" ' Filename with spacesLOADMP3 "coin pickup.wav", "coin" ' WAV file with spacesLOADMP3 "explosion.mp3", "boom" ' From local resourcesLOADMP3 "https://example.com/song.mp3", "remote" ' From URL' Note: Upload files via Resources button first' Then reference by exact filename in LOADMP3See also: PLAYMP3, STOPMP3, LOADWAV
LOADPNG
Resources · since 0.23.0
Legacy alias for LOADIMG. Loads PNG, JPG, or GIF images from bundle resources for use with PUTIMAGE.
In CrashNet multiplayer, resource loads are client-only — calling LOADPNG from server-side code is a fatal runtime error. See LOADIMG for the full pattern.
Syntax
LOADPNG filename, image_name [AS SPRITESHEET(width, height)]Parameters
- filename - Image filename in project bundle
- image_name - Reference name for later use with PUTIMAGE
- AS SPRITESHEET(width, height) - Optional: Load as sprite sheet with frame dimensions
Examples
LOADPNG "player.png", "player" ' Works, but prefer LOADIMGLOADIMG "player.png", "player" ' PreferredSee also: LOADIMG, PUTIMAGE, FRAMECOUNT
LOADWAV
Resources · since 0.23.0
Loads WAV audio files from local resources or URLs for playback. Supports filenames with spaces.
Audio handles are case-insensitive — LOADWAV "shoot.wav", "Shoot" and PLAYWAV "shoot" agree. File paths are also case-insensitive on native hosts, so LOADWAV "HELLOWORLD.WAV", "x" finds HelloWorld.Wav on disk via the sandbox's component walk on miss.
Re-loading the same file onto a handle you already loaded it into is a no-op, so a LOADWAV sitting in a game loop costs nothing after the first call. Loading a different file onto that handle swaps the clip — the old audio is freed and the handle plays the new file from then on. To drop a clip without replacing it, use FREE WAV name.
In CrashNet multiplayer, resource loads are client-only — calling LOADWAV from server-side code (host or player-server interpreter) is a fatal runtime error. Each client loads its own copies inside an IF ME.ISCLIENT THEN block.
Syntax
LOADWAV filename_or_url, audio_nameParameters
- filename_or_url - Local filename or full URL to WAV file
- audio_name - Reference name for later playback
- Supported format: WAV (uncompressed audio)
- Local files: Upload via Resource Manager, then reference by filename
- URLs: Full HTTP/HTTPS URLs for remote files
Examples
LOADWAV "sound.wav", "effect" ' From local resourcesLOADWAV "Laser Sound.wav", "laser" ' Filename with spacesLOADWAV "explosion.wav", "boom" ' From local resourcesLOADWAV "https://example.com/beep.wav", "beep" ' From URL' Note: Upload files via Resources button first' Then reference by exact filename in LOADWAVSee also: PLAYWAV, STOPWAV, LOADMP3
PLAYMP3
Resources · 📱 Client-only · since 1.5.0
Plays loaded audio files. Supports named modifiers for clarity, and they may appear in any order (EVENTSON, if present, comes last). LOOP enables continuous playback and VOLUME(n) sets volume (0.0-1.0). SECTION(start, end) plays or loops only that region of the file, in seconds — useful for a multi-part track where you want to repeat just one part; without LOOP it plays the region once, with LOOP it repeats just that region. CROSSFADE(secs) equal-power blends the loop seam of a looped section so it repeats without a click; it uses the audio just past the section end, so it applies to a looped SECTION (a whole-file loop has no tail to blend). The optional EVENTSON clause fires timed cue events during playback; with a SECTION its timestamps are relative to the section start. The audio_name lookup is case-insensitive — PLAYMP3 "theme" finds whatever was loaded under "Theme", "THEME", etc. In multiplayer this statement is client-only — wrap the call in IF ME.ISCLIENT THEN ... END IF, or trigger it from the server with EVENT CLIENT "name" so the client's handler runs the actual PLAYMP3. Calling it from a server-side interpreter raises a fatal runtime error.
Syntax
PLAYMP3 audio_name [SECTION(start, end)] [LOOP] [CROSSFADE(secs)] [VOLUME(n)] [EVENTSON(t1, t2, ...)]Parameters
- audio_name - Reference name of a loaded audio file
- SECTION(start, end) - Optional modifier that limits playback to the region from
starttoendseconds (needs 0 <= start < end). With LOOP, only this region repeats; without LOOP, the region plays once. - LOOP - Optional modifier that enables looping playback
- CROSSFADE(secs) - Optional modifier that equal-power blends a looped section's seam over
secsseconds (default 0 = hard cut). Applies to a looped SECTION; ignored otherwise. - VOLUME(n) - Optional modifier that sets playback volume (0.0 to 1.0, default 1.0)
- EVENTSON(t1, t2, ...) - Optional list of timestamps in seconds that trigger events when playback reaches them. Event name = UPPERCASED audio name + '_CUE' (e.g., PLAYMP3 "theme" → event 'THEME_CUE'). Callback receives the timestamp as an argument. Cue points re-fire on each loop iteration. With a SECTION, timestamps are relative to the section start.
Examples
PLAYMP3 "bgmusic"PLAYMP3 "bgmusic" LOOPPLAYMP3 "bgmusic" LOOP VOLUME(0.5)PLAYMP3 "explosion" VOLUME(0.8)' Loop just the second section of a multi-part track
PLAYMP3 "battletheme" SECTION(8.0, 24.0) LOOP' Loop a section with a 150ms blend so the seam is seamless
PLAYMP3 "battletheme" SECTION(8.0, 24.0) LOOP CROSSFADE(0.15) VOLUME(0.8)' Play one region once (no loop)
PLAYMP3 "stinger" SECTION(1.5, 3.0)PLAYMP3 "theme" LOOP VOLUME(0.8) EVENTSON(1.5, 3.0, 7.2)' Sync game events to music cue points
PLAYMP3 "theme" LOOP VOLUME(0.8) EVENTSON(1.5, 3.0, 7.2)
SUB OnCue(timestamp)
IF timestamp = 1.5 THEN PRINT "Beat drop!"
IF timestamp = 3.0 THEN PRINT "Chorus!"
END SUB
CALL EVENT OnCue() AS "THEME_CUE"' Legacy positional syntax (deprecated, still supported)
PLAYMP3 "bgmusic", 1, 0.5See also: LOADMP3, STOPMP3, FADEMP3, PLAYWAV
PLAYWAV
Resources · 📱 Client-only · since 0.23.0
Plays loaded WAV audio files with optional stereo positioning. The audio_name lookup is case-insensitive — PLAYWAV "shoot" finds whatever was loaded under "Shoot", "SHOOT", etc. In multiplayer this statement is client-only — wrap the call in IF ME.ISCLIENT THEN ... END IF, or trigger it from the server with EVENT CLIENT "name" so the client's handler runs the actual PLAYWAV. Calling it from a server-side interpreter (host or player-server) raises a fatal runtime error.
Syntax
PLAYWAV audio_name [, loop] [, volume] [AT(x, y)] (AT also accepts a coordinate: AT(coord))Parameters
- audio_name - Reference name
- loop - 1=loop, 0=once
- volume - 0.0 to 1.0
- AT(x, y) - Stereo position: x from -1.0 (left) to 1.0 (right), y reserved for future 3D audio
Examples
PLAYWAV "effect"PLAYWAV "laser", 0, 1.0PLAYWAV "ambient", 1, 0.3PLAYWAV "explosion" AT(-0.5, 0.0) ' Play from left sidePLAYWAV "shot", 0, 1.0 AT(1.0, 0.0) ' Play from right sideSee also: LOADWAV, STOPWAV, PLAYMP3
PUTIMAGE
Resources · since 0.23.0
Displays loaded images on screen with advanced rendering options. Supports sprite animation, transparency, scaling, region cropping, tile map rendering, flipping, and rotation. Use BEGIN FRAME/END FRAME for optimal performance when drawing multiple sprites.
Syntax
PUTIMAGE (x, y), image_name [, frame] [, ALPHA(a)] [, SCALE(sx, sy)] [, SCALETO(w, h)] [, REGION(x1, y1, x2, y2)] [, MAP(array, x1, y1, x2, y2)] [, FLIPX] [, FLIPY] [, ROTATE(angle [, originX, originY])]
(the (x, y) position and SCALE each also accept a coordinate value)Parameters
- x, y - Screen coordinates for image placement
- image_name - Name of image loaded with LOADIMG (must be sprite sheet for MAP)
- frame - Optional sprite sheet frame number (starts at 0)
- ALPHA(a) - Optional transparency (0.0=transparent, 1.0=opaque)
- SCALE(sx, sy) - Optional scale factors (1.0 = original, 2.0 = double, 0.5 = half)
- SCALETO(w, h) - Optional scaling to specific pixel dimensions
- REGION(x1, y1, x2, y2) - Optional source region to copy from the image
- MAP(array, x1, y1, x2, y2) - Optional tile map rendering. Draws tiles from 2D array where values are sprite frame indices. Values ≤0 are skipped (transparent). Coordinates specify the region of the map array to draw.
- FLIPX - Optional horizontal flip
- FLIPY - Optional vertical flip
- ROTATE(angle) - Optional rotation in degrees (0-360), rotates around center (0.5, 0.5) by default
- ROTATE(angle, originX, originY) - Rotation with custom pivot point using normalized values (0-1), relative to the image's top-left corner:
- (0, 0) = top-left corner of image
- (0.5, 0.5) = center of image (default)
- (1, 1) = bottom-right corner of image
- Modifiers can be combined in any order
- Coordinates can be expressions or variables
Examples
PUTIMAGE (100, 100), "player" ' Draw image at positionPUTIMAGE (x, y), "sprites", frame ' Draw specific sprite framePUTIMAGE (50, 50), "ghost", ALPHA(0.5) ' 50% transparent imagePUTIMAGE (200, 150), "icon", SCALE(2, 2) ' Draw at double sizePUTIMAGE (200, 150), "icon", SCALETO(64, 64) ' Scale to exactly 64x64 pixelsPUTIMAGE (0, 0), "tileset", REGION(32, 0, 64, 32) ' Copy a 32x32 region from sourcePUTIMAGE (playerX, playerY), "hero", 0, FLIPX ' Flip horizontallyPUTIMAGE (enemyX, enemyY), "boss", FLIPX, FLIPY ' Flip both directionsPUTIMAGE (shipX, shipY), "ship", ROTATE(45) ' Rotate 45 degrees around center (default)PUTIMAGE (shipX, shipY), "ship", ROTATE(45, 0.5, 0.5) ' Rotate 45 degrees around centerPUTIMAGE (doorX, doorY), "door", ROTATE(90, 0, 1) ' Rotate around bottom-left (hinge)PUTIMAGE (itemX, itemY), "powerup", currentFrame, ALPHA(0.8), SCALE(1.5, 1.5) ' Combined modifiersPUTIMAGE (x, y), "sprite", FLIPX, ROTATE(angle) ' Flip and rotatePUTIMAGE (0, 0), "room", REGION(20, 20, 400, 400), SCALETO(640, 480) ' Crop and scale to target size' Sprite animation example:
frame = (frame + 1) MOD FRAMECOUNT("player")
PUTIMAGE (playerX, playerY), "player", frame' Rotating sprite around center:
angle = (angle + 5) MOD 360
PUTIMAGE (centerX, centerY), "spinner", ROTATE(angle, 0.5, 0.5)' Fade effect:
FOR i = 0 TO 10
alpha = i / 10
PUTIMAGE (x, y), "item", ALPHA(alpha)
NEXT' Tile map rendering:
LOADIMG "tiles.png", "tileset" AS SPRITESHEET(16, 16)
DIM map(9, 9) AS INTEGER
map(0, 0) = 1 ' Wall
map(1, 1) = 2 ' Floor
PUTIMAGE (0, 0), "tileset", MAP(map, 0, 0, 9, 9) ' Draw entire 10x10 mapPUTIMAGE (100, 0), "tiles", MAP(levelMap, 0, 0, 4, 4) ' Draw 5x5 section of mapPUTIMAGE (0, 0), "tiles", MAP(map, 0, 0, 9, 9), ALPHA(0.5) ' Transparent tile mapPUTIMAGE (200, 0), "tiles", MAP(map, 0, 0, 9, 9), SCALE(2, 2) ' Draw tiles at double sizeNotes
LIMITATION: When using MAP() with ROTATE() on very large tilesets (over 2048px in either dimension), rotation around a custom origin may produce incorrect positioning. This is due to an interaction between texture chunking and the rotation coordinate system. Workaround: Use smaller tileset images, or avoid ROTATE with custom origins on large MAP renders.
See also: LOADIMG, FRAMECOUNT, ALPHA, SCALE, SCALETO, REGION, ROTATE, MAP, BEGIN FRAME, END FRAME
STOPMP3
Resources · 📱 Client-only · since 0.23.0
Stops audio playback.
Syntax
STOPMP3 [audio_name]Parameters
- audio_name - Audio to stop (or all if omitted)
Examples
STOPMP3 "bgmusic"STOPMP3 ' Stop all audioSee also: PLAYMP3, LOADMP3, STOPWAV
STOPWAV
Resources · 📱 Client-only · since 0.23.0
Stops WAV audio playback.
Syntax
STOPWAV [audio_name]Parameters
- audio_name - Audio to stop (or all if omitted)
Examples
STOPWAV "effect"STOPWAV ' Stop all WAV audioSee also: PLAYWAV, LOADWAV, STOPMP3
Multiplayer
CALL HOST
Multiplayer · 🚧 Pre-release · since 1.19.0
Registers a subroutine to handle host promotion events. The subroutine is called when this client becomes the host (either initially or due to host failover). In CrashNet, one player is always designated as the "host" and is authoritative for HOST SPRITES. If the host disconnects, the next player by join order becomes host.
The optional GUARD(flags) modifier (see CALL) can restrict registration by interpreter context and/or serialize handler invocations with ONCE. Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.
Syntax
CALL HOST SubName() [GUARD(flags) [FAILS]] AS "handlerId"Parameters
- SubName - Name of subroutine to call (no parameters)
- "handlerId" - String identifier for this handler (used with STOP HOST)
Examples
'Register host event handler
CALL HOST OnBecameHost() AS "hostHandler"
SUB OnBecameHost()
PRINT "You are now the host!"
' Initialize host-only NPCs, etc.
END SUB'Host takes control of NPCs
CALL HOST InitHostSprites() AS "hostInit"
SUB InitHostSprites()
' Set up NPC behaviors - only runs on host
FOR i = 40 TO 49
SET SPRITE i BEHAVIOR "patrol"
NEXT i
END SUBSee also: STOP HOST, CALL PLAYER, CRASHNET, ME.ISHOST
CALL PLAYER
Multiplayer · 🚧 Pre-release · since 1.19.0
Registers a subroutine to handle player join/leave events. The CALL PLAYER statement uses empty parens — the runtime automatically passes a single player object to the SUB at invocation time. The SUB declares one parameter to receive that object; field access on it gives .SLOT, .NAME$, .ISHOST, .CONNECTED, and .JOINED (1 = join, 0 = leave), plus any custom PLAYER DATA fields declared in the CRASHNET block. Both joins and leaves dispatch to the same handler; branch on .JOINED to differentiate. Handler ID is used with STOP PLAYER to unregister.
The optional GUARD(flags) modifier (see CALL) can restrict registration to a specific interpreter context (HOST/PLAYER/CLIENT) and/or serialize handler invocations with ONCE so back-to-back joins don't overlap. Without FAILS, a guard violation is a silent no-op; with FAILS it halts the program with a runtime error.
Syntax
CALL PLAYER SubName() [GUARD(flags) [FAILS]] AS "handlerId"Parameters
- SubName() - Name of the subroutine, with empty parens — the runtime supplies the player-object arg automatically; you do not pass anything in the call site.
- "handlerId" - String identifier for this handler (used with STOP PLAYER)
- (SUB parameter) - Your SUB declares one parameter to receive the player object. Fields: .SLOT (Integer), .NAME$ (String), .ISHOST (Integer 0/1), .CONNECTED (Integer 0/1), .JOINED (Integer 1=joined, 0=left), plus any PLAYER DATA fields you declared in the CRASHNET block.
Examples
'Register player event handler — empty parens on the CALL site;
'the runtime fills in the player arg.
CALL PLAYER OnPlayerJoin() AS "playerHandler"
SUB OnPlayerJoin(player)
IF player.JOINED THEN
PRINT player.NAME$ + " joined as player " + STR$(player.SLOT)
ELSE
PRINT player.NAME$ + " left the game"
END IF
END SUB'Track players in an array, indexed by slot
DIM playerNames$(4) AS STRING
CALL PLAYER TrackPlayers() AS "tracker"
SUB TrackPlayers(p)
IF p.JOINED THEN
playerNames$(p.SLOT) = p.NAME$
ELSE
playerNames$(p.SLOT) = ""
END IF
END SUB'Read a custom PLAYER DATA field on join
' (declared in CRASHNET block as: PLAYER DATA / team$ AS string / END PLAYER DATA)
CALL PLAYER OnJoin() AS "teamWatch"
SUB OnJoin(p)
IF p.JOINED THEN PRINT p.NAME$ + " joined team " + p.team$
END SUBSee also: STOP PLAYER, CALL HOST, CRASHNET, ME, PLAYERS
CRASHNET
Multiplayer · 🚧 Pre-release · since 1.19.0
Configures CrashNet multiplayer settings. Must appear before the main game loop.
Sprite ownership is first-come-first-served: any sprite the host or a player enables (via SET SPRITE / RESERVE SPRITE / ENABLE SPRITE) is tagged with the creator's slot. There are no preallocated sprite ranges. By default no sprite is broadcast to other clients — use SET SPRITE n SYNC to opt a sprite in to cross-client replication, and SET SPRITE n NOSYNC to opt back out. On disconnect, every sprite the leaving player created is automatically swept back into the free pool.
Under the hybrid execution model, sprites that the local CrashPlayer client creates (typically inside IF ME.ISCLIENT THEN) live in a separate ID space (32768+) so they never collide with server-owned sprites; this happens transparently when you use NEXTFREESPRITE / RESERVE SPRITE.
PLAYER DATA defines per-player custom fields that each player can set and all can read. HOST DATA defines authoritative game state that only the host can modify.
STREAM controls which client input categories are forwarded to the player's server-side interpreter. Default is none — bandwidth-first. Server-side KEYDOWN, MOUSEX, MOUSEDOWN, etc. silently return empty/zero unless the corresponding category is opted in. For occasional discrete actions (jump, fire, build), prefer EVENT SERVER "name"(...) — it's far cheaper than streaming raw input.
Syntax
CRASHNET
MAX PLAYERS n
PLAYER DATA...END PLAYER DATA
HOST DATA...END HOST DATA
PARTICLES
TICK RATE rate
STREAM <kind>+
END CRASHNETParameters
- MAX PLAYERS n - Maximum simultaneous players in the room (default 4).
- PLAYER DATA...END PLAYER DATA - Declares custom fields each player owns (accessed via ME.field or PLAYERS(n).field).
- HOST DATA...END HOST DATA - Declares host-authoritative variables (only host can write, all can read).
- PARTICLES - Enables replication of EMITPARTICLE commands (sprites 1000+).
- TICK RATE rate - Network ticks per second (1-60, default 20). Higher = smoother but more bandwidth. Outside that range is a runtime error, not a clamp: the server's tick loop divides by this value, so zero would take the room down rather than slow it. This is how often the server broadcasts state — for how fast behaviors run, see SET BEHAVIOR RATE.
- STREAM KEYBOARD - Forward KeyDown / KeyUp events to the player-server interpreter so server-side
KEYDOWN(key$)works. - STREAM MOUSE - Forward mouse position AND buttons. Position is the heavy one — every move generates an event.
- STREAM MOUSE BUTTONS - Forward mouse clicks only (no movement spam). Use when the server only cares about clicks.
Examples
'Full multiplayer config with custom data
CRASHNET
MAX PLAYERS 4
PLAYER DATA
score AS INTEGER
health AS INTEGER
team$ AS STRING
END PLAYER DATA
HOST DATA
gameMode$ AS STRING
roundNumber AS INTEGER
timeRemaining AS INTEGER
END HOST DATA
TICK RATE 20
END CRASHNET'Minimal config for 2-player game
CRASHNET
MAX PLAYERS 2
END CRASHNET'Action game — server needs to read keyboard authoritatively
CRASHNET
TICK RATE 30
STREAM KEYBOARD
END CRASHNET'Strategy game — only clicks matter on the server, not every mouse pixel
CRASHNET
STREAM MOUSE BUTTONS
END CRASHNET'On player join, host spawns a cross-client synced avatar
SUB OnJoin(p)
IF p.JOINED THEN
RESERVE SPRITE avatar
IF avatar >= 0 THEN
SET SPRITE avatar, 100, 100, "player" TAG "avatar" SYNC
END IF
END IF
END SUB
CALL PLAYER OnJoin() AS "avatarHandler"See also: SYNC, CALL PLAYER, CALL HOST, EVENT, CALL EVENT, OnServerBroadcast, ME, PLAYERS, PLAYER DATA, HOST DATA
CRASHNET (object)
Multiplayer · 🚧 Pre-release · since 1.19.0
Built-in object providing information about the current CrashNet session state. CONNECTED = 1 when networked, 0 otherwise.
In single-player launches the object is fully populated with sensible defaults (CONNECTED = 0, ROOMID$ = "", MAXPLAYERS = 1, TICKRATE = 0) so reads of CRASHNET.MAXPLAYERS, etc. work the same way they do in multiplayer — gating on CRASHNET.CONNECTED is enough; you don't have to first check that the field exists.
Syntax
CRASHNET.CONNECTED
CRASHNET.ROOMID$
CRASHNET.MAXPLAYERS
CRASHNET.TICKRATE
CRASHNET.PLAYERCOUNT
CRASHNET.TICKParameters
- CRASHNET.CONNECTED - 1 if connected to CrashNet server, 0 otherwise
- CRASHNET.ROOMID$ - String room ID for the current session
- CRASHNET.MAXPLAYERS - Maximum players allowed in this room (from MAX PLAYERS in the CRASHNET block)
- CRASHNET.TICKRATE - Network tick rate in Hz (from TICK RATE in the CRASHNET block)
- CRASHNET.PLAYERCOUNT - Number of currently connected players (equivalent to PLAYERS.COUNT)
- CRASHNET.TICK - Current server tick number — monotonically increasing counter that advances at TICKRATE Hz; useful for time-aligned logic
Examples
'Check connection status
IF CRASHNET.CONNECTED THEN
PRINT "Connected to room: " + CRASHNET.ROOMID$
PRINT "Players: " + STR$(CRASHNET.PLAYERCOUNT) + "/" + STR$(CRASHNET.MAXPLAYERS)
ELSE
PRINT "Not connected"
END IF'Display session info
PRINT "Tick Rate: " + STR$(CRASHNET.TICKRATE) + " Hz"
PRINT "Current tick: " + STR$(CRASHNET.TICK)'Run something every 60 ticks (~3s at 20Hz)
IF CRASHNET.TICK MOD 60 = 0 THEN
PRINT "Periodic tick: " + STR$(CRASHNET.TICK)
END IFSee also: CRASHNET, ME, PLAYERS
GAMECONTEXT$
Multiplayer · 🚧 Pre-release · since 1.27.0
Built-in pseudo-string that reports which of the four execution contexts this interpreter is running in. Returns one of:
"SINGLE"— single-player launch. CrashPlayer was started with a local game directory or .crashcart (no --remote flag, no crashnet:// descriptor). The interpreter is the only one running; nothing is connected over the network."HOST"— the headless host interpreter (slot 0, server-side) inside a CrashNet session. Authoritative for game state; broadcasts sprite frames; runs no renderer."PLAYER"— a player-server interpreter (slot 1+, server-side). One per connected player. Authoritative for sprites that player creates; sees STREAM-opted input from the client."CLIENT"— a local CrashPlayer interpreter connected to a CrashNet server. Renders broadcasts from the host, owns local-only sprites in the 32768+ ID space, forwards opted-in input.
Designed to make a single SELECT CASE do the work that a stack of nested ME.IS* checks used to do — same .bas runs in all four contexts and dispatches per-context behavior cleanly:
SELECT CASE GAMECONTEXT$
CASE "SINGLE": ' solo path
CASE "HOST": ' authoritative state
CASE "PLAYER": ' per-player server work
CASE "CLIENT": ' local rendering / input
END SELECTValues update live across host migrations: if a player gets promoted to host mid-session, their interpreter starts reporting "HOST" on the next read of GAMECONTEXT$.
GAMECONTEXT$ subsumes the deployment-shape question (single vs networked) AND the multiplayer-role question (host vs player-server vs client) into one pseudo-variable. ME.ISHOST / ME.ISSERVER / ME.ISCLIENT still exist and remain useful for individual boolean checks, but GAMECONTEXT$ is the recommended choice when you want a CASE-style dispatch.
Syntax
GAMECONTEXT$Examples
' Four-way SELECT CASE — the canonical pattern
SELECT CASE GAMECONTEXT$
CASE "SINGLE"
PRINT "Single-player. Press SPACE to start."
CASE "HOST"
HOST.gameMode$ = "deathmatch"
CASE "PLAYER"
' Per-player server-side setup (e.g. spawn the player's avatar)
CASE "CLIENT"
LOADIMG "player.ase", "avatar"
END SELECT' Run setup work only in non-CLIENT contexts
IF GAMECONTEXT$ <> "CLIENT" AND GAMECONTEXT$ <> "SINGLE" THEN
' Server-side only — host or player-server
CALL RegisterAuthHandlers()
END IF' Resource loading is client-only; gate cleanly with GAMECONTEXT$
IF GAMECONTEXT$ = "SINGLE" OR GAMECONTEXT$ = "CLIENT" THEN
CALL LoadResources()
END IF' Only the host should mint test-driver state at boot
IF GAMECONTEXT$ = "HOST" THEN
HOST.roundNumber = 0
HOST.gameStarted = 0
END IFSee also: ME, PLAYERS, CRASHNET, SELECT CASE
HOST DATA
Multiplayer · 🚧 Pre-release · since 1.19.0
Declares host-authoritative game state in a CrashNet multiplayer session.
Access pattern: HOST.<field> (symmetric with ME.<field> / PLAYERS(n).<field> for PLAYER DATA). Reads work from any execution context (host / player-server / local-client) and reflect the current authoritative state.
Write authority is strict: only the headless host (slot 0, server-side, where ME.ISHOST = 1) may mutate HOST.
If the host disconnects and a new host is promoted, the new host inherits write access. After every successful write, the server pushes a NetHostDataUpdate over the WebSocket so all clients see the new value within one tick.
Must be declared inside a CRASHNET block.
Single-player: HOST is also a usable central state container in solo runs — HOST.score = 0, HOST.gameMode$ = "deathmatch", etc. all persist across statements. Declared HOST DATA fields are zero-initialized when the CRASHNET block runs (so reads of declared-but-unwritten fields return the type-appropriate default), and the same code that drives multiplayer game state works in solo without a GAMECONTEXT$ guard. The defaults are install-once: re-running the CRASHNET block (e.g. via /dev/restart) preserves any values you've already written. There's no write-authority enforcement in solo (no host vs. client distinction), and undeclared fields can still be added on first write — they read back as Integer(0) until set.
Syntax
HOST DATA
fieldName AS type
...
END HOST DATAParameters
- fieldName AS INTEGER - Declares an integer field (default 0)
- fieldName AS FLOAT - Declares a floating-point field (default 0.0)
- fieldName$ AS STRING - Declares a string field (default empty string)
- fieldName AS BOOLEAN - Declares a boolean field (default 0)
- Supported field types are INTEGER, FLOAT, STRING, and BOOLEAN. SINGLE and DOUBLE are not supported here — use FLOAT. A field declared with an unsupported type is dropped from the schema, so a later write to it halts at runtime with a 'not declared' error.
Examples
'Declare host-controlled game state
CRASHNET
HOST DATA
gameMode$ AS STRING
roundNumber AS INTEGER
timeRemaining AS INTEGER
gameStarted AS INTEGER
END HOST DATA
END CRASHNET'Host sets game state — gated on ME.ISHOST
IF ME.ISHOST THEN
HOST.gameMode$ = "deathmatch"
HOST.roundNumber = 1
HOST.timeRemaining = 300
HOST.gameStarted = 1
END IF'Any context can read host data
PRINT "Mode: " + HOST.gameMode$
PRINT "Round " + STR$(HOST.roundNumber)
IF HOST.gameStarted THEN
PRINT "Time left: " + STR$(HOST.timeRemaining)
END IF'Client asks the host to start the round (write must originate on the host)
IF ME.ISCLIENT AND KEYDOWN("S") THEN
EVENT HOST "StartRound"
END IF
SUB OnStartRound()
IF ME.ISHOST THEN
HOST.gameStarted = 1
HOST.roundNumber = HOST.roundNumber + 1
END IF
END SUB
IF ME.ISHOST THEN
CALL EVENT OnStartRound() AS "StartRound"
END IFSee also: CRASHNET, ME.ISHOST, PLAYER DATA, SYNC, CALL HOST
ME
Multiplayer · 🚧 Pre-release · since 1.19.0
Built-in object representing the local player. In a CrashNet multiplayer session it provides identity (slot + name), role (host status), execution side (server vs client), and connection state.
In single-player launches ME is populated as a synthetic one-player CLIENT: SLOT = 1, NAME$ = "Player", ISCLIENT = 1, ISSERVER = 0, ISHOST = 0, CONNECTED = 1, ROOM$ = "". This means dual-mode (single + multiplayer) games can read ME.NAME$, ME.SLOT, etc. without gating every access on GAMECONTEXT$ = "SINGLE" — the same code that works against a real CrashNet client also works in solo. (GAMECONTEXT$ itself stays "SINGLE" so authors who want the shape distinction still have it.)
The same player slot in multiplayer has two interpreters running concurrently: one server-side (in CrashNet, authoritative) and one client-side (in the local CrashPlayer, for rendering and input). ISHOST/ISSERVER/ISCLIENT let a SUB tell which context it's running in so the same .bas file can branch on responsibility:
- ME.ISHOST = 1 only on the headless host server (slot 0 inside CrashNet)
- ME.ISSERVER = 1 on the host AND on every player-server-side interpreter
- ME.ISCLIENT = 1 only inside the player's local CrashPlayer process (and in single-player)
Sprite ownership is no longer per-player — sprites are first-come-first-served via RESERVE SPRITE / NEXTFREESPRITE, and ownership is tracked per-sprite by the slot of the creator. The historic ME.SPRITESTART / ME.SPRITEEND / ME.SPRITECOUNT / ME.SPRITES(n) accessors were removed in the 1.19+ rewrite of the sprite model.
Syntax
ME.SLOT
ME.NAME$
ME.ISHOST
ME.ISSERVER
ME.ISCLIENT
ME.CONNECTED
ME.ROOM$Parameters
- ME.SLOT - Integer slot number assigned to this player (0 to MAX PLAYERS - 1)
- ME.NAME$ - String display name of this player
- ME.ISHOST - 1 if this interpreter is the headless host (slot 0 server-side), 0 otherwise
- ME.ISSERVER - 1 if this interpreter is running server-side (host or any player-server interpreter), 0 if running in the local CrashPlayer
- ME.ISCLIENT - 1 if this interpreter is running client-side in the local CrashPlayer, 0 if running server-side. Mutually exclusive with ISSERVER.
- ME.CONNECTED - 1 if this player slot is currently connected, 0 otherwise
- ME.ROOM$ - String name of the CrashNet room this player is currently in. Empty string = no room (default; sees only global / unscoped sprites). Set via SET ROOM "name". Always empty for the host (slot 0), which is omnipresent across rooms.
Examples
'Show player info
PRINT "You are player " + STR$(ME.SLOT)
PRINT "Name: " + ME.NAME$
IF ME.ISHOST THEN PRINT "You are the host!"'Reserve and own a sprite (replaces the old per-player sprite range)
RESERVE SPRITE myAvatar
IF myAvatar >= 0 THEN
SET SPRITE myAvatar, 100, 100, "player" SYNC
END IF'Bail early if not in multiplayer
IF NOT ME.CONNECTED THEN
PRINT "Single-player mode"
EXIT SUB
END IF'Render only on the local client; mutate state only on the server
IF ME.ISCLIENT THEN
LOCATE 1, 1: PRINT "slot " + STR$(ME.SLOT)
END IF
IF ME.ISSERVER AND ME.ISHOST THEN
' authoritative game-state mutation goes here
END IFSee also: PLAYERS, CRASHNET, CALL PLAYER, RESERVE SPRITE, NEXTFREESPRITE
OnServerBroadcast
Multiplayer · 🚧 Pre-release · since 1.27.0
Built-in CrashNet event fired when a server operator sends a message from the CrashNet Server admin dashboard's "Broadcast message to all players" field.
The event fires in all three execution contexts at the same time:
- The host (slot 0, server-side) — for game-state side effects (e.g. log to disk, update a sign).
- Every player-server interpreter (slots 1+, server-side) — for authoritative responses (e.g. award currency to everyone).
- Every connected local-client interpreter (in CrashPlayer) — for rendering the message to the player's screen.
A single SUB handles all three; gate with ME.ISCLIENT / ME.ISSERVER / ME.ISHOST for context-specific behavior. If no handler is registered, the broadcast is silently ignored — registering a handler is opt-in.
Arguments are positional: message$ is the operator-typed body, kind$ is a free-form category supplied by the dashboard (currently always "system" — reserved for future severity / channel routing like "warning", "chat", etc.).
Note: this is the ONLY way an operator-issued broadcast surfaces to a running game. Without a registered handler, broadcasts have no visible effect on players.
Syntax
SUB OnServerBroadcast(message$, kind$)
CALL EVENT OnServerBroadcast() AS "handlerId"Parameters
- message$ - String message body typed by the operator in the dashboard.
- kind$ - String category ("system" by default; future-reserved for severities like "warning", "chat", etc.).
Examples
' Show admin broadcasts as a top-of-screen banner on every client
SUB OnServerBroadcast(message$, kind$)
IF ME.ISCLIENT THEN
LOCATE 1, 1
COLOR 14
PRINT "[" + kind$ + "] " + message$
END IF
END SUB
CALL EVENT OnServerBroadcast() AS "adminMsg"' Differentiate by kind — system messages popup, others go to chat log
SUB OnServerBroadcast(message$, kind$)
IF NOT ME.ISCLIENT THEN EXIT SUB
IF kind$ = "system" THEN
SHOW SPRITE bannerSprite
SET SPRITE bannerLabel, 100, 10, message$
ELSE
chatLog$ = chatLog$ + message$ + CHR$(10)
END IF
END SUB
CALL EVENT OnServerBroadcast() AS "adminMsg"' Host-side: log every broadcast to disk for moderation audit
SUB OnServerBroadcast(message$, kind$)
IF ME.ISHOST THEN
OPEN "broadcasts.log" FOR APPEND AS #1
PRINT #1, TIME$ + " [" + kind$ + "] " + message$
CLOSE #1
END IF
END SUB
CALL EVENT OnServerBroadcast() AS "auditLog"See also: CALL EVENT, ME, CRASHNET, EVENT
PLAYER DATA
Multiplayer · 🚧 Pre-release · since 1.19.0
Declares custom data fields that each player owns in a CrashNet multiplayer session.
Access pattern: ME.<field> for your own data, PLAYERS(n).<field> for any player's data. Reads work from any execution context (host / player-server / local-client) and reflect the current authoritative state.
Write authority — host > player-server > local-client:
- Host (slot 0, server-side): may write any player's PLAYER DATA via PLAYERS(n).
. - Player-server (slots 1+, server-side): may write its OWN PLAYER DATA via ME.
(or equivalently PLAYERS(my_slot). ). Writing to OTHER players' data is a fatal runtime error — only the host may cross slots. Use a CALL HOST handler if cross-player writes are needed. - Local-client (in CrashPlayer): read-only. Mutating ME.
from a client interpreter is a fatal runtime error — fire EVENT SERVER and let the player-server interpreter perform the write authoritatively.
After every successful write, the server pushes a NetStateUpdate over the WebSocket so other clients' local NetState mirror picks up the new value within one tick. SYNC is a hint, not required for replication.
Must be declared inside a CRASHNET block.
Single-player: declared PLAYER DATA fields are zero-initialized on ME (and equivalently PLAYERS(1)) when the CRASHNET block runs, so dual-mode code that reads ME.score before the first write gets 0 instead of an undefined member. Solo writes through ME.<field> persist across statements (no authority gate fires when there's no NetState), and PLAYERS(1).<field> reads see the same data because both surface the same Object internally. The defaults are install-once — re-running the CRASHNET block preserves any values you've written.
Syntax
PLAYER DATA
fieldName AS type
...
END PLAYER DATAParameters
- fieldName AS INTEGER - Declares an integer field (default 0)
- fieldName AS FLOAT - Declares a floating-point field (default 0.0)
- fieldName$ AS STRING - Declares a string field (default empty string)
- fieldName AS BOOLEAN - Declares a boolean field (default 0)
- Supported field types are INTEGER, FLOAT, STRING, and BOOLEAN. SINGLE and DOUBLE are not supported here — use FLOAT. A field declared with an unsupported type is dropped from the schema, so a later write to it halts at runtime with a 'not declared' error.
Examples
'Declare player stats
CRASHNET
PLAYER DATA
score AS INTEGER
health AS INTEGER
team$ AS STRING
END PLAYER DATA
END CRASHNET'Set my player data
ME.score = 100
ME.health = 75
ME.team$ = "red"'Read another player's data
IF PLAYERS(1).team$ = ME.team$ THEN
PRINT "Same team!"
END IF
PRINT "Player 1 score: " + STR$(PLAYERS(1).score)See also: CRASHNET, ME, PLAYERS, HOST DATA, SYNC
PLAYERS
Multiplayer · 🚧 Pre-release · since 1.19.0
Built-in array-like object providing access to all connected players. Use PLAYERS.COUNT for the count of connected players; index by slot number to access an individual player.
In single-player launches, PLAYERS.COUNT = 1 and PLAYERS(1) returns the synthetic solo player (same shape as the multiplayer case). Dual-mode games can iterate FOR i = 1 TO PLAYERS.COUNT ... PLAYERS(i).NAME$ without gating on GAMECONTEXT$.
The historic per-player sprite-range fields (PLAYERS(n).SPRITESTART / SPRITEEND / SPRITECOUNT / SPRITES(m)) were removed in the 1.19+ sprite-model rewrite — sprites are now first-come-first-served via RESERVE SPRITE / NEXTFREESPRITE, with ownership tracked per-sprite by the slot of the creator.
Syntax
PLAYERS.COUNT
PLAYERS(n).SLOT
PLAYERS(n).NAME$
PLAYERS(n).ISHOST
PLAYERS(n).CONNECTED
PLAYERS(n).ROOM$Parameters
- PLAYERS.COUNT - Total number of currently connected players
- PLAYERS(n).SLOT - Slot number of player n
- PLAYERS(n).NAME$ - Display name of player n
- PLAYERS(n).ISHOST - 1 if player n is the host, 0 otherwise
- PLAYERS(n).CONNECTED - 1 if player n is currently connected, 0 otherwise
- PLAYERS(n).ROOM$ - String name of the CrashNet room player n is currently in. Empty string = no room. Useful for filtering the roster to only players in your room.
Examples
'List all players
FOR i = 0 TO PLAYERS.COUNT - 1
PRINT PLAYERS(i).NAME$ + " (slot " + STR$(PLAYERS(i).SLOT) + ")"
IF PLAYERS(i).ISHOST THEN PRINT " [HOST]"
NEXT i'Find host player
FOR i = 0 TO PLAYERS.COUNT - 1
IF PLAYERS(i).ISHOST THEN
hostSlot = PLAYERS(i).SLOT
EXIT FOR
END IF
NEXT i'Index by slot to find someone you know about
otherName$ = PLAYERS(1).NAME$See also: ME, CRASHNET, CALL PLAYER
SET ROOM
Multiplayer · 🚧 Pre-release · since 1.27.0
Move the calling player into a CrashNet room. Rooms scope sprite visibility and cross-sprite interaction, so an MMORPG-style server can hold many concurrent areas in one process without Room A's sprites bleeding into Room B's renderer or colliding with Room B's sprites.
Pair with SET SPRITE n SYNC("RoomName") to author room-scoped sprites. Sprites without a room are global — visible to everyone in every room. Use SET ROOM "" to send the calling player back to the unscoped global view (where they see only the globals).
Where it's legal to call
In single-player, SET ROOM is always legal. The solo interpreter is treated as authoritative for its own room state — it's effectively both "the host" and "the client" of its own session. Use it for level switching in solo games: pair with WITH ROOM "levelN" blocks and SET ROOM "levelN" auto-mounts everything tagged for that level.
In multiplayer, SET ROOM is legal only on a player-server interpreter (ME.ISSERVER AND NOT ME.ISHOST).
- Calling on the host (slot 0) is a fatal runtime error. The host is omnipresent across all rooms; it doesn't belong to one.
- Calling on the local-client interpreter is a fatal runtime error. Moving rooms is an authoritative state change — clients ask, the server decides.
What happens on execution
- The server sweeps every sprite this player owns, sending
DisableSprite. That broadcasts naturally — players still in the old room see those sprites vanish. ME.ROOM$updates.- A per-recipient snapshot replays to this player's renderer: disable every sprite that lived in the old room (they're not visible to this player anymore), then re-enable and position every sprite that lives in the new room.
- You're expected to re-create the player's character / projectiles / whatever at the new room's spawn point. The old sprites are gone.
Tilemap transition
The same per-recipient replay path handles tilemaps too. On SET ROOM:
- Old room's tilemaps tear down (one
DestroyLayerper mounted screen-layer). - New room's tilemaps mount fresh — a
CreateLayer+CreateTilemappair per entry. - Tilemaps tagged
_globalare not touched — they stay visible across rooms.
Single-player gets level-switching for free. Author your tilemaps inside WITH ROOM "name" blocks ahead of time, then a single SET ROOM "name" auto-mounts everything tagged for that room. No manual CREATE MAP / RESET LAYERS choreography.
Syntax
SET ROOM exprParameters
- expr - String expression naming the target room. Empty string returns the player to the global / unscoped view.
Examples
' Player teleports from town to dungeon
IF ME.ISSERVER AND NOT ME.ISHOST THEN
SET ROOM "dungeon"
' Re-spawn this player's sprites at the dungeon entrance
RESERVE SPRITE avatar
IF avatar >= 0 THEN
SET SPRITE avatar, 50, 50, "player" TAG "avatar" SYNC("dungeon")
END IF
END IF' Lobby → game room transition driven by a client EVENT
SUB OnEnterGame(roomName$)
SET ROOM roomName$
END SUB
IF ME.ISSERVER AND NOT ME.ISHOST THEN
CALL EVENT OnEnterGame("") AS "roomChange"
END IF' Read your own room from anywhere
IF ME.ROOM$ = "dungeon" THEN
PRINT "Welcome, brave adventurer."
END IF' Roster filtered by room
FOR i = 0 TO PLAYERS.COUNT - 1
IF PLAYERS(i).ROOM$ = ME.ROOM$ THEN
PRINT PLAYERS(i).NAME$, " is here with you"
END IF
NEXT iSee also: SET SPRITE, ME, PLAYERS, CRASHNET, WITH, CREATE MAP
STOP HOST
Multiplayer · 🚧 Pre-release · since 1.19.0
Unregisters a host event handler by its ID. After calling STOP HOST, the associated subroutine will no longer be called for host promotion events.
Syntax
STOP HOST "handlerId"Parameters
- "handlerId" - String identifier of handler to unregister (from CALL HOST AS)
Examples
'Unregister host handler
STOP HOST "hostHandler"'Cleanup on game end
STOP HOST "hostInit"
STOP PLAYER "playerHandler"See also: CALL HOST, STOP PLAYER
STOP PLAYER
Multiplayer · 🚧 Pre-release · since 1.19.0
Unregisters a player event handler by its ID. After calling STOP PLAYER, the associated subroutine will no longer be called for player join/leave events.
Syntax
STOP PLAYER "handlerId"Parameters
- "handlerId" - String identifier of handler to unregister (from CALL PLAYER AS)
Examples
'Unregister player handler
STOP PLAYER "playerHandler"'Conditional unregister
IF gameOver THEN
STOP PLAYER "tracker"
END IFSee also: CALL PLAYER, STOP HOST
SYNC
Multiplayer · 🚧 Pre-release · since 1.19.0
Network synchronization point. When executed, sends pending sprite state changes to the CrashNet server and processes incoming updates from other players. Should be called once per frame in the main game loop. Unlike YIELD (which just gives up CPU time), SYNC specifically handles network state exchange.
Syntax
SYNCExamples
'Main game loop with network sync
DO
' Handle input
' Update game state
SYNC ' Exchange network state
LOOP'With frame timing
DO
' Game logic here
SYNC
YIELD ' Also yield CPU if needed
LOOPSee also: CRASHNET, YIELD, CALL PLAYER, CALL HOST
In-App Purchase
FOR PURCHASE
In-App Purchase · 🚧 Pre-release · since 1.7.0
Iterates over the locally cached list of in-app purchase products. Products are pushed by the platform when loaded from the app store - the loop executes immediately without blocking. Inside the loop, use PURCHASE.ID, PURCHASE.NAME, PURCHASE.PRICE, and PURCHASE.PURCHASED to access product information. Supports EXIT FOR to break out early. Note: The loop may iterate zero times if products haven't been loaded yet.
Syntax
FOR PURCHASE
statements
NEXT PURCHASEExamples
' Display all available products
FOR PURCHASE
PRINT PURCHASE.NAME; " - "; PURCHASE.PRICE
IF PURCHASE.PURCHASED THEN PRINT " (Owned)"
NEXT PURCHASE' Find a specific product
FOR PURCHASE
IF PURCHASE.ID = "premium" AND NOT PURCHASE.PURCHASED THEN
PRINT "Premium available for "; PURCHASE.PRICE
EXIT FOR
END IF
NEXT PURCHASESee also: PURCHASE, PURCHASE NEW, PURCHASE RESTORE
PURCHASE
In-App Purchase · 🚧 Pre-release · since 1.7.0
Pseudo-variable providing access to purchase product properties. AVAILABLE is always accessible and indicates whether IAP is connected to a provider (1) or offline/unavailable (0). Other properties (ID, NAME, DESCRIPTION, PRICE, PURCHASED) are populated during a FOR PURCHASE loop and read from the locally cached product list. Outside of a FOR PURCHASE loop, these loop-specific properties return empty/zero values. Note: PURCHASED only applies to non-consumable products (e.g., 'remove ads', 'premium upgrade'). Consumable products (e.g., coins, gems) always show PURCHASED=0 since they can be bought multiple times - use the purchase event to track consumable inventory in your own game variables.
Syntax
PURCHASE.ID
PURCHASE.NAME
PURCHASE.DESCRIPTION
PURCHASE.PRICE
PURCHASE.PURCHASED
PURCHASE.AVAILABLEParameters
- PURCHASE.ID - Product identifier string (e.g., "com.example.coins100")
- PURCHASE.NAME - Human-readable product name (e.g., "100 Coins")
- PURCHASE.DESCRIPTION - Extended product description (e.g., "Get 100 in-game coins to spend in the shop.")
- PURCHASE.PRICE - Formatted price string (e.g., "$0.99")
- PURCHASE.PURCHASED - 1 if already purchased (non-consumables only), 0 otherwise
- PURCHASE.AVAILABLE - (Global) 1 if connected to IAP provider, 0 if offline or unavailable
Examples
IF PURCHASE.AVAILABLE THEN
FOR PURCHASE
PRINT PURCHASE.NAME; " - "; PURCHASE.PRICE
PRINT " "; PURCHASE.DESCRIPTION
NEXT PURCHASE
ELSE
PRINT "Store unavailable"
END IFFOR PURCHASE
PRINT PURCHASE.NAME; ": "; PURCHASE.PRICE
IF PURCHASE.PURCHASED THEN
PRINT " [OWNED]"
END IF
NEXT PURCHASESee also: FOR PURCHASE, PURCHASE NEW, PURCHASE RESTORE
PURCHASE NEW
In-App Purchase · 🚧 Pre-release · since 1.7.0
Initiates a purchase for the specified product. This is an asynchronous operation - register an event handler with CALL EVENT to receive the result. The purchase event fires with parameters (productId$, success, errorMessage$). For consumable products (coins, gems, etc.), your game must track inventory in its own variables since consumables can be purchased multiple times. A purchase the store runs and refuses — a declined card, a cancelled dialog — is an ordinary result: the purchase event fires with success = 0 and the reason in errorMessage$. A purchase that can never start is not, and halts with an error instead: that means the store returned no checkout page, or the build has no way to open one. Today purchases run through a CrashNet IAP server, which is why PURCHASE is still pre-release; iOS and tvOS builds can also buy through StoreKit once the app is set up for it, and those never open a checkout page at all. Reaching the error means the provider in iap.json does not match the platform you are running on, which is a build problem rather than something a purchase handler can recover from.
Syntax
PURCHASE NEW productIdParameters
- productId - String expression containing the product identifier to purchase
Examples
' Simple purchase
PURCHASE NEW "com.example.removeads"' Consumable purchase - game manages inventory
DIM coins AS INTEGER
coins = 0
SUB OnPurchase(productId$, success, errorMsg$)
IF success THEN
IF productId$ = "coins100" THEN coins = coins + 100
IF productId$ = "coins500" THEN coins = coins + 500
END IF
END SUB
CALL EVENT OnPurchase() AS "purchase"
PURCHASE NEW "coins100"See also: FOR PURCHASE, PURCHASE RESTORE, CALL EVENT
PURCHASE RESTORE
In-App Purchase · 🚧 Pre-release · since 1.7.0
Restores previously purchased non-consumable products (e.g., after reinstalling the app or on a new device). Fires the purchase event for each restored product with success=1. If restore fails, fires a single event with success=0 and an error message. Note: Consumable purchases (coins, gems, etc.) cannot be restored - only non-consumables like 'remove ads' or 'premium upgrade' are restorable.
Syntax
PURCHASE RESTOREExample
' Restore purchases with event handling
SUB OnPurchaseComplete(productId$, success, errorMsg$)
IF success THEN
PRINT "Restored: "; productId$
ELSE
PRINT "Restore failed: "; errorMsg$
END IF
END SUB
CALL EVENT OnPurchaseComplete() AS "purchase"
PURCHASE RESTORESee also: FOR PURCHASE, PURCHASE NEW, CALL EVENT
AI
ASTAR
AI · since 1.12.0
A* pathfinding on a 2D array. Returns an array of coordinate waypoints from start to end. Grid values of 0 are walkable, non-zero are blocked. Supports 8-directional movement with proper diagonal costs. Returns an empty array if no path exists.
Syntax
ASTAR(grid, start, end)Parameters
- grid - 2D array (DIM grid(w, h) AS INTEGER) where 0 = walkable
- start - Starting coordinate (x, y)
- end - Goal coordinate (x, y)
Example
DIM grid(10, 10) AS INTEGER
grid(5, 0) = 1
grid(5, 1) = 1
grid(5, 2) = 1
path = ASTAR(grid, (0, 0), (9, 9))
FOR EACH wp IN path
PRINT wp.X; ","; wp.Y
NEXTSee also: RAYCAST, NEIGHBORS, DISTANCE
CHOOSE
AI · since 1.12.0
Returns a random element from an array. Returns 0 if the array is empty.
Syntax
CHOOSE(array)Parameters
- array - Array to pick from
Examples
greetings = ["Hello!", "Hi there!", "Hey!"]
PRINT CHOOSE(greetings)direction = CHOOSE(["left", "right", "up", "down"])
DISTANCE
AI · since 1.12.0
Returns the Euclidean distance between two coordinates.
Syntax
DISTANCE(a, b)Parameters
- a - First coordinate (x, y)
- b - Second coordinate (x, y)
Examples
d = DISTANCE((0, 0), (3, 4)) ' Returns 5.0IF DISTANCE(playerPos, enemyPos) < 50 THEN
PRINT "Enemy nearby!"
END IF
FLEE
AI · since 1.12.0
Returns a velocity coordinate pointing away from the threat at the given speed. Opposite of SEEK. Returns (0, 0) if positions are identical.
Syntax
FLEE(pos, threat, speed)Parameters
- pos - Current position coordinate (x, y)
- threat - Position to flee from (x, y)
- speed - Movement speed (magnitude of result)
Example
velocity = FLEE(playerPos, enemyPos, 5)
player.X = player.X + velocity.X
player.Y = player.Y + velocity.YSee also: SEEK, DISTANCE, WANDER
FLOODFILL
AI · since 1.12.0
Flood fills a 2D grid from the starting position, counting all connected cells that match the given value. Uses 4-directional connectivity. Returns the number of filled cells. Useful for checking area reachability.
It marks the grid. Every cell the fill reaches is set to -1 in the array you passed — the value is negative on purpose, so it cannot be mistaken for terrain by a later query. The return value is how many cells that was.
Syntax
FLOODFILL(grid, start, value)Parameters
- grid - 2D array to flood fill
- start - Starting coordinate (x, y)
- value - Cell value to match and fill (e.g., 0 for walkable cells)
Example
DIM grid(10, 10) AS INTEGER
grid(5, 0) = 1
grid(5, 1) = 1
grid(5, 2) = 1
reachable = FLOODFILL(grid, (0, 0), 0)
PRINT "Reachable cells:"; reachable
MAZE
AI · since 1.38.0
Procedurally generates a maze as a fresh 2D array where 1 = wall and 0 = floor. The array is exactly width x height (shape [height, width]); index it as maze(row, col). The output drops straight into ASTAR, FLOODFILL, RAYCAST and NEIGHBORS (a coordinate (x, y) maps to maze(y, x)), so you can generate a maze and pathfind across it in two lines, or read it back to build a tilemap. The outer border is always wall; even width/height values leave an extra wall strip on the far edge. width/height are clamped up to a minimum of 3. The trailing arguments are optional and matched by type, so MAZE(w, h), MAZE(w, h, "CAVE"), MAZE(w, h, 42) and MAZE(w, h, 42, "PRIM", 0.3) all work; when two numbers are supplied the first is the seed and the second is braid.
Turning a maze into a map. Write the maze into a tile array and hand that to CREATE MAP FROM. There is no SET TILE statement — to change tiles after the map is up, assign into the bound array (tiles(x, y) = idx) and call UPDATE MAP, which pushes just that change rather than rebuilding the map.
Syntax
MAZE(width, height [, seed] [, style$] [, braid])Parameters
- width - Number of columns in the returned array (minimum 3)
- height - Number of rows in the returned array (minimum 3)
- seed - Optional. 0 or omitted = a different maze each call; any nonzero value = reproducible
- style$ - Optional. "BACKTRACKER" (winding corridors, default), "PRIM" (bushier, many short dead-ends), "DIVISION" (straight walls, room-like), or "CAVE" (organic cellular caverns)
- braid - Optional, 0.0 to 1.0. 0.0 = perfect maze with exactly one path between any two cells (default); higher values remove that fraction of dead-ends, adding loops. Ignored for the CAVE style
Examples
' Generate a 31x31 maze and pathfind across it
maze = MAZE(31, 31)
path = ASTAR(maze, (1, 1), (29, 29))' Build a tilemap from a reproducible, looping maze
maze = MAZE(41, 41, 12345, "PRIM", 0.3)
DIM tiles(41, 41) AS INTEGER
FOR row = 0 TO 40
FOR col = 0 TO 40
IF maze(row, col) = 1 THEN
tiles(col, row) = 3 ' wall tile index
ELSE
tiles(col, row) = 0 ' floor tile index
END IF
NEXT
NEXT
CREATE MAP FROM tiles TILESET "dungeon.png" TILESIZE 16 AS LAYER 0' Organic cave layout for a dungeon
cave = MAZE(60, 40, 0, "CAVE")See also: ASTAR, FLOODFILL, NEIGHBORS, CHOOSE
NEAREST
AI · since 1.12.0
Returns the index (0-based) of the nearest coordinate in the array to the given position. Returns -1 if the array is empty.
Syntax
NEAREST(pos, points)Parameters
- pos - Reference coordinate (x, y)
- points - Array of coordinates to search
Example
enemies = [(10, 20), (50, 50), (30, 15)]
closest = NEAREST(playerPos, enemies)
PRINT "Nearest enemy at index:"; closest
NEIGHBORS
AI · since 1.12.0
Returns an array of coordinate positions for valid non-blocked neighbors of a position on a 2D grid. Uses 4-directional adjacency (up, right, down, left). Only includes cells where the grid value is 0.
Syntax
NEIGHBORS(grid, pos)Parameters
- grid - 2D array where 0 = walkable, non-zero = blocked
- pos - Position to check neighbors of (x, y)
Example
DIM grid(10, 10) AS INTEGER
n = NEIGHBORS(grid, (5, 5))
FOR EACH cell IN n
PRINT cell.X; ","; cell.Y
NEXT
RAYCAST
AI · since 1.12.0
Line-of-sight check on a 2D grid using Bresenham's line algorithm. Returns 1 if the line from start to end is clear (no blocked cells), 0 if blocked. Start and end positions are not checked (only cells between them).
Syntax
RAYCAST(grid, start, end)Parameters
- grid - 2D array where 0 = clear, non-zero = blocked
- start - Starting coordinate (x, y)
- end - Target coordinate (x, y)
Example
DIM grid(10, 10) AS INTEGER
grid(5, 5) = 1
IF RAYCAST(grid, (0, 0), (9, 9)) THEN
PRINT "Clear line of sight"
ELSE
PRINT "Blocked!"
END IF
SEEK
AI · since 1.12.0
Returns a velocity coordinate pointing from pos toward target at the given speed. Useful for basic "move toward" AI behavior. Returns (0, 0) if already at target.
Syntax
SEEK(pos, target, speed)Parameters
- pos - Current position coordinate (x, y)
- target - Target position coordinate (x, y)
- speed - Movement speed (magnitude of result)
Example
velocity = SEEK(enemyPos, playerPos, 3)
enemy.X = enemy.X + velocity.X
enemy.Y = enemy.Y + velocity.YSee also: FLEE, DISTANCE, WANDER
WANDER
AI · since 1.12.0
Returns a new position for gentle random wandering (Craig Reynolds style). Projects a circle at 'distance' ahead in the direction of 'angle' (degrees), then picks a random point on that circle. The angle is jittered randomly each call for natural movement.
Syntax
WANDER(pos, angle, radius, distance)Parameters
- pos - Current position coordinate (x, y)
- angle - Current heading in degrees
- radius - Wander circle radius (larger = more erratic)
- distance - How far ahead the wander circle is projected
Example
heading = 0
pos = (100, 100)
pos = WANDER(pos, heading, 10, 20)
WEIGHTED
AI · since 1.12.0
Returns a random element from the values array, weighted by the corresponding weights array. Higher weights mean higher probability of selection. Weights should be non-negative numbers.
Syntax
WEIGHTED(values, weights)Parameters
- values - Array of items to choose from
- weights - Array of numeric weights (same length as values)
Example
loot = ["common", "rare", "epic"]
chances = [80, 15, 5]
drop = WEIGHTED(loot, chances)See also: CHOOSE
String
FIXED$
String · since 1.41.0
Formats a number as text with exactly places decimal places (for example, FIXED$(-0.56789, 2) returns "-0.57"). Guarantees a clean display regardless of floating-point representation, making it ideal for showing noisy values such as joystick axes. Use FIXED for the rounded number form.
Syntax
FIXED$(value, places)Parameters
- value - Any numeric expression
- places - Number of decimal places (0-15; negative values are treated as 0)
Examples
PRINT FIXED$(3.14159, 2) ' Prints 3.14PRINT FIXED$(1, 3) ' Prints 1.000LOCATE 2, 1 : PRINT "X: " + FIXED$(JOYAXIS(0), 2)See also: FIXED