website is under construction
Lumen

Game Loop

At the heart of every Lumen game, the game loop drives the lifecycle of the application. This looping mechanism is responsible for setting the initial state, collecting and processing input, updating state, and rendering graphics to the screen.

Lumen calls load() once, then update(dt) and draw() every frame until the game quits.

The three callbacks — and every event callback below — are ordinary top-level functions in your game's entry file. Lumen looks them up by name, so there is nothing to register.

  1. load()
  2. update(dt)
  3. draw()

load()

Runs once, before the first frame. Load assets and build initial state here.

import "lumen:window"
import { Image } from "lumen:image"

game = {}

function load() {
  window.setTitle('My Game')

  game.player = {
    x: 0,
    y: 0,
    speed: 180,
    sprite: new Image('player.png')
  }
}

If load() raises an error, Lumen reports it and exits, rather than running a game whose state was never finished.

update(dt)

After initializing the game state, update() runs once per frame. This is where game logic goes: character behaviour, AI, physics, and other progression-related tasks.

dt is how many seconds the previous frame took. Scale anything that moves by it. A speed written as pixels-per-frame changes with the frame rate; a speed written as pixels-per-second does not.

function update(dt) {
  if (keyboard.isDown('right', 'd')) {
    game.player.x = game.player.x + game.player.speed * dt
  }
}

dt is capped at 0.25 seconds, so a frame that stalls — a breakpoint, a window drag, a machine going to sleep — cannot teleport everything through a wall.

A game that ignores the argument still works: Ghost drops arguments a function does not declare, so function update() is valid. Anything that moves will then run at whatever speed the machine happens to manage.

Remember that assignment inside a function is local. x = x + 1 inside update() creates a new local every frame and changes nothing. Keep state on a map or class instance and assign through a property, as above.

draw()

The draw() function is the final part of the cycle, executed after update(). Its sole purpose is rendering the current game state onto the screen.

function draw() {
  game.player.sprite.draw(game.player.x, game.player.y)
}

The draw state — transform, color, line width, blend mode, scissor — is reset to its defaults before draw() runs, so each frame is self-contained and nothing leaks from the last one. The current font is the exception: it is a choice a game makes once and keeps.

After draw() is executed, the game loop returns to update() and the cycle repeats until the game is closed.

Event Callbacks

Beyond the three above, Lumen calls a callback when something happens. Every one is optional; define the ones your game needs.

CallbackWhen
keypressed(key, isRepeat)a key goes down
keyreleased(key)a key comes up
textinput(text)text is typed, between keyboard.startTextInput() and stopTextInput()
mousepressed(x, y, button, clicks)a mouse button goes down
mousereleased(x, y, button)a mouse button comes up
mousemoved(x, y, dx, dy)the pointer moves
wheelmoved(x, y)the wheel turns
resize(width, height)the window is resized
focus(hasFocus)the window gains or loses focus
joystickadded(count)a controller is plugged in
joystickremoved(count)a controller is unplugged
quit()the window is closed; return true to cancel

Use keypressed rather than keyboard.isDown for menus and dialogue: it fires once per physical press, where isDown is true on every frame the key is held.

function keypressed(key, isRepeat) {
  if (key == 'escape') {
    lumen.quit()
  }
}

function quit() {
  if (game.unsaved) {
    game.showSavePrompt = true

    return true // cancel the quit
  }
}

Errors During the Loop

An error inside update(), draw(), or an input callback is reported on an error screen in the window as well as to the console, and the game holds still on it — pressing enter carries on past that frame. An error in load(), or in the game's source, is fatal, as described above: neither ever built the state every later frame reads. A failure carried on past is counted in the console rather than stopped for again, so one broken frame cannot bury everything above it.