TapTap

Creations

JulienGio

3 months ago

Let's start chapter 2 with a game that will pave the way for all future games.

Tap Tap, how fast can you tap?

Length 45 minutes

Level Beginner

TapTap is a simple 2-button game. The goal is hit the left and right arrows to match the falling bricks as fast as possible. The bricks are randomly generated and fall forever (or at least until you run out of power). For each brick cleared correctly, the player is awarded points (if he/she misses, the score is reset to 0). To force the player to play fast, the score slowly decrease constantly.

While programming TapTap for the Gamebuino, we will see three important game development concepts. First of all, we will learn how to organize our code. Then we will take a first look at arrays and constants in C++. These are three of the essential points of this chapter, they will pop-up in nearly all games you will ever do :0

Inputs, update, draw

Before diving head first into our code, let's hold up for a moment and ask ourselves "How will I organize my code?" Thankfully, in the world of video games, the main game loop is always the same :

  1. 1. Gather player inputs.
  2. 2. Update the game logic.
  3. 3. Draw the game to the display.

And actually, based on the platform you are working with, there may be more steps. For example, the throttling of the execution speed of our loop (so that the game is identical, regardless of whether you are playing on a PlayStation, an Xbox, or a computer). But this is already handled by gb.update() for us, our games will always run at 25 frames per second. So in reality, there should be a 0th step : while(!gb.update());

By the way, you have already used this very method in the Pong workshop! Step 1 of the method was simply to check for button presses. The 2nd step was responsible for all of the game logic (collisions, AI). And all the drawing at the end was step 3.

This "Input / Update / Draw" method is really useful. First of all it allows to be able to more rapidly juggle between different parts of our game, as each part is clearly separated. It also decouples the inputs from the logic from the drawing (we will see the advantages of this shortly). Let's start by placing comments to divide each part:

#include 

void setup() {
    gb.begin();
}

void loop() {
    while(!gb.update());

    // INPUTS //


    // LOGIC //


    // DRAW //
    gb.display.clear();
}

I also placed other things like gb.begin();, but nothing new ;)

Our first prototype

When we made Pong, we started by simply having a single paddle with a ball. Once that was functional, we added a second paddle and a scoring system. So, like Pong, we should begin by creating a simpler first version with the bare minimum of features. Then, we will build on top of those foundations.

So this first version will only display the first brick. This means that we only have to keep track of one thing, like so:

If the brick is to the left, and the player presses the left arrow, then 15 points will be awarded and a new brick appears. The logic is the same if the brick is to the right. If the player ever misses, his/her score will be reset. Finally, to add a little challenge, the score will progressively lower.

Constants

The game must have a way to keep track of which side the current brick occupies, so that is can check whether the player is right or not. Let's declare these variables.

#include 

int brick;  // Position of our brick. 1 corresponds to left and 2 to right
int arrow;  // 1 corresponds to left, 2 to right, and 0 means no presses yet (waiting on player)
int score = 0;


void setup() {
    gb.begin();
    brick = random(1, 3);  // 50% left, 50% right
    arrow = 0;
}

void loop() {
    while(!gb.update());

    // INPUTS //


    // LOGIC //


    // DRAW //
    gb.display.clear();
}

So here we just created three variables that we need. brick is an integer which is either 1 or 2. If it is 1, then we consider the brick to be on the left. Otherwise, if it is worth 2, then we consider it to be on the right. arrow is also an integer and, like brick, 1 is left, 2 is right, and if the player hasn't pressed a key yet, it is worth 0. Well! This was a lot to process for not a lot of behavior - we only have 2 directions here!

How could we improve on this code to make it easier to think about? Well with constants obviously :D

A constant is just like a variable, except for the fact that, once set, its value cannot change (hence the name). A constant is simply a name given to a value. It is very useful in situations like ours: when we have arbitrary values. Like the number 1 corresponding to the side 'left'. We could have chosen another value for 'left', like 12 or -358, instead. Without constants, we type brick = 1; to mean "the brick is on the left". But we can write brick = LEFT;! Much better right? We don't even need to comment this line really :p

So then, how do we create constants? Well, like I said, constants are nearly identical variables. All you need to do is add the key word const (short for 'constant') before the type in the declaration:

const int LEFT = 1;

However, unlike variables, you must give a value to your constant when it is declared. This is simply because, after the declaration, a constant's value cannot change.


#include 

