Skip to content

Gaming & GX

A game is not a pile of pixels. It is a small world that changes one honest turn at a time.

This page introduces GX, d/BASIC's library for 2D games: scenes, sprites, tilemaps, cameras and collision. You will learn what GX does and what it leaves to you, how a game keeps its whole world in a few ordinary arrays, and why the same game can run in a browser, on a desktop and on an 8-bit computer without being rewritten. When you are done you will be ready for the first-game tutorial.

GX gives a game a small, portable vocabulary. It does not own the window, the drawing, the controller, the files or the sound. Those belong to the runtime, the program that runs your compiled code, and there is a runtime for the browser, for each desktop and for d/OS, the operating system that brings modern software to 8-bit home computers such as the Atari 800, the Commodore 64 and the Apple II.

That boundary is deliberate. A tiny d/OS display, a browser and a desktop can draw the same game very differently, while the rules that make the game your game stay exactly the same.

Choose a path

Alpha availability

d/BASIC and GX are in alpha. Some of the engine features described in this guide are not yet available in every compiler, runtime or machine.

The games are the test suite

GX is not judged by drawing one cheerful rectangle. It is tested in three rings, and each ring catches a different kind of mistake:

Ring What is run What it proves
classic games the 96 classic BASIC game listings we counted; 95 run to completion in the tests, and one listing that is written never to end is left out of the output comparison the control flow, arrays, strings, arithmetic, input and repeatable execution that games depend on stay sound beneath GX
GX sample games the 17 sample programs that ship with the GX project, from small collision and tilemap demos to complete games such as Santa and Escape real GX programs find the engine vocabulary they expect
the engine itself the GX engine source, about 121 KB of declarations and code imports, types, arrays, BYREF, large programs, paging and shared libraries hold up under a real engine rather than a toy

These are one story, not three competing definitions of "the GX tests." The classic games prove the d/BASIC floor; the sample games exercise the public game surface; the engine source is the large-program stress test. As of today, the core of GX (the part documented on this page) has passed its compile-and-run tests. Running all 17 sample games and the full 121 KB engine is still alpha work in progress. We spell that boundary out so that "GX works" always means something exact.

Why GX exists

The first home-computer games were often wonderfully direct: move a number, test a hit, draw a thing. The trouble began when that logic quietly grew dependent on one display chip, one controller and one memory map. Porting then meant rewriting the game instead of adapting how it was shown.

GX keeps the good part, the directness, while separating three jobs:

Layer Owns
your program rules, arrays, object model, turns, score, save decisions, and what a collision means
GX checked, machine-neutral operations on the scene, entity, tile and camera arrays you hand it
the runtime turning hardware input into events, holding images and sounds, drawing, playing audio, showing frames, and checking what a program needs before it runs

Your program owns all the state. GX never keeps a hidden world behind a handle. You allocate the arrays with DIM, you can look inside them, you decide when to save them, and you choose when to draw them. If you pass GX an invalid slot, a bad dimension or an array that is too small, it leaves your data unchanged.

IMPORT gx

DIM Scene%(0 TO 7)
DIM Entities%(0 TO 19)  ' two entity slots; each occupies ten words
DIM Tiles%(0 TO 319)    ' a 20 by 16 tilemap
DIM Camera%(0 TO 1)

GXSceneOpen Scene%(), 20, 16
GXEntitySpawn Entities%(), 0, 16, 16, 8, 8, 1
GXEntityVelocity Entities%(), 0, 1, 0
GXTileSet Tiles%(), 20, 16, 4, 3, 7

GXSceneAdvance Scene%()
GXEntityStep Entities%(), 0
GXCameraFollow Camera%(), GXEntityX(Entities%(), 0), GXEntityY(Entities%(), 0), 160, 128, 80, 64

IMPORT gx loads the GX module that ships with d/BASIC. It supplies helpers for the game model. It does not open a game window, and it does not grant permission to draw, play sound, read files or read a controller.

The arrays GX works on

The current GX core keeps everything in ordinary one-dimensional INTEGER arrays that your program owns. GX calls these arrays carriers, and you will meet the word in GXCarrierWords. Keeping them plain is deliberate: they follow d/BASIC's normal array rules, they hide no memory address, and they give the game an explicit, visible memory budget.

Array Words needed What GX stores
Scene() 8 marker and version, world width and height, tick, active flag
Entities() 10 per slot alive, position, size, velocity, animation frame and tick, kind
Tiles() Columns * Rows one integer tile value per cell
Camera() 2 camera X and Y

