Skip to content

Instantly share code, notes, and snippets.

@bluesaunders
Last active January 14, 2019 06:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bluesaunders/ec25b58da6e2dfca53827884c2cecc1e to your computer and use it in GitHub Desktop.
Save bluesaunders/ec25b58da6e2dfca53827884c2cecc1e to your computer and use it in GitHub Desktop.
code for an Arduino based jump game, displayed on a 16x2 LCD.
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 2, 3, 4, 5);
const byte btnPin = 13;
const unsigned long debounceTime = 10; // milliseconds
const byte screenWidth = 16;
const byte screenHeight = 2;
// game constants
unsigned long refreshDelay = 125;
const byte maxScreenCol = screenWidth - 1;
const byte minScreenCol = 0;
const byte groundRow = screenHeight - 1;
const byte skyRow = 0;
const byte glyphSize = 8;
// player glyph
byte playerSprite[glyphSize] = {
B01110,
B01010,
B10001,
B11011,
B10001,
B11111,
B10101,
B00000
};
// enemy glyphs
byte enemyOpenedSprite[glyphSize] = {
B00000,
B00100,
B11110,
B01111,
B00111,
B01111,
B11110,
B00100
};
byte enemyClosingSprite[glyphSize] = {
B00000,
B00100,
B01110,
B11111,
B00111,
B11111,
B01110,
B00100
};
byte enemyClosedSprite[glyphSize] = {
B00000,
B00100,
B01110,
B11111,
B11111,
B11111,
B01110,
B00100
};
// health glyphs
byte heartEmptySprite[glyphSize] = {
B00000,
B00000,
B01010,
B10101,
B10001,
B01010,
B00100,
B00000
};
byte heartFilledSprite[glyphSize] = {
B00000,
B00000,
B01010,
B11111,
B11111,
B01110,
B00100,
B00000
};
byte featherSprite[glyphSize] = {
B00001,
B00011,
B00111,
B00110,
B01101,
B01010,
B01100,
B10000
};
byte invertedPlayerSprite[glyphSize] = {
B01110,
B01110,
B11111,
B10101,
B11111,
B11111,
B10101,
};
struct glyphs {
const byte player = 0;
const byte enemyOpened = 1;
const byte enemyClosing = 2;
const byte enemyClosed = 3;
const byte heartEmpty = 4;
const byte heartFilled = 5;
const byte feather = 6;
const byte invertedPlayer = 7;
} glyphs;
struct entityTypes {
// the byte at each index represents the entity.type entity type is related to the glyph
const byte enemy = 0;
const byte pointBonus = 1; // $
const byte healthRegen = 2; // <3
const byte floatTrigger = 3; // feather
const byte invincibilityTrigger = 4; // @
} entityTypes;
const byte entityChancesSize = 50;
const byte entityChances[entityChancesSize] = {
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.healthRegen,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.invincibilityTrigger,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.invincibilityTrigger,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.floatTrigger,
entityTypes.enemy,
entityTypes.invincibilityTrigger,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.healthRegen,
entityTypes.floatTrigger,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.pointBonus,
entityTypes.enemy,
entityTypes.enemy,
entityTypes.healthRegen,
entityTypes.enemy,
entityTypes.enemy,
};
struct entityObject {
int xPos;
byte type;
byte state;
bool spawned;
} entity;
// game states
// unstarted -> started -> ended
// ^---------|
struct gameStates {
const byte unstarted = 0;
const byte started = 1;
const byte ended = 2;
} gameStates;
struct gameObject {
byte btnWas;
byte state;
unsigned int score;
unsigned int highestScore;
unsigned long lastEntitySpawnAt;
unsigned long nextEntitySpawnAt;
} game;
struct playerObject {
int yPos;
int hangtime; // maybe it could be negative? if not then change to a byte
byte health;
byte maxHealth;
bool invincible;
byte invincibleTime;
} player;
void setup() {
Serial.begin(9600); // for debugging
randomSeed(analogRead(0));
// register jump button
pinMode(btnPin, INPUT_PULLUP);
// register and clear LCD
lcd.begin(screenWidth, screenHeight);
lcd.clear();
// register custom glyphs
lcd.createChar(glyphs.player, playerSprite);
lcd.createChar(glyphs.enemyOpened, enemyOpenedSprite);
lcd.createChar(glyphs.enemyClosing, enemyClosingSprite);
lcd.createChar(glyphs.enemyClosed, enemyClosedSprite);
lcd.createChar(glyphs.heartEmpty, heartEmptySprite);
lcd.createChar(glyphs.heartFilled, heartFilledSprite);
lcd.createChar(glyphs.feather, featherSprite);
lcd.createChar(glyphs.invertedPlayer, invertedPlayerSprite);
// intialize
initializeGameState();
// can't be in ^^ because that would reset it.
game.highestScore = 0;
}
void loop() {
detectButtonPress();
if (game.state == gameStates.started) {
// decrement invicible time if positive
if (player.invincibleTime > 0) {
player.invincibleTime--;
} else {
player.invincible = false;
}
// update player vertical position
if (player.hangtime > 0) {
player.hangtime--;
} else {
// reset player to groundRow level
player.yPos = groundRow;
}
if (shouldSpawnEntity()) {
byte type = entityChances[random(entityChancesSize + 1)];
spawnEntityOfType(type);
}
// update entity position if one is spawned
if (entity.spawned) {
entity.xPos -= 1;
// detect entity / player collision
if (entity.xPos == minScreenCol && player.yPos == groundRow) {
//despawn entity
entity.spawned = false;
handlePlayerEntityCollision();
// detect if player successfully hurdled entity (entity made it off screen)
} else if (entity.xPos < 0) {
entity.spawned = false;
if (entity.type == entityTypes.enemy) {
game.score++;
}
}
}
}
updateScreen();
}
void initializeGameState() {
game.state = gameStates.unstarted;
game.score = 0;
game.btnWas = HIGH;
game.lastEntitySpawnAt = 0;
game.nextEntitySpawnAt = refreshDelay * 3;
player.yPos = groundRow;
player.hangtime = 0;
player.health = 3;
player.maxHealth = 3;
player.invincible = false;
}
void detectButtonPress() {
byte btnNow = digitalRead(btnPin);
// button press detected
if (game.btnWas != btnNow) {
game.btnWas = btnNow;
// combat contact bouncing
delay(debounceTime);
if (btnNow == LOW) {
// a button press that we care about
handleButtonPress();
}
}
}
void handleButtonPress() {
// start game
if(game.state == gameStates.unstarted) {
transitionGameState();
// reset game
} else if (game.state == gameStates.ended) {
transitionGameState();
// game in progess means jump!
} else if(game.state == gameStates.started) {
player.yPos = skyRow;
player.hangtime = player.hangtime > 0 ? player.hangtime : 2; // don't unset if already set.
}
}
void handlePlayerEntityCollision() {
if (entity.type == entityTypes.enemy) {
// if player is invincible, skip enemy problems.
if(!player.invincible) {
flashEnemyCollisionAnimation();
player.health -= 1;
// player has died :'(
if (player.health <= 0) {
if(game.score > game.highestScore) {
game.highestScore = game.score;
}
transitionGameState();
}
}
} else if (entity.type == entityTypes.pointBonus) {
game.score += 10;
} else if (entity.type == entityTypes.healthRegen && player.health < player.maxHealth) {
player.health += 1;
} else if(entity.type == entityTypes.floatTrigger) {
player.yPos = skyRow;
player.hangtime = 5000 / refreshDelay; // 5 secs in frames
} else if(entity.type == entityTypes.invincibilityTrigger) {
player.invincible = true;
player.invincibleTime = 5000 / refreshDelay; // 5 secs in frames;
}
}
bool shouldSpawnEntity() {
unsigned long now = millis();
return !entity.spawned && now - game.lastEntitySpawnAt >= game.nextEntitySpawnAt;
}
void spawnEntityOfType(byte type) {
entity.spawned = true;
entity.type = type;
entity.state = 1;
entity.xPos = maxScreenCol;
game.lastEntitySpawnAt = millis();
// give the current entity enought time to cross, so at minimum the next sprite is immediate
// but add some randomness (up to 1/4 of the time it takes an entity to cross)
unsigned long fullScreenCrossingTime = refreshDelay * screenWidth;
game.nextEntitySpawnAt = random(fullScreenCrossingTime, fullScreenCrossingTime * 1.25);
}
void updateScreen() {
lcd.clear();
if(game.state == gameStates.unstarted) {
drawWelcomeScreen();
} else if(game.state == gameStates.started) {
drawInProgress();
} else if(game.state == gameStates.ended) {
drawEndGame();
}
delay(refreshDelay);
}
void flashEnemyCollisionAnimation() {
unsigned int shortFlash = 50;
// hide enemy player and the enemy glyph that's in column 2
lcd.setCursor(minScreenCol, groundRow);
lcd.print(" ");
lcd.setCursor(minScreenCol, groundRow);
lcd.write(glyphs.enemyOpened);
delay(refreshDelay);
lcd.setCursor(minScreenCol, groundRow);
lcd.print(" ");
delay(shortFlash);
lcd.setCursor(minScreenCol, groundRow);
lcd.write(glyphs.player);
delay(refreshDelay);
lcd.setCursor(minScreenCol, groundRow);
lcd.print(" ");
delay(shortFlash);
lcd.setCursor(minScreenCol, groundRow);
lcd.write(glyphs.enemyOpened);
delay(refreshDelay);
lcd.setCursor(minScreenCol, groundRow);
lcd.print(" ");
delay(shortFlash);
}
void drawWelcomeScreen() {
String readyText= "Get Ready!";
int cursorX = determineCursorCenter(readyText);
lcd.setCursor(cursorX, skyRow);
lcd.print(readyText);
readyText = "click to start";
cursorX = determineCursorCenter(readyText);
lcd.setCursor(cursorX, groundRow);
lcd.print(readyText);
}
void drawInProgress() {
drawScore();
drawHealth();
drawPlayer();
drawEntity();
}
void drawEndGame() {
drawScore();
String gameOver = "Game Over!";
int cursorX = determineCursorCenter(gameOver);
lcd.setCursor(cursorX, groundRow);
lcd.print(gameOver);
}
void drawScore() {
String stringifiedScore = String(game.score);
// centered on top line
if (game.state == gameStates.ended) {
stringifiedScore = String(game.highestScore);
String combinedScoreString = "Highest: " + stringifiedScore;
int cursorX = determineCursorCenter(combinedScoreString);
lcd.setCursor(cursorX, skyRow);
lcd.print(combinedScoreString);
// top right corner
} else {
int cursorX = max((screenWidth - stringifiedScore.length()), 0);
lcd.setCursor(cursorX, skyRow);
lcd.print(stringifiedScore);
}
}
void drawHealth() {
for (int i = 1; i <= player.maxHealth; i++) {
byte healthGlyph;
if (i <= player.health) {
healthGlyph = glyphs.heartFilled;
} else {
healthGlyph = glyphs.heartEmpty;
}
lcd.setCursor(i, skyRow);
lcd.write(healthGlyph);
}
}
void drawPlayer() {
lcd.setCursor(minScreenCol, player.yPos);
if(player.invincible) {
lcd.write(glyphs.invertedPlayer);
} else {
lcd.write(glyphs.player);
}
}
void drawEntity() {
if (entity.spawned) {
lcd.setCursor(entity.xPos, groundRow);
if(entity.type == entityTypes.enemy) {
lcd.write(entity.state);
} else if(entity.type == entityTypes.pointBonus) {
lcd.print("$");
} else if(entity.type == entityTypes.healthRegen) {
lcd.write(glyphs.heartFilled);
} else if(entity.type == entityTypes.floatTrigger) {
lcd.write(glyphs.feather);
} else if(entity.type == entityTypes.invincibilityTrigger) {
lcd.print("@");
}
updateEntityState();
}
}
void updateEntityState() {
if(entity.state == glyphs.enemyOpened) {
entity.state = glyphs.enemyClosing;
} else if(entity.state == glyphs.enemyClosing) {
entity.state = glyphs.enemyClosed;
} else if(entity.state == glyphs.enemyClosed) {
entity.state = glyphs.enemyOpened;
}
}
void transitionGameState() {
if(game.state == gameStates.unstarted) {
game.state = gameStates.started;
} else if(game.state == gameStates.started) {
game.state = gameStates.ended;
} else if(game.state == gameStates.ended) {
initializeGameState();
game.state = gameStates.started;
}
}
int determineCursorCenter(String displayString) {
return max((screenWidth - displayString.length()) / 2, minScreenCol);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment