Skip to main content

Lesson 5: Vectors with pygame.math.Vector2

  • Module 3: Vectors & Angles
  • Lesson 5 of 14
  • ⏱️ About 2 h (instruction + lab)

By the end of this lesson you will move players, enemies and bullets with pygame.Vector2, so diagonal movement is fair, chasers keep a steady speed and slow objects never get stuck. Vectors are the everyday language of game movement, and your first small class will hold one.

🎯 Learning Objectives

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

  • Create pygame.Vector2 objects and add, subtract and scale them to move things.
  • Explain why a direction must be normalized before you multiply it by a speed, and guard against the zero vector.
  • Use subtraction and the dot product to find the direction to a target and whether it is in front or behind.
  • Build a game object as a small class that keeps a float Vector2 position and a Rect that follows it.
  • Debug the classic vector bugs: fast diagonals, a crash on normalize() and a sprite that never moves.

Project: Predator and Prey, a chase where three hunters see you only when you are in front of them.

In This Lesson

🏹 What Is a Vector?

Picture an archer's arrow lying on a map. It has a length (how far) and it points somewhere (which way). You can also describe the same arrow as "so many steps right, so many steps down": its x and y components. That is all a 2D vector is: a pair of numbers (x, y) that you can read either as a point or as an arrow.

Games use that one idea for three different jobs:

  • Position: where something is, measured from the top-left corner of the screen, such as (400, 300).
  • Displacement: how to get from one place to another, such as "120 right and 80 up".
  • Velocity: how far something moves each second, such as (240, 0) for 240 pixels per second to the right.
A two-dimensional vector arrow starts at a point and points up and to the right. Dashed lines show its horizontal (delta x) and vertical (delta y) components, and the angle theta sits between the arrow and the x-axis.
A vector has a length (magnitude) and a direction, and you store it as two components, Δx and Δy. This figure is drawn math-class style with y pointing up; on a pygame screen, as you saw in the Coordinate Systems lesson, y grows downward, so "up" is a negative y.

💡 Why this matters

Until now you moved things with separate x and y variables and wrote every calculation twice. A vector keeps the pair together, so "move by velocity times dt" becomes one line, and questions like "which way is the player?" and "how far away is it?" each become one method call.

🧮 Meet pygame.Vector2

pygame-ce ships a ready-made vector type, pygame.Vector2 (the same class is also available as pygame.math.Vector2). It stores two floats, x and y, and understands arithmetic. Run this short program; it opens no window, it just prints:

import pygame

position = pygame.Vector2(100, 200)     # x = 100, y = 200
velocity = pygame.Vector2(3, -4)        # 3 right, 4 UP (y grows downward)

print(position.x, position.y)           # 100.0 200.0
print(position + velocity)              # [103, 196]
print(position - velocity)              # [97, 204]
print(velocity * 2)                     # [6, -8]
print(velocity.length())                # 5.0
print(position + (10, 0))               # tuples work too: [110, 200]

What each operation means in a game:

CodeMeaningTypical use
a + bPut the arrows tip to tailMove a position by a displacement; combine a push and a velocity
b - aThe arrow from a to b"Which way, and how far, is the player from this enemy?"
v * 2, v / 2Same direction, longer or shorterTurn a direction into a velocity; half speed in water
-vSame length, opposite wayRun away instead of chasing
v.length()How long the arrow isSpeed, distance
Two panels. The addition panel shows vector a from the origin, vector b drawn from the tip of a, and the resultant a plus b from the origin to the tip of b. The subtraction panel shows a player point and an enemy point with an arrow from the player to the enemy labelled enemy minus player.
Addition is tip to tail: the result runs from the start of the first arrow to the end of the second. Subtraction answers "where is it, relative to me?": enemy - player is the arrow pointing from the player to the enemy.

Try it yourself. Drag the tips of the blue arrow a and the orange arrow b, then switch between the operations. The readouts use screen coordinates, so pulling a tip upward makes its y negative.