// Constants
const int LEFT = 1;
const int RIGHT = 2;
const int NO_DIRECTION = 3;

int brick;  // Position of our brick. Either LEFT or RIGHT
int arrow;  // LEFT, RIGHT, or NO_DIRECTION
int score = 0;

void setup() {
    gb.begin();
    brick = random(LEFT, RIGHT + 1);  // +1 because random(1, 3) => 1 or 2 (not 3)
    arrow = NO_DIRECTION;
}

void loop() {
    // loop...
}

And there we go, we are now using some very useful constants :D The brick is either to the left or to the right. The same goes for arrow (except that it can also be in NO_DIRECTION). Way easier to read right?


If you read somebody else's code, you might stumble on another way of making constants. They might use #define LEFT 1. This is (almost) identical to const int LEFT = 1;. The differences are not important right now, be we suggest that you use the const method we just saw. It is easier to debug in the eventality of a compiling error. We will only be using const in these workshops.


Now we can implement the inputs as well as the drawing with ease :

#include 

// Constants
const int LEFT = 1;
const int RIGHT = 2;
const int NO_DIRECTION = 3;

int brick;  // Position of our brick. Either LEFT or RIGHT
int arrow;  // LEFT, RIGHT, or NO_DIRECTION
int score = 0;

void setup() {
    gb.begin();
    brick = random(LEFT, RIGHT + 1);  // +1 because random(1, 3) => 1 or 2 (not 3)
    arrow = NO_DIRECTION;
}

void loop() {
    while(!gb.update());

    // INPUTS //
    if (gb.buttons.released(BUTTON_LEFT)) {
        arrow = LEFT;
    }
    else if (gb.buttons.released(BUTTON_RIGHT)) {
        arrow = RIGHT;
    }

    // LOGIC //


    // DRAW //
    gb.display.clear();

    if (brick == LEFT) {
        gb.display.fillRect(20, 40, 20, 10);
    } 
    else {  // RIGHT
        gb.display.fillRect(40, 40, 20, 10);
    }

    // Score
    gb.display.print(score);
}

Our simplified prototype is nearly done :) We are just missing the game logic.

Here is a reminder of our goal:


  • If the player didn't press anything, do nothing
  • Else, if the arrow corresponds to the brick, 15 points are awarded, and a new brick is generated.
  • Else, reset the score

So there are three possible outcomes every single frame. But the first one is an outcome that says "no outcome". We can move the logic around to obtain this:

  • If the player pressed an arrow:
    • if the arrow corresponds to the brick, 15 points are awarded, and a new brick is generated.
    • Else reset the score

This give us the following code:

// Constants //
// Variables //

void setup() {
    // setup //
}

void loop() {
    // INPUTS //
    // ...

    // LOGIC //
    // Slowly lower the score
    if (score > 0) {
        score -= 1;
    }

    // Did the player press an arrow?
    if (arrow != NO_DIRECTION) {
        if (brick == arrow) {  // Correct arrow press
            score += 15;
            brick = random(LEFT, RIGHT + 1);
        }
        else {
            // Lost :(
            score = 0;
        }

        arrow = NO_DIRECTION;  // User input handled
    }

    // DRAW //
    // ...
}

I also added a mechanic that lowers the score over time : score -= 1;. The x -= y operator is a shortcut for x = x - y;. This also exists for most operators:

  • +=
  • -=
  • *=
  • /=
  • And others...

You can find += when I increase the score. Don't forget to reset arrow's value after it was handled. Otherwise the Gamebuino will think that the arrow is constantly being pressed!



