PluginBench
Skill
Pass
Audit score 90

pygame-core

gamedev-skills/awesome-gamedev-agent-skills

Build the core pygame game loop: init, events, delta-time movement, drawing, input, and sprite/group collision.

What is pygame-core?

pygame-core teaches the foundation of a pygame-ce game in Python: the main event/update/draw loop, frame-rate-independent movement via delta-time, Surface/Rect blitting, keyboard and mouse input handling, and Sprite/Group management with collision detection. Use this when starting a pygame project, fixing the game loop, handling input, or debugging sprite and collision logic.

  • Set up pygame-ce window, clock, and the main game loop (events → update → draw → flip)
  • Implement delta-time movement to make speed frame-rate independent using `clock.tick()` and `dt`
  • Handle input via events (discrete actions like jump) and polled state (continuous movement)
  • Organize game objects with `pygame.sprite.Sprite` subclasses and `Group` for batch updates and drawing
  • Detect collisions between sprites and groups using `spritecollide()`, `groupcollide()`, and rect overlap
  • Blit images and surfaces efficiently using `.convert()` and `.convert_alpha()` for fast rendering

How to install pygame-core

npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill pygame-core
Prerequisites
  • Python 3.7+
  • Install pygame-ce: `pip install pygame-ce` (not legacy pygame)
Claude Code
Cursor
Windsurf
Cline

How to use pygame-core

  1. 1.Initialize pygame with `pygame.init()` and create a display window using `pygame.display.set_mode()`
  2. 2.Create a `Clock` object and set up the main loop that runs at a target FPS (e.g., 60)
  3. 3.In each frame, drain the event queue with `pygame.event.get()`, update game state, clear the screen, draw all objects, and call `pygame.display.flip()`
  4. 4.Calculate delta-time as `dt = clock.tick(fps) / 1000` and scale all movement by `dt` to ensure frame-rate independence
  5. 5.Use `pygame.key.get_pressed()` for continuous input (held keys) and event-based input for discrete actions (key press, mouse click)
  6. 6.Subclass `pygame.sprite.Sprite` with `image` and `rect` attributes, add instances to a `Group`, then call `group.update(dt)` and `group.draw(screen)` each frame
  7. 7.Use `pygame.sprite.spritecollide()` or `groupcollide()` to detect collisions and respond (e.g., remove coins when collected)

Use cases

Good for
  • Starting a new 2D game and setting up the main loop structure
  • Fixing frame-rate-dependent movement that runs at different speeds on different machines
  • Adding keyboard or mouse input to control a player character or trigger actions
  • Organizing multiple game objects (enemies, projectiles, collectibles) with sprite groups
  • Debugging collision detection between the player and obstacles or collectibles
Who it's for
  • Game developers building 2D games in Python
  • Students learning game loop architecture and real-time input handling
  • Developers migrating from legacy pygame to the maintained pygame-ce fork
  • Anyone debugging sprite rendering, collision, or input issues in an existing pygame project

pygame-core FAQ

Should I use pygame or pygame-ce?

Use pygame-ce (`pip install pygame-ce`). It is the actively maintained community fork and imports as `pygame`. Legacy pygame is no longer maintained; do not install both in the same environment.

Why does my game run at different speeds on different machines?

You are likely moving objects by a fixed amount per frame instead of scaling by delta-time. Always compute `dt = clock.tick(fps) / 1000` and multiply movement by `dt` so speed is in pixels-per-second, not pixels-per-frame.

My window freezes and says 'not responding'. What's wrong?

You are not pumping the event queue. Call `pygame.event.get()` (or `pygame.event.pump()`) every frame inside the main loop, even if you don't use the events. The OS needs to see the window responding to events.

My sprites are blitting slowly and the framerate drops. How do I fix it?

Call `.convert()` (for opaque images) or `.convert_alpha()` (for transparent images) on loaded surfaces once at startup. Un-converted surfaces blit much slower. Store the converted surface and reuse it.

How do I handle both continuous movement (holding a key) and discrete actions (pressing a key once)?

Use `pygame.key.get_pressed()` in the update loop for continuous input like movement. Use the event loop (`pygame.KEYDOWN` events) for discrete actions like jump or pause, since you only want to trigger them once per press.

Full instructions (SKILL.md)

Source of truth, from gamedev-skills/awesome-gamedev-agent-skills.


name: pygame-core description: > Structure a pygame (pygame-ce) game in Python: the init/event/update/draw loop, delta-time movement, Surface/Rect blitting, keyboard/mouse input, and Sprite/Group management with collision. Use when building or debugging a pygame game — when the user mentions pygame, pygame-ce, the game loop, blit, Surface, Rect, sprite groups, or clock.tick. Targets pygame-ce.

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.8 (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

  1. 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.
  2. Init and open a window. pygame.init(), screen = pygame.display.set_mode((w, h)), clock = pygame.time.Clock().
  3. 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().
  4. 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.
  5. Handle input two ways: event-based (KEYDOWN/MOUSEBUTTONDOWN, for discrete actions) and polled (pygame.key.get_pressed(), for held movement).
  6. 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.