Vector addition drawn tip to tail, and vector subtraction drawn as the arrow from a player to an enemy.
On a larger screen this is an interactive playground. The picture shows the same two ideas: add tip to tail, and subtract to get the arrow from one point to another.

One trap: = does not copy a vector

A Vector2 can be changed in place (pos.x += 5, pos += velocity). That is handy, but it means two names can share one vector without you noticing:

import pygame

start = pygame.Vector2(400, 300)
player = start               # NOT a copy: both names point at the same vector
player.x += 50
print(start)                 # [450, 300]  start moved too!

player = start.copy()        # a separate vector; pygame.Vector2(start) also works
player.x += 50
print(start, player)         # [450, 300] [500, 300]

Whenever you want a vector to remember a starting point (a respawn spot, the middle of the screen), store a .copy().

📏 Length and Normalize: Fair Diagonals

Suppose you move a player with the arrow keys by adding to x and y separately, using pygame.key.get_pressed() from the Keyboard, Mouse & Gamepad lesson. Hold Right and Down together and something unfair happens: the player moves right at full speed and down at full speed, so the diagonal step is longer than a straight one.

Keys heldDirection vectorlength()Speed if you multiply by 200
Right(1, 0)1.0200 px/s
Right + Down(1, 1)1.414…about 283 px/s, 41% too fast
Right + Down, normalized(0.707, 0.707)1.0200 px/s

Normalizing keeps a vector's direction but sets its length to exactly 1. A length-1 vector is called a unit vector, and it is the perfect "which way" value: multiply it by any speed and you get a velocity of exactly that speed.

keys = pygame.key.get_pressed()
direction = pygame.Vector2(keys[pygame.K_RIGHT] - keys[pygame.K_LEFT],
                           keys[pygame.K_DOWN] - keys[pygame.K_UP])
if direction.length_squared() > 0:     # a zero vector cannot be normalized
    direction = direction.normalize()
pos += direction * SPEED * dt          # SPEED in pixels per second

Why the if? When no key is held, direction is (0, 0). A zero-length arrow has no direction, so normalize() raises ValueError: Can't normalize Vector of length zero. Checking length_squared() > 0 first avoids the crash. (Use the length check, not if direction:. A zero vector does count as false, but the explicit check says what you mean and reads the same everywhere you need it.)

✅ Growth Mindset: "Can't normalize Vector of length zero" Is a Rite of Passage

Nearly everyone who learns vectors crashes on this error, usually the first time the player stands still or an enemy reaches its target exactly. It does not mean you misunderstand vectors; it means your code met the one input that has no direction. Read the traceback's last line, find the normalize() it points to, and ask "could this vector be zero here?" Adding that guard is a habit you are building, not a mistake you should have avoided.

🎯 Direction and Distance to a Target

Every chaser, homing missile and "walk to where I clicked" needs the same two facts: which way is the target, and how far? Subtraction answers both at once.

to_target = target - enemy_pos            # the arrow from the enemy to the target
distance = to_target.length()             # same value as enemy_pos.distance_to(target)
if distance > 0:
    step = min(ENEMY_SPEED * dt, distance)        # never step past the target
    enemy_pos += to_target.normalize() * step

The recipe is subtract, normalize, then scale. If you skip the normalize and write enemy_pos += to_target * 0.05, the step is proportional to the distance: far-away enemies rocket toward you and close ones crawl. Normalizing throws the distance away and keeps only the direction, so the speed is whatever you choose.

The min(…, distance) stops the enemy from overshooting and jittering back and forth across the target. pygame-ce has a built-in for exactly this pattern: enemy_pos.move_towards_ip(target, ENEMY_SPEED * dt) moves the vector in place by at most that many pixels. Here it is in a complete program where a dot follows your mouse at a steady 250 pixels per second:

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Follow the Mouse")
clock = pygame.time.Clock()

