Since the code is so small Iāll post it here with your comments for others to see more easily.
Expand for source code
#include <Arduboy2.h>
#include <Sprites.h>
#include <ArduboyTones.h>
#include "graphics.h"
static const uint8_t actionButtons[] = {0, UP_BUTTON, DOWN_BUTTON, LEFT_BUTTON, RIGHT_BUTTON, A_BUTTON, B_BUTTON};
static const uint8_t ACTION_IDLE = 0;
static const uint8_t ACTION_JUMP = 1;
static const uint8_t ACTION_DUCK = 2;
static const uint8_t ACTION_SLIDE_LEFT = 3;
static const uint8_t ACTION_SLIDE_RIGHT = 4;
static const uint8_t ACTION_RAISE_LEFT = 5;
static const uint8_t ACTION_RAISE_RIGHT = 6;
static const uint8_t ACTION_DONE = 7;
static const uint8_t STATE_MAINMENU = 0;
static const uint8_t STATE_LEARN = 1;
static const uint8_t STATE_PLAY = 2;
static const uint8_t STATE_DONE = 3;
// tuxinator
//Using Arduboy2 instead of Arduboy2Base for now so I can use arduboy.print for debug info
Arduboy2 arduboy;
// lyellick
// Using direct declaration to save resources
ArduboyTones tones(arduboy.audio.enabled);
Sprites sprites;
// tuxinator
//notes starts at 208hz since below that can't be heard well on the Arduboy's tiny piezo
//notes stops at 7902hz for similar reasons
//I've also removed all the sharps/flats to keep it to normal notes for increased pitch as the player progresses
//5 octaves with 7 notes per octave and 6 actions to perform leaving 1 note for the interim beat.
uint16_t notes[] =
{
262, 294, 330, 349, 392, 440, 494, //octave 0
523, 587, 659, 698, 784, 880, 988, //octave 1
1047,1175,1319,1397,1568,1760,1976, //octave 2
2093,2349,2637,2794,3136,3520,3951, //octave 3
4186,4699,5274,5588,6272,7040,7902 //octave 4
};
uint8_t jumpOffsets[] =
{
4, 8, 11, 14, 16, 18, 19, 20,
20, 19, 18, 16, 14, 11, 8, 4
};
uint32_t startSeed;
uint32_t currentSeed;
uint32_t score = 0;
uint8_t octave = 0;
uint8_t action = ACTION_IDLE;
uint8_t actionDuration = 0;
uint8_t actionRate = 5;
uint8_t increaseRate = 6 - actionRate;
int8_t roadOffset = 0;
uint8_t state = STATE_DONE;
uint8_t numMoves = 3;
uint8_t currentMove = 0;
// lyellick
// Is this basically scrambling the generated seed to make it even more random?
// I have rudimentary understanding of left/right shifting and bitwise operators,
// but is there a reason you chose prime numbers?
void rnd()
{
currentSeed ^= currentSeed << 13;
currentSeed ^= currentSeed >> 7;
currentSeed ^= currentSeed << 17;
}
void setup()
{
arduboy.begin();
arduboy.clear();
arduboy.setFrameRate(60);
startSeed = currentSeed = arduboy.generateRandomSeed();
}
void loop()
{
if (!arduboy.nextFrame())
return;
// lyellick
// When the actionDuration is 0, meaning done animiating, it checks the state it should prep for.
if (actionDuration == 0)
{
// lyellick
// Sets up the moves it is about to show you until the max number of moves have been played. Then it lets the player input them.
if (state == STATE_LEARN)
{
rnd();
// lyellick
// Could you expand on this line here?
// You have your seed, then you half it efficiently with the right shift, but I get lost on why you modulus 6 and add 1.
// Seems this algorthm always returns a random actions.
action = (currentSeed >> 1) % 6 + 1;
actionDuration = 16;
// tones.tone(notes[action - 1 + octave * 7] + TONE_HIGH_VOLUME);
++currentMove;
if (currentMove == numMoves)
{
state = STATE_PLAY;
currentMove = 0;
currentSeed = startSeed;
}
}
// lyellick
// I am not totally sure how you are keeping track of what moves were shown to a player and how it is kept track of when the player inputs moves.
else if (state == STATE_PLAY)
{
action = 0;
// lyellick
// The docs say to do it once per frame and since both your pollButtons() are within a condition.
// I ran the game removing both pollButtons() and placed it right after the nextFrame() code block and it ran the same.
// Do you place this here because you want pollButtons() close to where you are calling justPressed()?
arduboy.pollButtons();
// lyellick
// This is looping through the available buttons and checking which one was pressed and setting it to the current action.
// I like this way better than checking via a switch statement, which I tend to do.
// Pharap gave a code example similar to this.
for (uint8_t i = 1; i < 7; ++i)
{
if (arduboy.justPressed(actionButtons[i]))
action = i;
}
if (action != 0)
{
rnd();
if (action == (currentSeed >> 1) % 6 + 1)
{
// tones.tone(notes[action - 1 + octave * 7] + TONE_HIGH_VOLUME);
actionDuration = 16;
++currentMove;
++score;
if (currentMove == numMoves)
{
currentSeed = startSeed;
score += numMoves;
currentMove = 0;
++numMoves;
--increaseRate;
if (increaseRate == 0 && actionRate > 1)
{
--actionRate;
++octave;
increaseRate = 6 - actionRate;
}
state = STATE_LEARN;
}
}
else
{
state = STATE_DONE;
action = ACTION_DONE;
}
}
}
else if (state == STATE_DONE)
{
// if (!tones.playing())
// tones.tone(notes[4] + TONE_HIGH_VOLUME, 100, notes[2] + TONE_HIGH_VOLUME, 100, notes[0] + TONE_HIGH_VOLUME, 100);
arduboy.pollButtons();
if (arduboy.justPressed(A_BUTTON|B_BUTTON))
{
startSeed = currentSeed = arduboy.generateRandomSeed();
score = 0;
octave = 0;
action = ACTION_IDLE;
actionDuration = 0;
actionRate = 5;
increaseRate = (6 - actionRate) * 2;
state = STATE_LEARN;
numMoves = 3;
currentMove = 0;
}
}
}
// lyellick
// This must be the core of the animations on screen prior to printing.
// I was trying to do something similar to this in my September game, but mostly brute forced duration by counting frames.
// This will be nice to come back and try to impliment this.
else if (arduboy.everyXFrames(actionRate))
{
--actionDuration;
if (actionDuration <= 4)
{
// tones.noTone(); // tuxinator
if (action != ACTION_JUMP) //end ducking animation slightly early
action = ACTION_IDLE;
}
if (action == ACTION_SLIDE_LEFT)
--roadOffset;
else if (action == ACTION_SLIDE_RIGHT)
++roadOffset;
}
if (roadOffset < 0)
roadOffset += 8;
else if (roadOffset >= 8)
roadOffset -= 8;
// tuxinator
//Draw some debug info
arduboy.setCursor(0, 0);
// tuxinator
//Always surround constant strings in F() in order to put them in PROGMEM and save precious RAM
arduboy.print(F("Score: "));
arduboy.println(score);
arduboy.print(F("Speed: "));
arduboy.println(6 - actionRate);
// tuxinator
//Draw the current action
sprites.drawSelfMasked(112, 0, icons, action);
// tuxinator
//Draw the sprite
if (action == ACTION_JUMP)
sprites.drawSelfMasked(52, 24 - jumpOffsets[actionDuration], sprite, action);
else
sprites.drawSelfMasked(52, 24, sprite, action);
// tuxinator
//The screen is 16 tiles wide meaning at most 17 can be visible
for (uint8_t i = 0; i <= 17; ++i)
sprites.drawSelfMasked(i * 8 - roadOffset, 56, road, 0);
arduboy.display(CLEAR_BUFFER);
}
// lyellick
// Is this basically scrambling the generated seed to make it even more random?
// I have rudimentary understanding of left/right shifting and bitwise operators,
// but is there a reason you chose prime numbers?
void rnd()
For the rnd function it actually is just an Xorshift Random Number Generator algorithm that takes a starting value (the seed) and generates a new number (that new number is also the seed for the next number). Giving it the same seed will generate the same sequence which is why thereās a variable startSeed and currentSeed so that when switching over to the playerās control the sequence is able to easily be restarted from the beginning (and re-seeded when a new game begins).
rnd();
// lyellick
// Could you expand on this line here?
// You have your seed, then you half it efficiently with the right shift, but I get lost on why you modulus 6 and add 1.
// Seems this algorthm always returns a random actions.
action = (currentSeed >> 1) % 6 + 1;
The call to rnd(); just before the comment is important here as it updates currentSeed to the next value in the pseudo-random sequence used to pick a random action.
The (currentSeed >> 1)
part is used because I remember these simpler types of pseudo-random number generators tend to have more apparent randomness in the upper bits then the lower ones. So I shift the bits over by 1 to minimize repetition of actions and get a slightly more ārandomā output. Try playing around the the amount of shifting done (up to 28 for this formula) and see what the resulting sequences looks like (you can change numMoves = 3
to numMoves = 255
to just watch a long sequence of actions for testing).
The second part % 6
is because there are 6 different actions for the 6 different buttons (UP=Jump, DOWN=Duck, LEFT=Slide Left, RIGHT=Slide Right, A=Raise hand on playerās left, B=Raise hand on playerās right). So a modulus by 6 will give us a value between 0 and 5 inclusive (meaning either a 0, 1, 2, 3, 4, or 5). Since ACTION_IDLE=0
and our first action (ACTION_JUMP
) starts at 1 we add a 1 to the result of the modulo to get a value from 1-6 inclusive (ie. 1, 2, 3, 4, 5, or 6) which directly corresponds to the chosen action.
// lyellick
// I am not totally sure how you are keeping track of what moves were shown to a player and how it is kept track of when the player inputs moves.
Haha this was the fun little semi-advanced trick making use of how Xorshift Random Number Generators work. Iāll mention again here briefly but when supplying an initial starting value (seed) you will always get the exact same number sequence. So by storing the starting seed for the sequence and running the generator for each step allows the sequence to be started back from the beginning by simply setting currentSeed = startSeed
. This way when the player presses a button to perform an action it effectively regenerates the randomly chosen action at that step in the given sequence and sees if it matches the action chosen by the player.
// lyellick
// The docs say to do it once per frame and since both your pollButtons() are within a condition.
// I ran the game removing both pollButtons() and placed it right after the nextFrame() code block and it ran the same.
// Do you place this here because you want pollButtons() close to where you are calling justPressed()?
arduboy.pollButtons();
The reason I placed the pollButtons here was because I wanted to use the justPressed
routine instead of the pressed
routine thus forcing the player to let go of the button and press it again for repeated actions (helps with people with slower reaction times from accidentally issuing the same action twice). The way justPressed
works is it only fires once between calls to pollButtons()
. That is to say if a call to pollButtons()
is made and then a button is pressed and held up to the next call to pollButtons()
justPressed()
will return true for that button but will then be false after subsequent calls to pollButtons()
until the button is released and pressed again. Doing it this way allows the player to press and hold the button for the next step in the sequence while the currently chosen actionās animation is played out (only requiring them to wait if repeating the same action more than once).
Normally you would have pollButtons()
done every frame though.
// lyellick
// This is looping through the available buttons and checking which one was pressed and setting it to the current action.
// I like this way better than checking via a switch statement, which I tend to do.
// Pharap gave a code example similar to this.
for (uint8_t i = 1; i < 7; ++i)
{
if (arduboy.justPressed(actionButtons[i]))
action = i;
}
There is actually an even better method for checking if the correct button was pressed but I didnāt use it since itās not as easily done (requires creating an Arduboy2EX class to extend the base classes functionality). The trick is to get the previous button state and current button state and generate a justPressed state (each bit corresponding to a button is on if it was just pressed or off if it wasnāt just pressed). Then you could simply check if the value is non-zero (meaning at least one button was pressed) and if itās equal to the chosen actionās button bit-mask (meaning only that button was just pressed). This can be a little more complex for beginners though so I avoided it intentionally. Not sure which one would take less code though overall.
// lyellick
// This must be the core of the animations on screen prior to printing.
// I was trying to do something similar to this in my September game, but mostly brute forced duration by counting frames.
// This will be nice to come back and try to impliment this.
else if (arduboy.everyXFrames(actionRate))
This part is also interesting because Iām varying the value passed to everyXFrames
in order to increase the overall speed as you progress. At speed 1 actionRate = 5
so every 5 frames it steps the animation (thus playing slowly). In the part that checks for a sequence being completed it will track the number of sequences completed since the last speed increase and if itās equal to the current speed then itāll increase it by 1 up to a speed of 5 (every 1 frame, 16 frames of animation at 60 frames per second means each animation takes about 0.25 seconds). When the speed increases it also increases the octave for the notes played (thus increasing their pitch as the speed increases).
If you, or anyone else, has anymore questions feel free to ask. Like I said the main goal of this project was to be a very basic (but functional and fun) project for beginners to take a loot at and dissect itās inner workings. The fact that I stumbled upon something thatās actually been rather fun to play (I find myself pausing what Iām doing and playing it quite frequently now) is kind of cool in itās own regard.
I really hope this project (and these questions and answers) will help beginners who arenāt really familiar with c++ or even programming in general realize that you can make a nice, fun, simple project with only a small amount of code. Also, there will always be those of us here that are more than willing to help newcomers to the Arduboy, even if theyāve never done c++ before or any programming at all for that matter.