/*
* FORMULA ARDUINO
* Simple racing game for arduino boards
* with 20x4 lcd display and buzzer sound
* effects support
* https://github.com/rikovmike/formulaArduino
*
* Developed by rikovmike
* mike@rikovmike.ru
*
* ver a1.0
*/
/*
* TODO: Add sfx sequencer for music and sfx
*/
/* Using LiquidCrystal lib
*/
#include <LiquidCrystal.h>
LiquidCrystal lcd( 12, 13, 8, 9, 10, 11); // Change it to your LCD connetcions
/*
* Including some header files with initial game data
*/
#include "f1_carSetup.h"
#include "f1_roadSetup.h"
/*
* Main game variables
*/
int gscore=0; // Global score
int state=4; // State-machine state
/*
* Car physics (ho-ho)
*/
float distanceMonemtum=0; // Special variable to move road tiles for 1 step (character) forward
float accelerationPoint=0.8; // Acc will grow up to this value for smooth speeding up
float acceleration=0; // Current acceleration
int blocksCursor=0; //Draw-buffer cursor
int analogPin = A3; //Analog pin for steering wheel
// car setup
int carPos=0; //Current car position on road
int maxCarPos=26; //Max position by y.
int steeringVal=0; //Current data from analogue pin
int carTilesFilled[4]={99,99,99,99}; //Using 4 free vert characters on each lcd sctring to draw car.
int deadTile=0; //This is for game over effect
/*
* TITLE data
* Too lazy for it. Setting FULL 20-char strings ((
* Flashing title text to PROGMEM to save RAM space
*/
const char title_string1[20] PROGMEM = {' ',' ',' ',' ',' ','F','O','R','M','U','L','A',' ',' ',' ',' ',' ',' ',' ',' '};
const char title_string2[20] PROGMEM = {' ',' ',' ',' ',' ',' ',' ',' ','A','R','D','U','I','N','O',' ',' ',' ',' ',' '};
const char title_message1[] = "steer to start"; // Not nessersary to flash this string. Code to read it will be more painful, than few bytes of RAM, used by it. I think...
int titleStringsAnims[3]={0,0,0}; //Utility array for animation effect
int animPosChars[12]{5,5,4,4,3,3,2,2,1,1,0,0}; //Utility array for draw car tiles, especially in-between characters
String ingameMessage=""; //In-game message string. Used by in-game message effect
int ingameMessageTime=0; //Used by in-game message effect
/*
* Setup
*/
void setup()
{
pinMode(7, OUTPUT); // Buzzer pin. PWM7.
lcd.begin(20, 4); //Setting up LCD
// Adding car custom characters to LCD. Reading from PROGMEM
for (int i=0;i<=5;i++){
byte tmpChar[8];
for (int j=0;j<8;j++){
tmpChar[j]=pgm_read_byte_near(carFrames[i]+j);
}
lcd.createChar(i, tmpChar);
}
}
/*
* Main loop
*/
void loop()
{
/*
* Classic state machine!
*/
switch(state){
case 4:
statePreStartScreen(); // Preparing initial vars
break;
case 5:
stateStartScreenAnim(); // Cool title screen animation
break;
case 6:
stateStartScreen(); // Title screen
break;
case 7:
statePostStartScreen(); // Cool post-title animation
break;
case 10:
stateGame(); // Game logic
break;
case 11:
stateDie(); // Duying logic
break;
case 90:
stateGameOver(); // Game over screen and animation
break;
default:
stateStartScreen(); // If state unknown - start over
break;
}
}
/*
* START SCREEN BLOCK. States 4,5,6
*/
void statePreStartScreen(){
titleStringsAnims[0]=0;
titleStringsAnims[1]=1;
titleStringsAnims[2]=0;
gscore=0;
state=5;
memcpy(roadArray, roadArrayInit, sizeof(roadArrayInit));
blocksCursor=0;
distanceMonemtum=0;
lastPatternResolveredPattern=1;
ingameMessageTime=0;
}
void stateStartScreenAnim(){
if (titleStringsAnims[0]<20){
titleStringsAnims[0]++;
// FORMULA
if (titleStringsAnims[0]<19){
lcd.setCursor(titleStringsAnims[0],0);
lcd.write(byte(255));
}
for (int i=0;i<titleStringsAnims[0];i++){
lcd.setCursor(i,0);
lcd.write(pgm_read_byte_near(&title_string1[i]));
}
// ARDUINO
if (titleStringsAnims[0]>0){
lcd.setCursor(19-titleStringsAnims[0],1);
lcd.write(byte(255));
}
for (int i=19-titleStringsAnims[0];i<20;i++){
lcd.setCursor(i+1,1);
lcd.write(pgm_read_byte_near(&title_string2[i+1]));
}
delay(50);
}else{
state=6;
}
}
void stateStartScreen(){
int sVal=0;
titleStringsAnims[1]++;
if (titleStringsAnims[1]>0&&titleStringsAnims[1]<=1){
lcd.setCursor(3,3);
lcd.print(title_message1);
}
if (titleStringsAnims[1]>1&&titleStringsAnims[1]<=2){
lcd.setCursor(0,3);
lcd.print(" ");
}
if (titleStringsAnims[1]>2){
titleStringsAnims[1]=0;
}
sVal = analogRead(analogPin);
delay(320);
if (abs(analogRead(analogPin)-sVal)>100){
state=7;
}
}
void ffxAnimateScreenWave(int sybmol=255){
for (int i=-5;i<26;i++){
//1st string
if (i>=0&&i<=19){
for (int j=0;j<4;j++){
lcd.setCursor(i,j);
lcd.write(byte(sybmol));
}
}
delay(10);
}
for (int i=-5;i<26;i++){
//1st string
if (i>=0&&i<=19){
for (int j=0;j<4;j++){
lcd.setCursor(i,j);
lcd.print(" ");
}
}
delay(10);
}
}
// Trying to use in-state loop. Maybe it's fun ))
void statePostStartScreen(){
ffxAnimateScreenWave();
state=10;
}
/*
* Game logic block
*/
// State 10
void stateGame(){
drawRoad(true);
ffxIngameMessage();
if (acceleration<accelerationPoint){
acceleration+=0.005;
}
tone(7, 50+100*acceleration, 50+100*acceleration); // Make some noise :)
steeringVal = analogRead(analogPin);
carPos=32-int(round(steeringVal/32)); // Adapting steering data to screen coords
if (carPos>maxCarPos){carPos=maxCarPos;} // Bad practice, but fast ) TODO: Make more precise calculations
// Check and draw car tiles. Drawing is sub-char
for (int i=0;i<=3;i++){
carTilesFilled[i]=99;
checkCarTile(i);
}
// Mover
distanceMonemtum+=acceleration;
if (distanceMonemtum>1){
distanceMonemtum=0;
blocksCursor++;
roadMoveStep();
}
drawScore();
}
/*
* Draw road function. Take current state of draw buffer and draw it to screen.
* Also calling check-collision function while drawing
*/
void drawRoad(bool checkCollisions){
int curs=0;
for (int i=26-blocksCursor;i>=9-blocksCursor;i--){
for (int j=0;j<=3;j++){
lcd.setCursor((curs),j);
if (roadArray[i][j]>0){
// Check using special roadblocks from custom blocks array
int roadBlock=0;
if (curs==0&&checkCollisions){
int col=checkColisiion(j,roadArray[i][j]);
// If we get collision with powerup, remove powerup from pattern
if (col==1){
roadArray[i][j]=0;
roadBlock=0;
}
}
if (roadArray[i][j]<9){
roadBlock=roadBlocks[roadArray[i][j]];
}else{
roadBlock=roadArray[i][j];
}
lcd.write(byte(roadBlock));
}else{
if (curs!=0){
lcd.write(' ');
}else{
if (carTilesFilled[j]==99){
lcd.write(' ');
}
}
}
}
curs++;
}
}
/*
* Road mover. Moving draw buffer towards player. Filling off-screen buffer part with new patterns.
* Patterns selected by pattern-resolver mechanism
*/
void roadMoveStep(){
gscore+=1;
if (blocksCursor>9){
for (int i=0;i<9;i++){
for (int j=0;j<4;j++){
currPattern[i][j]=pgm_read_byte_near(&patterns[lastPatternResolveredPattern-1][i][j]);
// If epmty space, spawn random powerup:
if (currPattern[i][j]==0){
if (random(0,100)==9){
currPattern[i][j]=bonusBlocks[random(0,2)];
}
}
}
}
lastPatternResolveredPattern=pgm_read_byte_near(&pattern_resolvers[lastPatternResolveredPattern-1][random(0, 7)]);
blocksCursor=0;
for (int i=17;i>=0;i--){
for (int j=0;j<=3;j++){
roadArray[i+9][j]=roadArray[i][j];
}
}
for (int i=0;i<=8;i++){
for (int j=0;j<=3;j++){
roadArray[i][j]=currPattern[i][j];
}
}
}
}
// Car drawer
void checkCarTile(int tileNum){
int myCoord=tileNum*8;
lcd.setCursor(0,tileNum);
lcd.write(" ");
int myCoordPos=myCoord-carPos;
if (myCoordPos<=4&&myCoordPos>=-7){
carTilesFilled[tileNum]=animPosChars[((myCoord-carPos)+7)];
lcd.setCursor(0,tileNum);
lcd.write(byte(carTilesFilled[tileNum]));
}
}
// Ultra-precision fast and robust car physics collision detection!
int checkColisiion(int tileNum, int roadBlock){
bool collision=true,iamsafe=false,iambonus=false;
int ret=0;
// Is safe block?
if (carTilesFilled[tileNum]!=99){
for (int i=0;i<(sizeof(safeBlocks) / sizeof(int));i++){
if (roadBlock==safeBlocks[i]){
iamsafe=true;
}
}
// Or maybe bonus block?
for (int i=0;i<(sizeof(bonusBlocks) / sizeof(int));i++){
if (roadBlock==bonusBlocks[i]){
iambonus=true;
}
}
// If so, or if it just a road - no colision at all
if (roadBlock==0||roadBlock>9||iamsafe||iambonus){
collision=false;
}
//If there is a bonus, so take it. Returning "1" to road drawer to remove bonus from road data
if (iambonus){
ffxLaunchPowerup(tileNum,roadBlock);
ret=1;
}
// Oh, no!
if (collision){
//DEAD!
deadTile=tileNum;
state=11;
}
}
return ret;
}
/*
* END GAME BLOCK. States 11,90
*/
void stateDie(){
acceleration=0;
drawRoad(false);
char rndChar[9]={'*','*','*','+','+','.','.','.'};
for (int i=0;i<9;i++){
lcd.setCursor(i,deadTile);
lcd.write(rndChar[i]);
lcd.setCursor(i,deadTile-(int(round(float(i)/4))));
lcd.write(rndChar[int(float(i)+1.4)]);
lcd.setCursor(i,deadTile-(int(round(float(i)/1))));
lcd.write(rndChar[int(float(i)+1.6)]);
lcd.setCursor(i,deadTile+(int(round(float(i)/4))));
lcd.write(rndChar[int(float(i)+1.4)]);
lcd.setCursor(i,deadTile+(int(round(float(i)/1))));
lcd.write(rndChar[int(float(i)+1.6)]);
delay(0+i*10);
}
state=90;
}
void stateGameOver(){
lcd.setCursor(4,1);
lcd.print(" GAME OVER ");
delay(1300);
ffxAnimateScreenWave(42);
state=4;
}
/*
* Special effects block
*/
// Powerup effect. Pauses game for short time to show animation.
void ffxLaunchPowerup(int posy,int powrpTile){
if (powrpTile==5){
gscore+=500;
ingameMessage="+500$";
}
if (powrpTile==6){
accelerationPoint+=accelerationPoint/100;
ingameMessage="CHARGE!";
}
ingameMessageTime=30;
}
// Ingame message drawer
void ffxIngameMessage(){
int startX=0;
int txtSize=0;
if (ingameMessageTime>0) {
ingameMessageTime--;
Serial.println(ingameMessageTime);
startX=9-(ingameMessage.length())/2;
lcd.setCursor(startX,1);
lcd.print(ingameMessage);
}
}
// Draw score on screen
void drawScore(){
lcd.setCursor(18,0);
lcd.print("$K");
lcd.setCursor(18,1);
lcd.print(ExtractDigit(gscore,3));
lcd.print(ExtractDigit(gscore,6));
lcd.setCursor(18,2);
lcd.print(ExtractDigit(gscore,2));
lcd.print(ExtractDigit(gscore,5));
lcd.setCursor(18,3);
lcd.print(ExtractDigit(gscore,1));
lcd.print(ExtractDigit(gscore,4));
}
/*
* Utilites
*/
//Extracting digit from integer number. v - number, p - number of digit to extract
int ExtractDigit(int v, int p)
{
return int(v/(pow(10,p-1))) - int(v/(pow(10,p)))*10;
}
/*
* Not used function.
* TODO: Make road template reverse method to save flash memory space
*/
void templateReverse(int arrTmpl[9][4]){
int bufsize=4;
for (int j=0;j<9;j++){
int buf[4];
for (int i = 0; i < 4; i++) {
buf[i] = arrTmpl[j][i];
}
for (int i = 0; i < 4; i++) {
arrTmpl[j][i] = buf[3 - i];
}
}
}
uno:A5.2
uno:A4.2
uno:AREF
uno:GND.1
uno:13
uno:12
uno:11
uno:10
uno:9
uno:8
uno:7
uno:6
uno:5
uno:4
uno:3
uno:2
uno:1
uno:0
uno:IOREF
uno:RESET
uno:3.3V
uno:5V
uno:GND.2
uno:GND.3
uno:VIN
uno:A0
uno:A1
uno:A2
uno:A3
uno:A4
uno:A5
lcd1:VSS
lcd1:VDD
lcd1:V0
lcd1:RS
lcd1:RW
lcd1:E
lcd1:D0
lcd1:D1
lcd1:D2
lcd1:D3
lcd1:D4
lcd1:D5
lcd1:D6
lcd1:D7
lcd1:A
lcd1:K
r1:1
r1:2
pot1:GND
pot1:SIG
pot1:VCC