Camera and viewport
Understand world coordinates versus screen coordinates and how a camera offset lets you scroll through a level larger than the window.
- Explain the difference between world coordinates and screen coordinates
- Describe how a camera offset converts one to the other
- Understand camera-follow logic and edge clamping at world boundaries
The beginner labs used a world that fit entirely inside the window: 640x480 pixels, player and obstacles all within that rect. Many games need a larger world — a platformer level that is 3000 pixels wide, or a top-down map that is taller than the screen. You cannot hold the whole world on screen at once, so the game must decide which part of the world to show.
This is what a camera does. Not a camera in the cinematic sense — there is no physical camera object in your code. A camera is simply a pair of numbers (an offset) that describes how the world is positioned relative to the window.
World coordinates versus screen coordinates
Every object in your game has a position in world coordinates: where it lives in the full game world. The player starts at world position (200, 400). A platform is at (1000, 300). A coin is at (2500, 200).
The window can only show a portion of that world at any given time. The position on screen where an object is drawn is its screen coordinate, computed by subtracting the camera offset from its world coordinate:
screen_x = world_x - camera_offset_x
screen_y = world_y - camera_offset_yIf the camera offset is (800, 0), an object at world position (1000, 300) appears at screen position (200, 300). An object at world position (200, 300) appears at screen position (−600, 300) — off the left edge of the screen, so it is not visible.
Camera follow
The simplest camera behaviour is to keep the player centred in the window:
# Target: player is centred horizontally and vertically
camera_x = player.rect.centerx - SCREEN_WIDTH // 2
camera_y = player.rect.centery - SCREEN_HEIGHT // 2You then draw every object at (world_x - camera_x, world_y - camera_y).
The problem with raw centring is that it shows empty space beyond the world edges. Fix this by clamping the camera to the world boundaries:
camera_x = max(0, min(camera_x, WORLD_WIDTH - SCREEN_WIDTH))
camera_y = max(0, min(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT))With clamping, the camera stops scrolling when the player is near an edge rather than showing the void beyond the world.
Applying the offset when drawing
Instead of drawing pygame.draw.rect(screen, colour, obj.rect), subtract the
offset:
draw_rect = obj.rect.move(-camera_x, -camera_y)
pygame.draw.rect(screen, colour, draw_rect)Rect.move() returns a new rect without modifying the original, so obj.rect
stays in world coordinates. Every draw call applies the same offset, so the
entire world appears to slide under the fixed window frame.
When you use pygame.sprite.Group.draw(), override each sprite's rect to
the screen-space rect before the draw call, or maintain a separate screen_rect
attribute. The lab in this module shows one practical approach.
Why "moving the camera" is really "shifting all draw positions"
There is no camera object in pygame. Moving the camera is just changing two
numbers and using them as an offset in every draw call. This means the camera
is pure math — you can have multiple cameras (split-screen), animate the
camera offset for screen-shake effects, or ease the offset towards the target
position for a smooth follow feel. All of those are just changes to how
camera_x and camera_y are computed before the draw phase.
Where to go next
Next: the Game Architecture lab — refactor the polished beginner game into a three-scene state machine with an event bus, a camera offset, and a live score HUD.
Event systems
Implement a minimal publish/subscribe event bus so game objects communicate without holding direct references to each other.
Lab: State machine refactor
Refactor the polished beginner game into a three-scene state machine with an event bus for score updates and a camera offset for a scrolling level.