Build a GameCreating Shatterfield

10  Heartbeat

Goal

Until now, a wave sounded the same from its first second to its last: shots, cracks, and quiet in between. By the end of this chapter, a heartbeat runs under every wave. Two low notes take turns, one a second at first, then a little sooner each time, until the field races at about four beats a second. Clear the field, and the beat stops.

The game as it is at the end of this chapter. Click the game, then press Enter to start.

If your game and this chapter part ways, here is the chapter’s finished project. Open it and compare, or start Chapter 11 from it.

Shatterfield, Chapter 10 finishedZip of a Myth project · 5.0 MB

To open it, right-click the downloaded file, choose Extract All... and then Extract. Start Myth Engine, choose File › Open Project... and pick the .mythproj file in the new folder. Beside it, OFL.txt is the license of the project’s font, Press Start 2P.

10.1 The Idea

WAVE 1 fades, and four rocks drift in from the edges of the field. Under everything, a low note sounds. A second later comes another one, a little higher, and after it the first again, a little sooner than before.

You turn and fire. A rock cracks in two, and the notes keep coming, each one closer to the last. Half a minute in, they race. Break the last rock, and the beat stops dead. Sector Clear climbs its three notes, and the field is quiet.

All of it comes from one new Logic Component on Game Rules, Heartbeat, which plays the notes and decides when the next one comes.

It decides with a timer that rings once. At each ring a note plays, and the same Event starts the timer again for the next beat, with a shorter time. So every beat sets up the one after it. You met a timer like this in Chapter 8, where a saucer’s nextWeave timer started itself again from its own Event, at a random time.

Here the time shrinks. By how much? Drop a ball on a hard floor and listen. Each bounce comes sooner than the last, by the same share of the gap every time, until the bounces run together into a rattle. The heartbeat’s gaps shrink the same way.

Each one is 0.97 of the gap before it. A ball rattles to a stop, but the heartbeat has a floor, fastestSeconds, and it stops getting faster there.

The beat plays only while a wave’s rocks are out. Waves knows both moments. Its bannerTime timer brings the rocks out, and No Instances Left sees the last one go. So Waves now tells the whole game, with two new messages, waveStarted and waveCleared. Heartbeat listens for both. In Chapter 11, the sky will listen for waveCleared too.

10.2 The Plan

Here’s the same story as a chain, read from left to right. Keep it beside you while you build.

  • A wave’s rocks come out -> Waves tells the whole game waveStarted -> Heartbeat sets beatSeconds to startSeconds and starts its heartbeat timer.
  • The heartbeat timer rings -> a note plays, and nextNote turns to the other one -> beatSeconds shrinks to 0.97 of itself, never below fastestSeconds -> the timer starts again, for beatSeconds.
  • The last rock is gone -> Waves tells the whole game waveCleared -> Heartbeat stops its timer.
  • A game starts -> Play Switch switches Heartbeat on.
  • A game ends -> Play Switch switches it off, and its On Disable stops the timer.

10.3 Set Up

This chapter adds two sounds, Beat1 and Beat2. They’re already in your project from the Starter Kit. Each is a short, low thump, less than a tenth of a second long, and Beat2 is a little higher than Beat1.

Set upWhat this chapter adds
SOUNDS (from the starter kit)
Beat1
Beat2

10.4 Waves Speaks Up

First, Waves tells the game what it already knows.

10.4.1 Waves

When does a wave really begin? Not when WAVE 1 shows. It begins a moment later, when the bannerTime timer rings, the banner goes, and the rocks come out. Waves runs that moment, so it knows. And it knows the end, the moment No Instances Left sees the last rock go.

So each of those two Events gets one more step at the bottom: a Trigger Custom Event to the whole game, the way nextWave goes out. The bannerTime Event sends waveStarted, once the rocks are made. No Instances Left sends waveCleared.

Logic ComponentWavesAttach to: Game RulesAddDelete

Each wave announces itself with the banner, then brings its Large Asteroids; clearing every Asteroid starts the sector jump and then the next wave.

