Introduction To A Simple Python Game

New to programming? Follow along one step at a time with the step by step version, where every step is on its own page.

I wanted a tiny first game that someone who has never programmed can build in an afternoon, and that actually looks like a game when it runs. This one is called Bobgame. Bob is a round head seen from above, you move him with the W A S D keys, and an arrow on him always points at your mouse. That’s it, but it’s the exact base that real top-down shooters start from.

The finished code is open source, so you can always compare against mine: github.com/carlpaton/bobgame. The whole game is only about 90 lines.

This post is written so an 11 year old can follow it. If something doesn’t make sense, run the code anyway, change a number, and see what happens. That is how most programmers learn.

This is what you’ll have at the end. Bob is the round head, and the red arrow points at the mouse.

Bobgame running

1. Install Python

Python is the language we write the game in. Your computer needs to understand it first.

  1. Go to python.org/downloads and download the latest Python 3 (this game needs 3.10 or newer).
  2. Run the installer. On the very first screen, tick the box that says “Add python.exe to PATH” before you click Install. If you miss this, the commands below won’t work.

Open a terminal (press the Windows key, type cmd, press Enter) and check it worked

1
py --version

You should see something like Python 3.14.0.

On Ubuntu, Python is usually already installed, so use python3 --version instead, and if you’re missing the venv tool in step 3 run sudo apt install python3-venv.

Docs: Python downloads • Using Python on Windows

2. Install VS Code and the Python extension

VS Code is the program we type our code into (an editor). It’s free.

  1. Download and install it from code.visualstudio.com.
  2. Open VS Code, click the Extensions icon on the left (it looks like four squares) or press Ctrl+Shift+X.
  3. Search for Python and install the one published by Microsoft. This is the only extension we need. It colours your code so it’s easier to read, points out mistakes, and lets you run your game with one click.

Docs: Python in Visual Studio Code

3. Make the project folder and a virtual environment

