Skip to main content

Lesson 4: Coordinate Systems

  • Module 2: Input & Screen Space
  • Lesson 4 of 14
  • โฑ๏ธ About 1 h 15 min (instruction + lab)

Most game worlds are much bigger than the window you see them through. In this lesson you learn to keep "where a thing is in the world" separate from "where it appears on screen", and build a camera that scrolls across a big level with two lines of arithmetic.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Explain pygame's screen coordinates, including why the last pixel of an 800-pixel-wide window is at x = 799.
  • Convert between screen coordinates and math-style coordinates (origin in the middle, y pointing up).
  • Convert world positions to screen positions with a camera offset, and screen positions (such as a mouse click) back to world positions.
  • Build a camera that follows the player and never shows anything outside the world.
  • Debug the classic sign mix-up that makes clicked objects slide when the camera moves.

Project: Pan, Click, Convert: pan a camera over a large world, drop markers where you click, and watch one mouse position in three coordinate systems at once.

In This Lesson

๐Ÿ” Screen Space, Up Close

You already know the basics from Drawing Shapes & Surfaces: screen coordinates are measured in pixels from the window's top-left corner, with x growing right and y growing down. Two details matter once things start moving:

  • Counting starts at 0. An 800-pixel-wide window has pixels 0, 1, 2, โ€ฆ 799. There are 800 of them, but the last one is at x = 799. A point at x = 800 is just outside, which is why off-by-one mistakes are so common.
  • A rectangle's right edge is "one past". For pygame.Rect(0, 0, 800, 600), rect.right is 800 and rect.bottom is 600: the first column and row after the rectangle. So a square of size SIZE fits inside the window while x stays between 0 and WIDTH - SIZE, the clamp you used in Keyboard, Mouse & Gamepad.
import pygame

window = pygame.Rect(0, 0, 800, 600)
print(window.right, window.bottom)          # 800 600: one past the last pixel
print(window.collidepoint(799, 599))        # True: the bottom-right pixel is inside
print(window.collidepoint(800, 599))        # False: x = 800 is outside

๐Ÿ“ˆ Math-Style Coordinates

Screen coordinates are perfect for drawing, but sometimes you want to think the way math class does: the origin in the middle and "up" meaning positive y. Plotting a graph, placing things symmetrically around the center of a menu, or reading a formula from a physics book are all easier that way. The conversion is two subtractions:

WIDTH, HEIGHT = 800, 600

def screen_to_math(screen_x, screen_y):
    """Origin at the window's center, y pointing up."""
    return screen_x - WIDTH / 2, HEIGHT / 2 - screen_y

def math_to_screen(math_x, math_y):
    return math_x + WIDTH / 2, HEIGHT / 2 - math_y

x only shifts, so the center column becomes 0. y shifts and flips: HEIGHT / 2 - screen_y is positive above the center line and negative below it. Check it with the corners: the top-left pixel (0, 0) becomes (-400, 300), and the window's center (400, 300) becomes (0, 0).

Two drawings of the same 800 by 600 window. On the left, screen coordinates: the origin is the top-left corner, x grows right and y grows down, and a point is labeled (600, 150). On the right, math-style coordinates: the origin is the center of the window, x grows right and y grows up, and the same point is labeled (200, 150). Below, the formulas: math x equals screen x minus 400, and math y equals 300 minus screen y. Screen (pygame) origin top-left ยท y grows down Math-style origin at center ยท y grows up (0, 0) x y (600, 150) (0, 0) x y (200, 150) math_x = screen_x โˆ’ 400 math_y = 300 โˆ’ screen_y
One spot in an 800 ร— 600 window, two names. On screen it is (600, 150): 600 right and 150 down from the top-left. In math-style coordinates it is (200, 150): 200 right of the center and 150 up. Only y flips direction.

Here is a complete program that thinks in math-style coordinates and converts only when it draws. It plots the curve y = xยฒ รท 200, a parabola, with its lowest point at the center of the window:

import pygame

WIDTH, HEIGHT = 800, 600


def math_to_screen(math_x, math_y):
    """Origin at the window's center, y pointing up."""
    return math_x + WIDTH / 2, HEIGHT / 2 - math_y


pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Plotting in Math Coordinates")
clock = pygame.time.Clock()