The positions of these values are exported as constants such as GX_ENTITY_X_AT, GX_ENTITY_VX_AT and GX_SCENE_TICK_AT. Most games should use the named procedures below instead. The constants exist for deliberate tools, such as an editor, an inspector or a save-file format, not as an invitation to build a second, incompatible engine.

Scene turns

GXSceneOpen Scene(), Width, Height
GXSceneAdvance Scene()
Ready% = GXSceneReady(Scene())
Words% = GXCarrierWords(Scene())

GXSceneOpen accepts only a positive world width and height and fills in the eight-word array. GXSceneAdvance adds one to the scene tick and wraps from 32767 back to 0. GXSceneReady returns -1 only for an array that carries the current GX scene marker and version; otherwise it returns 0.

A scene tick is game time, not a claim about real-world milliseconds. Your program decides how often to advance it.

Entities: small actors, explicit rules

Each entity slot uses ten words. GXEntityCapacity tells you how many complete slots fit; any leftover words at the end are not an entity. Slots are numbered from zero.

GXEntitySpawn Entities(), Slot, X, Y, Width, Height, Kind
GXEntityVelocity Entities(), Slot, VX, VY
GXEntityStep Entities(), Slot

X% = GXEntityX(Entities(), Slot)
Y% = GXEntityY(Entities(), Slot)
Capacity% = GXEntityCapacity(Entities())
Valid% = GXEntityValid(Entities(), Slot)

Spawning marks the entity alive, sets its position, size and Kind, and resets its velocity and animation fields. GXEntityVelocity and GXEntityStep only touch a valid, living slot. Your game decides how to remove an entity, apply gravity, keep it inside the world, or react to a hit. The core deliberately does not guess.

Collision is a question, not a policy

IF GXEntitiesOverlap(Entities%(), 0, 1) THEN
  PRINT "THE HERO REACHED THE KEY"
END IF

IF GXRectOverlap(10, 10, 8, 8, 16, 10, 8, 8) THEN
  PRINT "RECTANGLES TOUCH WITH AREA"
END IF

GXRectOverlap takes rectangles with positive width and height and returns -1 only when they share some area; touching at an edge does not count. GXEntitiesOverlap first checks that both slots are valid and both entities are alive. The far edges are computed in LONG, so an entity near the edge of the integer range cannot wrap around and produce a false hit.

What happens after -1 is the game: collect a key, bounce, lose health, start a conversation, or do nothing.

Tilemaps: a grid you own

A GX tilemap is an integer array stored row by row. X runs across the columns; Y runs down the rows. GXTileSet allocates nothing; you provide the array.

DIM Map%(0 TO 239)

GXTileSet Map%(), 20, 12, 3, 4, 9
Tile% = GXTileAt(Map%(), 20, 12, 3, 4)
At% = GXTileIndex(Map%(), 20, 12, 3, 4)

The array must hold at least Columns * Rows words. A zero or negative dimension, a coordinate outside the grid, or an array too small for the declared grid is handled safely: GXTileIndex returns -1, GXTileAt returns 0, and GXTileSet leaves the array alone.

Tile numbers mean whatever you say they mean. 0 might be empty sky in one game and a solid wall in another. A future drawing layer can turn the same numbers into images, colours, animated tiles, or a deliberately simpler picture on a small display without changing the world data.

Camera: follow without falling off the map

DIM View%(0 TO 1)

GXCameraFollow View%(), HeroX%, HeroY%, WorldWidth%, WorldHeight%, ViewWidth%, ViewHeight%
INTENT GFX.FILL_RECT(GXEntityX(Entities%(), 0) - GXCameraX(View%()), GXEntityY(Entities%(), 0) - GXCameraY(View%()), 8, 8, -1)

GXCameraFollow centres the view on the point you give it, then keeps the camera inside the declared world. If the view is larger than the world on one axis, that axis becomes zero rather than negative. GXCameraX and GXCameraY return zero if the camera array is too small.

The GFX.FILL_RECT line is ordinary d/BASIC on purpose. It is an intent: a named request such as "fill this rectangle" that the machine carries out however it can. GX supplied the position; your program chose how to draw it. In a real window, put drawing inside WINDOW SUB DRAW() between the usual frame begin and present operations.

Animation: select; do not sleep

Frame% = GXAnimationFrame(12, 4, Tick%, 6)

This picks frames 12, 13, 14 and 15 in turn, holding each for six scene ticks. GXAnimationFrame(FirstFrame, FrameCount, Tick, TicksPerFrame) returns FirstFrame when the count, the ticks per frame or the tick is invalid. It never waits, allocates or draws, which is exactly what an event-driven program wants.