And there it is, our prototype is done! You can upload it (if you still haven't do so already :P) and play.

Arrays

Our little demo isn't very engaging. There is only one brick on-screen, it is impossble to play fast. So let's add some bricks to help the player. Just so we are clear, the player will have to hit the brick that is on the bottom of the screen, but the other brick we help him/her anticipate :D

Naturally, you may say that we just need to add brick1, brick2, brick3, etc to do this. And this is technically a viable solution, it is not the right way to do this.

How will we code the bricks then? Well, we will be using an array. In C++, an array is a list that contains a specific number of elements. If we think of variables as boxes, then an array is a chain of boxes.

And instead of having a name of each box, we will have a name for the chain. Array are powerful because they can contain any type and quantity of elements. Here we want an array of integers. To access an element of the array, simply place the index of the element in square brackets [ ]: arrayName[elementIndex]. But be careful, arrays start at 0, so:

First element :        arrayName[0]
Second element :    arrayName[1]
Third element :        arrayName[2]
...
Nth element :        arrayName[N - 1]
...
Last element :        arrayName[NUM_OF_ELEMENTS - 1]

Okay, but this is to interact with the elements of the array. How do we create an array?


int arrayName[NUM_OF_ELEMENTS];

To declare an array of integers, it is almost like declaring an integer. We first place the type of the array's elements. Then it's name, followed by the number of elements within square brackets [ ]


int gamebuinoTeamAges[3] = {26, 23, 19};

You can also initialize the array with your own values by using curly brackets { }. BUT, you can only do this during the declaration. If you try to do this elsewhere, you will get a compiling error!


To get back to our TapTap game, we will need an array bricks. To start off, we will have four bricks. And we will use a constant to keep track of how many bricks we have. Also, bricks[0] will be the bottom-most brick (the one the player has to hit).

#include 

// Constants
// ...
const int NUM_OF_BRICKS = 4;

int bricks[NUM_OF_BRICKS];  // Our bricks. Either LEFT or RIGHT
int arrow;  // LEFT, RIGHT, or NO_DIRECTION
int score = 0;

void setup() {
    gb.begin();

    // Random bricks
    bricks[0] = random(LEFT, RIGHT + 1);  // +1 because random(1, 3) => 1 or 2 (not 3)
    bricks[1] = random(LEFT, RIGHT + 1);
    bricks[2] = random(LEFT, RIGHT + 1);
    bricks[3] = random(LEFT, RIGHT + 1);

    arrow = NO_DIRECTION;
}

void loop() {
    while(!gb.update());

    // INPUTS //
    // ...

    // LOGIC //
    if (score > 0) {
        score -= 1;
    }
    if (arrow != NO_DIRECTION) {
        if (bricks[0] == arrow) {  // Correct arrow
            score += 15;

            // Shift bricks down ([0] becomes [1], etc...)
            bricks[0] = bricks[1];
            bricks[1] = bricks[2];
            bricks[2] = bricks[3];
            bricks[3] = random(LEFT, RIGHT + 1);  // New brick
        }
        else {
            // Lost :(
            score = 0;
        }

        arrow = NO_DIRECTION;  // User input handled
    }

    // DRAW //
    gb.display.clear();

    if (bricks[0] == LEFT) {
        gb.display.fillRect(20, 40, 20, 10);
    } 
    else {  // RIGHT
        gb.display.fillRect(40, 40, 20, 10);
    }
    if (bricks[1] == LEFT) {
        gb.display.fillRect(20, 30, 20, 10);
    } 
    else {  // RIGHT
        gb.display.fillRect(40, 30, 20, 10);
    }
    if (bricks[2] == LEFT) {
        gb.display.fillRect(20, 30, 20, 10);
    } 
    else {  // RIGHTRIGHT
        gb.display.fillRect(40, 30, 20, 10);
    }
    if (bricks[3] == LEFT) {
        gb.display.fillRect(20, 20, 20, 10);
    } 
    else {  // RIGHT
        gb.display.fillRect(40, 20, 20, 10);
    }


    // Score
    gb.display.print(score);
}

Now that we use an array bricks of size 4, we give it random values inside of setup(). Regarding inputs, nothing to report (thanks to the fact that we "decoupled" inputs from the rest). For the logic and drawing of the game, we must adapt the existing code to handle our array.

For the logic update, we also made it so that bricks "fall". Each brick takes the value of the one above it, and the top bricks gets a random value. In the drawing section, we display each brick one by one. You may note that our code almost has identical lines repeating themeseves. In the next workshop, we will explore a way to simplify this kind of code :D

But before that...

It's your turn!

This is the first workshop of chapter 2, and it allowed us to present three key concepts.

The "Input, Update, Draw" structure is very useful. It allows us to better organize our code, and splits the player's actions, the game logic, and what the player sees. Constants allow us to write code with ease and arrays are a new data structure that allows us to group many variables into a list (more of their power will we unleashed in the next tutorial)!

With all of this, we built a game: TapTap. It is simple but fun. Now, it is up to you to improve it. Here's what I propose. Start by adding a highscore system. Then, to improve gameplay, add another brick or two. You can also make the game harder by tweeking the score system so that, the bigger the score, the faster it lowers per frame!

  • Tip #1 : To add bricks, change NUM_OF_BRICKS and don't forget to modify the drawing so that all bricks fit in the screen.
  • Tip #2 : To make the game harder as your score goes up, you can try to remove a part of the current score with the division operator /.

Show off your game online with #gamebuino #workshop #TapTap, we go through them all the time ;)

Solution Example

If you ran out of ideas, here is what we did on our side. Hope it helps :)

