Chapter 9: Parallel Universes

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 = 0

While 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
LOOP

See 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
LOOP

The 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 SUB

CALL 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!"
PAUSE

While 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 SUB

The 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?

  1. AddPoints reads score (it's 50)
  2. PlayerScored reads score (it's also 50—hasn't been updated yet!)
  3. AddPoints writes 50 + 10 = 60
  4. 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 SUB

Now 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 SUB

Everything 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 SUB

Crash-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 SUB

Without 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 SUB

Look 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 SUB

3. 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 SUB

Quick 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:

  1. Responsive UIs — Loading screens with animations, not freezes
  2. Complex AI — Multiple enemies thinking independently
  3. Background Music — Audio that plays without blocking gameplay
  4. Smooth Animations — Timers ensure consistent frame rates
  5. 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!

  1. Create a countdown timer that displays "3... 2... 1... GO!" with one-second delays while the player can still move around
  2. Make a "typing" effect where text appears letter by letter using a timer, while the game continues
  3. Build a particle system where a timer spawns new particles and another timer updates their positions
  4. 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!