SPEED = 250                              # pixels per second
dot = pygame.Vector2(400, 300)

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

    target = pygame.Vector2(pygame.mouse.get_pos())
    dot.move_towards_ip(target, SPEED * dt)       # at most SPEED * dt pixels, no overshoot

    screen.fill((17, 24, 39))
    pygame.draw.line(screen, (75, 85, 99), dot, target)
    pygame.draw.circle(screen, (96, 165, 250), dot, 14)
    pygame.display.flip()

pygame.quit()

Comparing distances

To ask "is the enemy within 50 pixels?", compare a distance with a number:

if enemy_pos.distance_to(player_pos) < 50:
    print("Too close!")

# Same answer without the square root: compare squared distance with the squared radius.
if enemy_pos.distance_squared_to(player_pos) < 50 * 50:
    print("Too close!")

Both lines give the same answer, because for positive numbers, a smaller distance always has a smaller square. You will see the squared version in a lot of game code because it skips the square root. If you use it, remember to square the radius too; comparing a squared distance with plain 50 makes the radius about 7 pixels. For a handful of objects, pick whichever reads more clearly to you.

Subtraction also gives you the camera trick from the Coordinate Systems lesson in one line: screen_pos = world_pos - camera, where both are vectors.

👀 The Dot Product: In Front or Behind?

A guard should only notice you when you are in front of it. You could work out angles, but there is a simpler tool: the dot product. For two vectors it is a.x * b.x + a.y * b.y, one number, and pygame-ce computes it with a.dot(b).

Two vectors a and b share an origin with angle theta between them. A dashed line drops from the tip of a onto b, and the segment it marks is the projection of a onto b. A legend says the dot product is positive under ninety degrees, zero at ninety degrees, and negative over ninety degrees.
The dot product measures how much two vectors agree in direction. Games mostly use its sign: positive means roughly the same way, zero means exactly sideways (90°), negative means roughly opposite ways.
def is_in_front(observer_pos, facing, target_pos):
    """facing is a unit vector pointing where the observer looks."""
    to_target = target_pos - observer_pos
    return facing.dot(to_target) > 0     # positive: the target is in the front half

Notice that to_target is not normalized. Normalizing only divides by a positive length, and dividing by a positive number never changes a sign, so the check gives the same answer either way. In the playground above, choose a.dot(b) and swing b around a: the number turns negative the moment b passes 90° away.

facingto_targetfacing.dot(to_target)Verdict
(1, 0) looking right(200, 50)200In front
(1, 0)(0, 120)0Exactly to the side
(1, 0)(-50, 20)−50Behind

🧱 Float Positions and Your First Class

You have drawn things with pygame.Rect, and it is tempting to store a player's position in its rect and write rect.x += speed * dt. There is a catch: a Rect holds whole numbers only. Every time you store a fraction, it is cut off toward zero. At 60 FPS, something moving 20 pixels per second should move about 0.33 pixels a frame, and a rect cuts every one of those steps to nothing. Run this and watch:

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 200))
pygame.display.set_caption("Rect vs Vector2")
clock = pygame.time.Clock()
SPEED = 20                                   # a slow 20 pixels per second

rect_only = pygame.Rect(20, 40, 40, 40)      # position stored in the Rect: whole numbers
pos = pygame.Vector2(40, 140)                # float position...
rect_follow = pygame.Rect(0, 0, 40, 40)      # ...and a Rect that follows it
rect_follow.center = pos

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

    rect_only.x += SPEED * dt                # about 0.33 px a frame, cut back to a whole number
    pos.x += SPEED * dt                      # about 0.33 px a frame, and it adds up
    rect_follow.center = pos                 # the Rect follows the float position

    screen.fill((17, 24, 39))
    pygame.draw.rect(screen, (248, 113, 113), rect_only)
    pygame.draw.rect(screen, (74, 222, 128), rect_follow)
    pygame.display.flip()

pygame.quit()

