Lesson 6: Add Bullets (Pygame)
In this lesson you will code bullets in Python with Pygame. When the player presses Space, a bullet appears at the tip of the ship and travels upward. No enemies yet — just firing and cleanup.
Step 0 — Start from Lesson 5
Open the file you built in Lesson 5 (window + left/right movement). Keep your while running: game loop.
Step 1 — Add bullet state (top of your file)
Put these just under your player variables so they’re easy to find:
# === BULLETS ===
bullets = [] # list of bullet dictionaries
BULLET_W, BULLET_H = 4, 10 # size in pixels
BULLET_SPEED = -6 # negative = up
FIRE_COOLDOWN_MS = 250 # about 4 shots per second
last_shot_at = 0 # time of last shot (ms)
Step 2 — Write a helper to spawn a bullet
Add this function above your game loop. It uses the player’s position to place the bullet at the ship’s nose and enforces a cooldown so holding Space isn’t a laser.
import pygame # (already at the top of your file)
# ... keep your other setup code ...
def fire_bullet(now_ms, player_x, player_y, player_w):
"""Create one bullet if cooldown passed."""
global last_shot_at, bullets
if now_ms - last_shot_at < FIRE_COOLDOWN_MS:
return # too soon, wait a bit
# center bullet at the top of the ship
bx = player_x + player_w // 2 - BULLET_W // 2
by = player_y - BULLET_H
bullets.append({"x": bx, "y": by, "vy": BULLET_SPEED})
last_shot_at = now_ms
Step 3 — Update bullets every frame
Add these functions to keep your code tidy:
def update_bullets():
"""Move bullets and remove the ones that go off-screen."""
global bullets
for b in bullets:
b["y"] += b["vy"]
# keep bullets whose bottom is still visible
bullets = [b for b in bullets if b["y"] + BULLET_H > 0]
def draw_bullets(screen):
BULLET_COLOR = (255, 211, 105) # soft yellow
for b in bullets:
pygame.draw.rect(screen, BULLET_COLOR, pygame.Rect(b["x"], b["y"], BULLET_W, BULLET_H))
Step 4 — Hook bullets into your game loop
- When the player presses Space, call
fire_bullet(...). - Each frame, call
update_bullets()before drawing. - When drawing, call
draw_bullets(screen)after you draw the background and player.
Inside your event loop (where you already handle ←/→):
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
player_speed = -4
if event.key == pygame.K_RIGHT:
player_speed = 4
if event.key == pygame.K_SPACE:
now = pygame.time.get_ticks()
fire_bullet(now, player_x, player_y, player_w)
if event.type == pygame.KEYUP:
if event.key in (pygame.K_LEFT, pygame.K_RIGHT):
player_speed = 0
Each frame (in your main loop, after moving the player):
update_bullets()
When drawing (after filling the background and drawing the player):
draw_bullets(screen)
Step 5 — Simple player shape (if you don’t have an image)
If you didn’t load player.png in Lesson 4, you can draw a simple rectangle ship and set these sizes so bullets spawn from the center:
# player size & position
player_w, player_h = 40, 20
player_x = 800 // 2 - player_w // 2
player_y = 600 - 60
# draw the player (rectangle + tiny cockpit)
PLAYER_COLOR = (154, 180, 255)
pygame.draw.rect(screen, PLAYER_COLOR, pygame.Rect(player_x, player_y, player_w, player_h))
player_w = player_img.get_width() and draw with screen.blit(player_img, (player_x, player_y)).Full example after Lesson 6
Here is a small, complete script you can run to test bullets. It uses a rectangle for the ship (no image needed).
import sys, pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Space Invader — Lesson 6")
clock = pygame.time.Clock()
# colors
SPACE = (10, 15, 30)
PLAYER_COLOR = (154, 180, 255)
BULLET_COLOR = (255, 211, 105)
# player
player_w, player_h = 40, 20
player_x = 800 // 2 - player_w // 2
player_y = 600 - 60
player_speed = 0 # -4, 0, or +4
# bullets
bullets = []
BULLET_W, BULLET_H = 4, 10
BULLET_SPEED = -6
FIRE_COOLDOWN_MS = 250
last_shot_at = 0
def fire_bullet(now_ms, player_x, player_y, player_w):
global last_shot_at, bullets
if now_ms - last_shot_at < FIRE_COOLDOWN_MS:
return
bx = player_x + player_w // 2 - BULLET_W // 2
by = player_y - BULLET_H
bullets.append({"x": bx, "y": by, "vy": BULLET_SPEED})
last_shot_at = now_ms
def update_bullets():
global bullets
for b in bullets:
b["y"] += b["vy"]
bullets = [b for b in bullets if b["y"] + BULLET_H > 0]
def draw_bullets():
for b in bullets:
pygame.draw.rect(screen, BULLET_COLOR, pygame.Rect(b["x"], b["y"], BULLET_W, BULLET_H))
running = True
while running:
now = pygame.time.get_ticks()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
player_speed = -4
if event.key == pygame.K_RIGHT:
player_speed = 4
if event.key == pygame.K_SPACE:
fire_bullet(now, player_x, player_y, player_w)
if event.type == pygame.KEYUP:
if event.key in (pygame.K_LEFT, pygame.K_RIGHT):
player_speed = 0
# move player + clamp to screen
player_x += player_speed
pad = 10
if player_x < pad:
player_x = pad
if player_x + player_w > 800 - pad:
player_x = 800 - pad - player_w
# update bullets
update_bullets()
# draw
screen.fill(SPACE)
pygame.draw.rect(screen, PLAYER_COLOR, pygame.Rect(player_x, player_y, player_w, player_h))
# tiny cockpit
pygame.draw.polygon(screen, (207,225,255), [
(player_x + player_w // 2, player_y - 6),
(player_x + player_w // 2 - 6, player_y),
(player_x + player_w // 2 + 6, player_y)
])
draw_bullets()
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()
Mini‑challenges
- Faster or slower? Try different values for
BULLET_SPEED. - Single‑shot mode: Only fire when Space is first pressed (remove the cooldown & call
fire_bulletonly onKEYDOWN). - Two‑barrel ship: Spawn two bullets a few pixels left/right of center.