# Work out the points once, in math coordinates: y = x * x / 200.
points = []
for math_x in range(-280, 281, 10):
    math_y = math_x * math_x / 200
    points.append(math_to_screen(math_x, math_y))

running = True
while running:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill((20, 24, 36))
    # The axes: the horizontal line math_y = 0 and the vertical line math_x = 0.
    pygame.draw.line(screen, (90, 96, 120), math_to_screen(-400, 0), math_to_screen(400, 0), 2)
    pygame.draw.line(screen, (90, 96, 120), math_to_screen(0, -300), math_to_screen(0, 300), 2)
    pygame.draw.lines(screen, (250, 204, 21), False, points, 3)
    pygame.draw.circle(screen, (255, 107, 107), math_to_screen(200, 200), 6)   # a point ON the curve
    pygame.display.flip()

pygame.quit()

The curve opens upward on screen, as it would in a math book, even though pygame's y grows downward: the conversion does the flipping. pygame.draw.lines (plural) connects a whole list of points; False means "don't join the last point back to the first".

๐Ÿงญ Pick one convention and stick to it

This course uses screen coordinates for everything in a game and converts to math-style coordinates only when a calculation reads better that way. Whichever you use, name your functions after both ends (screen_to_math, math_to_screen) so every conversion says which way it goes.

๐ŸŒ World Space and the Camera

Picture a huge poster on a wall and a phone camera pointed at part of it. The poster is the world: every tree, coin and enemy has a fixed world position on it, measured from the poster's own top-left corner. The phone screen is the window. Where a tree appears on your phone depends on where the camera is pointing. Move the camera right, and everything on the phone slides left, while the poster itself never changes.

In code, the camera is just a position: the world point that sits at the window's top-left corner. Converting is one subtraction or one addition:

def world_to_screen(world_x, world_y, cam_x, cam_y):
    """Where a world point appears in the window: subtract the camera."""
    return world_x - cam_x, world_y - cam_y


def screen_to_world(screen_x, screen_y, cam_x, cam_y):
    """Which world point sits under a screen point: add the camera."""
    return screen_x + cam_x, screen_y + cam_y

Try it with numbers. A coin at world (1000, 250) with the camera at (300, 0) appears at screen (700, 250). A click at screen (700, 250) with the same camera lands on world (1000, 250): the two functions undo each other.

graph LR A["World position<br/>(where it IS)"] -->|"minus camera"| B["Screen position<br/>(where it is DRAWN)"] B -->|"plus camera"| A C["Camera position<br/>(world point at the<br/>window's top-left)"] -.-> A C -.-> B

Explore it below. Point at the canvas (or tap it) to read one spot in all three systems, press the arrows to move the camera, and click to drop markers. Markers are stored in world coordinates, so they stay on their spot in the world while the view pans. The mini-map in the corner shows the whole world and the camera's view of it.

Camera:

๐Ÿ”ฎ Predict, then check

Put a marker on the well at world (520, 260). Now press โ†’ twice, so the camera moves to x = 200. Where will the well be on screen? Work it out with world_to_screen, then point at it to check. (Answer: screen (320, 260).)

โœ… Growth Mindset: Plus or Minus? Everyone Checks

Whether to add or subtract the camera trips up beginners and veterans alike, and there is no shame in checking every time. Don't try to memorize it; test it. Put a marker exactly under the mouse and pan: if it slides along with the view, one sign is backwards. Plug in one easy set of numbers, like a camera at (100, 0), and see if the answer makes sense. The habit of checking with a tiny example will save you in every math-heavy lesson ahead.

๐ŸŽฅ A Camera That Follows the Player

A follow camera is one idea: put the player in the middle of the window. If the window is 800 wide, the camera's left edge should be 400 pixels left of the player's center. Then clamp the camera so it never shows past the edges of the world:

import random

import pygame

WIDTH, HEIGHT = 800, 600
WORLD_W, WORLD_H = 2400, 1800
SIZE = 40
SPEED = 350                         # pixels per second


def world_to_screen(world_x, world_y, cam_x, cam_y):
    return world_x - cam_x, world_y - cam_y


pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Follow Camera")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 28)