Make a folder called bobgame somewhere easy to find, then in VS Code choose File > Open Folder and open it. Open the built-in terminal with Ctrl+` (that’s the key above Tab).

Now create a virtual environment. Think of it as a private toolbox just for this game, so the things we install for Bobgame don’t get mixed up with other projects.

1
py -3 -m venv .venv

This makes a hidden folder called .venv. VS Code will usually pop up a message asking if you want to use it, click Yes. If it doesn’t, press Ctrl+Shift+P, type Python: Select Interpreter, and pick the one that has .venv in its path.

On Ubuntu this is python3 -m venv .venv.

Docs: venv - Creation of virtual environments

4. Install pygame-ce

pygame-ce is a library, which is code somebody else wrote and shared so we don’t have to start from nothing. It gives us a window, drawing shapes, the keyboard and the mouse. Even though we install pygame-ce, in our code we still write import pygame.

In VS Code create a new file in the bobgame folder called requirements.txt with this single line in it

1
pygame-ce

A requirements.txt is just a shopping list of libraries the project needs. Install everything on the list

1
.venv\Scripts\pip install -r requirements.txt

On Ubuntu use .venv/bin/pip install -r requirements.txt.

You’ll see a lot of text scroll by. When it stops without a red error you’re done.

Docs: pygame-ce documentation

5. Plan the files

A game gets messy fast if all the code is in one big file. We’ll use four small files, each with one job. Create these now (empty is fine). The bobgame folder inside the bobgame folder is on purpose, it’s called a package.

1
2
3
4
5
6
7
8
bobgame/
requirements.txt
main.py <- the start button
bobgame/
__init__.py <- empty, tells Python this folder is a package
settings.py <- numbers and colours we might want to tweak
player.py <- Bob: where he is, how he moves, how he looks
game.py <- the window and the game loop

In VS Code right-click in the left panel and choose New Folder / New File.

6. Write settings.py

Open bobgame/settings.py and type this in

1
2
3
4
5
6
7
8
9
10
WIDTH = 800
HEIGHT = 600
FPS = 60
BG_COLOR = (30, 32, 38)

BOB_RADIUS = 20
BOB_SPEED = 250 # pixels per second
SKIN_COLOR = (233, 189, 150)
HAIR_COLOR = (70, 45, 25)
ARROW_COLOR = (240, 80, 60)

Each line is a variable: a name with a value stored in it, like a labelled box. We write the names in CAPITAL LETTERS because they are settings that never change while the game runs.

  • WIDTH and HEIGHT are the size of the window in pixels (the tiny dots your screen is made of).
  • FPS is frames per second. A game is like a flipbook, it draws a new picture many times a second. 60 looks smooth.
  • Colours are three numbers: how much red, green and blue (0 to 255). (0, 0, 0) is black, (255, 255, 255) is white, and (30, 32, 38) is a dark grey-blue.
  • BOB_SPEED is how many pixels Bob moves each second. The # starts a comment, a note for humans that Python ignores.

Try changing a colour later and see what happens. Pick colours with any “RGB colour picker” on the web.

7. Write game.py and open a window

Now the most important file. Open bobgame/game.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pygame

from .settings import BG_COLOR, FPS, HEIGHT, WIDTH


class Game:
def __init__(self) -> None:
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Bobgame")
self.clock = pygame.time.Clock()
self.running = True

def step(self, dt: float) -> None:
for event in pygame.event.get():
if event.type == pygame.QUIT or (
event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
):
self.running = False
self.screen.fill(BG_COLOR)
pygame.display.flip()

def run(self) -> None:
while self.running:
self.step(self.clock.tick(FPS) / 1000)
pygame.quit()

That’s a lot, so here it is piece by piece.

  • import pygame brings in the library. The line from .settings import ... brings in our own numbers from settings.py. The dot means “from the same folder”.
  • class Game is a class, a blueprint. Imagine a blueprint for a house: it isn’t a house, but you can build a house from it. Our blueprint describes a game.
  • __init__ runs once, the moment the game is built. It starts pygame, makes the window with set_mode, gives it a title, and creates a Clock, which keeps time so the game runs at the same speed on every computer. self just means “this game”. self.running = True is a switch that says the game is on.
  • run is the game loop, the heart of every game. while self.running: means “keep repeating this for as long as running is True”. Each lap of the loop is one frame, which is one picture of the flipbook. When the switch becomes False, the loop ends and pygame.quit() closes everything.
  • self.clock.tick(FPS) waits so we don’t go faster than 60 laps a second, and tells us how many milliseconds that lap took. Dividing by 1000 turns it into seconds. We call that number dt (short for “delta time”, or “the time since the last frame”). We’ll use it soon.
  • step is what happens on each lap. First it checks the events, things that happened like a key press or clicking the window’s close button. If you close the window (QUIT) or press Escape, we flip the switch to False. Then it paints the whole screen with the background colour, and display.flip() shows the finished picture.

The -> None and : float bits are called type hints. They are notes saying “this gives back nothing” and “this is a decimal number”. Python doesn’t need them, but they help you and VS Code understand the code.

8. Write main.py and run it

Open main.py, in the top folder (not inside the inner bobgame folder), and type

1
2
3
4
from bobgame.game import Game

if __name__ == "__main__":
Game().run()

This builds a Game from the blueprint and calls run() on it. The if __name__ == "__main__": line just means “only do this when I run this file directly”.

Run it in the VS Code terminal

1
.venv\Scripts\python main.py

On Ubuntu use .venv/bin/python main.py. You can also click the play button in the top right of VS Code while main.py is open.

A dark 800 by 600 window should appear. Nothing is in it yet, but you’ve made the window and the game loop. Press Escape to close it.

If you get ModuleNotFoundError: No module named 'pygame' then you probably didn’t run it with the .venv python. Use the full command above.

9. Write player.py and draw Bob

Time to make Bob. Open bobgame/player.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pygame

from .settings import (
ARROW_COLOR, BOB_RADIUS, BOB_SPEED, HAIR_COLOR, HEIGHT, SKIN_COLOR, WIDTH,
)


class Bob:
def __init__(self) -> None:
self.pos = pygame.Vector2(WIDTH / 2, HEIGHT / 2)
self.facing = pygame.Vector2(0, -1)

def draw(self, surface: pygame.Surface) -> None:
center = (round(self.pos.x), round(self.pos.y))
pygame.draw.circle(surface, SKIN_COLOR, center, BOB_RADIUS)
# Hair: ring around the head plus a cap on the back half.
pygame.draw.circle(surface, HAIR_COLOR, center, BOB_RADIUS, 4)
back = self.pos - self.facing * (BOB_RADIUS * 0.35)
pygame.draw.circle(
surface, HAIR_COLOR, (round(back.x), round(back.y)), BOB_RADIUS * 0.6
)

Bob is another class, another blueprint.

  • self.pos is Bob’s position. A Vector2 is a pair of numbers, x (how far across) and y (how far down). In games, (0, 0) is the top-left corner and y gets bigger as you go down, which is the opposite of the maths you do at school. We start Bob in the middle of the window, WIDTH / 2 across and HEIGHT / 2 down.
  • self.facing is the direction Bob looks. It is also a Vector2, but this time think of it as an arrow with a length of 1 that points somewhere. (0, -1) means 0 across and 1 up (remember, up is negative), so he starts looking at the top of the screen.
  • draw paints Bob onto a surface (the canvas, in our case the window). pygame.draw.circle takes a canvas, a colour, a centre and a size.
  • First we draw a skin coloured circle. Then we draw a hair coloured circle with a 4 on the end, which means “only draw the outline, 4 pixels thick”. That gives a ring of hair. Lastly we draw a smaller hair circle slightly towards Bob’s back. We find that spot by going from his centre in the opposite direction (the minus sign) of where he faces. From above, it looks like he has hair on the back of his head.
  • round(...) turns a decimal like 400.4 into a whole number, because pixels can’t be cut in half.

Now we need the game to make a Bob and draw him. Change game.py in three places

1
from .player import Bob

add that near the top, below import pygame. Then in __init__ add one line

1
self.bob = Bob()

below self.clock = .... And in step draw him after the background is painted

1
2
3
self.screen.fill(BG_COLOR)
self.bob.draw(self.screen)
pygame.display.flip()

The order matters. If we drew Bob first and then painted the background, we’d cover him up!

Run it again. You should see a round head with hair in the middle of the window. He’s not moving yet, so let’s fix that.

10. Move Bob with W A S D

Add this update method to the Bob class in player.py, between __init__ and draw

1
2
3
4
5
6
7
8
9
10
def update(self, dt: float, keys, mouse_pos) -> None:
direction = pygame.Vector2(
keys[pygame.K_d] - keys[pygame.K_a],
keys[pygame.K_s] - keys[pygame.K_w],
)
if direction.length_squared() > 0:
direction.normalize_ip()
self.pos += direction * BOB_SPEED * dt
self.pos.x = max(BOB_RADIUS, min(WIDTH - BOB_RADIUS, self.pos.x))
self.pos.y = max(BOB_RADIUS, min(HEIGHT - BOB_RADIUS, self.pos.y))

keys is a list of every key on the keyboard, and each one is True if it’s being held down or False if not. Python treats True as 1 and False as 0, so we can do maths with them.

  • Across: D minus A. If you hold D that is 1 - 0 = 1 (right). Hold A and it’s 0 - 1 = -1 (left). Hold both and they cancel out to 0.
  • Down: S minus W, the same trick. W is -1 because up is negative.
  • So direction ends up as a little arrow telling us which way you want to go. Hold D and S together and it’s (1, 1), down and to the right.
  • There’s a catch. (1, 1) is a longer arrow than (1, 0), so going diagonally would be faster than going straight. That’s a sneaky bug lots of games have. normalize_ip() fixes it by squashing any arrow to a length of exactly 1, while keeping the direction. We only do that if the arrow isn’t zero length, because you can’t squash nothing.
  • self.pos += direction * BOB_SPEED * dt is the actual move. Direction (which way) times speed (250 pixels per second) times dt (how many seconds this frame took). If a frame takes 1/60 of a second, Bob moves about 4 pixels. Using dt means he moves at the same speed on a fast computer or a slow one.
  • The last two lines are a fence. min picks the smaller of two numbers and max picks the bigger, so together they trap x and y between the edges of the window. We leave a gap of BOB_RADIUS so the whole head stays in view.

The mouse_pos isn’t used yet, we’ll use it in the next step. In game.py we need to call update on every frame. Add this to step, right before the line that paints the background

1
self.bob.update(dt, pygame.key.get_pressed(), pygame.mouse.get_pos())

pygame.key.get_pressed() hands over the list of keys and pygame.mouse.get_pos() gives the mouse position. Run the game and use W A S D. Bob should glide around and stop at the edges.

11. Make the arrow follow the mouse

Add the rest of the update method, at the bottom of it, so Bob works out where to look

1
2
3
to_mouse = pygame.Vector2(mouse_pos) - self.pos
if to_mouse.length_squared() > 0:
self.facing = to_mouse.normalize()

Subtracting Bob’s position from the mouse position gives an arrow that goes from Bob to the mouse. We squash it to length 1 with normalize() and store it as facing. The if stops us trying to squash an arrow of length 0, which happens if the mouse is exactly on Bob’s centre.

Now draw the arrow. Add this to the bottom of the draw method

1
2
3
4
5
6
7
# Arrow: line from the head edge plus a triangle tip.
start = self.pos + self.facing * (BOB_RADIUS * 0.5)
tip = self.pos + self.facing * (BOB_RADIUS + 18)
base = tip - self.facing * 10
side = pygame.Vector2(-self.facing.y, self.facing.x) * 6
pygame.draw.line(surface, ARROW_COLOR, start, base, 3)
pygame.draw.polygon(surface, ARROW_COLOR, [tip, base + side, base - side])

It’s built from three points along the line Bob is facing:

  • start is a bit inside Bob’s head.
  • tip is the sharp point, 18 pixels past the edge of his head.
  • base is 10 pixels back from the tip, where the triangle begins.
  • side is a small arrow pointing sideways to the facing direction. Swapping the two numbers and flipping one sign turns an arrow by a quarter turn, which is a neat trick. We use it to find the two corners of the triangle either side of base.
  • draw.line draws the shaft (3 pixels thick) and draw.polygon fills in the triangle from the three corners: the tip, and the two sides.

Run it and wiggle the mouse around. The arrow should spin to follow it while you walk around with the keyboard.

12. Check it against the finished code

Here is the finished game.py so you can check yours

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import pygame

from .player import Bob
from .settings import BG_COLOR, FPS, HEIGHT, WIDTH


class Game:
def __init__(self) -> None:
pygame.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Bobgame")
self.clock = pygame.time.Clock()
self.bob = Bob()
self.running = True

def step(self, dt: float) -> None:
for event in pygame.event.get():
if event.type == pygame.QUIT or (
event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
):
self.running = False
self.bob.update(dt, pygame.key.get_pressed(), pygame.mouse.get_pos())
self.screen.fill(BG_COLOR)
self.bob.draw(self.screen)
pygame.display.flip()

def run(self) -> None:
while self.running:
self.step(self.clock.tick(FPS) / 1000)
pygame.quit()

Every frame the game loop does the same three things: handle input, update the world, then draw it. Almost every game ever made is built on that idea, from this one to the biggest ones you play.

If something isn’t working, compare your files with the ones in the bobgame repo. Common culprits are:

  1. Indentation. Python cares about the spaces at the start of a line. Everything inside a class or a def is indented by 4 spaces.
  2. Typos in names. self.pos and self.Pos are different things.
  3. A missing colon at the end of a line starting with def, class, if or while.
  4. Wrong folder. main.py must be in the top bobgame folder, with the inner bobgame folder beside it.

The error message in the terminal tells you the file and the line number. Read it, it’s usually trying to help.

Docs: pygame.draw • pygame.math.Vector2 • pygame.time.Clock

Challenges

Now it’s yours. Change one thing at a time, run it, and see what happens.

  1. Change BG_COLOR to a different colour. Can you make the background white?
  2. Make Bob twice as fast by changing BOB_SPEED. Then make him very slow.
  3. Make Bob bigger or smaller with BOB_RADIUS. What happens to the arrow?
  4. Change WIDTH and HEIGHT to make a tall, thin window.
  5. Give Bob blonde hair, then green skin.
  6. Make the arrow longer by changing the 18 in tip.
  7. Make the game use the arrow keys instead of W A S D. Hint: look for pygame.K_UP.
  8. Change the FPS to 10. Does Bob still move at the same speed? Why?

Have fun with it, and send me what you build.