gale.camera
love2d has love.graphics.push/translate/scale/pop to
scroll and zoom a whole scene at once; pygame has no GPU transform
stack to piggyback on the same way, so gale.camera.Camera instead
turns world coordinates into screen ones for you — you still blit
each entity yourself, just at the position (and size) the camera says
to.
See examples/scavenger for a full game built on this: a coin-collecting game where the camera follows the player around a world much bigger than the viewport, with zoom and screen shake.
Basic usage
from gale.camera import Camera
camera = Camera(settings.VIRTUAL_WIDTH, settings.VIRTUAL_HEIGHT)
# In your state:
def update(self, dt):
camera.update(dt)
def render(self, surface):
for entity in self.entities:
surface.blit(entity.image, camera.apply(entity.rect))
camera.x/camera.y are the world point the camera is centered
on — set them directly for an instant cut (a new room, a teleport).
Following a target
Any object with x/y attributes (a gale.physics.Node, your
own player, …) can be tracked automatically:
camera.follow(player) # snaps to the player every update()
camera.follow(player, rate=6.0) # or eases toward it instead — higher rate, snappier
camera.unfollow() # stop tracking, x/y stay wherever they last were
Zoom
camera.zoom = 2.0 # zoomed in
camera.zoom = 0.5 # zoomed out
apply() scales both the position and the size of the rect you pass
it, so a zoomed camera also needs whatever you’re blitting to be drawn
at (or scaled to) that same size — see pygame.transform.scale if
your sprites are a fixed image size.
Keeping the camera inside the map
import pygame
camera.bounds = pygame.Rect(0, 0, world_width, world_height)
Every update(), x/y are clamped so the viewport never
shows anything past bounds — unless the map itself is smaller than
the viewport, in which case the camera centers on it instead of
clamping to an inverted range.
Screen shake
camera.shake(magnitude=8, duration=0.3) # e.g. on taking damage, an explosion
A single call replaces any shake already running rather than stacking
with it. The offset decays linearly to 0 over duration seconds.
Screen ↔ world coordinates
For anything that isn’t a plain blit — mouse picking, a minimap,
particles spawned at a world position:
world_point = camera.screen_to_world(rescaled_mouse_position)
screen_point = camera.world_to_screen(entity.position)
Known limitations
No rotation — only scroll (
x/y) and uniform zoom. Rotating a whole pygame scene means rotating every surface individually (pygame.transform.rotate, which is also lossy/blurry at non-right angles), which is a much bigger cost than scroll/zoom and rarely needed for a 2D top-down/platformer camera; add it yourself withpygame.transform.rotateon a per-entity basis if a specific effect needs it.A single
Camerarenders one viewport. For split-screen, use oneCameraper player, each with its ownbounds, and blit each player’s entities to their own sub-surface.