rng = random.Random(7)              # same "random" trees every run
trees = [(rng.randint(0, WORLD_W), rng.randint(0, WORLD_H)) for _ in range(120)]
player_x, player_y = WORLD_W / 2, WORLD_H / 2     # WORLD position of the player's top-left

running = True
while running:
    dt = clock.tick(60) / 1000
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Move the player in WORLD space (held keys, as in Keyboard, Mouse & Gamepad).
    keys = pygame.key.get_pressed()
    dx = (keys[pygame.K_RIGHT] or keys[pygame.K_d]) - (keys[pygame.K_LEFT] or keys[pygame.K_a])
    dy = (keys[pygame.K_DOWN] or keys[pygame.K_s]) - (keys[pygame.K_UP] or keys[pygame.K_w])
    if dx != 0 and dy != 0:
        dx *= 0.7071
        dy *= 0.7071
    player_x = max(0, min(WORLD_W - SIZE, player_x + dx * SPEED * dt))
    player_y = max(0, min(WORLD_H - SIZE, player_y + dy * SPEED * dt))

    # Camera: center the player, then keep the view inside the world.
    cam_x = player_x + SIZE / 2 - WIDTH / 2
    cam_y = player_y + SIZE / 2 - HEIGHT / 2
    cam_x = max(0, min(WORLD_W - WIDTH, cam_x))
    cam_y = max(0, min(WORLD_H - HEIGHT, cam_y))

    screen.fill((34, 90, 50))
    left, top = world_to_screen(0, 0, cam_x, cam_y)
    pygame.draw.rect(screen, (250, 204, 21), (left, top, WORLD_W, WORLD_H), 6)   # world border
    for tree_x, tree_y in trees:
        pygame.draw.circle(screen, (20, 60, 30), world_to_screen(tree_x, tree_y, cam_x, cam_y), 18)
    px, py = world_to_screen(player_x, player_y, cam_x, cam_y)
    pygame.draw.rect(screen, (255, 107, 107), (px, py, SIZE, SIZE))

    info = f"player world ({player_x:.0f}, {player_y:.0f})   screen ({px:.0f}, {py:.0f})"
    screen.blit(font.render(info, True, (240, 240, 240), (20, 20, 28)), (10, 10))
    pygame.display.flip()

pygame.quit()

Walk toward a corner and watch the info line. In the middle of the world the player's screen position stays at about (380, 280) while its world position changes: the world scrolls, not the player. Near an edge the camera stops (the clamp), and now the player walks toward the edge of the window instead. Every object in the world is drawn through the same world_to_screen, which is what keeps everything lined up.

  • dx = (right) - (left) is a compact form of the four ifs from the last lesson: True counts as 1 and False as 0, so it gives -1, 0 or +1.
  • rng = random.Random(7) makes a random-number generator of its own, started from the seed 7. It has the same methods as the random module (rng.randint(...), rng.choice(...), and rng.uniform(a, b) for a random float between a and b), and like random.seed(7) it gives the same "random" trees every run.
  • The world border is drawn every frame, even when most of it is off screen. pygame draws only the part that falls inside the window and ignores the rest, so you don't have to check which corners are visible first.
  • Positions are floats. The camera and player move a fraction of a pixel per frame; pygame-ce's drawing functions accept float positions, so nothing gets stuck.

๐Ÿ–ฑ๏ธ Clicking in the World

Mouse positions are always screen coordinates: event.pos and pygame.mouse.get_pos() know nothing about your camera. Before you use a click to place a building, pick up a coin or choose a destination, convert it to the world with screen_to_world:

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        running = False
    elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
        world_x, world_y = screen_to_world(event.pos[0], event.pos[1], cam_x, cam_y)
        flags.append((world_x, world_y))      # store WORLD positions; draw with world_to_screen

Store things in world coordinates and convert to screen coordinates only at the moment you draw. If you store screen positions instead, every object you placed slides along with the camera, the bug in the table below.

SymptomLikely causeFix
Placed objects move with the cameraStored the click's screen positionConvert with screen_to_world before storing
Everything scrolls the wrong wayAdded the camera when drawingWorld to screen subtracts the camera
Clicks land far from the cursor after panningSubtracted the camera from the clickScreen to world adds the camera
Black strip at the world's edgeCamera not clampedClamp to 0 โ€ฆ WORLD_W - WIDTH
Something is one pixel off at the right edgeUsed WIDTH where WIDTH - 1 or WIDTH - SIZE was neededRemember counting starts at 0