The red square never moves. (Moving left is broken the other way: cutting toward zero turns each −0.33 step into a full −1, so it moves about three times too fast.) The green square works because of the pattern this course uses from now on:

📌 The float-position pattern

  1. Keep the real position in a Vector2 (floats).
  2. Move the vector: pos += velocity * dt.
  3. Copy it into the rect every frame: rect.center = pos. The rect is only for drawing (and, later, collisions); the vector is the truth.

A player now has a position, a velocity, a rect and some code that updates and draws them. Keeping all of that in loose variables gets messy the moment you want three enemies. This is where a class helps: it bundles one object's data with the functions that work on it.

Here is the float-position pattern wrapped in a class. Each Mover keeps its own vector and rect, and the game loop just calls update and draw on each one:

import pygame

WIDTH, HEIGHT = 800, 400


class Mover:
    """A square with a float position that bounces between the side walls."""

    def __init__(self, x, y, speed, color):
        self.pos = pygame.Vector2(x, y)          # the real position (floats)
        self.vel = pygame.Vector2(speed, 0)      # pixels per second
        self.color = color
        self.rect = pygame.Rect(0, 0, 40, 40)    # for drawing only
        self.rect.center = self.pos

    def update(self, dt):
        self.pos += self.vel * dt
        if self.pos.x < 20 or self.pos.x > WIDTH - 20:
            self.pos.x = max(20, min(WIDTH - 20, self.pos.x))   # back inside
            self.vel = -self.vel                                # turn around
        self.rect.center = self.pos              # the rect follows, every frame

    def draw(self, screen):
        pygame.draw.rect(screen, self.color, self.rect, border_radius=6)


pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Movers")
clock = pygame.time.Clock()
movers = [Mover(100, 100, 30, (248, 113, 113)),
          Mover(200, 200, 150, (250, 204, 21)),
          Mover(300, 300, 400, (74, 222, 128))]

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

    for mover in movers:
        mover.update(dt)

    screen.fill((17, 24, 39))
    for mover in movers:
        mover.draw(screen)
    pygame.display.flip()

pygame.quit()

Even the slow red square at 30 pixels per second glides smoothly, because its position is a float.

✅ Growth Mindset: Classes Feel Strange at First, and That Is Normal

If self looks like noise right now, you are in the same place every programmer was the week they met classes. You do not need to understand everything about them yet. Use the pattern: attributes are set with self. in __init__, methods take self first, and each object keeps its own data. After you have written two or three classes in this course, re-read the sidebar; it will read very differently.

🔧 How It Works: Build Your Own Vector

This part is optional. You will always use pygame.Vector2 in this course, but seeing a vector class written out takes the magic away.

Show the hand-written vector class

Python lets a class decide what operators mean. When you write a + b, Python calls a.__add__(b); -a calls a.__neg__(); print(a) uses __repr__. These double-underscore methods are called special methods (or "dunder" methods). Here is a small vector that supports the operations from this lesson:

import math


class Vec:
    """A tiny 2D vector, to show what pygame.Vector2 does for you."""

    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y

    def __repr__(self):                 # how print() shows it
        return f"Vec({self.x}, {self.y})"

    def __add__(self, other):           # a + b
        return Vec(self.x + other.x, self.y + other.y)

    def __sub__(self, other):           # a - b
        return Vec(self.x - other.x, self.y - other.y)

    def __mul__(self, number):          # a * 2
        return Vec(self.x * number, self.y * number)

    def __rmul__(self, number):         # 2 * a
        return self * number

    def __neg__(self):                  # -a
        return Vec(-self.x, -self.y)

    def length(self):
        return math.sqrt(self.x * self.x + self.y * self.y)

    def normalize(self):
        size = self.length()
        if size == 0:
            raise ValueError("can't normalize a zero vector")   # the same rule as pygame
        return Vec(self.x / size, self.y / size)

    def dot(self, other):
        return self.x * other.x + self.y * other.y