Attach toAny Object
SHARED VARIABLES
NameTypeKindStarting Value
asteroidObjectConstantNone
bannerObjectConstantNone
bannerSecondsNumberConstant1.6
jumpSecondsNumberConstant1.4
maxAsteroidsPerBandNumberConstant6
edgeBandNumberConstant3.438
sideBandNumberConstant4.688
When Enabled
WhenOn Enable
1Change Variable
Variable
$wave
Value
0
2Trigger Custom Event
Custom Event
nextWave
On Next Wave
WhennextWaveCustom Event
1Change Variable
Variable
$wave
Operator
+=
Value
1
2Set Visible
Applies To
@self.banner
Visible
Yes
3Start Recurring Timer
Timer Name
bannerTime
Repeat Every (s)
@self.bannerSeconds
Ring Once
Yes
On the Banner Time Timer
WhenRecurring TimerTimer name: bannerTime
1Declare Variable
Name
perBandThisWave
Value
1
2Set Visible
Applies To
@self.banner
Visible
No
3Change Variable
Variable
perBandThisWave
Value
$wave + 1
4IfperBandThisWave > @self.maxAsteroidsPerBand
Then
5Change Variable
Variable
perBandThisWave
Value
@self.maxAsteroidsPerBand
6Create Asteroids Along EdgesFunction
creator
self
asteroid
@self.asteroid
perBand
perBandThisWave
edgeBand
@self.edgeBand
sideBand
@self.sideBand
7Trigger Custom Event
Custom Event
waveStarted
On No Instances Left · Anything Tagged Asteroid
WhenNo Instances LeftObject: tag:Asteroid
1Control Sound
Sound
"Sector Clear"
2Control Sound
Sound
"Hyper"
3Start Recurring Timer
Timer Name
sectorJump
Repeat Every (s)
@self.jumpSeconds
Ring Once
Yes
4Trigger Custom Event
Custom Event
waveCleared
On the Sector Jump Timer
WhenRecurring TimerTimer name: sectorJump
1Trigger Custom Event
Custom Event
nextWave
When Disabled
WhenOn Disable
1Stop Recurring Timer
Timer Name
bannerTime
2Stop Recurring Timer
Timer Name
sectorJump
3Set Visible
Applies To
@self.banner
Visible
No

On Waves’ canvas, find On the Banner Time Timer. Add Trigger Custom Event from its Mode 1’s right-click, type waveStarted into its Custom Event box, and choose Connect to sequence. It joins at the very end, below the call to Create Asteroids Along Edges. Do the same in the No Instances Left Event, with waveCleared, and it joins below Start Recurring Timer.

Nothing listens for either message yet. Heartbeat will.

10.5 The Beat

Now the heart of the chapter.

10.5.1 Heartbeat

Heartbeat has four Events. Three of them are short, so start with those.

Heartbeat keeps two numbers between its Events, in a small form of its own, Heartbeat Data. The first is beatSeconds, the time until the next beat.

The rocks are out, and waveStarted arrives. Heartbeat sets beatSeconds back to startSeconds, which is 1 second at the start of every wave. It starts its heartbeat timer, Ring Once, for beatSeconds. However fast the last wave ended, a new one always starts slow.

When waveCleared arrives, Stop Recurring Timer stops the heartbeat timer, and the beat goes quiet until the next wave’s rocks are out. The fourth Event, On Disable, does the same as a game ends. It’s the tidying Waves’ On Disable did in Chapter 9. Switching Heartbeat off doesn’t stop its timer. The timer would keep counting, on into the next game, and ring there before the rocks are out. On Disable stops it.

The real work is in the heartbeat timer’s own Event, so take it one ring at a time.

The timer rings, and first comes a note. The If asks whose turn it is: is nextNote 1? If it is, Beat1 plays, and nextNote becomes 2, ready for the other note next time. Otherwise Beat2 plays, and nextNote goes back to 1. It’s the flip Blink makes in Chapter 9, with a sound on each side.

