pyodide: loading…

[challenge]Game Development with Pygame

Project: Tower Defense

# theory

what you're building

Tower Defense: enemies walk down a path, you place towers to shoot them. More complex than Brick Breakaway because you need waypoint pathfinding, targeting logic, and projectiles that track moving targets.


🎬 gerber's video breakdown (12 videos)

Video 1: Project Setup

  • Folder structure, main.py, settings.py
  • Basic window and game loop

Video 2: Game Settings

  • Constants file: screen size, colors
  • Tower cost, enemy speed, projectile damage
  • Starting gold and lives

Video 3: Game Loop Setup

  • Clock and dt for frame-rate independence
  • Event handling structure
  • Update/draw pattern

Video 4: Adding the Map

  • Loading tilemap from 2D list
  • 0 = grass (buildable), 1 = path
  • Drawing tiles with nested loops

Video 5: Creating the Level Class

  • Encapsulate map data
  • Tile drawing method
  • can_build_tower(x, y) check

Video 6: Adding Level Data and Cursors

  • Cursor highlight for tower placement
  • Click detection on tiles
  • Visual feedback before building

Video 7: Enemy Class Attributes

  • Waypoints list (path to follow)
  • current_waypoint index
  • Speed, HP, alive flag

Video 8: Enemy Methods and Spawning

  • move_toward_waypoint() using normalize
  • Spawn timer, enemy list
  • Check if reached end

Video 9: Tower Class and Spawning

  • Click to place (if gold and valid tile)
  • Range circle for targeting
  • Find nearest enemy in range

Video 10: Projectile Class

  • Velocity toward target at creation
  • math.atan2(dy, dx) for angle
  • Store reference to target sprite

Video 11: Firing Projectiles

  • Tower fire rate / cooldown timer
  • Spawn projectile toward nearest enemy
  • Track cooldown between shots

Video 12: Collision Detection

  • Projectile hits enemy (distance check)
  • Deal damage, kill when HP <= 0
  • Remove projectile after hit

project structure

TowerDefense/ ├── main.py ├── settings.py ├── level.py # map data + waypoints ├── enemy.py ├── tower.py └── projectile.py

key concepts

Waypoint Path Following

class Enemy:
    def __init__(self, waypoints):
        self.waypoints = waypoints
        self.wp_index = 0
        self.pos = pygame.math.Vector2(waypoints[0])
        self.speed = 80

    def update(self, dt):
        if self.wp_index >= len(self.waypoints):
            return True  # reached end, damage player

        target = pygame.math.Vector2(self.waypoints[self.wp_index])
        direction = target - self.pos
        dist = direction.length()

        if dist < self.speed * dt:
            self.pos = target
            self.wp_index += 1
        else:
            direction = direction.normalize()
            self.pos += direction * self.speed * dt
        return False

Tower Targeting with math.atan2

import math

def find_closest_enemy(tower_pos, enemies, tower_range):
    closest = None
    closest_dist = tower_range + 1
    for enemy in enemies:
        if not enemy.alive:
            continue
        dist = math.dist(tower_pos, enemy.pos)
        if dist <= tower_range and dist < closest_dist:
            closest = enemy
            closest_dist = dist
    return closest

def calc_projectile_velocity(start, target_pos, speed):
    dx = target_pos[0] - start[0]
    dy = target_pos[1] - start[1]
    angle = math.atan2(dy, dx)
    return math.cos(angle) * speed, math.sin(angle) * speed

Projectile Tracking

class Projectile:
    def __init__(self, pos, target):
        self.pos = pygame.math.Vector2(pos)
        self.target = target  # reference to enemy
        self.speed = 400
        self.damage = 25
        self.alive = True

    def update(self, dt):
        if not self.target or not self.target.alive:
            self.alive = False
            return

        target_pos = pygame.math.Vector2(self.target.pos)
        direction = target_pos - self.pos
        dist = direction.length()

        if dist < self.speed * dt:
            self.target.hp -= self.damage
            if self.target.hp <= 0:
                self.target.alive = False
            self.alive = False
        else:
            self.pos += direction.normalize() * self.speed * dt

tips

  • Gerber tip: Store enemy reference in projectile, not just position; tracks moving targets
  • Tip: math.atan2(dy, dx) gives angle in radians; essential for aiming
  • Tip: Check enemy.alive before targeting; prevents crashes
  • Tip: Wave scaling: increase enemy HP/speed each wave for difficulty curve

common mistakes

  • Storing position instead of reference: projectiles miss moving enemies
  • Not checking if target is alive: crashes when enemy dies mid-flight
  • Forgetting to normalize direction: speed varies with distance
  • Spawning too many enemies at once: overwhelms player immediately

step 2 options

  • Tower Sprite (5pts): load image instead of drawing shape
  • Projectile Rotation (5pts): rotate sprite to face target
  • Wave Balance (5pts): enemies scale with waves
  • Enemy HP Bar (10pts): draw health above enemies
  • Additional Levels (10pts): new maps after X waves
  • New Tower Type (15pts): subclass with splash damage or slow

# examples [2]

# example 01 · enemy HP bar

Step 2 option (10pts); health bar above enemies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# example 02 · splash tower (new type)

Step 2 option (15pts); damages all enemies near target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# challenges [2]

# challenge 01/02todo
Why store a reference to the target enemy instead of just its position?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
🐍
Loading PythonSetting up pandas & numpy...
# challenge 02/02todo
What does math.atan2(dy, dx) return?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
🐍
Loading PythonSetting up pandas & numpy...