a = Vec(3, 4)
b = Vec(1, 0)
print(a + b, a - b, 2 * a, -a)     # Vec(4, 4) Vec(2, 4) Vec(6, 8) Vec(-3, -4)
print(a.length(), a.normalize())   # 5.0 Vec(0.6, 0.8)
print(a.dot(b))                    # 3

pygame.Vector2 does all of this and more, written in C, and it also accepts plain tuples wherever a vector is expected. For the full treatment of special methods, see Intermediate Python, Lesson 3: Magic Methods & Operator Overloading.

🏋️ Practice Exercise: Predator and Prey

Objective: build a chase where you steer the prey with the keyboard and three hunters chase you at a steady speed, but only while you are in front of them.

Time: about 40 minutes. Starter file: predator_prey_starter.py (your instructor has it). It opens the window, moves the prey (too fast on diagonals) and lets the predators wander. Its numbered comments match the steps below. A 10-minute warm-up, slow_mover_starter.py, practices the float-position pattern first.

  1. Run the starter. The blue prey is stuck in the top-left corner even though you are moving it. Make its rect follow the float position with prey_rect.center = prey_pos. (≈ 3 min)
  2. In read_direction(), normalize the direction when it is not a zero vector. Hold two keys and check that diagonals are no faster. (≈ 5 min)
  3. In Predator.can_see(), subtract to get the vector from the predator to the target and return whether its dot product with self.facing is greater than 0. (≈ 8 min)
  4. In Predator.update(), when the prey is seen: point self.facing at the prey (subtract, guard, normalize), then move self.pos along it at CHASE_SPEED pixels per second. (≈ 12 min)
  5. In caught(), return whether the two positions are closer than CATCH_RADIUS. (≈ 5 min)
  6. Play. Stand still in front of a predator, then behind one. Change CHASE_SPEED and PREY_SPEED until the chase feels fair. (≈ 7 min)

You are done when:

  • the prey moves at the same speed straight and diagonally, and the game never crashes when you stand still;
  • a predator turns red and chases when you are in front of its facing line, and yellow when you are behind it;
  • a chasing predator moves at the same speed whether you are near or far;
  • getting caught adds one to the counter, and closing the window prints a line like Caught 3 times.
💡 Hint

Every step is one of three recipes from this lesson. Direction to a target: target - self.pos. A pure direction: .normalize(), but only after length_squared() > 0. Movement: pos += direction * speed * dt. If the predators rocket toward you from far away and crawl up close, you forgot to normalize. If every predator is yellow all the time, check the sign in can_see().

✅ 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; when you run it yourself they do nothing.

"""Predator and Prey: Intro Lesson 5 practice exercise (solution).

Move the blue prey with WASD or the arrow keys. Three predators chase it,
but only while the prey is in front of them (a dot-product check); the
rest of the time they wander. Close the window to quit.
"""
import random

import pygame


WIDTH, HEIGHT = 800, 600
PREY_SPEED = 220          # pixels per second
CHASE_SPEED = 150         # pixels per second
WANDER_SPEED = 60         # pixels per second
CATCH_RADIUS = 26         # pixels
BG_COLOR = (20, 25, 35)
PREY_COLOR = (60, 180, 240)
CHASE_COLOR = (230, 70, 70)
WANDER_COLOR = (230, 200, 70)
TEXT_COLOR = (235, 235, 235)


def read_direction(keys):
    """Turn the held keys into a direction of length 1 (or a zero vector)."""
    direction = pygame.Vector2(0, 0)
    if keys[pygame.K_a] or keys[pygame.K_LEFT]:
        direction.x -= 1
    if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
        direction.x += 1
    if keys[pygame.K_w] or keys[pygame.K_UP]:
        direction.y -= 1
    if keys[pygame.K_s] or keys[pygame.K_DOWN]:
        direction.y += 1
    if direction.length_squared() > 0:     # a zero vector cannot be normalized
        direction = direction.normalize()  # diagonals are no faster than straight lines
    return direction


