Game Loop
January 10, 2026 ยท View on GitHub
GMR uses a simple, predictable game loop based on global hooks. This is the recommended approach for small games and prototypes.
Global Hooks
Define these three functions at the top level of your script:
include GMR
def init
# Called once when the game starts (and on each hot-reload)
@player_x = 400
@player_y = 300
end
def update(dt)
# Called every frame. dt = time since last frame in seconds
@player_x += 100 * dt if Input.key_down?(:right)
@player_x -= 100 * dt if Input.key_down?(:left)
end
def draw
# Called every frame for rendering
Graphics.clear(rgb(20, 20, 40))
Graphics.draw_circle(@player_x, @player_y, 20, rgb(100, 200, 255))
end
Your entire game can live in main.rb with these three hooks.
Frame Timing
The dt Parameter
update(dt) receives the delta time - seconds elapsed since the last frame:
def update(dt)
# At 60 FPS, dt is approximately 0.0166
# At 30 FPS, dt is approximately 0.0333
speed = 200 # Pixels per second
@x += speed * dt # Frame-rate independent movement
end
Always multiply velocities and time-based values by dt. This ensures consistent behavior regardless of frame rate.
Accessing Time Globally
The Time module provides additional timing information:
def update(dt)
puts Time.delta # Same as dt parameter
puts Time.fps # Current frames per second
puts Time.elapsed # Total seconds since game start
end
Time Scale
Slow down or speed up game time:
Time.scale = 0.5 # Half speed (slow motion)
Time.scale = 2.0 # Double speed
Time.scale = 0.0 # Pause (dt becomes 0)
Time.scale = 1.0 # Normal speed
Time scale affects the dt value passed to update. Use this for pause menus, slow-motion effects, or speed-up power-ups.
Execution Order
Each frame executes in this guaranteed order:
- Input Polling - Engine captures current keyboard/mouse state
update(dt)- Your game logic runsdraw- Your rendering code runs- Present - Frame buffer displayed to screen
Input state is always fresh when update runs. All drawing happens after logic.
The init Hook
init is called:
- Once when the game first starts
- Once on each hot-reload during development
Use init for setup that should run once:
def init
Window.set_title("My Game")
Window.set_size(960, 540)
input do |i|
i.move_left [:a, :left]
i.move_right [:d, :right]
i.jump [:space, :w]
end
@player = Player.new(400, 300)
@camera = Camera.new
end
Preserving State Across Hot-Reload
Instance variables persist across hot-reloads. Use ||= to keep existing state:
def init
# Always re-apply configuration
Window.set_title("My Game")
# Only create if doesn't exist (preserves state during development)
@player ||= Player.new(400, 300)
@score ||= 0
@level ||= 1
end
This pattern lets you tweak code without losing your current game state.
Pausing the Game
Simple Pause Flag
For games using global hooks, use a boolean flag:
def init
@paused = false
end
def update(dt)
if Input.key_pressed?(:escape)
@paused = !@paused
end
return if @paused
# Game logic only runs when not paused
@player.update(dt)
end
def draw
Graphics.clear(:dark_gray)
@player.draw
if @paused
Graphics.draw_rect(0, 0, 960, 540, [0, 0, 0, 180])
Graphics.draw_text("PAUSED", 400, 250, 48, :white)
end
end
Using Time Scale
Alternatively, set time scale to zero:
def update(dt)
if Input.key_pressed?(:escape)
Time.scale = Time.scale == 0 ? 1.0 : 0.0
end
# With Time.scale = 0, dt is 0, so nothing moves
@player.x += @speed * dt
end
Console Integration
The developer console can pause your game. Check if it's open:
def update(dt)
return if Console.open? # Skip game logic when console is active
@player.update(dt)
end
This prevents input conflicts and lets you inspect game state without things moving.
When to Use Scenes Instead
Global hooks work well for:
- Prototypes and experiments
- Game jam entries
- Single-screen games
- Learning GMR
Consider Scene classes when you need:
- Multiple distinct screens (title, gameplay, pause, game over)
- Clean separation between game states
- Pause menus that preserve game state
- Overlay systems (HUD, minimap)
Complete Example
include GMR
SPEED = 200
GRAVITY = 600
JUMP_FORCE = -300
GROUND_Y = 400
def init
Window.set_size(960, 540)
Window.set_title("Platformer")
input do |i|
i.move_left [:a, :left]
i.move_right [:d, :right]
i.jump [:space, :w, :up]
end
@x, @y = 100, GROUND_Y
@vx, @vy = 0, 0
@on_ground = true
end
def update(dt)
return if Console.open?
# Horizontal movement
@vx = 0
@vx -= SPEED if Input.action_down?(:move_left)
@vx += SPEED if Input.action_down?(:move_right)
# Jump
if Input.action_pressed?(:jump) && @on_ground
@vy = JUMP_FORCE
@on_ground = false
end
# Gravity
@vy += GRAVITY * dt unless @on_ground
# Apply velocity
@x += @vx * dt
@y += @vy * dt
# Ground collision
if @y >= GROUND_Y
@y = GROUND_Y
@vy = 0
@on_ground = true
end
end
def draw
Graphics.clear("#1e1e32")
Graphics.draw_rect(0, GROUND_Y, 960, 140, "#3c3c50")
Graphics.draw_circle(@x, @y, 20, :cyan)
Graphics.draw_text("Arrows/WASD to move, Space to jump", 10, 10, 16, :white)
end
See Also
- Engine Model - Execution guarantees and invariants
- Scenes - Scene classes for larger games
- Input - Input handling details
- Console - Developer console usage