๐Ÿ’ก Why this matters

Scrolling levels, strategy maps, mini-maps and "click to move" all rest on this one idea: things live in the world, and the screen is a view of it. Keep the two apart and a camera costs you two lines of code. Mix them up and every feature you add fights the camera.

๐Ÿ‹๏ธ Practice Exercise: Pan, Click, Convert

Objective: pan a camera over a world bigger than the window, drop markers that stay put in the world, and show the mouse position in screen, math-style and world coordinates.

Time: about 35 minutes. Starter file: pan_click_convert_starter.py (your instructor has it). It already draws a world grid, a yellow world border, five markers and a HUD, but the camera never moves and the conversion functions are unfinished. Its numbered comments match the steps below.

  1. Run the starter. You see part of the world; the HUD shows four lines of coordinates. (โ‰ˆ 2 min)
  2. After the event loop, read pygame.key.get_pressed(), set dx and dy to -1, 0 or +1 from the arrows or WASD, and add dx * CAMERA_SPEED * dt to cam_x (and the same for y), passing the result through clamp_camera(). Run it: the HUD's Camera line changes, but the world doesn't move yet. (โ‰ˆ 7 min)
  3. Finish world_to_screen(): subtract the camera. Now the grid, border and markers scroll. (โ‰ˆ 4 min)
  4. Finish screen_to_world(): add the camera. Then, in the event loop, on a left click convert event.pos and append the result to markers. Click, pan away and pan back: the green marker must still be where you clicked. (โ‰ˆ 8 min)
  5. Finish clamp_camera() so the view never goes past the world border in any direction. (โ‰ˆ 5 min)
  6. Finish screen_to_math(). Point at the center of the window: the Math line should read about (0, 0); at the top-left corner about (-400, 300). (โ‰ˆ 5 min)
  7. Pan to the bottom-right corner of the world and click there. Compare the Screen and World lines: why is the world position so much bigger? (โ‰ˆ 4 min)

You are done when:

  • the arrow keys (or WASD) pan smoothly, and the camera stops at every edge of the world;
  • a new marker appears exactly under the cursor and stays on the same spot in the world as you pan;
  • the Math line reads (0, 0) at the window's center, with positive y above it;
  • closing the window prints lines like Markers: 8 and Last marker (world): (1730, 1210).
๐Ÿ’ก Hint

Test each conversion with one easy example before you run the game. With the camera at (300, 0), world_to_screen(1000, 0, 300, 0) should give (700, 0), and screen_to_world(700, 0, 300, 0) should give (1000, 0). For the clamp, the largest camera x is WORLD_W - WIDTH, because at that point the window's right edge sits exactly on the world's right edge. If markers slide when you pan, the click was stored in screen coordinates.

โœ… Example Solution

If your instructor hands you the lab file, you will see a few extra lines marked lab runtime near the top and and frame_budget() in the loop. They let the instructor's checker run the program automatically for a fixed number of frames; when you run it yourself they do nothing. You never need to write them.

"""Pan, Click, Convert: Intro Lesson 4 practice exercise (solution).

Hold the arrow keys (or WASD) to pan a camera over a world that is bigger
than the window. Click to drop a marker: the click is converted from screen
to world coordinates, so the marker stays put in the world while you pan.
The HUD shows the mouse in screen, math and world coordinates.
Close the window to quit.
"""
import pygame


WIDTH, HEIGHT = 800, 600           # the window (screen space)
WORLD_W, WORLD_H = 2000, 1500      # the whole world (world space)
FPS = 60
CAMERA_SPEED = 400                 # pixels per second
GRID_STEP = 200                    # a world grid line every 200 pixels
BG_COLOR = (32, 34, 44)
GRID_COLOR = (58, 62, 78)
BORDER_COLOR = (250, 204, 21)
MARKER_COLOR = (255, 107, 107)
NEW_MARKER_COLOR = (74, 222, 128)
TEXT_COLOR = (235, 235, 235)
LABEL_BG = (20, 20, 28)