Next, the gap shrinks. One second becomes 0.97 seconds, and the gap after that is 0.97 of the new one, and so on down. That’s beatSeconds times speedUp.

After the shrink comes the floor. A second If asks whether beatSeconds has dropped below fastestSeconds, 0.24 seconds. If it has, beatSeconds becomes fastestSeconds. Four beats a second is fast enough to feel. Any faster, and the beat would blur into a buzz.

Last, Start Recurring Timer starts the heartbeat timer again, Ring Once, for the new beatSeconds. The next beat is already on its way.

Count the beats. The rocks drift in from the edges, and for a moment nothing sounds. One second, and Beat1. A little under a second, and Beat2 answers it, a step higher. A little less again, and it’s Beat1’s turn. On it goes, each gap shorter than the one before, until about twenty-five seconds in, when the gaps stop at 0.24 seconds.

Data StructureHeartbeat Data

Object Data of Logic "Heartbeat" (its state that lasts between Events).

beatSecondsNumberdefault 1
nextNoteNumberdefault 1
Logic ComponentHeartbeatAttach to: Game Rules

While a wave's Asteroids are out, a two-note heartbeat plays, starting startSeconds apart and speedUp times sooner each beat, down to fastestSeconds.

Attach toAny Object
DATA STRUCTURES
Heartbeat Dataself.heartbeatData
SHARED VARIABLES
NameTypeKindStarting Value
startSecondsNumberConstant1
speedUpNumberConstant0.97
fastestSecondsNumberConstant0.24
On Wave Started
WhenwaveStartedCustom Event
1Change Component Value
Component Value
self.heartbeatData.beatSeconds
Value
@self.startSeconds
2Start Recurring Timer
Timer Name
heartbeat
Repeat Every (s)
self.heartbeatData.beatSeconds
Ring Once
Yes
On the Heartbeat Timer
WhenRecurring TimerTimer name: heartbeat
1Ifself.heartbeatData.nextNote == 1
Then
2Control Sound
Sound
"Beat1"
3Change Component Value
Component Value
self.heartbeatData.nextNote
Value
2
4Otherwise
5Control Sound
Sound
"Beat2"
6Change Component Value
Component Value
self.heartbeatData.nextNote
Value
1
7Change Component Value
Component Value
self.heartbeatData.beatSeconds
Operator
*=
Value
@self.speedUp
8Ifself.heartbeatData.beatSeconds < @self.fastestSeconds
Then
9Change Component Value
Component Value
self.heartbeatData.beatSeconds
Value
@self.fastestSeconds
10Start Recurring Timer
Timer Name
heartbeat
Repeat Every (s)
self.heartbeatData.beatSeconds
Ring Once
Yes
On Wave Cleared
WhenwaveClearedCustom Event
1Stop Recurring Timer
Timer Name
heartbeat
When Disabled
WhenOn Disable
1Stop Recurring Timer
Timer Name
heartbeat

Make Heartbeat Data first, with Add > Data Structure, as you made Blink Data in Chapter 9. Its two fields are Numbers, and each Value takes the card’s 1.

Then make Heartbeat itself, with Add > Logic Component, as an Empty Logic Component like the others. Its three SHARED VARIABLES are Numbers, all Constant. Press the + on its Data Structures card, and pick Heartbeat Data under Existing Data Structures. Its row reads self.heartbeatData, and every path below starts with that name.

waveStarted is already in the Add Event window’s filter, because Waves sends it. Type it, and there it is. Its Change Component Value takes the path typed, self.heartbeatData.beatSeconds, then Enter, and @self.startSeconds in its Value. Start Recurring Timer’s Timer Name takes heartbeat, bare. Type the same path into Repeat Every (s), and pick Yes in Ring Once.

The heartbeat timer’s own Event is a Recurring Timer, picked in the Add Event window, as Blink’s was. A second window asks which timer. Pick Type a timer name… there, and a third window takes the name. Type heartbeat.

