The Game Loop and Pyxel
Learning Objectives
- You can install Pyxel and run a minimal Pyxel program.
- You can explain the game loop: that
updatechanges the state anddrawshows it, many times per second. - You can write the standard Pyxel skeleton as a class with
updateanddrawmethods. - You can read input with
pyxel.btnandpyxel.btnp, and explain the difference. - You can use
pyxel.frame_countas a clock and explain why games measure time in frames.
Every program you have written so far ran from top to bottom and then stopped. It read some input, computed something, printed a result, and exited. Even the study log, which looped to collect many sessions, eventually fell off the end of its loop and the program ended.
A game does not work like that. A game runs continuously. Sixty times a second, or thirty, it looks at what the player is doing, updates the state of the world a little, and redraws the screen. It keeps doing this until the player quits. This continuous cycle is called the game loop, and it is the one idea this whole part is built on.
The Two Halves of a Frame
Each pass through the game loop is called a frame. In each frame, two things happen, in this order:
- Update. Look at the input (which keys are held), and change the state of the world accordingly. Move the player, move the enemies, check for collisions, increase the score. This step changes data — it draws nothing.
- Draw. Look at the current state of the world and put it on the screen. Clear the screen, then draw each thing where its data says it is. This step draws only — it changes no data.
The separation between these two is the most important habit in this part. Update changes the world; draw shows the world. Keeping update and draw apart is what makes a game possible to reason about: at any moment, the state is a set of variables, and the screen is just a picture of those variables.
Installing Pyxel
Use the same study-log project environment: you created the project in Working on Your Own Computer, made its data persistent in Making Your Study Log Persistent, and learned how to add packages in Installing Third-Party Libraries:
python -m pip install --upgrade "pyxel>=2.5.13"Version 2.5.13 is the minimum version these materials support. The lower bound avoids older APIs while allowing pip to install a newer release. If your usual command is py or python3, use that command in place of python. Using -m pip connects the installation to that particular Python interpreter.
After installation, print the interpreter path and installed Pyxel version:
python -c "import sys; from importlib.metadata import version; print(sys.executable); print(version('pyxel'))"The first line should name your study-log project environment, and the second should be 2.5.13 or higher. If the version is older, or the interpreter path does not name the expected environment, revisit the Installing Third-Party Libraries environment checks before continuing.
Once it is installed, you can check that it works by running this tiny program, which opens an empty window:
import pyxel
pyxel.init(160, 120)
def update(): pass
def draw(): pass
pyxel.run(update, draw)If a black window appears, Pyxel is working. Close it (or press the quit key, usually Esc) and read on. If nothing appears, Pyxel’s own documentation is the first place to check.
Game as a Class
Here’s the same program using a class:
import pyxel
class App: def __init__(self): pyxel.init(160, 120) pyxel.run(self.update, self.draw)
def update(self): pass
def draw(self): pass
if __name__ == "__main__": App()Read it from the bottom. When you run the file directly, Python sets __name__ to "__main__", so App() creates an instance and runs __init__. When another program imports the file, the condition is false and the window does not open unexpectedly. This main guard makes a game safe to import for testing or reuse, so every complete game in this part will use it.
__init__ does two things: pyxel.init(160, 120) opens a 160-by-120-pixel window, and pyxel.run(self.update, self.draw) hands Pyxel the two methods and starts the loop. From that point on, Pyxel calls self.update() and then self.draw(), over and over, thirty times a second, until you quit.
Pixels in Pyxel are not your normal screen pixels. Each “pixel” is a square that is big enough to see. This is deliberate: the name “Pyxel” comes from “Python” and “pixel”, and the library is designed to let you focus on programming rather than on graphics.
The class is doing exactly what classes did in Adding Behavior: Methods: bundling the state (which will live in self) together with the behaviour (update and draw) that acts on it. Right now there is no state and the behaviour does nothing, but the shape is the one everything else hangs on.
Many Pyxel examples show games created with plain functions and global variables. But the state of a game — the player’s position, the score, the list of enemies — is exactly the kind of thing you’ve learned to keep together in an object. As the games in this part get more complex, the class-based approach will make them easier to understand and modify.
Drawing
Pyxel’s drawing functions are few and simple. You will use these constantly:
| Function | What it draws |
|---|---|
pyxel.cls(col) | Clears the whole screen to colour col |
pyxel.rect(x, y, w, h, col) | A filled rectangle |
pyxel.rectb(x, y, w, h, col) | A rectangle outline (b for border) |
pyxel.circ(x, y, r, col) | A filled circle |
pyxel.circb(x, y, r, col) | A circle outline |
pyxel.line(x1, y1, x2, y2, col) | A line |
pyxel.text(x, y, s, col) | A string of text |
Two things about coordinates. The point (0, 0) is the top-left corner, and y increases downward — so y = 100 is near the bottom of a 120-tall screen, not the top. This trips up everyone at first. And col is a number from 0 to 15: Pyxel has exactly sixteen colours. For example, 0 is black, 7 is white, 8 is red, 11 is green, and 12 is blue. You can use the named constants (pyxel.COLOR_RED is 8) or just the numbers.
Here is a program that draws a white box and some text on a dark blue background:
import pyxel
class App: def __init__(self): pyxel.init(160, 120, title="Drawing") pyxel.run(self.update, self.draw)
def update(self): pass
def draw(self): pyxel.cls(1) # clear to dark blue pyxel.rect(70, 50, 20, 20, 7) # white box near the centre pyxel.text(50, 90, "Hello!", 7) # white text below it
if __name__ == "__main__": App()Notice that draw always starts with pyxel.cls(...). Without it, every frame would draw on top of the previous one and you would get smears. Clearing the screen each frame and redrawing everything from scratch is the normal way games work.
Reading Input
To make something move, the update step needs to know what the player is pressing. Pyxel gives you two functions for this, and the difference between them matters:
pyxel.btn(key)isTruefor every frame the key is held down. Use it for continuous actions — moving while a key is held.pyxel.btnp(key)isTrueonly on the single frame the key was first pressed (pfor “pressed”). Use it for one-off actions — firing a shot, confirming a menu choice, restarting.
The keys are named constants: pyxel.KEY_LEFT, pyxel.KEY_RIGHT, pyxel.KEY_UP, pyxel.KEY_DOWN, pyxel.KEY_SPACE, pyxel.KEY_RETURN, and a constant for every letter (pyxel.KEY_A and so on).
This program moves a dot left and right with the arrow keys:
import pyxel
class App: def __init__(self): self.x = 80 # game state: the dot's position pyxel.init(160, 120, title="Move") pyxel.run(self.update, self.draw)
def update(self): if pyxel.btn(pyxel.KEY_LEFT): self.x = self.x - 2 if pyxel.btn(pyxel.KEY_RIGHT): self.x = self.x + 2
def draw(self): pyxel.cls(0) pyxel.circ(self.x, 60, 4, 11) # green dot at (x, 60)
if __name__ == "__main__": App()This is the first program that is recognisably a game. self.x is the state. The update step reads the keys and changes self.x. The draw step reads self.x and draws the dot there. The two are cleanly separated: update never draws, draw never moves anything. Run it, hold an arrow key, and watch the dot slide. That sliding is the game loop doing its job thirty times a second.
Try replacing
pyxel.btnwithpyxel.btnpand running it again. Now the dot moves only one step per press, no matter how long you hold the key. That is the difference between the two functions, made visible. Then, try commenting out thepyxel.cls(0)line indrawand running it again. The dot leaves a trail behind it, because the screen is never cleared.
Frames Are the Clock
There is no time in a Pyxel game, at least not the kind you measure in seconds. There are only frames. The variable pyxel.frame_count holds the number of frames since the program started: it is 0 on the first frame, 1 on the second, and so on, ticking up thirty times a second.
This is the game’s clock, and you use it for anything that should happen on a schedule. “Spawn a new enemy every second” becomes “spawn a new enemy when frame_count is a multiple of 30”. The modulo operator from the chapter on Numbers and Arithmetic does the work:
def update(self): if pyxel.frame_count % 30 == 0: # this runs once a second (every 30th frame) self.spawn_enemy()Measuring time in frames rather than seconds takes getting used to, but it is simpler than it sounds: everything in the game ticks forward at the same steady rate, and frame_count is the tick counter. When a later chapter says “every 45 frames”, it means “every second and a half” at 30 frames per second.
In every program before this part, you wrote the loop. The study log had a while True: that you typed yourself, and you decided when to break out of it.
In a Pyxel game, the loop lives inside pyxel.run. You never see it. You hand Pyxel your update and draw functions, and Pyxel runs the loop for you, calling them once per frame forever. This is a pattern you will meet again and again in larger programs and frameworks: the library owns the loop, and you supply the pieces it calls. Your job is no longer “write the loop” but “fill in what happens each time around it.” Getting comfortable with that inversion — the library calls your code, rather than your code calling the library — is part of what this part teaches.
Check Your Understanding
- What two things happen in each frame of the game loop, and in what order?
- Why should the
drawmethod never change the game’s state, and theupdatemethod never draw anything? - What is the difference between
pyxel.btnandpyxel.btnp? Give an example of an action suited to each. - Why does
drawalmost always start with a call topyxel.cls? - If you wanted something to happen once every two seconds at 30 frames per second, what condition on
pyxel.frame_countwould you write?
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.