Last active
January 14, 2019 06:49
-
-
Save bluesaunders/ec25b58da6e2dfca53827884c2cecc1e to your computer and use it in GitHub Desktop.
code for an Arduino based jump game, displayed on a 16x2 LCD.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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