Game Loop
Pyxen runs your game at a fixed 30 frames per second. You don’t write a main loop — you define two functions and Pyxen calls them for you.
start() and update()
def start():
# called once, at the beginning
pass
def update():
# called every frame, 30 times per second
pass
start() is where you set up your world: spawn entities, place them, configure the scene.
update() is where your game logic runs: read input, move entities, check conditions, update state. After each call to update(), the engine renders the frame.
Fixed frame rate
Every frame takes exactly the same amount of time: 1/30th of a second (~33ms). This means:
- Game logic is deterministic — the same inputs produce the same results
- You can count frames directly (
time.frame) instead of working with variable delta times - The time scrubber and frame inspector work reliably because each frame is a clean snapshot
You can still use time.dt (delta time) if you prefer time-based movement, but it will always be 1/30.
Frame order
Each frame, Pyxen runs these steps in order:
- Input — read keyboard, mouse, touch, and gamepad state
- update() — your code runs
- Collision — the engine resolves grid bodies and calls collision callbacks
- Render — the engine draws all visible entities
This means when you read input in update(), the values are fresh. When you move an entity, collision resolution happens right after.
Collision callbacks
If you define on_collision or on_tile_collision at the top level, the engine calls them during the collision step:
def on_collision(entity, other):
# two grid bodies overlapped
pass
def on_tile_collision(entity, tile):
# a grid body hit a tilemap tile
pass
These run after update() but before rendering.
Timing
The pyxen.time module gives you:
| Property | Value | Use |
|---|---|---|
time.frame | 0, 1, 2, ... | Frame counter since start |
time.dt | 0.0333... | Delta time (always 1/30) |
time.t | 0.0, 0.033, 0.066, ... | Elapsed time in seconds |
time.fps | 30 | Frames per second |
For timed events, you can use either approach:
# Frame-based: fire every 60 frames (2 seconds)
if time.frame % 60 == 0:
fire()
# Time-based: same thing
if int(time.t * 30) % 60 == 0:
fire()
Frame counting is usually simpler and more predictable.