Skip to main content

Stage 3: Fire Lasers

Course progressStage 3 of 10
~40 min
One game, one Trinket

Keep building in the workspace on the right.

This stage is part of the same Python Arcade project you started in Setup. Type each new code block into the Trinket rail and keep building on the last stage.

Build

a list of lasers, a fire button, and a game loop

Learn

how a game updates many moving objects every frame

Ship

lasers that launch upward from the cannon and disappear at the top

The big idea

Up until now, every object in the game has been created once and lived forever — one screen, one cannon. Lasers are different. They are temporary: a laser is born when the player presses space, climbs up the screen, and disappears when it leaves the playfield. The game might have zero lasers, or one, or three, all at the same time.

To handle "an unknown number of similar things," we use a list. A list in Python is a container that holds many items in order. You can add to it, walk through it, and remove items as the game changes.

lasers = [ laser0 , laser1 , laser2 ]
│ │ │
▼ ▼ ▼
(each one is its own Turtle, climbing the screen)

We also need to introduce the game loop. A game loop is a piece of code that runs over and over — many times per second — and on each pass it updates every moving thing in the game. One pass of the loop is called a frame. Lasers move because every frame, the loop walks the list and nudges each one upward.

The list pattern from this stage is the most reused pattern in the whole course — aliens, hits, and game-over checks all use it. Today is the day to understand it.

Before you start

Your cannon should move left and right with the arrow keys.

Build it

Step 1 — Set up the laser list and limits

A list starts empty. We also pick a top speed and a cap on how many lasers can be on screen at once. Caps keep the game readable — a screen full of lasers is a screen the player can't see through.

Add these near your other constants:

LASER_SPEED = 18
MAX_LASERS = 3
lasers = []

lasers = [] creates an empty list. By the end of the stage it will fill up and drain as the player shoots.

Step 2 — Write the fire function

Pressing space should create a new laser at the cannon's current position — unless there are already too many. We check the cap first, then build a fresh Turtle and add it to the list.

Build this function above the keyboard setup:

Think first

Why a list?

Why does the game need `lasers = []` instead of one variable called `laser`?

Check your thinking

There can be many lasers on screen at the same time. A list lets the game remember all of them.

Your turn

Write the guard first

Type only the first three lines of fire_laser() before looking at the rest. Explain to a partner what problem those lines prevent.

Need a hint?

The first two lines inside `fire_laser()` should check `len(lasers)` and return early if there are already too many.

Stuck? Compare carefully
Answer check
Debug compare only

main.py

Write the guard and the laser yourself first, then reveal to check.

def fire_laser():
if len(lasers) >= MAX_LASERS:
return

laser = turtle.Turtle()
laser.penup()
laser.color("red")
laser.shape("triangle")
laser.setheading(90)
laser.setposition(cannon.xcor(), FLOOR_LEVEL + 30)
lasers.append(laser)

len(lasers) is how many items the list currently holds. lasers.append(laser) adds the new laser to the end of the list. From this moment on, the game owns that laser through the list.

Step 3 — Bind the space key

Same pattern as Stage 2 — the screen listens, and when it sees the space bar it calls fire_laser.

Wire this with your key bindings:

screen.onkey(fire_laser, "space")

Run the game and press space. You'll see red triangles appear at the cannon — but they will not move yet. That's the next step.

Step 4 — Build the game loop

Now we make the lasers move. We add a while True: loop at the bottom of the program that walks the laser list every frame, nudges each one upward, and removes any that have left the screen.

First, add this at the top of the file with your imports:

import time

Then add this before your key bindings so animation is smooth:

screen.tracer(0)

screen.tracer(0) tells Turtle to stop drawing automatically. We will tell it when to draw, once per frame, with screen.update().

Now replace your final turtle.done() with this loop:

Trace it

Trace one frame

  1. The loop visits each laser in a copy of the list.
  2. Each laser moves upward by LASER_SPEED.
  3. If a laser passes TOP, it hides and leaves the list.
while True:
for laser in lasers[:]:
laser.sety(laser.ycor() + LASER_SPEED)

if laser.ycor() > TOP:
laser.hideturtle()
lasers.remove(laser)

time.sleep(0.02)
screen.update()

Run the game. Press space — lasers should now climb the screen, hit the top, and disappear.

File order checkpoint