def keep_on_screen(pos):
    """Clamp a position to the window, in place."""
    pos.x = max(0, min(WIDTH, pos.x))
    pos.y = max(0, min(HEIGHT, pos.y))


class Predator:
    """A hunter that chases what it can see and wanders otherwise."""

    def __init__(self, x, y, rng):
        self.pos = pygame.Vector2(x, y)       # float position
        self.facing = pygame.Vector2(1, 0)    # always length 1
        self.rng = rng
        self.wander_time = 0.0                # seconds until a new wander direction
        self.sees_prey = False

    def can_see(self, target):
        """True when target is in front of us (the front half of a circle)."""
        to_target = target - self.pos
        return to_target.dot(self.facing) > 0

    def pick_wander_direction(self):
        direction = pygame.Vector2(self.rng.uniform(-1, 1), self.rng.uniform(-1, 1))
        if direction.length_squared() > 0:
            self.facing = direction.normalize()
        self.wander_time = self.rng.uniform(0.5, 1.5)

    def respawn(self):
        """Jump back to a random spot along the top edge of the window."""
        self.pos = pygame.Vector2(self.rng.uniform(0, WIDTH), 0)

    def update(self, prey_pos, dt):
        self.sees_prey = self.can_see(prey_pos)
        if self.sees_prey:
            to_prey = prey_pos - self.pos
            if to_prey.length_squared() > 0:
                self.facing = to_prey.normalize()   # direction only, no distance
            self.pos += self.facing * CHASE_SPEED * dt
        else:
            self.wander_time -= dt
            if self.wander_time <= 0:
                self.pick_wander_direction()
            self.pos += self.facing * WANDER_SPEED * dt
        keep_on_screen(self.pos)

    def draw(self, screen):
        color = CHASE_COLOR if self.sees_prey else WANDER_COLOR
        pygame.draw.circle(screen, color, self.pos, 12)
        pygame.draw.line(screen, color, self.pos, self.pos + self.facing * 30, 3)


def caught(predator_pos, prey_pos):
    """True when the two positions are closer than CATCH_RADIUS."""
    return predator_pos.distance_to(prey_pos) < CATCH_RADIUS


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Predator and Prey")
    clock = pygame.time.Clock()
    font = pygame.font.Font(None, 26)       # create fonts once, before the loop
    rng = random.Random(7)                  # seeded, so every run starts the same way

    start = pygame.Vector2(WIDTH / 2, HEIGHT / 2)
    prey_pos = start.copy()                 # copy(), or both names share one vector
    prey_rect = pygame.Rect(0, 0, 28, 28)   # for drawing only; prey_pos is the truth
    predators = [Predator(rng.uniform(60, WIDTH - 60), rng.uniform(60, HEIGHT - 60), rng)
                 for _ in range(3)]
    catches = 0

    running = True
    while running:
        dt = clock.tick(60) / 1000              # seconds since the last frame
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # Update
        direction = read_direction(pygame.key.get_pressed())
        prey_pos += direction * PREY_SPEED * dt
        keep_on_screen(prey_pos)
        prey_rect.center = prey_pos             # the rect follows the float position

        for predator in predators:
            predator.update(prey_pos, dt)
            if caught(predator.pos, prey_pos):
                catches += 1
                prey_pos = start.copy()         # prey back to the middle
                predator.respawn()              # hunter back to the top edge

        # Draw
        screen.fill(BG_COLOR)
        pygame.draw.rect(screen, PREY_COLOR, prey_rect, border_radius=6)
        for predator in predators:
            predator.draw(screen)
        hud = font.render(f"Caught: {catches}   Red = chasing, yellow = wandering",
                          True, TEXT_COLOR)
        screen.blit(hud, (10, 10))
        pygame.display.flip()

    pygame.quit()
    print(f"Caught {catches} times.")


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 "subtract, normalize, then scale" in your own words, as if you were telling a friend how a homing missile knows where to go.
  2. Think of a game you have played where an enemy only notices you from the front. How would you describe its vision check with a dot product?
  3. Write down one thing about classes that still feels fuzzy. You will come back to it after the Images & Sprite Classes lesson.

