Make a game, step by step¶
By the end of this page you will have planned and built a small game called Catcher. Dots fly across the screen from the left, and you slide a paddle up and down to catch them. A catch scores a point, flashes the lights green and plays a short high beep. A miss costs one of your three lives and gives a red flash and a lower, longer tone - except the last miss, when the game-over screen takes over and turns the lights and sound off. The dots get faster as your score grows. When your lives run out, the game shows your score, and a button starts a new game.
What you'll make: Catcher running in the emulator. The slider moves the paddle on the right; the score and lives are at the top.
You plan it first, then build it in eight small steps. Each step adds a few lines and you run the game after each one, so you always know which change did what.
Two ways to start
You already have an idea for a game: this page is for you. It starts from your ideas for a game and turns them into steps you can build.
You'd rather see what the Fidget can do first: start with Explore what it can do. Try the screen, buttons, slider, lights and sound, then come back here when an idea grabs you.
One app, built up step by step
This page builds one app across all its steps. Every step adds to the code from the steps before it, so do them in order and do not skip any. (The Make apps pages work differently: each of their examples stands alone and goes into a fresh blank template.)
What you need
- A computer with Chrome, Edge or Firefox, and an internet connection. A Cyber Fidget is optional: the emulator, a copy of the device that runs on the page, is enough.
- About 30 minutes.
- It helps to have done Build your first app first. This page assumes you know where REBUILD is.
Step 0: plan the game¶
Before any code, get your ideas out of your head, sort them, and decide which step builds each one. Then every step below has a reason: you always know which part of the game you are making. This takes five minutes with a pencil.
Already planned, or just want to code? Skip to Start: a blank app called Catcher.
1. Brainstorm: write down every idea¶
Write down everything you want in the game, in any order, as short notes. Don't sort or judge them yet; messy is fine. This is the brainstorm for Catcher:
2. Group the ideas by what they do¶
Now sort the notes into groups of ideas that do the same kind of job. These five groups work for most small games:
The core loop the one thing you do over and over
slider moves the paddle 1 a paddle 1 dots flying across 2Rules what happens when
catch them 3 score 3 3 lives? 3Feedback what you see and hear
score at the top 4 flash the lights on a catch 5 red flash on a miss 5 beep! 6 sad sound on a miss 6Start and end how a game begins, ends and begins again
call it Catcher Start game over screen 7 play again button 7Challenge what makes it harder as you get better
gets faster 8The number on each note is the step on this page that builds it.
We call the action the player repeats all game long the core loop; here, moving the paddle to meet a dot. The extra feedback that makes an action feel good, like a flash and a beep on every catch, we call juice.
Breaking a big idea into small pieces like this is called decomposition.
3. Put the groups in build order, and sharpen each idea¶
Find the fun first: build the core loop so you can try it and feel whether it's fun before you add anything else. Then add the rules that make it a game, the feedback that makes it feel good, and the end, so you have a whole game you can lose and play again. Leave the challenge for last: how fast the dots should get is easier to tune once everything else works.
As you put each idea in order, make vague words exact. "Flash the lights on a catch" becomes "flash green on a catch, red on a miss". A piece is the right size when you can build it in a few lines and see it change when you run the game. In the table, press a step number to jump to that step.
| Step and group | Ideas from the brainstorm | Sharpened into something you can build |
|---|---|---|
| Start Start and end | call it Catcher | A blank app named Catcher. |
| 1 Core loop | a paddle; slider moves the paddle | A bar near the right edge that follows the slider. |
| 2 Core loop | dots flying across | One dot at a time, from the left edge at a random height. |
| 3 Rules | catch them; score; 3 lives? | When the dot reaches the paddle: a catch scores a point, a miss costs one of three lives. |
| 4 Feedback | score at the top | Show the score and lives at the top of the screen. |
| 5 Feedback | flash the lights on a catch; red flash on a miss | Green on a catch, red on a miss, off again after a fifth of a second. The last miss goes straight to the game-over screen with lights off. |
| 6 Feedback | beep!; sad sound on a miss | A short high beep on a catch, a lower, longer tone on a miss. The last miss goes straight to the game-over screen with sound off. |
| 7 Start and end | game over screen; play again button | A game over screen with your score, and a button to play again. |
| 8 Challenge | gets faster | The dots get faster the more you score, up to a top speed. |
Planning is the cheapest time to change your mind: moving a note or adding a new one costs nothing.
4. Ask the same questions about each piece¶
The groups tell you what to build. For each piece, also think about these five things, in plain words:
- Input: what the player does. (The slider moves the paddle; a button restarts.)
- What to remember: the numbers the game keeps from one moment to the next. (Paddle position, dot position, speed, score, lives.)
- Rules: what happens when. (The dot reaches the paddle: catch or miss.)
- Feedback: how the player finds out. (The score on screen, a flash, a beep.)
- The end: when it stops, and how to start again. (No lives left: game over; a button plays again.)
5. Write it in plain words¶
Last, write the game as pseudo-code: steps written in plain English and laid out like code. Every app has three parts, which the code below calls begin(), update() and end(), so the outline uses them too:
Remember between frames:
paddle position, dot position,
dot speed, score, lives
begin(): once, when the game opens
start fresh: score 0, 3 lives,
a new dot at the left, slow speed
listen to the middle-right button
update(): about 50 times a second
(input) read the slider, move the paddle
move the dot right by its speed
(rule) if the dot reached the paddle:
paddle in the way? catch: score + 1
green flash, high beep
otherwise? miss: lives - 1
red flash, lower tone
start a new dot at the left
(feedback) draw paddle, dot, score, lives
turn the lights off soon after a flash
(the end) if no lives are left:
stop the dot, show game over and score
otherwise: faster as the score grows
end(): once, when the game closes
stop the sound, let go of the button,
lights off
Each step below turns a few lines of this outline into C++.
Start: a blank app called Catcher¶
- Go to cyberfidget.com/create/ and press + Create an app.
- Press Code from Blank Template.
- Type
Catcheras the App Name. The Code Name fills in asCatchertoo. Pick Games as the Menu Category if you like. - Press Create.
You are looking at the starter code in the .cpp file. Every step below is one or more blocks of code. The first line of each part says where it goes:
| First line of the part | Where it goes |
|---|---|
// Insert at the top of the file, below the #include lines |
Under the four #include lines at the top, above the line Catcher catcher(HAL::buttonManager());. |
// Insert inside begin(), below setColorsOff(); |
Inside begin(), under the line setColorsOff();. |
// Insert inside update(), just above display.display(); |
Inside update(), on the line above display.display();. |
// Insert inside end(), above unregisterButtonCallbacks(); |
Inside end(), above the line unregisterButtonCallbacks();. |
When a step inserts into a place you have already used, put the new lines below the lines you added there before. Include the comment lines (the ones starting with //) too: they keep your file readable, and they match the finished listing at the end of this page. You don't need the placement line itself.
Never delete or change earlier lines. Every step only adds.
Keep this, make it your own
Keep: the starter's shape: begin(), update() and end(), the back-button code at the bottom, and display.display(); as the last line of update(). That is the framework every app runs on (see App lifecycle).
Make it your own: the numbers, sizes, speeds, colors, sounds and words in each step. Boxes like this one along the way say what each number does, and what else changes when you change it.
Step 1: a paddle that follows the slider¶
The paddle is a thin upright bar near the right edge of the screen. The slider moves it.
// Insert at the top of the file, below the #include lines
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
In the starter, nullptr means a pointer has no object. * in a declaration marks a pointer (an address), and before a pointer reads its object. & in a type refers to an existing object; before a value it gives its address. const means the code cannot change the value through that declaration.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
int is a whole number. static at the top of the file keeps a variable alive between calls to update(), so paddleY remembers its value from one frame to the next. const means the value never changes: the paddle's x position and height are fixed, so they are const and written in capitals as a reminder. sliderPosition_8Bits_Inverted_Filtered reads 0 with the slider at the bottom and 255 at the top. map() turns that 0-255 range into a screen position from 50 (low on the screen) to 0 (the top). The screen is 64 pixels tall and y counts down from the top, so 64 - PADDLE_H is the lowest the paddle can go and still fit. display.fillRect(x, y, width, height) draws a filled rectangle 3 pixels wide.
Press REBUILD & RUN (the first build of a new app; for later builds, use REBUILD). You see a short bar on the right. Drag the slider up and down: the bar follows it.
Make it your own
PADDLE_H is the paddle's height: make it smaller for a harder game. PADDLE_X moves the paddle sideways. Later steps use both names, so the catch check follows whatever you choose. Keep PADDLE_X below 125 so the paddle stays on the screen.
Step 2: a dot that flies across¶
// Insert at the top of the file, below the #include lines
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside begin(), below setColorsOff();
newDot();
dotSpeed = 1;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
newDot() is a small function of our own: calling it puts the dot back at the left edge. void in front of its name means it doesn't hand back a value. dotX += dotSpeed adds dotSpeed to dotX. random(12, 54) picks a whole number from 12 up to 53. The score in step 4 lights pixels down to y=9. The dot has a radius of 2, so its top pixel stays at y=10 or below, clear of the score. dotSpeed starts at 1 pixel per frame; step 8 will change it. update() runs about 50 times a second, so the dot moves about 50 pixels a second.
Press REBUILD. One dot crosses the screen and flies off the right edge. It does not come back yet: that is the next step. Press << RESTART to watch it again.
Make it your own
The last number in display.fillCircle(dotX, dotY, 2) is the dot's radius. If you make the dot bigger, also change the small numbers in step 3's catch check (- 2 and + 1), which allow for a radius of 2, and raise the 12 in random(12, 54) so a big dot stays clear of the score.
Step 3: catch or miss¶
When the dot reaches the paddle, the game checks whether the paddle is in its way.
// Insert at the top of the file, below the #include lines
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside begin(), below setColorsOff();
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
A bool holds either true or false. In the if, && means "and": the check runs only while you have lives left and the dot has reached the paddle. score++ adds one to the score, and lives-- takes one away.
The dot is a circle with a radius of 2 pixels, so it touches the paddle when its center is 2 pixels to the left of PADDLE_X. The - 2 and + 1 count a catch when the edge of the dot touches the paddle's top or bottom pixel (the paddle's bottom pixel is at paddleY + PADDLE_H - 1). caught and missed are only true in the frame where it happens. The next steps use them to flash the lights and beep.
begin() sets the score and lives back to their starting values. Without that, closing the game and opening it again would carry on with the old score.
Press REBUILD. Dots keep coming: each time one reaches the paddle, a new one starts at the left. The game counts the score without displaying it. After three misses the lives are used up, so the next dot flies off the edge and no new one comes.
Make it your own
To start with more lives, change the 3 in lives = 3;. It appears here twice (at the top of the file and in begin()) and once more in step 7, so change all three.
Step 4: show the score¶
// Insert at the top of the file, below the #include lines
#include <stdio.h>
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
char text[24] is a buffer: room for 24 characters of text. snprintf() fills it, putting the numbers where the %d marks are. The sizeof(text) part stops it from writing more than the buffer holds. #include <stdio.h> makes snprintf() available.
The text is centered at the top middle of the screen. The case hides about 8 pixels at each corner, so keep text out of the corners.
Press REBUILD. The score goes up when you catch a dot, and the lives go down when you miss one. At zero lives, the counting stops. Use << RESTART to reset the game until step 7 adds the game over screen and a restart button.
Step 5: flash the lights¶
// Insert at the top of the file, below the #include lines
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside begin(), below setColorsOff();
lit = false;
litAt = 0;
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
setDeterminedColorsAll() sets all four LEDs. Its numbers are red, green, blue and white, each from 0 (off) to 255 (full). millis() is the time in milliseconds (thousandths of a second), so the lights turn off just after 200 milliseconds. Subtracting unsigned times (millis() - litAt) also works when the clock wraps back to zero. The game keeps running in the meantime: there is no pause.
unsigned long is a whole number that is never negative and big enough for millis(). lit && millis() - litAt > 200 means "the lights are on and more than 200 milliseconds have passed".
Press REBUILD. The LEDs on the emulator flash green when you catch a dot and red when you miss.
Make it your own
Change the four numbers in each setDeterminedColorsAll() to pick other colors: (0, 0, 255, 0) is blue. Change 200 to make the flash longer or shorter. Keep setColorsOff() and lit = false together, so the lights always go off.
Step 6: add sound¶
// Insert inside update(), just above display.display();
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside end(), above unregisterButtonCallbacks();
HAL::audioManager().stopTone();
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
playTone(1568, 60) plays a short high beep: 1568 Hz (the note G6) for 60 milliseconds. A miss plays a lower, longer tone. stopTone() in end() makes sure no sound keeps playing after the game closes. (The starter's end() already turns the lights off.)
Press REBUILD. Catches beep. Misses buzz lower. Turn the sound up with the emulator's volume control if you cannot hear it.
Make it your own
The first number in playTone() is the pitch in Hz (higher is higher), and the second is how long it plays, in milliseconds. Keep stopTone() in end(): it makes sure your game never leaves a sound playing. Sound & Music has notes and melodies.
Step 7: game over and play again¶
When the lives run out, the game shows a game over screen. The middle-right button starts a new game.
// Insert at the top of the file, below the #include lines
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside begin(), below setColorsOff();
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside update(), just above display.display();
if (lives <= 0) {
// Pause movement and clear every effect.
dotSpeed = 0;
dotX = 8;
setColorsOff();
lit = false;
HAL::audioManager().stopTone();
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "Game over");
display.setFont(ArialMT_Plain_10);
snprintf(text, sizeof(text), "Score %d", score);
display.drawString(64, 30, text);
display.drawString(64, 44, "Right: play again");
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
if (lives <= 0) {
// Pause movement and clear every effect.
dotSpeed = 0;
dotX = 8;
setColorsOff();
lit = false;
HAL::audioManager().stopTone();
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "Game over");
display.setFont(ArialMT_Plain_10);
snprintf(text, sizeof(text), "Score %d", score);
display.drawString(64, 30, text);
display.drawString(64, 44, "Right: play again");
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
// Insert inside end(), above unregisterButtonCallbacks();
buttonManager.unregisterCallback(button_MiddleRightIndex);
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
if (lives <= 0) {
// Pause movement and clear every effect.
dotSpeed = 0;
dotX = 8;
setColorsOff();
lit = false;
HAL::audioManager().stopTone();
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "Game over");
display.setFont(ArialMT_Plain_10);
snprintf(text, sizeof(text), "Score %d", score);
display.drawString(64, 30, text);
display.drawString(64, 44, "Right: play again");
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
buttonManager.unregisterCallback(button_MiddleRightIndex);
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
onRestart is a callback: a function the device calls for you when the button changes. It is registered in begin() and let go of in end(), so the button does nothing once the game closes. It only restarts when the game is over (lives <= 0: lives at or below zero), so a press during play does nothing.
const ButtonEvent& e is what the device hands the callback: the button event, with e.eventType saying whether the button was pressed or released. The & means the callback looks at the device's own event instead of a copy, and const means it only reads it. Write callbacks with this exact first line, changing only the name.
The game over code reuses the text buffer from step 4, because both are inside update(). At game over, the dot is parked at the left with speed 0. The catch check also requires lives above 0. The score and lives stay fixed, and the lights and sound turn off. Restarting clears the score, effects and catch flags, restores three lives and the starting speed, and picks another dot height. The text is still centered, because step 4 set the alignment earlier in the same frame.
Press REBUILD. Miss three dots on purpose. The game over screen shows your score. Press the middle-right button (Middle Right - Right in the emulator) to play again. The bottom-left button still closes the game.
Step 8: speed up as you score¶
// Insert inside update(), just above display.display();
// Set the next frame's speed only while playing.
if (lives > 0) {
dotSpeed = min(3, 1 + score / 5);
}
Full code for this step
This full code is for code name Catcher (create it with App Name "Catcher"); if your app has another code name, change Catcher and catcher to yours.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
if (lives <= 0) {
// Pause movement and clear every effect.
dotSpeed = 0;
dotX = 8;
setColorsOff();
lit = false;
HAL::audioManager().stopTone();
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "Game over");
display.setFont(ArialMT_Plain_10);
snprintf(text, sizeof(text), "Score %d", score);
display.drawString(64, 30, text);
display.drawString(64, 44, "Right: play again");
}
// Set the next frame's speed only while playing.
if (lives > 0) {
dotSpeed = min(3, 1 + score / 5);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
buttonManager.unregisterCallback(button_MiddleRightIndex);
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
Dividing whole numbers in C++ drops the remainder, so score / 5 is 0 for scores 0 to 4, 1 for 5 to 9, and so on. min(3, ...) chooses the smaller number, so the speed stops growing at 3 pixels per frame. At about 50 frames per second, even the fastest dot takes about 0.7 seconds to reach the paddle. This leaves time to aim with the slider. The speed applies on the next frame and stays at 0 during game over.
Press REBUILD. After 5 catches the dots move twice as fast. After 10 they reach their top speed of 3 pixels per frame. Miss three dots, wait on the game over screen, then restart: the dots begin slowly again.
Make it your own
5 is how many catches it takes to speed up, and 3 is the top speed in pixels per frame. A top speed much above 4 is very hard to catch with the slider. The 1 is the starting speed; if you change it, change dotSpeed = 1; in step 2 and step 7 to match.
The whole game¶
This is the complete Catcher.cpp after all eight steps. If your game does not build, compare your file with it. The Catcher.h file is unchanged from the template.
#include "Catcher.h"
#include "globals.h"
#include "HAL.h"
#include "MenuManager.h"
// The paddle: a thin upright bar near the right edge.
static const int PADDLE_X = 116;
static const int PADDLE_H = 14;
static int paddleY = 25;
// The dot starts at the left edge, at a random height.
static int dotX = 8;
static int dotY = 32;
// Pixels to move each frame. Zero pauses the dot.
static int dotSpeed = 1;
static void newDot() {
dotX = 8;
dotY = random(12, 54);
}
static int score = 0;
static int lives = 3;
// Set for one frame when the dot is caught or missed.
static bool caught = false;
static bool missed = false;
#include <stdio.h>
// When the lights came on, so they can go off again.
static bool lit = false;
static unsigned long litAt = 0;
// The middle-right button starts a new game after game over.
static void onRestart(const ButtonEvent& e) {
if (e.eventType == ButtonEvent_Pressed && lives <= 0) {
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
setColorsOff();
HAL::audioManager().stopTone();
newDot();
dotSpeed = 1;
}
}
Catcher catcher(HAL::buttonManager());
Catcher* Catcher::instance = nullptr;
Catcher::Catcher(ButtonManager& btnMgr) :
buttonManager(btnMgr), display(HAL::displayProxy()) { instance = this; }
// begin() runs once when your app opens from the menu.
// Set things up here: hook up the buttons and start with the lights off.
void Catcher::begin() {
registerButtonCallbacks();
setColorsOff();
newDot();
dotSpeed = 1;
// Start every game fresh when the app opens.
score = 0;
lives = 3;
caught = false;
missed = false;
lit = false;
litAt = 0;
buttonManager.registerCallback(button_MiddleRightIndex, onRestart);
}
// update() runs about 50 times a second while your app is open.
// Clear the screen, draw what you want, then show it.
// The case hides about 8 pixels at each screen corner, so keep text and
// important pixels out of those corners.
void Catcher::update() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
display.drawString(64, 24, "Catcher");
// Wipe the template's title so the game has the whole screen.
display.clear();
// Slider at the top puts the paddle at the top of the screen.
paddleY = map(sliderPosition_8Bits_Inverted_Filtered, 0, 255, 64 - PADDLE_H, 0);
display.fillRect(PADDLE_X, paddleY, 3, PADDLE_H);
// Move the dot at its current speed, then draw it.
dotX += dotSpeed;
display.fillCircle(dotX, dotY, 2);
caught = false;
missed = false;
if (lives > 0 && dotX >= PADDLE_X - 2) {
if (dotY >= paddleY - 2 && dotY <= paddleY + PADDLE_H + 1) {
caught = true;
score++;
} else {
missed = true;
lives--;
}
newDot();
}
// Write the score and lives into a short piece of text, then draw it.
char text[24];
snprintf(text, sizeof(text), "Score %d Lives %d", score, lives);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 0, text);
if (caught) {
setDeterminedColorsAll(0, 255, 0, 0);
lit = true;
litAt = millis();
}
if (missed) {
setDeterminedColorsAll(255, 0, 0, 0);
lit = true;
litAt = millis();
}
if (lit && millis() - litAt > 200) {
setColorsOff();
lit = false;
}
if (caught) {
HAL::audioManager().playTone(1568, 60);
}
if (missed) {
HAL::audioManager().playTone(900, 150);
}
if (lives <= 0) {
// Pause movement and clear every effect.
dotSpeed = 0;
dotX = 8;
setColorsOff();
lit = false;
HAL::audioManager().stopTone();
display.clear();
display.setFont(ArialMT_Plain_16);
display.drawString(64, 10, "Game over");
display.setFont(ArialMT_Plain_10);
snprintf(text, sizeof(text), "Score %d", score);
display.drawString(64, 30, text);
display.drawString(64, 44, "Right: play again");
}
// Set the next frame's speed only while playing.
if (lives > 0) {
dotSpeed = min(3, 1 + score / 5);
}
display.display();
}
// end() runs when your app closes. Let go of the buttons and turn the
// lights off (if your app plays sounds, call stopTone() here too).
void Catcher::end() {
HAL::audioManager().stopTone();
buttonManager.unregisterCallback(button_MiddleRightIndex);
unregisterButtonCallbacks();
setColorsOff();
}
// The bottom-left button closes your app and goes back to the menu.
void Catcher::onButtonBackPressed(const ButtonEvent& event) {
if (event.eventType == ButtonEvent_Released) {
instance->end();
MenuManager::instance().returnToMenu();
}
}
void Catcher::registerButtonCallbacks() {
buttonManager.registerCallback(button_BottomLeftIndex, onButtonBackPressed);
}
void Catcher::unregisterButtonCallbacks() {
buttonManager.unregisterCallback(button_BottomLeftIndex);
}
The finished game¶
The finished Catcher in the emulator: dots fly in from the left, the paddle catches them, and a miss costs a life.
The game is yours: install it on your Cyber Fidget the same way as in Build your first app.
Swap the dot for a sprite
To draw a picture instead of the dot, follow one of the three options in Sprites & Animation, then draw your sprite at dotX, dotY in place of display.fillCircle(). If you use an actor, name it something like dotActor, not after your sprite.
Ideas to try next¶
- Play with the buttons instead of the slider. Read
buttonManager.isPressed(button_TopLeftIndex)inupdate()and move the paddle a pixel at a time. See Buttons & Events. - Swap the dot for a picture. Draw a sprite in the Studio and draw it where the dot is. See Sprites & Animation.
- A best score. Keep a
static int bestand update it at game over. See Drawing to the Screen for more about text. - A tune at game over. Play a short melody when the lives run out. See Sound & Music.
- Rainbow lights. Light the LEDs in a color that changes with the score. See LEDs & Color.
- Tilt to play. Move the paddle by tilting the Cyber Fidget. See Motion Sensing and The Slider.
- Look up anything else in the function reference.