/*
* Project name: T-rex-duino
* Description: T-rex game from Chrome browser rewritten for Arduino
* Project page: https://github.com/AlexIII/t-rex-duino
* Author: github.com/AlexIII
* E-mail: [email protected]
* License: MIT
* -------Hardware------
* Board: Arduino Uno / Nano / Pro / pro mini
* Display: OLED SSD1309 SPI 128x64 *OR* SH1106/SSD1306 I2C 128x64 (130x64) (132x64)
*/
/* Hardware Connections */
// -- Buttons --
#define JUMP_BUTTON 6
#define DUCK_BUTTON 5
// -- Display Selection (uncomment ONE of the options) --
//#define LCD_SSD1309
#define LCD_SH1106 //If you see abnormal vertical line at the left edge of the display, select LCD_SSD1306
//#define LCD_SSD1306 //If you see abnormal vertical line at the right edge of the display, select LCD_SH1106
// -- Display Connection for SSD1309 --
#define LCD_SSD1309_CS 2
#define LCD_SSD1309_DC 3
#define LCD_SSD1309_RESET 4
//LCD_SSD1309_SDA -> 11 (SPI SCK)
//LCD_SSD1309_SCL -> 13 (SPI MOSI)
// -- Display Connection for SH1106/SSD1306 --
//LCD_SH1106_SDA -> A4 (I2C SDA)
//LCD_SH1106_SCL -> A5 (I2C SCL)
/* Misc. Settings */
//#define AUTO_PLAY //uncomment to enable auto-play mode
//#define RESET_HI_SCORE //uncomment to reset HI score, flash your device, than comment it back and flash again
//#define PRINT_DEBUG_INFO
/* Game Balance Settings */
#define PLAYER_SAFE_ZONE_WIDTH 32 //minimum distance between obstacles (px)
#define CACTI_RESPAWN_RATE 50 //lower -> more frequent, max 255
#define GROUND_CACTI_SCROLL_SPEED 3 //pixels per game cycle
#define PTERODACTY_SPEED 5 //pixels per game cycle
#define PTERODACTY_RESPAWN_RATE 255 //lower -> more frequent, max 255
#define INCREASE_FPS_EVERY_N_SCORE_POINTS 256 //better to be power of 2
#define LIVES_START 3
#define LIVES_MAX 5
#define SPAWN_NEW_LIVE_MIN_CYCLES 800
#define DAY_NIGHT_SWITCH_CYCLES 1024 //better to be power of 2
#define TARGET_FPS_START 23
#define TARGET_FPS_MAX 48 //gradually increase FPS to that value to make the game faster and harder
/* Display Settings */
#define LCD_HEIGHT 64U
#define LCD_WIDTH 128U
/* Render Settings */
#ifdef LCD_SSD1309
//#define VIRTUAL_HEIGHT_BUFFER_ROWS_BY_8_PIXELS 1
#define VIRTUAL_WIDTH_BUFFER_COLS 16
#else
//VIRTUAL_HEIGHT_BUFFER_ROWS_BY_8_PIXELS is not supported
#define VIRTUAL_HEIGHT_BUFFER_ROWS_BY_8_PIXELS 4
#endif
/* Includes */
#include <EEPROM.h>
#include "array.h"
#include "TrexPlayer.h"
#include "Ground.h"
#include "Cactus.h"
#include "Pterodactyl.h"
#include "HeartLive.h"
/* Defines and globals */
#define EEPROM_HI_SCORE 16 //2 bytes
#define LCD_BYTE_SZIE (LCD_WIDTH*LCD_HEIGHT/8)
#ifdef VIRTUAL_WIDTH_BUFFER_COLS
#define LCD_IF_VIRTUAL_WIDTH(TRUE_COND, FALSE_COND) TRUE_COND
#define LCD_PART_BUFF_WIDTH VIRTUAL_WIDTH_BUFFER_COLS
#define LCD_PART_BUFF_HEIGHT LCD_HEIGHT
#else
#define LCD_IF_VIRTUAL_WIDTH(TRUE_COND, FALSE_COND) FALSE_COND
#ifdef VIRTUAL_HEIGHT_BUFFER_ROWS_BY_8_PIXELS
#define LCD_PART_BUFF_WIDTH LCD_WIDTH
#define LCD_PART_BUFF_HEIGHT (VIRTUAL_HEIGHT_BUFFER_ROWS_BY_8_PIXELS*8)
#else
#define LCD_PART_BUFF_WIDTH LCD_WIDTH
#define LCD_PART_BUFF_HEIGHT LCD_HEIGHT
#endif
#endif
#define LCD_PART_BUFF_SZ ((LCD_PART_BUFF_HEIGHT/8)*LCD_PART_BUFF_WIDTH)
#ifdef LCD_SSD1309
#include <SPI.h>
#include "SSD1309.h"
static SSD1309<SPIClass> lcd(SPI, LCD_SSD1309_CS, LCD_SSD1309_DC, LCD_SSD1309_RESET, LCD_BYTE_SZIE);
#else
#include "I2C.h"
#include "SH1106.h"
I2C i2c;
SH1106<I2C> lcd(i2c, LCD_BYTE_SZIE);
#endif
static uint16_t hiScore = 0;
static bool firstStart = true;
/* Misc Functions */
bool isPressedJump() {
return !digitalRead(JUMP_BUTTON);
}
bool isPressedDuck() {
return !digitalRead(DUCK_BUTTON);
}
uint8_t randByte() {
static uint16_t c = 0xA7E2;
c = (c << 1) | (c >> 15);
c = (c << 1) | (c >> 15);
c = (c << 1) | (c >> 15);
c = analogRead(A2) ^ analogRead(A3) ^ analogRead(A4) ^ analogRead(A5) ^ analogRead(A6) ^ analogRead(A7) ^ c;
return c;
}
/* Main Functions */
void renderNumber(BitCanvas& canvas, Point2Di8 point, const uint16_t number) {
uint16_t base = 10000;
while(base) {
const uint8_t digit = (number/base)%10;
canvas.render(numbers.getSprite(digit, point));
base /= 10;
point.x += numbers.getWidth() + 1;
}
}
void gameLoop(uint16_t &hiScore) {
uint8_t lcdBuff[LCD_PART_BUFF_SZ];
VirtualBitCanvas bitCanvas(
LCD_IF_VIRTUAL_WIDTH(VirtualBitCanvas::VIRTUAL_WIDTH, VirtualBitCanvas::VIRTUAL_HEIGHT),
lcdBuff,
LCD_PART_BUFF_HEIGHT,
LCD_PART_BUFF_WIDTH,
LCD_IF_VIRTUAL_WIDTH(LCD_WIDTH, LCD_HEIGHT) //virtual size in selected direction
);
SpawnHold spawnHolder;
//dinamic sprites
TrexPlayer trex;
Ground ground1(-1);
Ground ground2(63);
Ground ground3(127);
Cactus cactus1(spawnHolder);
Cactus cactus2(spawnHolder);
Pterodactyl pterodactyl1(spawnHolder);
HeartLive heartLive;
const array<SpriteAnimated*, 8> sprites{{&ground1, &ground2, &ground3, &cactus1, &cactus2, &pterodactyl1, &heartLive, &trex}};
const array<SpriteAnimated*, 3> enemies{{&cactus1, &cactus2, &pterodactyl1}};
//static sprites
const Sprite gameOverSprite(&game_overver_bm, {15, 12});
const Sprite restartIconSprite(&restart_icon_bm, {55, 25});
const Sprite hiSprite(&hi_score, {44, 0});
Sprite heartsSprite(&hearts_5x_bm, {95, 8});
//game variables
uint32_t prvT = 0;
bool gameOver = false;
uint16_t score = 0;
uint8_t targetFPS = TARGET_FPS_START;
uint8_t lives = LIVES_START;
bool night = false;
lcd.setInverse(night);
//main cycle
while(1) {
//render cycle
while(1) {
//score
bitCanvas.render(hiSprite);
renderNumber(bitCanvas, {60, 0}, hiScore);
renderNumber(bitCanvas, {95, 0}, score);
bitCanvas.render(heartsSprite);
//game objects
for(uint8_t i = 0; i < sprites.size(); ++i)
bitCanvas.render(*sprites[i]);
//game over
if(gameOver) {
bitCanvas.render(gameOverSprite);
bitCanvas.render(restartIconSprite);
}
//update screen
lcd.fillScreen(lcdBuff, LCD_PART_BUFF_SZ, LCD_IF_VIRTUAL_WIDTH(LCD_PART_BUFF_WIDTH, 0));
if(bitCanvas.nextPart()) break;
}
//exit game on game over
if(gameOver) {
if(score > hiScore) hiScore = score;
return;
}
//collision detection
if(!trex.isBlinking() && CollisionDetector::check(trex, enemies.data, enemies.size())) {
if(lives) {
trex.blink();
--lives;
} else {
trex.die();
gameOver = true;
continue;
}
}
if(lives < LIVES_MAX && CollisionDetector::check(trex, heartLive)) {
++lives;
heartLive.eat();
}
#ifndef AUTO_PLAY
//constrols
if(isPressedJump()) trex.jump();
trex.duck(isPressedDuck());
#else
const int8_t trexXright = trex.bitmap->width + trex.position.x;
//auto jump
if(
(cactus1.position.x <= trexXright + 5 && cactus1.position.x > trexXright) ||
(cactus2.position.x <= trexXright + 5 && cactus2.position.x > trexXright) ||
(pterodactyl1.position.y > 30 && pterodactyl1.position.x <= trexXright + 5 && pterodactyl1.position.x > trexXright)
) trex.jump();
//auto duck
trex.duck(
(pterodactyl1.position.y <= 30 && pterodactyl1.position.y > 20 && pterodactyl1.position.x <= trexXright + 15 && pterodactyl1.position.x > trex.position.x)
);
#endif
//logic and animation step
for(uint8_t i = 0; i < sprites.size(); ++i)
sprites[i]->step();
//score keeping
if(score < 0xFFFE) ++score;
//make game progressively faster
if(!(score%INCREASE_FPS_EVERY_N_SCORE_POINTS) && targetFPS < TARGET_FPS_MAX) ++targetFPS;
heartsSprite.limitRenderWidthTo = 6*lives + 1;
//switch day and night
if(!(score%DAY_NIGHT_SWITCH_CYCLES)) lcd.setInverse(night = !night);
const uint8_t frameTime = 1000/targetFPS;
#ifdef PRINT_DEBUG_INFO
//print CPU load statistics
const uint32_t dt = millis() - prvT;
uint32_t left = frameTime > dt? frameTime - dt : 0;
Serial.print("CPU: ");
Serial.print(100 - 100*left / frameTime);
Serial.print("% ");
Serial.println(dt);
#endif
//throttle
while(millis() - prvT < frameTime);
prvT = millis();
}
}
void spalshScreen() {
lcd.setAddressingMode(lcd.HorizontalAddressingMode);
uint8_t buff[32];
for(uint8_t i = 0; i < LCD_BYTE_SZIE/sizeof(buff); ++i) {
memcpy_P(buff, splash_screen_bitmap + 2 + uint16_t(i) * sizeof(buff), sizeof(buff));
lcd.fillScreen(buff, sizeof(buff));
}
for(uint8_t i = 50; i && !isPressedJump(); --i) delay(100);
}
void setup() {
pinMode(JUMP_BUTTON, INPUT_PULLUP);
pinMode(DUCK_BUTTON, INPUT_PULLUP);
Serial.begin(250000);
lcd.begin();
spalshScreen();
lcd.setAddressingMode(LCD_IF_VIRTUAL_WIDTH(lcd.VerticalAddressingMode, lcd.HorizontalAddressingMode));
srand((randByte()<<8) | randByte());
#ifdef RESET_HI_SCORE
EEPROM.put(EEPROM_HI_SCORE, hiScore);
#endif
EEPROM.get(EEPROM_HI_SCORE, hiScore);
if(hiScore == 0xFFFF) hiScore = 0;
}
void loop() {
if(firstStart || isPressedJump()) {
firstStart = false;
gameLoop(hiScore);
EEPROM.put(EEPROM_HI_SCORE, hiScore);
//wait until the jump button is released
while(isPressedJump()) delay(100);
delay(500);
}
}