Add the first If from Mode 1’s right-click, and join it with Connect to sequence. Its condition is self.heartbeatData.nextNote == 1, in its one box. Add inside Then gives Control Sound, with Beat1 picked in its Sound list. Then it gives the Change Component Value on nextNote’s path, with 2, and Connect to sequence joins each one as it lands.

Now right-click the If and choose Add Else. Add inside Else gives the other pair: Control Sound with Beat2, and nextNote’s Change Component Value with 1.

The next block goes on the Event’s own line, below the If. Add it from Mode 1’s right-click, and Connect to sequence puts it there. It’s a Change Component Value on the path of beatSeconds. Its Operator list starts at =, so pick *= there, and type @self.speedUp into its Value. The second If joins below it the same way, with self.heartbeatData.beatSeconds < @self.fastestSeconds in its box. Its one Change Component Value goes in with Add inside Then. Last comes Start Recurring Timer, on the main line again, filled in like the first.

waveCleared is in the filter too, and so is On Disable. Each of the two gets a Stop Recurring Timer, with heartbeat in its Timer Name, bare.

Nothing switches Heartbeat on yet. That’s next.

10.6 Part of Play

Heartbeat joins the Logic that only runs during a game.

10.6.1 Play Switch

Heartbeat belongs to play, like the five Logic Components Play Switch already switches. So it joins them. One more Set Logic State switches it on when the Game State becomes Playing. Another, with State Disable, switches it off at any other Game State.

Logic ComponentPlay SwitchAttach to: Game RulesAddDelete

Switches the Logic that only makes sense during play on when a game starts, and off when it ends.

Attach toAny Object
On Game State Changed
WhenGame State Changed
1Check Game State
Action Name
isPlaying
Game State
Playing
2IfisPlaying.value == true
Then
3Set Logic State
Logic Component
"Lives and Respawn"
4Set Logic State
Logic Component
"Waves"
5Set Logic State
Logic Component
"Extra Ships"
6Set Logic State
Logic Component
"Saucer Spawner"
7Set Logic State
Logic Component
"Pause"
8Set Logic State
Logic Component
"Heartbeat"
9Otherwise
10Set Logic State
Logic Component
"Lives and Respawn"
State
Disable
11Set Logic State
Logic Component
"Waves"
State
Disable
12Set Logic State
Logic Component
"Extra Ships"
State
Disable
13Set Logic State
Logic Component
"Saucer Spawner"
State
Disable
14Set Logic State
Logic Component
"Pause"
State
Disable
15Set Logic State
Logic Component
"Heartbeat"
State
Disable
16Trigger Custom Event
Custom Event
playStopped

Right-click the If and choose Add inside Then. Pick Set Logic State. Pick Heartbeat in its Logic Component list. Connect to sequence joins it at the bottom of the True line, below the step that switches on Pause, right where the card has it.

Right-click the Else and choose Add inside Else for the second Set Logic State. Pick Heartbeat again, and Disable in its State list. Connect to sequence joins this one at the bottom of the Else line, below the Trigger Custom Event that sends playStopped.

The card has it one step higher. The game plays the same either way, since nothing that hears playStopped cares whether Heartbeat is on or off. A new block always joins at the end of its line, so matching the card takes a move. Use the round handles. Click the one at the bottom of the step that switches Pause off, then the one at the top of your new step. It moves up between them, and the Trigger Custom Event stays joined below it.

10.6.2 Game Rules

Last, attach Heartbeat to Game Rules, and turn off the switch beside its name, as you did for Pause. Heartbeat Data comes along with it, as the card’s Data Structure lines show. Like the others, it starts every game switched off, because the game starts on the title. On the card, its whole row is green, marked (turned off).

ObjectGame RulesAddDelete
Data Structure
From LogicExtra Ships
Data StructureExtra Ships Data
Data Structure
From LogicHeartbeat
Data StructureHeartbeat Data
Logic
Lives and Respawn(turned off)
Waves(turned off)
Score
Extra Ships(turned off)
Saucer Spawner(turned off)
Title Screen
Start Game
Game Over Screen
Menu Pause Lifecycle
Pause(turned off)
Play Switch
Heartbeat(turned off)
Shared Variables
shipShip
asteroidAsteroid Large
bannerWave Banner
largeSaucerSaucer Large
smallSaucerSaucer Small
promptTitle Prompt
titleAsteroidTitle Asteroid
playAgainPromptPlay Again Prompt
menuElementPaused