By the end of Stage 3, your main.py should be ordered like this:

  1. #!/bin/python3, then import turtle and import time
  2. Screen setup and constants, including LASER_SPEED, MAX_LASERS, and lasers
  3. Cannon setup, move_left(), move_right(), draw_cannon(), and fire_laser()
  4. screen.tracer(0), key bindings, screen.listen(), and draw_cannon()
  5. The while True: game loop at the very bottom

Understand it

The piece worth slowing down on is lasers[:]. That little [:] is not decoration — it makes a copy of the list. When we walk through a list with for laser in ... and remove items from the original list at the same time, the loop loses track of where it is and skips items. Walking a copy means we can safely modify the real list mid-loop. This is a real Python gotcha, and you'll see it again in Stage 5.

The screen.tracer(0) / screen.update() pair is a speed choice, not a correctness one. By default, Turtle redraws the screen after every single change — which is slow when you have lots of moving things. Turning off auto-draw and updating manually once per frame is how games keep their framerate up.

time.sleep(0.02) is the other half of that choice. Trinket runs in the browser, so an unlimited while True: loop can run too fast and make the editor feel stuck. A tiny pause gives the browser time to breathe and makes the game speed predictable.

We deliberately picked MAX_LASERS = 3 instead of allowing unlimited shots. A cap makes the game better: it forces the player to aim instead of mash space, and it keeps the screen readable. Game designers call this a constraint that creates skill — the same idea that makes a chess board only 8×8.

Try this

Learning beat

Try this

Three short experiments. Predict before you run, then test your guess.

Predict first

Change LASER_SPEED to 2. Run the game and fire a few lasers. Does the game still feel like an arcade shooter? Write one sentence about why the speed matters this much.

Compare

Comment out screen.tracer(0) (put a # in front of it) and run. Watch what happens to the cannon and the lasers as the screen tries to draw every change. Put it back when you're done.

Connect

Stage 4 spawns aliens. Aliens are also temporary game objects that need to live in a list and move every frame. Look at your laser code. What three pieces will the alien code reuse almost word-for-word?

Test your stage

Stuck? Compare carefully
Answer check
Debug compare only

required Stage 3 code

Where it goes: Compare this with your Stage 3 additions: `import time` at the top, laser constants with the other constants, `fire_laser()` before key bindings, and the loop at the bottom replacing `turtle.done()`.

Use this only after tracing your own loop and checking the key binding.

import time

LASER_SPEED = 18
MAX_LASERS = 3
lasers = []

def fire_laser():
if len(lasers) >= MAX_LASERS:
return

laser = turtle.Turtle()
laser.penup()
laser.color("red")
laser.shape("triangle")
laser.setheading(90)
laser.setposition(cannon.xcor(), FLOOR_LEVEL + 30)
lasers.append(laser)

screen.tracer(0)
screen.onkey(fire_laser, "space")

while True:
for laser in lasers[:]:
laser.sety(laser.ycor() + LASER_SPEED)

if laser.ycor() > TOP:
laser.hideturtle()
lasers.remove(laser)

time.sleep(0.02)
screen.update()
  • Pressing space creates a laser at the cannon.
  • Lasers move upward smoothly.
  • Lasers disappear when they reach the top.
  • You cannot fill the screen with more than three lasers at once.
  • The cannon still moves while lasers are flying.
  • Design check. Hold the space bar for two seconds. Does the rate of fire feel right, or does the cap make the game feel slow?
  • From memory. Close this page and rewrite the two lines that stop a fourth laser from being created. Reopen and compare — did you remember the >= guard and the early return?

If it breaks

  • The game freezes and the window stops responding. screen.update() is probably missing from inside the while True: loop, or it's outside the loop. The screen needs to be updated every frame, not once.
  • Lasers appear but don't move. Check that LASER_SPEED is a number, not zero, and that the line laser.sety(laser.ycor() + LASER_SPEED) is inside the for laser in lasers[:]: loop.
  • Lasers fly forever and the game gets slower. The if laser.ycor() > TOP: block is what cleans them up. If a laser passes the top and never gets removed, it stays in the list and the loop keeps moving it. Double-check the > direction.
  • Lasers come from the wrong place. cannon.xcor() is what aims them — it asks the cannon "where are you right now?" If lasers always start in the middle, you may have used a number like 0 instead of cannon.xcor().
  • I press space and nothing happens. Re-read your key bindings. screen.onkey(fire_laser, "space") must come before screen.listen().