Okay, things are about to get weird. But also REALLY cool.
So far, all our programs have been doing one thing at a time. Check input. Update game. Draw screen. Repeat. Like a train chugging down a single track.
But what if you could have MULTIPLE trains running at the SAME time? What if your main game loop could run while ALSO having a background music player, a particle system, an enemy AI, and a countdown timer—all doing their own thing, independently?
That's what threads and timers are for. Welcome to the world of parallel programming!
(Don't worry, I'll hold your hand through this one. It can be tricky, but once it clicks, you'll feel like a wizard.)
Why Run Things in Parallel?
Here's the problem with our single-track approach:
' What if we want a countdown timer AND a game?
count = 10
DO
PRINT "Countdown: "; count
count = count - 1
DELAY 1000 ' Wait a whole second!
' Uh oh... player can't do ANYTHING during that second!
LOOP UNTIL count = 0While we're waiting that 1000 milliseconds, the player is frozen. They can't move, can't shoot, can't do anything. The whole game stops!
What we WANT is for the countdown to happen "in the background" while the game keeps running smoothly. And that's exactly what timers let us do!
CALL TIMER: Scheduled Callbacks
A timer runs a subroutine automatically at set intervals—without blocking your main program.
EVERY: Repeating Timers
SCREEN 13
' First, declare and define the SUB
DECLARE SUB Tick()
' Global counter that persists between timer calls
count = 0
SUB Tick()
count = count + 1
LOCATE 1, 1
PRINT "Timer ticks: "; count; " "
END SUB
' Schedule it to run every 500 milliseconds (twice per second)
CALL TIMER Tick() EVERY 500 MILLISECONDS AS "myTimer"
' Meanwhile, the main program keeps running!
playerX = 160
playerY = 100
DO
IF KEYDOWN("LEFT") THEN playerX = playerX - 3
IF KEYDOWN("RIGHT") THEN playerX = playerX + 3
IF KEYDOWN("UP") THEN playerY = playerY - 3
IF KEYDOWN("DOWN") THEN playerY = playerY + 3
BEGIN FRAME
CLS
CIRCLE (playerX, playerY), 10, 14
LOCATE 1, 1
PRINT "Timer ticks: "; count; " "
LOCATE 2, 1
PRINT "Move with arrow keys!"
END FRAME
DELAY 16
LOOPSee what's happening? The main loop runs at 60fps, smooth as butter. Meanwhile, the Tick() subroutine runs on its own schedule—every half second—counting up independently. The two don't block each other!
AFTER: One-Shot Timers
Sometimes you just want something to happen once, after a delay:
SCREEN 13
DECLARE SUB ShowMessage()
SUB ShowMessage()
LOCATE 10, 10
PRINT "SURPRISE! 3 seconds have passed!"
END SUB
PRINT "Wait for it..."
CALL TIMER ShowMessage() AFTER 3 SECONDS AS "surprise"
' Game continues normally
DO
' Your game loop here
DELAY 16
LOOPThe message appears after 3 seconds, but your game keeps running the whole time. No freezing!
Time Units
You can use different time units:
CALL TIMER MyFunc() EVERY 100 MILLISECONDS AS "fast"
CALL TIMER MyFunc() EVERY 1 SECOND AS "normal"
CALL TIMER MyFunc() EVERY 2 SECONDS AS "slow"
CALL TIMER MyFunc() EVERY 1 MINUTE AS "rare"
' AFTER works the same way
CALL TIMER Explode() AFTER 5 SECONDS AS "bomb"
CALL TIMER GameOver() AFTER 60 SECONDS AS "timeLimit"Stopping Timers
When you're done with a timer, stop it:
STOP TIMER "myTimer"Good news: When your program ends, Crash BASIC automatically cleans up all running timers and threads. You won't have zombie timers haunting your computer!
But still clean up! Even though things get cleaned up at the end, you should stop timers and threads when you're done with them DURING the game. Why? A timer that keeps firing when you don't need it anymore is wasting energy. Your game might slow down, or worse—the old timer might interfere with new game logic. Think of it like turning off lights when you leave a room. The house won't burn down if you forget, but your electricity bill will be higher!
If you're inside the timer's subroutine and want to stop yourself:
SUB CountdownTick()
countdown = countdown - 1
IF countdown <= 0 THEN
STOP TIMER ' Stop the timer I'm running in
PRINT "BOOM!"
END IF
END SUBCALL THREAD: Background Work
Threads are for longer-running tasks that you want to happen in the background without blocking the main program.
SCREEN 13
DECLARE SUB LoadStuff()
' This will run in the background
loading = 1
SUB LoadStuff()
' Simulate loading resources
FOR i = 1 TO 100
DELAY 50 ' Pretend we're loading something
progress = i
NEXT i
loading = 0
PRINT "Loading complete!"
END SUB
' Start the background thread
CALL THREAD LoadStuff() AS "loader"
' Main program shows a loading animation
angle = 0
DO WHILE loading
BEGIN FRAME
CLS
' Draw spinning loading indicator
x = 160 + COS(angle) * 30
y = 100 + SIN(angle) * 30
CIRCLE (x, y), 5, 14
LOCATE 15, 12
PRINT "Loading... "; progress; "%"
END FRAME
angle = angle + 0.2
DELAY 16
LOOP
PRINT "Ready to play!"
PAUSEWhile LoadStuff() slowly processes in the background, the main program keeps running, showing a nice spinning loading indicator. The player sees smooth animation, not a frozen screen!
Stopping Threads
Just like timers:
STOP THREAD "loader" ' Stop by name
' Or from inside the thread:
SUB MyThread()
DO
' Work...
IF shouldStop THEN
STOP THREAD ' Stop myself
END IF
LOOP
END SUBThe Danger Zone: Shared Variables
Here's where things get tricky. When multiple things run at the same time, they might try to change the same variable at the same moment. This causes WEIRD bugs.
' DANGER: This code has a bug!
score = 0
SUB AddPoints()
' This timer adds 10 points every second
score = score + 10
END SUB
SUB PlayerScored()
' This runs when player does something
score = score + 100
END SUB
CALL TIMER AddPoints() EVERY 1 SECOND AS "bonus"What if AddPoints() and PlayerScored() both try to update score at the EXACT same moment?
- AddPoints reads score (it's 50)
- PlayerScored reads score (it's also 50—hasn't been updated yet!)
- AddPoints writes 50 + 10 = 60
- PlayerScored writes 50 + 100 = 150
Wait, what happened to the +10? It got lost! This is called a race condition, and it's the #1 headache of parallel programming.
BEGIN LOCK / END LOCK: Thread Safety
The solution is to LOCK variables when you're using them. A lock says "Hey, I'm using this—everyone else wait your turn!"
score = 0
SUB AddPoints()
BEGIN LOCK(score)
score = score + 10
END LOCK(score)
END SUB
SUB PlayerScored()
BEGIN LOCK(score)
score = score + 100
END LOCK(score)
END SUBNow when AddPoints() locks score, PlayerScored() has to wait until it's unlocked. No more race conditions!
Lock Whole Blocks, Not Just One Line
SUB UpdateGameState()
BEGIN LOCK(gameData)
' All of this is protected
playerX = playerX + velocityX
playerY = playerY + velocityY
IF playerX < 0 THEN playerX = 0
IF playerX > 319 THEN playerX = 319
health = health - damageThisFrame
IF health < 0 THEN health = 0
END LOCK(gameData)
END SUBEverything between BEGIN LOCK and END LOCK is atomic—no other thread can interfere.
Lock Multiple Variables
If you're updating multiple related variables, lock them all:
SUB MoveEnemy(id)
BEGIN LOCK(enemyX)
BEGIN LOCK(enemyY)
enemyX(id) = enemyX(id) + enemyDX(id)
enemyY(id) = enemyY(id) + enemyDY(id)
END LOCK(enemyY)
END LOCK(enemyX)
END SUBCrash-tan's Pro Tip: Only lock what you need, and unlock as soon as you're done. Long locks can slow down your program because other threads are waiting!
Don't Panic About Locks!
Here's a secret that might make you feel better: most games don't need locks at all.
Why? Because games are self-correcting! If an enemy's position is slightly wrong for ONE frame because of a race condition... who cares? The next frame (16 milliseconds later!) will fix it. The player won't notice a single-frame glitch.
Locks matter when:
- You're doing something that MUST be exactly right (like saving a high score)
- A wrong value could crash the game (like an array index going negative)
- You're building up a calculation over multiple steps
Locks usually DON'T matter when:
- You're just reading a position to draw something
- A value being "one frame behind" is harmless
- Things update 60 times per second anyway
So don't be scared! Start WITHOUT locks. If you see weird bugs (enemies teleporting, scores jumping randomly), THEN add locks to the specific variables causing trouble. You might never need them!
YIELD: Playing Nice with Others
Sometimes you have a long-running loop that needs to give other code a chance to run. That's what YIELD does—it pauses for just one tick to let other things happen.
SUB ProcessBigList()
FOR i = 0 TO 10000
' Do some work
ProcessItem(i)
' Every 100 items, let other stuff run
IF i MOD 100 = 0 THEN YIELD
NEXT i
END SUBWithout YIELD, this loop would hog all the processing time. With YIELD, timers can fire, the screen can update, and everything stays responsive.
Real Example: A Game with Timers
Let's put it all together—a game with multiple things happening in parallel:
SCREEN 13
' ============================================
' DECLARE all our subs
' ============================================
DECLARE SUB SpawnEnemy()
DECLARE SUB UpdateEnemies()
DECLARE SUB DrawGame()
' ============================================
' Global game state
' ============================================
DIM enemyX(10)
DIM enemyY(10)
DIM enemyActive(10)
enemyCount = 0
playerX = 160
playerY = 180
score = 0
gameOver = 0
' ============================================
' Initialize enemies
' ============================================
FOR i = 0 TO 9
enemyActive(i) = 0
NEXT i
' ============================================
' Set up timers
' ============================================
' Spawn a new enemy every 2 seconds
CALL TIMER SpawnEnemy() EVERY 2 SECONDS AS "spawner"
' Update enemy positions 30 times per second
CALL TIMER UpdateEnemies() EVERY 33 MILLISECONDS AS "enemyAI"
' ============================================
' Main game loop (just handles player input and drawing)
' ============================================
DO UNTIL gameOver
' Player input
IF KEYDOWN("LEFT") AND playerX > 10 THEN playerX = playerX - 4
IF KEYDOWN("RIGHT") AND playerX < 310 THEN playerX = playerX + 4
' Check collisions with enemies
FOR i = 0 TO 9
IF enemyActive(i) THEN
dx = playerX - enemyX(i)
dy = playerY - enemyY(i)
dist = SQR(dx * dx + dy * dy)
IF dist < 15 THEN
gameOver = 1
END IF
END IF
NEXT i
CALL DrawGame()
DELAY 16
LOOP
' Clean up timers
STOP TIMER "spawner"
STOP TIMER "enemyAI"
LOCATE 12, 12
PRINT "GAME OVER!"
LOCATE 14, 10
PRINT "Final Score: "; score
PAUSE
' ============================================
' Subroutines (these run on timers!)
' ============================================
SUB SpawnEnemy()
' Find an inactive enemy slot
FOR i = 0 TO 9
IF enemyActive(i) = 0 THEN
BEGIN LOCK(enemyX)
BEGIN LOCK(enemyY)
BEGIN LOCK(enemyActive)
enemyX(i) = INT(RND * 280) + 20
enemyY(i) = 0
enemyActive(i) = 1
END LOCK(enemyActive)
END LOCK(enemyY)
END LOCK(enemyX)
EXIT FOR
END IF
NEXT i
END SUB
SUB UpdateEnemies()
BEGIN LOCK(enemyY)
BEGIN LOCK(enemyActive)
FOR i = 0 TO 9
IF enemyActive(i) THEN
enemyY(i) = enemyY(i) + 2
' Off the bottom of the screen?
IF enemyY(i) > 200 THEN
enemyActive(i) = 0
score = score + 10 ' Survived!
END IF
END IF
NEXT i
END LOCK(enemyActive)
END LOCK(enemyY)
END SUB
SUB DrawGame()
BEGIN FRAME
CLS
' Draw player
CIRCLE (playerX, playerY), 8, 14
' Draw enemies
FOR i = 0 TO 9
IF enemyActive(i) THEN
CIRCLE (enemyX(i), enemyY(i)), 6, 4
END IF
NEXT i
' Draw score
LOCATE 1, 1
PRINT "Score: "; score
END FRAME
END SUBLook at how clean the main loop is! It just handles player input and drawing. The enemy spawning and movement happen automatically on timers. Each piece of the game runs independently, and locks keep everything thread-safe.
When to Use What
| Situation | Use This |
|---|---|
| Something needs to happen repeatedly at fixed intervals | CALL TIMER ... EVERY |
| Something needs to happen once after a delay | CALL TIMER ... AFTER |
| Long-running task that shouldn't block the game | CALL THREAD |
| Multiple things modifying the same variable | BEGIN LOCK / END LOCK |
| Long loop that needs to stay responsive | YIELD |
Common Pitfalls
1. Forgetting to Stop Timers
' BAD: Timer keeps running forever!
CALL TIMER Update() EVERY 100 MILLISECONDS AS "gameLoop"
' ... game ends but timer keeps firing ...
' GOOD: Clean up when done
STOP TIMER "gameLoop"2. Deadlocks (Waiting Forever)
' BAD: Deadlock! Each is waiting for the other!
SUB TaskA()
BEGIN LOCK(x)
BEGIN LOCK(y) ' Waiting for y...
' ...
END LOCK(y)
END LOCK(x)
END SUB
SUB TaskB()
BEGIN LOCK(y)
BEGIN LOCK(x) ' Waiting for x... but TaskA has it!
' ...
END LOCK(x)
END LOCK(y)
END SUB
' GOOD: Always lock in the same order
SUB TaskA()
BEGIN LOCK(x)
BEGIN LOCK(y)
' ...
END LOCK(y)
END LOCK(x)
END SUB
SUB TaskB()
BEGIN LOCK(x) ' Same order as TaskA
BEGIN LOCK(y)
' ...
END LOCK(y)
END LOCK(x)
END SUB3. Over-Locking
' BAD: Locking way more than necessary
SUB UpdateScore()
BEGIN LOCK(everything)
score = score + 1 ' This is all we needed to protect!
' Now nothing else can run until we're done
FOR i = 0 TO 1000
' Lots of unrelated work...
NEXT i
END LOCK(everything)
END SUB
' GOOD: Lock only what needs protection
SUB UpdateScore()
BEGIN LOCK(score)
score = score + 1
END LOCK(score)
' Unrelated work doesn't need the lock
FOR i = 0 TO 1000
' ...
NEXT i
END SUBQuick Reference
| Syntax | What It Does |
|---|---|
CALL TIMER Sub() EVERY n UNIT AS "id" |
Run Sub every n milliseconds/seconds/minutes |
CALL TIMER Sub() AFTER n UNIT AS "id" |
Run Sub once after n time |
CALL THREAD Sub() AS "id" |
Run Sub in the background |
STOP TIMER ["id"] |
Stop a timer (or current timer if no id) |
STOP THREAD ["id"] |
Stop a thread (or current thread if no id) |
BEGIN LOCK(variable) |
Lock a variable for exclusive access |
END LOCK(variable) |
Unlock a variable |
YIELD |
Pause for one tick to let other code run |
Why This Matters
Parallel programming might seem complicated, but it unlocks SO much:
- Responsive UIs — Loading screens with animations, not freezes
- Complex AI — Multiple enemies thinking independently
- Background Music — Audio that plays without blocking gameplay
- Smooth Animations — Timers ensure consistent frame rates
- Real-Time Updates — Countdown timers, score popups, visual effects
The key is to start simple. Use one timer. Get comfortable with it. Then add another. Lock your shared variables. Test often. Before you know it, you'll be running a whole orchestra of parallel processes!
Try It Yourself!
- Create a countdown timer that displays "3... 2... 1... GO!" with one-second delays while the player can still move around
- Make a "typing" effect where text appears letter by letter using a timer, while the game continues
- Build a particle system where a timer spawns new particles and another timer updates their positions
- Create a boss fight where the boss has its own timer controlling its attack pattern
You're now wielding the power of parallel programming. Use it wisely!
See you in Chapter 10!
Next Chapter: We'll peel back the layers—literally! Graphics layers, sprites, and visual depth!