The entity array reserves a frame field and a frame-tick field (GX_ENTITY_FRAME_AT and GX_ENTITY_FRAME_TICK_AT) for a game that wants to remember its choice. The core does not impose an animation controller.

An object-shaped game model

GX is built on arrays, but it sits comfortably inside an object-oriented program. Put the game rules in a class, keep the compact arrays in storage your program owns, and use methods to name the meaning GX cannot know.

IMPORT gx

DIM SHARED World%(0 TO 7)
DIM SHARED Actors%(0 TO 19)
DIM SHARED View%(0 TO 1)

CLASS GameSession
  PUBLIC:
  DECLARE SUB Reset ()
  DECLARE SUB Tick ()
  DECLARE FUNCTION HeroMetGuard () AS INTEGER
END CLASS

SUB GameSession.Reset ()
  GXSceneOpen World%(), 160, 128
  GXEntitySpawn Actors%(), 0, 20, 20, 8, 8, 1
  GXEntitySpawn Actors%(), 1, 44, 20, 8, 8, 2
  GXEntityVelocity Actors%(), 0, 1, 0
END SUB

SUB GameSession.Tick ()
  GXSceneAdvance World%()
  GXEntityStep Actors%(), 0
  GXCameraFollow View%(), GXEntityX(Actors%(), 0), GXEntityY(Actors%(), 0), 160, 128, 80, 64
END SUB

FUNCTION GameSession.HeroMetGuard () AS INTEGER
  GameSession.HeroMetGuard = GXEntitiesOverlap(Actors%(), 0, 1)
END FUNCTION

DIM Game AS GameSession
Game = NEW GameSession()
Game.Reset
Game.Tick

IF Game.HeroMetGuard() THEN PRINT "THE GUARD SAYS HELLO"
END

This is the useful split: GameSession owns the words "hero" and "guard"; GX owns the checked rectangle maths and the array layout. In a larger program, a window event changes a velocity, a frame turn calls Tick, and DRAW paints the result.

Drawing, input, resources and sound: the intended direction

GX is deliberately not a private drawing or audio stack. A game combines its GX model with the same portable facilities every other d/BASIC program uses:

Need Use now The runtime's job
drawing GFX intents in the window's draw turn draw richly where the machine allows, or fall back to the simpler local display, keeping the game's geometry either way
keyboard and pointer ON KEY SUB and ON POINTER SUB turn hardware or browser input into d/BASIC events
packaged art named resources, supplied by the part of the host that handles media decode, cache, scale, or pick a declared simpler version, all by the resource's name
sound the available AUDIO intents choose a real route (native chip, mixer, tone, or a bounded stream) and say clearly when a capability is missing
frame scheduling window invalidation, and a planned ON FRAME choose a sensible pace without making GX busy-wait

Game turns stay short. Use the runtime's frame and event surface, and never put an endless game loop inside WINDOW SUB DRAW().

This is the same adaptive promise found everywhere in d/BASIC: a browser can use high-resolution images and its own presentation, a desktop can use richer local facilities, and a constrained machine can lower detail or decline a capability it does not have. Your GX arrays, collision results, tile numbers and game rules do not change to chase a machine.

For the surrounding rules, see Windows & Events, Resources, Images & Sound, and Intents & Services.

The current GX core at a glance

Group Available core operations
arrays and scene GXCarrierWords, GXSceneReady, GXSceneOpen, GXSceneAdvance
entities GXEntityCapacity, GXEntityBase, GXEntityValid, GXEntitySpawn, GXEntityVelocity, GXEntityStep, GXEntityX, GXEntityY
collision GXRectOverlap, GXEntitiesOverlap
tiles GXTileIndex, GXTileSet, GXTileAt
camera GXCameraFollow, GXCameraX, GXCameraY
animation GXAnimationFrame

GXEntityBase is there for inspection and tooling; normal game code should use the named entity functions. The public constants describe the current core layouts: GX_SCENE_*, GX_ENTITY_* and GX_CAMERA_*.

A calm first-game checklist

  1. Allocate exact, visible arrays with DIM.
  2. Open one scene and spawn a small number of entities.
  3. Advance the scene only from a short, bounded turn.
  4. Ask GX questions; let your game decide the consequence.
  5. Draw from state rather than changing rules while painting.
  6. Treat resources, input, audio and storage as services that may be missing, and handle a missing one clearly.
  7. Keep the game playable when the display falls back to something simpler; when a capability the game truly needs is absent, stop and say so.

Next: Tutorial: Your First GX Game →