Introduction to Robotics @Unibuc
🏋️♀️ How to play the game
Goal is to eat the food and get a new high score. The game ends when the snake eats himself or collides with the obstacles/walls.
You can scroll through the menus moving the joystcik up/down. You can choose an option from the menu moving the joystick right. Menu design:
- MAIN MENU
- START - choose to start the game
- HIGH SCORE
- SETTINGS (MENU)
- DIFFICULTY
- LOW/MEDIUM/HIGH
- SOUND
- MUTE/UNMUTE
- LCD CONTRAST
- 1/2/3
- LCD BRIGHTNESS
- 1/2/3
- MATRIX BRIGHTNESS
- 1/2/3
- BACK (TO MAIN MENU)
- ABOUT
The score depends on the difficulty:
- LOW: 1 point/level
- MEDIUM: 2 points/level
- HIGH: 5 points/level
The movement speed of the snake increases once at 4 levels. In case of a new high score beeing reached, the player name and the score are written into the EEPROM memory of Arduino. You can see them in the Main menu -> HIGH SCORE section.
Hardware components:
- Arduino UNO
- 16x2 LCD
- joystick
- active buzzer
- 8x8 led matrix
- two breadboards
- wires
- 10UF electrolytic capacitor
- 100UF electrolytic capacitor
- 0.033UF ceramic capacitor
- 10k resistor
The application is divided into 14 headers, each corresponding to a part of the game.
When Arduino is starting, the EEPROM values are loaded for: LCD brightness, LCD contrast, matrix brightness, mute/unmute the sound and game difficulty level. A welcome message will appear on the screen for 1.5s
and after that, the main menu is loaded on the LCD.
The joystick input values are read continuously in loop()
through readFromJoystick();
method. There are 5 functions designed to test the direction of the joystick during the game in Joystick.h
file.
The delay(millis);
function is used only when the game blocking is allowed: the automatic scrolling in the ABOUT and HIGH SCORE sections. The rest of the application uses the millis();
function, along with different values: 0.15s
for food blinking, 0.5s
for heart animation, 0.25s
for menu movement, 0.11s
for the initial speed of the snake.
Utility.h
void changeState() {
// changing states during the entire app
if (!gameHasStarted) {
// menu && settings
if (millis() - lastMoved > moveMenuInterval) {
connectMenus();
lastMoved = millis();
}
} else {
if (congratsScreen) {
//the game is over but we don't have a new high score
switchHeart();
if (millis() - lastMoved > moveMenuInterval) {
exitCongratsScreen();
lastMoved = millis();
}
} else if (congratsHighScoreScreen) {
//the game is over && we have a new high score
switchHeart();
if (millis() - lastMoved > moveMenuInterval) {
exitCongratsHighScoreScreen();
lastMoved = millis();
}
} else if (enteringPlayerName) {
//we have a new high score
if (millis() - lastMoved > moveMenuInterval) {
changePlayerName();
lastMoved = millis();
}
} else if (playAgainScreen) {
//game over
//play again or go to menu
if (millis() - lastMoved > moveMenuInterval) {
answerPlayAgain();
lastMoved = millis();
}
} else {
// during the game
blinkingFood();
if (millis() - lastMoved > moveGameInterval) {
updateSnakePosition();
showSnake();
lastMoved = millis();
}
}
}
}
Several boolean variables ensure the interchanging between LCD screens and game states: congratsHighScoreScreen, congratsScreen, playAgainScreen, gameHasStarted, mainMenuOpened, subMainMenuOpened, animationHeart, settingsMenuOpened, subSettingsMenuOpened
. For each part of the menu that requires scrolling (horizontal or vertical) there is a function called changeX(value);
, where X
is the name of the screen that it corresponds, through which the new value is displayed. So, only one part of the screen is rendered each time, not the entire LCD.
The snake is saved into two arrays, one for the row coordinates and one for the column coordinates: snakeRow
and snakeCol
. The initial length is 2, and the head is random generated. The head is on the last position of the arrays(snakeLength - 1
) and the tail is on the first position.
The snake is moved on the game board as follows:
updateSnakePosition()
- calculates the next row/column on the board depending on the joystick inputmoveGame(nextPosition, directionRow)
- checks if it has food to eat and if the snake is deadmoveTheSnake(nextPosition, directionRow)
- calculates the new coordinates for the snake
nextPosition
represents the next row if the directionRow
is true
, or the next column if the directionRow
is false
.
When a food is eaten, the length of the snake is increased by one unit and the new tail is generated by the method below:
GamePlay.h
void updateSnake() {
...
snakeLength ++;
for (int i = snakeLength - 1; i > 0; i--) {
snakeRow[i] = snakeRow[i - 1];
snakeCol[i] = snakeCol[i - 1];
}
tailRow = snakeRow[1];
tailCol = snakeCol[1];
tailRow2 = snakeRow[2];
tailCol2 = snakeCol[2];
if (tailRow2 == tailRow) {
if (tailCol < tailCol2) {
// right
snakeRow[0] = tailRow;
tailCol --;
if (tailCol < minMatrixValue) {
tailCol = maxMatrixValue;
}
snakeCol[0] = tailCol;
} else {
// left
snakeRow[0] = tailRow;
tailCol ++;
if (tailCol > maxMatrixValue) {
tailCol = minMatrixValue;
}
snakeCol[0] = tailCol;
}
} else if (tailCol2 == tailCol) {
if (tailRow < tailRow2) {
// under
tailRow --;
if (tailRow < minMatrixValue) {
tailRow = maxMatrixValue;
}
snakeRow[0] = tailRow;
snakeCol[0] = tailCol;
} else {
// above
tailRow ++;
if (tailRow > maxMatrixValue) {
tailRow = minMatrixValue;
}
snakeRow[0] = tailRow;
snakeCol[0] = tailCol;
}
}
}
Game levels:
- LOW: no walls, just random generated food/level
- MEDIUM: random 4/3 walls generated at level 1 and random generated food/level
- HIGH: fixed corner walls nd random generated food/level
The walls, food and the head snake are random generated using random(minInterval, maxInterval);
and randomSeed(analogread(0));
, because it's important for a sequence of values generated by random()
to differ at the beginning of each new game.
The game saves the last 3 high scores along with the player's nicknames (maximum 4 letters/nickname). These are displayed in the Main menu -> HIGH SCORE section. It also saves the LCD contrast/brightness, sound, difficulty level and matrix settings from the settings menu in the EEPROM memory, so that they can be loaded when the Arduino board is restarted.
EEPROM schema
The new values of the name and the high score are saved in the EEPROM by shifting the memory byte by byte to make room for the new values.
Memory.h
void writeNewStringNameToEEPROM(String player1) {
...
// player1's name
for (int i = 0; i < len1; i++) {
EEPROM.update(11 + i, player1[i]);
}
// player2's name
for (int i = 0; i < len2; i++) {
EEPROM.update(11 + len1 + i, player2[i]);
}
// player3's name
for (int i = 0; i < len3; i++) {
EEPROM.update(11 + len1 + len2 + i, player3[i]);
}
}
Matrix animations
Each matrix animation displayed is defined as an array of bytes (ex: three[] = {B00000000, B00111100, B00100000, B00111000, B00100000, B00100100, B00111100, B00000000};
). During the game the updateMatrixDisplay(const byte matrix[]);
function is called for displaying the animations on the matrix.
Matrix.h
void updateMatrixDisplay(const byte matrix[]) {
// show the icons on the matrix display
lc.clearDisplay(0);
for (int row = 0; row < matrixSize; row++) {
lc.setRow(0, row, matrix[row]);
}
}
- Geeting message
- Moving through the menu
- Starting a game
- Game over
- Start again?