Run the game, and press Enter. WAVE 1 shows, the rocks come in, and a second later there’s the first beat.

Listen to it quicken.

Play it!

Listen and watch for each of these.

  • The title is quiet, with no beat until a game starts.
  • Press Enter. WAVE 1 shows, the rocks come in, and about a second later the first note sounds. The second follows, a little higher.
  • The two notes take turns all through the wave.
  • Each beat comes a little sooner than the one before. About 25 seconds into the wave, the beat stops speeding up, at about four beats a second.
  • Lose a Ship, and the beat goes on.
  • Press Esc. Under PAUSED, the beat holds. Menu Pause Lifecycle holds the whole game, timers too, so there’s nothing to build for it. Press Esc again, and it carries on where it left off.
  • Break the last rock. The beat stops at once, and stays quiet through Sector Clear and the next WAVE banner.
  • When the next wave’s rocks are out, the beat starts again, slow, about one a second.
  • Lose your last Ship. Under GAME OVER, there’s no beat.
How it works

Waves tells the whole game when a wave’s rocks are out, and when the last one is gone. Heartbeat listens. It starts slow, plays a note, and sets its own next ring a little sooner. It stops when the field is clear, or when the game ends.

Underneath that is one idea. A timer that rings once can keep a rhythm going as long as you like, when each ring starts the next. And each ring can choose when the next one comes. The saucer’s weave chose at random. The heartbeat chooses a little sooner, every time.

10.7 If It Didn’t Work

Something may be a little off the first time, and that’s normal. Start a game, let a wave run for half a minute, and listen.

No beat at all? Heartbeat only beats while a wave’s rocks are out, so start a game first. Then look in Heartbeat for a block with no arrow into it, inside the Ifs too. Then look at Waves. Its two new Trigger Custom Events need arrows into them as well. Check their spelling too: waveStarted and waveCleared. Play Switch’s True line needs the Set Logic State that names Heartbeat, too.

One note, then silence? The heartbeat timer’s Event has to start the timer again. Look for Start Recurring Timer at the very bottom of its line, joined, with heartbeat as its Timer Name and Ring Once on Yes.

The same note every time? Each side of the first If gives nextNote the other number: 2 on its True line, and 1 on its Else line.

A steady beat, a little under a second, that never quickens? The Change Component Value below the first If needs *= in its Operator list. With =, every gap is speedUp itself, 0.97 seconds.

The beat jumps to its fastest at once, or races on into a buzz? Look at the second If, the one below that Change Component Value. Its box should read self.heartbeatData.beatSeconds < @self.fastestSeconds.

The beat keeps going after the last rock? Perhaps Waves’ No Instances Left is missing its new Trigger Custom Event, the one sending waveCleared. Or perhaps Heartbeat’s waveCleared Event has no Stop Recurring Timer, with heartbeat in its Timer Name.

The beat goes on under GAME OVER? Play Switch’s Else line needs the Set Logic State that names Heartbeat, with Disable in its State list.

Each of these is a small fix. Make it, run the game again, and you’re back with the chapter. The catch-up download in the Goal is this chapter, finished, if you’d like to compare.

Challenge

What does a crowded field sound like to you? Try speedUp at 0.9, under Game Rules’ Variables, and the beat hits its fastest in under eight seconds. Try fastestSeconds at 0.12 for a frantic patter. Or give the heartbeat a third note: nextNote could count 1, 2 and 3, with an Else If. Chapter 11 is written for the card’s numbers. Set them back before you go on, and write yours down if you’d like to keep them.

Still stuck? Search the manual, or ask in the Myth Engine Discord (opens a new tab).

The field has a pulse now, and it quickens. In Chapter 11, the empty screen behind it becomes deep space.