def world_to_screen(world_x, world_y, cam_x, cam_y):
    """Where a world point appears in the window: subtract the camera."""
    return world_x - cam_x, world_y - cam_y


def screen_to_world(screen_x, screen_y, cam_x, cam_y):
    """Which world point sits under a screen point: add the camera."""
    return screen_x + cam_x, screen_y + cam_y


def screen_to_math(screen_x, screen_y):
    """Math-class coordinates: origin at the window's center, y pointing up."""
    return screen_x - WIDTH / 2, HEIGHT / 2 - screen_y


def clamp_camera(cam_x, cam_y):
    """Keep the camera's view inside the world."""
    cam_x = max(0, min(WORLD_W - WIDTH, cam_x))
    cam_y = max(0, min(WORLD_H - HEIGHT, cam_y))
    return cam_x, cam_y


def draw_world(screen, cam_x, cam_y, markers, first_new):
    """Draw the grid, the world's border and every marker, converted to the screen."""
    for gx in range(0, WORLD_W + 1, GRID_STEP):
        sx, _ = world_to_screen(gx, 0, cam_x, cam_y)
        pygame.draw.line(screen, GRID_COLOR, (sx, 0), (sx, HEIGHT))
    for gy in range(0, WORLD_H + 1, GRID_STEP):
        _, sy = world_to_screen(0, gy, cam_x, cam_y)
        pygame.draw.line(screen, GRID_COLOR, (0, sy), (WIDTH, sy))

    # The border is drawn every frame; pygame clips whatever falls outside the window.
    left, top = world_to_screen(0, 0, cam_x, cam_y)
    pygame.draw.rect(screen, BORDER_COLOR, (left, top, WORLD_W, WORLD_H), 4)

    for i, (wx, wy) in enumerate(markers):
        color = NEW_MARKER_COLOR if i >= first_new else MARKER_COLOR
        pygame.draw.circle(screen, color, world_to_screen(wx, wy, cam_x, cam_y), 8)


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Pan, Click, Convert")
    clock = pygame.time.Clock()
    font = pygame.font.Font(None, 26)

    cam_x, cam_y = 0.0, 0.0
    markers = [(100, 80), (550, 320), (1200, 450), (1800, 1300), (300, 1100)]
    first_new = len(markers)             # markers from here on were clicked

    running = True
    while running:
        dt = clock.tick(FPS) / 1000

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos
                markers.append(screen_to_world(mx, my, cam_x, cam_y))

        keys = pygame.key.get_pressed()
        dx = dy = 0
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            dx -= 1
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            dx += 1
        if keys[pygame.K_UP] or keys[pygame.K_w]:
            dy -= 1
        if keys[pygame.K_DOWN] or keys[pygame.K_s]:
            dy += 1
        cam_x, cam_y = clamp_camera(cam_x + dx * CAMERA_SPEED * dt,
                                    cam_y + dy * CAMERA_SPEED * dt)

        screen.fill(BG_COLOR)
        draw_world(screen, cam_x, cam_y, markers, first_new)

        mx, my = pygame.mouse.get_pos()
        math_x, math_y = screen_to_math(mx, my)
        world_x, world_y = screen_to_world(mx, my, cam_x, cam_y)
        lines = [
            f"Screen: ({mx}, {my})",
            f"Math:   ({math_x:.0f}, {math_y:.0f})",
            f"World:  ({world_x:.0f}, {world_y:.0f})",
            f"Camera: ({cam_x:.0f}, {cam_y:.0f})",
        ]
        for i, text in enumerate(lines):
            screen.blit(font.render(text, True, TEXT_COLOR, LABEL_BG), (10, 10 + i * 24))
        pygame.display.flip()

    pygame.quit()
    print(f"Markers: {len(markers)}")
    wx, wy = markers[-1]
    print(f"Last marker (world): ({wx:.0f}, {wy:.0f})")


if __name__ == "__main__":
    main()

๐Ÿ““ Learning Journal