📝 Summary

You swapped pairs of loose x and y variables for pygame.Vector2, which adds, subtracts and scales in one line. Normalizing turned any direction into a unit vector, which fixed fast diagonals and made chasers move at a speed you choose. Subtraction gave you the arrow to a target, the dot product told you whether that target was in front, and a small class kept each game object's float position and its rect together.

🎓 Key Takeaways

  • A Vector2 is a pair of floats you can read as a point or as an arrow; +, - and * work on the pair at once.
  • target - pos is the arrow from pos to the target; its length is the distance.
  • Normalize, then multiply by speed, and guard with length_squared() > 0 so a zero vector never reaches normalize().
  • The sign of a dot product tells you "in front" (positive), "to the side" (zero) or "behind" (negative).
  • Keep positions in a Vector2 and set rect.center = pos every frame; a Rect alone drops the fractions.
  • A class bundles attributes (set with self. in __init__) and methods that take self first.

🔭 Looking Ahead

Vectors tell you which way. In the next lesson, Trigonometry for Games, you turn directions into angles and back again, so you can orbit, bob, aim a turret at the mouse and rotate a shape to face where it is going.

❓ Common Questions

Is pygame.Vector2 different from pygame.math.Vector2?

No. They are the same class under two names. This course writes pygame.Vector2 because it is shorter.

Why does print(v) show [103, 196] in square brackets?

That is just how pygame-ce prints a vector. It is still a Vector2, not a list. repr(v) shows Vector2(103, 196), and tuple(v) gives you a real tuple if a function needs one.

I passed whole numbers. Why is v.x a float?

A Vector2 always stores floats, so Vector2(100, 200).x is 100.0. That is exactly what you want for positions: small fractional steps add up instead of being thrown away.

Can I pass a Vector2 straight to drawing functions?

Yes. pygame.draw.circle(screen, color, pos, 10), pygame.draw.line(screen, color, a, b) and rect.center = pos all accept a Vector2 anywhere they accept an (x, y) tuple.

Do I have to make a class for every game object?

No. A single player can live in a few variables. Classes pay off as soon as you have several objects of the same kind, like the three predators in the exercise, because each one keeps its own data and the loop stays short.

Should I always use distance_squared_to?

Use whichever reads more clearly. Both give the same yes/no answer when you compare them with the right radius (squared for the squared version). If you ever wonder whether one is faster in your game, measure it; guessing about speed is a habit worth avoiding.

🎯 Quick Quiz

Question 1: What does pygame.Vector2(3, 4).length() return?

Question 2: Why do you normalize a movement direction before multiplying it by a speed?

Question 3: Which expression gives the arrow pointing from the enemy to the player?

Question 4: A guard faces Vector2(1, 0). The vector from the guard to you is Vector2(-50, 20). Where are you?

Question 5: A sprite should move right at 30 pixels per second. Its position lives only in a pygame.Rect and it runs at 60 FPS. What happens?

🌟 Going Further

  • Flee mode: give the predators a "scared" timer after they catch you. While it runs, they move along -facing instead, away from the prey.
  • Vision range: make predators see you only when you are in front and closer than 250 pixels. You now need both a dot product and a distance check.
  • Knockback: when you are caught, push the prey away from the predator: (prey_pos - predator.pos).normalize() * 120 pixels, instead of resetting to the middle. Guard the zero vector.
  • Read the docs: skim the pygame-ce pygame.math page. Try lerp() and reflect() in a tiny test program.
  • Coming up in Game Dev II: Intermediate: Velocity, Acceleration & Timesteps uses these vectors for speed that builds up and slows down smoothly.
  • Coming up in Game Dev III: Advanced: Steering & Flocking turns "subtract, normalize, then scale" into seek, flee and arrive behaviors, and whole flocks of birds.