pygame Core
Build the foundation of a pygame game in Python: the main loop, delta-time
movement, drawing with Surface/Rect, input, and Sprite/Group management.
Targets pygame-ce 2.5.7 (the actively maintained community fork; same
import pygame).
When to use
- Use when starting a pygame game, fixing the loop, frame-rate-dependent speed,
input handling, blitting, or sprite/group collision.
- Use when code does
import pygame and the project depends on pygame-ce
(or pygame).
When not to use: Python language questions unrelated to pygame. 3D rendering
(pygame is 2D). For cross-engine save/load use save-systems; for rebindable input
architecture see input-systems.
Core workflow
- Install pygame-ce, not legacy pygame.
pip install pygame-ce — it's the
maintained fork and imports as pygame. Don't install both in one environment.
- Init and open a window.
pygame.init(), screen = pygame.display.set_mode((w, h)), clock = pygame.time.Clock().
- Run one loop: events → update → draw → flip. Pump the event queue every
frame (
for event in pygame.event.get()), update state, redraw, then
pygame.display.flip().
- Make it frame-rate independent. Get
dt = clock.tick(60) / 1000 (seconds)
and scale all motion by dt. Keep positions as floats; blit at integer rects.
- Handle input two ways: event-based (
KEYDOWN/MOUSEBUTTONDOWN, for discrete
actions) and polled (pygame.key.get_pressed(), for held movement).
- Organise objects with
Sprite + Group. Subclass pygame.sprite.Sprite
with image/rect; group.update(dt) and group.draw(screen) handle the
batch. Run it and watch the window before assuming it works.
Patterns
1. Minimal game loop (the skeleton)
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()
running = True
while running:
dt = clock.tick(60) / 1000 # cap at 60 FPS; dt = seconds since last frame
for event in pygame.event.get(): # MUST drain the queue or the OS thinks it hung
if event.type == pygame.QUIT:
running = False
# update game state here, scaled by dt ...
screen.fill((18, 18, 28)) # clear each frame
# draw everything here ...
pygame.display.flip() # present the frame
pygame.quit()
2. Delta-time movement (frame-rate independent)
from pygame.math import Vector2
pos = Vector2(100, 100) # keep position as floats
speed = 220 # PIXELS PER SECOND, not per frame
# inside the loop, after computing dt:
keys = pygame.key.get_pressed()
direction = Vector2(
keys[pygame.K_RIGHT] - keys[pygame.K_LEFT],
keys[pygame.K_DOWN] - keys[pygame.K_UP],
)
if direction.length_squared() > 0:
direction = direction.normalize() # equal speed on diagonals
pos += direction * speed * dt # RIGHT: dt-scaled
screen.blit(player_img, (round(pos.x), round(pos.y))) # blit at integer pixels
3. Input: events vs polling
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN: # discrete press: jump, menu, pause
if event.key == pygame.K_SPACE:
jump()
elif event.key == pygame.K_ESCAPE:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
shoot_at(event.pos) # event.pos = (x, y)
# Polled state (read once per frame) for continuous/held input:
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
move_left(dt)
4. A Sprite subclass + a Group
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
# convert() once at load makes blits much faster; _alpha keeps transparency.
self.image = pygame.image.load("player.png").convert_alpha()
self.rect = self.image.get_rect(center=(x, y))
self.pos = pygame.math.Vector2(self.rect.center)
self.speed = 240
def update(self, dt): # Group.update(dt) calls this per sprite
keys = pygame.key.get_pressed()
self.pos.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed * dt
self.rect.center = (round(self.pos.x), round(self.pos.y))
all_sprites = pygame.sprite.Group()
all_sprites.add(Player(400, 300))
# in the loop:
all_sprites.update(dt) # calls each sprite's update(dt)
all_sprites.draw(screen) # blits each sprite at its rect
5. Collision detection
# Sprite vs group: e.g. player picking up coins (True = remove collided coins).
collected = pygame.sprite.spritecollide(player, coins, dokill=True)
score += len(collected)
# Group vs group: bullets vs enemies (kill both on hit).
hits = pygame.sprite.groupcollide(bullets, enemies, True, True)
# Plain rect overlap (no sprites needed):
if player.rect.colliderect(door_rect):
open_door()
Pitfalls
- Window freezes / "not responding" → you didn't pump the event queue. Call
pygame.event.get() (or pygame.event.pump()) every frame.
- Speed differs on faster machines → you moved by a fixed amount per frame.
Scale by
dt = clock.tick(fps) / 1000 and use pixels-per-second values.
- Sub-pixel movement snaps/jitters →
rect coordinates are integers; store the
true position as a Vector2 of floats and assign rect.center = round(...) each
frame.
- Blits are slow / framerate drops → call
.convert() (opaque) or
.convert_alpha() (transparent) on loaded images once; un-converted surfaces blit
far slower.
- Nothing appears → you forgot
pygame.display.flip() (or update()), or you
drew before screen.fill(...) so it was cleared away.
- Wrong draw order → pygame uses painter's order; later blits cover earlier ones.
Draw background first, sprites last.
pip install pygame got the old one → for the maintained fork use
pip install pygame-ce; having both installed causes import conflicts.
- Diagonal movement is faster → normalise the direction vector before scaling by
speed.
References
- For
Group variants (GroupSingle, LayeredUpdates for z-order), pixel-perfect
collision with mask, slicing a spritesheet, simple animation, sound/music, and
text rendering, read references/sprites-and-collision.md.
Related skills
love2d-core — the same loop concepts in LÖVE/Lua.
bevy-ecs — a heavier ECS engine when a project outgrows pygame.
input-systems / save-systems — engine-agnostic input and persistence.
platformer / roguelike — genre templates that pair with pygame.
1---
2name: pygame-core
3description: >
4 Structure a pygame (pygame-ce) game in Python: the init/event/update/draw loop,
5 delta-time movement, Surface/Rect blitting, keyboard/mouse input, and
6 Sprite/Group management with collision. Use when building or debugging a pygame
7 game — when the user mentions pygame, pygame-ce, the game loop, blit, Surface,
8 Rect, sprite groups, or clock.tick. Targets pygame-ce.
9---
10
11# pygame Core
12
13Build the foundation of a pygame game in Python: the main loop, delta-time
14movement, drawing with `Surface`/`Rect`, input, and `Sprite`/`Group` management.
15Targets **pygame-ce 2.5.7** (the actively maintained community fork; same
16`import pygame`).
17
18## When to use
19
20- Use when starting a pygame game, fixing the loop, frame-rate-dependent speed,
21 input handling, blitting, or sprite/group collision.
22- Use when code does `import pygame` and the project depends on `pygame-ce`
23 (or `pygame`).
24
25**When *not* to use:** Python language questions unrelated to pygame. 3D rendering
26(pygame is 2D). For cross-engine save/load use `save-systems`; for rebindable input
27architecture see `input-systems`.
28
29## Core workflow
30
311. **Install pygame-ce, not legacy pygame.** `pip install pygame-ce` — it's the
32 maintained fork and imports as `pygame`. Don't install both in one environment.
332. **Init and open a window.** `pygame.init()`, `screen =
34 pygame.display.set_mode((w, h))`, `clock = pygame.time.Clock()`.
353. **Run one loop: events → update → draw → flip.** Pump the event queue every
36 frame (`for event in pygame.event.get()`), update state, redraw, then
37 `pygame.display.flip()`.
384. **Make it frame-rate independent.** Get `dt = clock.tick(60) / 1000` (seconds)
39 and scale all motion by `dt`. Keep positions as floats; blit at integer rects.
405. **Handle input two ways:** event-based (`KEYDOWN`/`MOUSEBUTTONDOWN`, for discrete
41 actions) and polled (`pygame.key.get_pressed()`, for held movement).
426. **Organise objects with `Sprite` + `Group`.** Subclass `pygame.sprite.Sprite`
43 with `image`/`rect`; `group.update(dt)` and `group.draw(screen)` handle the
44 batch. Run it and watch the window before assuming it works.
45
46## Patterns
47
48### 1. Minimal game loop (the skeleton)
49
50```python
51import pygame
52
53pygame.init()
54screen = pygame.display.set_mode((800, 600))
55pygame.display.set_caption("My Game")
56clock = pygame.time.Clock()
57
58running = True
59while running:
60 dt = clock.tick(60) / 1000 # cap at 60 FPS; dt = seconds since last frame
61 for event in pygame.event.get(): # MUST drain the queue or the OS thinks it hung
62 if event.type == pygame.QUIT:
63 running = False
64
65 # update game state here, scaled by dt ...
66
67 screen.fill((18, 18, 28)) # clear each frame
68 # draw everything here ...
69 pygame.display.flip() # present the frame
70
71pygame.quit()
72```
73
74### 2. Delta-time movement (frame-rate independent)
75
76```python
77from pygame.math import Vector2
78
79pos = Vector2(100, 100) # keep position as floats
80speed = 220 # PIXELS PER SECOND, not per frame
81
82# inside the loop, after computing dt:
83keys = pygame.key.get_pressed()
84direction = Vector2(
85 keys[pygame.K_RIGHT] - keys[pygame.K_LEFT],
86 keys[pygame.K_DOWN] - keys[pygame.K_UP],
87)
88if direction.length_squared() > 0:
89 direction = direction.normalize() # equal speed on diagonals
90pos += direction * speed * dt # RIGHT: dt-scaled
91screen.blit(player_img, (round(pos.x), round(pos.y))) # blit at integer pixels
92```
93
94### 3. Input: events vs polling
95
96```python
97for event in pygame.event.get():
98 if event.type == pygame.QUIT:
99 running = False
100 elif event.type == pygame.KEYDOWN: # discrete press: jump, menu, pause
101 if event.key == pygame.K_SPACE:
102 jump()
103 elif event.key == pygame.K_ESCAPE:
104 running = False
105 elif event.type == pygame.MOUSEBUTTONDOWN:
106 shoot_at(event.pos) # event.pos = (x, y)
107
108# Polled state (read once per frame) for continuous/held input:
109keys = pygame.key.get_pressed()
110if keys[pygame.K_a]:
111 move_left(dt)
112```
113
114### 4. A Sprite subclass + a Group
115
116```python
117class Player(pygame.sprite.Sprite):
118 def __init__(self, x, y):
119 super().__init__()
120 # convert() once at load makes blits much faster; _alpha keeps transparency.
121 self.image = pygame.image.load("player.png").convert_alpha()
122 self.rect = self.image.get_rect(center=(x, y))
123 self.pos = pygame.math.Vector2(self.rect.center)
124 self.speed = 240
125
126 def update(self, dt): # Group.update(dt) calls this per sprite
127 keys = pygame.key.get_pressed()
128 self.pos.x += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.speed * dt
129 self.rect.center = (round(self.pos.x), round(self.pos.y))
130
131all_sprites = pygame.sprite.Group()
132all_sprites.add(Player(400, 300))
133
134# in the loop:
135all_sprites.update(dt) # calls each sprite's update(dt)
136all_sprites.draw(screen) # blits each sprite at its rect
137```
138
139### 5. Collision detection
140
141```python
142# Sprite vs group: e.g. player picking up coins (True = remove collided coins).
143collected = pygame.sprite.spritecollide(player, coins, dokill=True)
144score += len(collected)
145
146# Group vs group: bullets vs enemies (kill both on hit).
147hits = pygame.sprite.groupcollide(bullets, enemies, True, True)
148
149# Plain rect overlap (no sprites needed):
150if player.rect.colliderect(door_rect):
151 open_door()
152```
153
154## Pitfalls
155
156- **Window freezes / "not responding"** → you didn't pump the event queue. Call
157 `pygame.event.get()` (or `pygame.event.pump()`) every frame.
158- **Speed differs on faster machines** → you moved by a fixed amount per frame.
159 Scale by `dt = clock.tick(fps) / 1000` and use pixels-per-second values.
160- **Sub-pixel movement snaps/jitters** → `rect` coordinates are integers; store the
161 true position as a `Vector2` of floats and assign `rect.center = round(...)` each
162 frame.
163- **Blits are slow / framerate drops** → call `.convert()` (opaque) or
164 `.convert_alpha()` (transparent) on loaded images once; un-converted surfaces blit
165 far slower.
166- **Nothing appears** → you forgot `pygame.display.flip()` (or `update()`), or you
167 drew before `screen.fill(...)` so it was cleared away.
168- **Wrong draw order** → pygame uses painter's order; later blits cover earlier ones.
169 Draw background first, sprites last.
170- **`pip install pygame` got the old one** → for the maintained fork use
171 `pip install pygame-ce`; having both installed causes import conflicts.
172- **Diagonal movement is faster** → normalise the direction vector before scaling by
173 speed.
174
175## References
176
177- For `Group` variants (`GroupSingle`, `LayeredUpdates` for z-order), pixel-perfect
178 collision with `mask`, slicing a spritesheet, simple animation, sound/music, and
179 text rendering, read `references/sprites-and-collision.md`.
180
181## Related skills
182
183- `love2d-core` — the same loop concepts in LÖVE/Lua.
184- `bevy-ecs` — a heavier ECS engine when a project outgrows pygame.
185- `input-systems` / `save-systems` — engine-agnostic input and persistence.
186- `platformer` / `roguelike` — genre templates that pair with pygame.