Take five minutes to write in your learning journal (a notebook or a plain text file works). Jot down:

  • Key concepts you learned today
  • Techniques that clicked (and the ones that haven't, yet)
  • Questions or confusion to bring to the next session
  • Ideas to try in your own game
  • Progress and feelings: how did this lesson go for you?

โœ๏ธ This lesson's prompts:

  1. Explain world coordinates and screen coordinates to a friend using a real-world comparison of your own (not the poster and the phone).
  2. Write down, from memory, which conversion adds the camera and which subtracts it. Then explain why with one small example.
  3. Think of a game with a big map. What would break if it stored clicked positions in screen coordinates?

๐Ÿ“ Summary

You met three ways to name the same spot. Screen coordinates count pixels from the window's top-left, y down, with the last pixel at WIDTH - 1. Math-style coordinates put the origin in the middle with y up, and are one subtraction and one flip away. World coordinates say where things really are in a level bigger than the window; a camera position turns them into screen coordinates by subtraction, and turns mouse clicks back into world coordinates by addition. With that, you built a camera that follows the player and never shows past the edge of the world.

๐ŸŽ“ Key Takeaways

  • Screen pixels run from 0 to WIDTH - 1; rect.right is one past the last pixel.
  • Math-style from screen: (x - WIDTH / 2, HEIGHT / 2 - y). Only y flips.
  • World to screen subtracts the camera; screen to world adds it.
  • Store positions in world coordinates; convert to screen coordinates only when drawing.
  • A follow camera centers the player, then clamps to 0 โ€ฆ WORLD_W - WIDTH (and the same for y).

๐Ÿ”ญ Looking Ahead

You have been juggling x and y as separate numbers, and writing everything twice. In the next lesson, Vectors with pygame.math.Vector2, you bundle each position and velocity into a single object, so movement, direction and distance each take one line.

โ“ Common Questions

Why is the camera position the window's top-left corner and not its center?

Because pygame draws from the top-left, so "subtract the camera" then gives screen coordinates directly. You can still aim the camera at a center point: subtract half the window size when you set it, as the follow camera does.

Do I need math-style coordinates in my game?

Often not. Most 2D games work entirely in screen and world coordinates, both with y pointing down. Math-style coordinates are handy for plotting, for symmetric layouts around a center point, and for formulas copied from a math or physics book that assume y points up.

Should I skip drawing things that are off screen?

pygame already ignores the parts of a drawing that fall outside the window, so your picture is correct either way. With thousands of objects, skipping the far-away ones can save time; measure your own game before deciding it is worth the extra code.

Can the camera zoom?

Yes. Multiply after subtracting: screen_x = (world_x - cam_x) * zoom, and divide when going back: world_x = screen_x / zoom + cam_x. Sizes (radii, widths) must be multiplied by zoom too. Try it in Going Further.

My follow camera jitters by a pixel when the player moves slowly.

Keep both the player and the camera positions as floats, and compute the player's screen position from them each frame, as the example does. Rounding one of them to a whole number and not the other makes them disagree by a pixel now and then.

๐ŸŽฏ Quick Quiz

Question 1: Your window is 800 pixels wide. What is the largest x at which a single pixel is still on screen?

Question 2: A coin sits at world (1000, 250) and the camera is at (300, 0). Where is the coin drawn on screen?

Question 3: In an 800 ร— 600 window, what are the math-style coordinates of the screen point (400, 100)?

Question 4: The camera is at (500, 300) and the player clicks at screen (50, 20). Which world point did they click?

Question 5: Why does the follow camera clamp cam_x to between 0 and WORLD_W - WIDTH?

๐ŸŒŸ Going Further

  • Mini-map: draw a small copy of the world in a corner of the Pan, Click, Convert window at one-tenth scale (multiply world positions by 0.1), with a rectangle showing what the camera sees, like the demo's mini-map.
  • Zoom: add a zoom number changed by the mouse wheel (from Keyboard, Mouse & Gamepad). Use (world_x - cam_x) * zoom to draw and screen_x / zoom + cam_x for clicks, and scale the marker radius too.
  • Edge scrolling: pan the camera when the mouse is within 20 pixels of a window edge, as many strategy games do. Use pygame.mouse.get_pos() (state) every frame.
  • Read the docs: the pygame-ce page for pygame.Rect lists attributes such as right, bottom and center. Find two you haven't used yet.
  • Coming up in Game Dev II: Intermediate: Tile Maps and Cameras build smooth follow cameras, dead zones and isometric grids on the same world-to-screen idea.