#include 

 




const int LEFT = 1;
const int RIGHT = 2;
const int NO_DIRECTION = 3;
const int NUM_OF_BRICKS = 5; 




int bricks[NUM_OF_BRICKS];  // Our bricks. Either LEFT or RIGHT
int arrow;  // Either LEFT, RIGHT, or NO_DIRECTION
int score = 0;
int highscore = 0;




void setup() {
  gb.begin();




  // Shuffle bricks
  bricks[0] = random(LEFT, RIGHT + 1);
  bricks[1] = random(LEFT, RIGHT + 1);
  bricks[2] = random(LEFT, RIGHT + 1);
  bricks[3] = random(LEFT, RIGHT + 1);
  bricks[4] = random(LEFT, RIGHT + 1);




  arrow = NO_DIRECTION;
}




void loop() {
  while (!gb.update());




  // INPUTS //
  if (gb.buttons.released(BUTTON_LEFT) || gb.buttons.released(BUTTON_A)) {
    arrow = LEFT;
  } else if (gb.buttons.released(BUTTON_RIGHT) || gb.buttons.released(BUTTON_B)) {
    arrow = RIGHT;
  }




  // UPDATE //
  // Lower score with time. The bigger the score, the faster is goes down
  if (score > 0) {
    score -= score / 60;
  }




  if (arrow != NO_DIRECTION) {
    if (bricks[0] == arrow) {
      score += 20;
      if (score > highscore) {  // Highscore ??
        highscore = score;
      }




      // Shift down
      bricks[0] = bricks[1];
      bricks[1] = bricks[2];
      bricks[2] = bricks[3];
      bricks[3] = bricks[4];
      bricks[4] = random(LEFT, RIGHT + 1);  // New brick
    }
    else {  // Lost :(
      score = 0;
    }




    arrow = NO_DIRECTION;  // User input handled
  }




  // DRAW //
  gb.display.clear();




  if (bricks[0] == LEFT) {
    gb.display.fillRect(25, 40, 20, 10);  // fillRect for the lower brick. drawRect for the others
  } else {
    gb.display.fillRect(35, 40, 20, 10);
  }
  if (bricks[1] == LEFT) {
    gb.display.drawRect(25, 30, 20, 10);
  } else {
    gb.display.drawRect(35, 30, 20, 10);
  }
  if (bricks[2] == LEFT) {
    gb.display.drawRect(25, 20, 20, 10);
  } else {
    gb.display.drawRect(35, 20, 20, 10);
  }
  if (bricks[3] == LEFT) {
    gb.display.drawRect(25, 10, 20, 10);
  } else {
    gb.display.drawRect(35, 10, 20, 10);
  }
  if (bricks[4] == LEFT) {
    gb.display.drawRect(25, 0, 20, 10);
  } else {
    gb.display.drawRect(35, 0, 20, 10);
  }




  // Score
  gb.display.fillRect(0, gb.display.height() - (score / 5), 4, (score / 5));  // We divide the score by 5 to make it fit inside the screen




  // Highscore
  gb.display.drawFastHLine(0, gb.display.height() - (highscore / 5), 6);
}



Next Workshop

By Julien Giovinazzo

Questions / comments / suggestions, be sure to drop a comment down below!

View full creation

jicehel

NEW 3 months ago

Super, très clair. PS: Le bouton Atelier suivant apparaît en "Next Workshop" sur les derniers tutos.

JulienGio

2 months ago

Merci :)

JulienGio

NEW 2 months ago

jicehel jicehel

Merci :)

Juice_Lizard

NEW 1 month ago

Bon tuto. Les tableaux, c'est pas évident à intégrer. Il y a des petits erreurs dans l'article: "setUp" avec un "u" majuscule. J'ai un peu galéré à trouver pourquoi ça marchait pas.

Aurélien Rodot

1 month ago

Merci, c'est corrigé ! :)

Aurélien Rodot

NEW 1 month ago

Juice_Lizard Juice_Lizard

Merci, c'est corrigé ! :)

You must be logged in in order to post a message on the forum

Log in