Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
Recently I did a project called Tiny Space Impact Game. After finishing it, I realized that the console itself was very generic and could be used for other LCD 1602 style games. Also there was plenty of unused Flash memory in the microprocessor to add more than just one game.
Keeping with the space theme, I have added LCD Invaders by arduinocelentano along with a simple menu system to select the game.
VideoSchematicThe circuit is designed around a ATtiny1614 microprocessor. This 14pin device has 16K Flash memory and 2K Static memory which is more than enough for this game. Unlike the ATmega328 microprocessor, the ATtiny1614 can run with a 1MHz clock. This means it uses at lot less power while it is running and while it is in sleep mode thus allowing the battery to last longer.
I designed the schematic with a 3V3 regulator and 3.7V battery socket. If you want to use a battery exclusively, leave off U$1, U$2 and C8. If you only want to power the game using a 5-12V power brick, you can leave off the battery connector CN1.
Converting a 5V 1602 LCD display to run on 3.3VMost 1602 LCD displays are designed to run at 5V. There are 3.3V versions available but they can be quite expensive. Most 5V boards can be converted to run from a 3.3V supply by added three components and changing a jumper.
1. Add a ICL7660 SOIC chip to U3
2. Add two 10uF 1206 capacitors to C1 and C2
3. Open J1 (remove solder) and close J3 (add solder bridge)
3D printingThe STL files are included. Either take these to a 3D print shop or if you have your own printer, run them through your slicing software. I used a 0.2mm layer height and no supports.
Drill out the four PCB mount holes with a 2.5mm drill and create a thread with a 3mm tap.
If the lid is lose, add blue painters tape around the rim of the bottom until the fit is tight.
Make sure the X-Pad and Button top don't get stuck on the case when they are inserted into the top. Use a file to remove any lip that might of occurred with the first few layers while they were sitting on the heated build-plate.
The PCBAs the ATtiny1614 microprocessor only comes as a Surface Mount Device (SMD), I decided to use SMD packages for most of the components in the build.
The Eagle files have been included should you wish to have the board commercially made or you can do as I did and make it yourself. I used the Toner method.
Assembly - Step 1Start by adding the SMD components. I find it easier to use solder paste rather than use solder from a reel when soldering SMD components.
Also solder the pin header for the UPDI programmer and the battery socket onto the copper side of the board.
Careful of shorts when soldering the 10K trimmer potentiometer that is used to set the contrast of the LCD.
Add the 16 pin header for the 1602 LCD display. Also super glue some padding onto the top of the board to support the LCD PCB. Make sure when the display is placed on PCB, it's PCB sits parallel.
Add the LCD display, switches and buzzer to the top side of the board.
Screw the assembled board to the case top with four 6mm M3 screws.
The ATtiny1614 is part of the new breed of ATtiny microprocessors. Unlike the earlier series such as the ATtiny85, the new breed use the RESET pin to program the CPU. To program it you need a UPDI programmer. I made one using a Arduino Nano. You can find complete build instructions at Create Your Own UPDI Programmer. It also contains the instructions for adding the megaTinyCore boards to your IDE.
My home made UPDI programmer outputs 5V. Since the 1602 LCD display is now configured to run from 3.3V, you can't power the board with the UPDI programmer. Instead connect only the ground and UPDI wires (leave the 5V wire unconnected) and apply the power from the 3.7V LIPO battery or an external power brick.
Once the board has been installed in the IDE, select it from the Tools menu.
Select board, chip (ATtiny1614), millis()/micros() timer (TCD0) and the COM port that the Arduino Nano is connected to.
If you plan to run the game on a battery, set the clock speed to 1MHz. If you plan to run the game using external power, set the clock speed to 8MHz.
The Programmer needs to be set to jtag2updi (megaTinyCore).
Open the sketch and upload it to the ATtiny1614.
Software changes - Space InvadersLCD screens tend to be very slow to update in comparison to an OLED display. This becomes an issue with fast moving objects. I had to add a delay to the bullet class in the original code so that the bullets were more visible.
It also added sound effects and a high score system that is retained in EEPROM.
Software changes - Space ImpactAfter modifying the original code to use a X-Pad rather than a Joystick, I found that the Space Impact game itself was difficult to play due to the controls not being responsive enough. This occurred because the main program loop which tests whether the controls are activated also does the screen updating. So while the screen updates, the controls are non-responsive.
The software has been changed so that the controls are continually tested using a periodic interrupt. It means the controls will be responsive even when the screen is being updated.
It also has more sounds and a high score system that is retained in EEPROM.
ConclusionThere is still more room in the microprocessor to add another game if you wish. The console itself is easy to hold and play games on.
/**************************************************************************
Space Games for 1602 console
Author: John Bradnam (jbrad2089@gmail.com)
2021-07-14
- Created menu system for Space Invaders and Space Impact games
--------------------------------------------------------------------------
Arduino IDE:
--------------------------------------------------------------------------
BOARD: ATtiny1614/1604/814/804/414/404/214/204
Chip: ATtiny1614
Clock Speed: 8MHz (if using external power) or 1MHz (if using a battery)
millis()/micros() Timer: "TCD0 (1-series only, default there)
Programmer: jtag2updi (megaTinyCore)
ATtiny1614 Pins mapped to Ardunio Pins
+--------+
VCC + 1 14 + GND
(SS) 0 PA4 + 2 13 + PA3 10 (SCK)
1 PA5 + 3 12 + PA2 9 (MISO)
(DAC) 2 PA6 + 4 11 + PA1 8 (MOSI)
3 PA7 + 5 10 + PA0 11 (UPDI)
(RXD) 4 PB3 + 6 9 + PB0 7 (SCL)
(TXD) 5 PB2 + 7 8 + PB1 6 (SDA)
+--------+
**************************************************************************/
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/power.h>
#include <LiquidCrystal.h>
#include <TimerFreeTone.h> // https://bitbucket.org/teckel12/arduino-timer-free-tone/wiki/Home
#include <EEPROM.h>
#include "button.h"
//LCD Screen
#define LCD_RS 0 //PA4
#define LCD_EN 1 //PA5
#define LCD_D4 2 //PA6
#define LCD_D5 3 //PA7
#define LCD_D6 4 //PB3
#define LCD_D7 5 //PB2
//Switches
#define SW_FIRE 6 //PB1
#define SW_XPAD 10 //PA3
//Other
#define SPEAKER 7 //PB0
#define LIGHT 8 //PA1
#define POWER 9 //PA2
//Initialize the LCD
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
//Buttons
enum buttonEnum { SW_NONE, SW_LEFT, SW_RIGHT, SW_DOWN, SW_UP };
Button* leftButton;
Button* rightButton;
Button* downButton;
Button* upButton;
Button* fireButton;
//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
uint32_t magic;
uint32_t impactHighScore;
uint32_t invaderHighScore;
} EEPROM_DATA;
EEPROM_DATA EepromData; //Current EEPROM settings
enum modesEnum { MNU_MAIN, MNU_INVADERS, MNU_IMPACT };
modesEnum menuMode = MNU_MAIN;
modesEnum menuSelect = MNU_INVADERS;
#define SLEEP_TIMEOUT 15000
unsigned long sleepTimeOut;
volatile bool gameover = true;
int score = 0; //Player's score
//-------------------------------------------------------------------------
// Initialise Hardware
void setup()
{
randomSeed(analogRead(SPEAKER));
pinMode(POWER, OUTPUT);
digitalWrite(POWER, HIGH);
pinMode(LIGHT, OUTPUT);
digitalWrite(LIGHT, HIGH);
pinMode(SPEAKER, OUTPUT);
//Initialise buttons
leftButton = new Button(SW_LEFT, SW_XPAD, 250, 399, false); //22K/10K = 320
rightButton = new Button(SW_RIGHT, SW_XPAD, 520, 700, false); //22K/30K = 590
downButton = new Button(SW_DOWN, SW_XPAD, 400, 519, false); //22K/20K = 487
upButton = new Button(SW_UP, SW_XPAD, 0, 100, false); //GND = 0
fireButton = new Button(SW_FIRE);
//Get last high scores
readEepromData();
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
gameover = false;
score = 0;
switch (menuMode)
{
case MNU_INVADERS: invaderSetup(); break;
case MNU_IMPACT: impactSetup(); break;
}
}
//-------------------------------------------------------------------------
// Main program loop
void loop()
{
switch (menuMode)
{
case MNU_MAIN:
displayMenuScreen();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
while(!fireButton->Pressed())
{
if (downButton->Pressed() && menuSelect == MNU_INVADERS)
{
menuSelect = MNU_IMPACT;
displayMenuCursor();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
}
else if (upButton->Pressed() && menuSelect == MNU_IMPACT)
{
menuSelect = MNU_INVADERS;
displayMenuCursor();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
}
else if (millis() > sleepTimeOut)
{
Sleep();
displayMenuScreen();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
while (fireButton->State() == LOW); //Wait until fire button is released
}
};
menuMode = menuSelect;
gameover = false;
score = 0;
switch (menuMode)
{
case MNU_INVADERS: invaderSetup(); break;
case MNU_IMPACT: impactSetup(); break;
}
break;
case MNU_INVADERS:
gameover = invaderLoop();
break;
case MNU_IMPACT:
gameover = impactLoop();
break;
}
if (gameover)
{
switch (menuMode)
{
case MNU_INVADERS: invaderShutDown(); break;
case MNU_IMPACT: impactShutDown(); break;
}
menuMode = MNU_MAIN;
}
}
//-------------------------------------------------------------------------
// display main menu cursor
void displayMenuCursor()
{
lcd.setCursor(0, 0);
lcd.print((menuSelect == MNU_INVADERS) ? ">" : " ");
lcd.setCursor(0, 1);
lcd.print((menuSelect == MNU_IMPACT) ? ">" : " ");
}
//-------------------------------------------------------------------------
// display main menu screen
void displayMenuScreen()
{
lcd.setCursor(0, 0);
lcd.print(" SPACE INVADERS");
lcd.setCursor(0, 1);
lcd.print(" SPACE IMPACT ");
displayMenuCursor();
}
//--------------------------------------------------------------------
// Set all LCD pins to INPUTs or OUTPUTs
void setLcdPins(int state)
{
static char Outputs[] = {LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7};
for (int i=0; i<6; i++)
{
pinMode(Outputs[i], state);
}
}
//--------------------------------------------------------------------
// Handle pin change interrupt when to wake up processor
void wakeUpProcessor()
{
}
//--------------------------------------------------------------------
// Put the processor to sleep
void Sleep()
{
switch (menuMode)
{
case MNU_INVADERS: invaderShutDown(); break;
case MNU_IMPACT: impactShutDown(); break;
}
attachInterrupt(SW_FIRE, wakeUpProcessor, CHANGE); //Used to wake up processor
lcd.clear();
setLcdPins(INPUT);
digitalWrite(LIGHT, LOW);
digitalWrite(POWER, LOW);
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
sleep_enable();
sleep_mode(); // System actually sleeps here
sleep_disable(); // System continues execution here when watchdog timed out
// Continue after sleep
pinMode(POWER, OUTPUT);
digitalWrite(POWER, HIGH);
pinMode(LIGHT, OUTPUT);
digitalWrite(LIGHT, HIGH);
setLcdPins(OUTPUT);
lcd.begin(16, 2);
detachInterrupt(SW_FIRE);
gameover = false;
score = 0;
switch (menuMode)
{
case MNU_INVADERS: invaderSetup(); break;
case MNU_IMPACT: impactSetup(); break;
}
}
//------------------------------------------------------------------
void playHitTone()
{
TimerFreeTone(SPEAKER, 300, 150);
}
//------------------------------------------------------------------
void playMissTone()
{
TimerFreeTone(SPEAKER, 50, 150);
}
//-----------------------------------------------------------------------------------
//Play the sound for the start of a round
void playStartRound()
{
#define MAX_NOTE 4978 // Maximum high tone in hertz. Used for siren.
#define MIN_NOTE 31 // Minimum low tone in hertz. Used for siren.
for (int note = MIN_NOTE; note <= MAX_NOTE; note += 5)
{
TimerFreeTone(SPEAKER, note, 1);
}
}
//------------------------------------------------------------------
//Play a high note as a sign you lost
void playWinSound()
{
//TimerFreeTone(SPEAKER,880,300);
TimerFreeTone(SPEAKER,880,100); //A5
TimerFreeTone(SPEAKER,988,100); //B5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,988,100); //B5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,659,100); //E5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,659,100); //E5
TimerFreeTone(SPEAKER,659,100); //E5
delay(250);
}
//------------------------------------------------------------------------------------------------------------------
//Play wah wah wah wahwahwahwahwahwah
void playLoseSound()
{
delay(400);
//wah wah wah wahwahwahwahwahwah
for(double wah=0; wah<4; wah+=6.541)
{
TimerFreeTone(SPEAKER, 440+wah, 50);
}
TimerFreeTone(SPEAKER, 466.164, 100);
delay(80);
for(double wah=0; wah<5; wah+=4.939)
{
TimerFreeTone(SPEAKER, 415.305+wah, 50);
}
TimerFreeTone(SPEAKER, 440.000, 100);
delay(80);
for(double wah=0; wah<5; wah+=4.662)
{
TimerFreeTone(SPEAKER, 391.995+wah, 50);
}
TimerFreeTone(SPEAKER, 415.305, 100);
delay(80);
for(int j=0; j<7; j++)
{
TimerFreeTone(SPEAKER, 391.995, 70);
TimerFreeTone(SPEAKER, 415.305, 70);
}
delay(400);
}
//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
//This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
EEPROM.put(EEPROM_ADDRESS,EepromData);
}
//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
//Eprom
EEPROM.get(EEPROM_ADDRESS,EepromData);
if (EepromData.magic != EEPROM_MAGIC)
{
EepromData.magic = EEPROM_MAGIC;
EepromData.impactHighScore = 0;
EepromData.invaderHighScore = 0;
writeEepromData();
}
}
/**************************************************************************
Space Invader Game
Author: John Bradnam (jbrad2089@gmail.com)
Modified code from LCD Invaders by arduinocelentano
(https://www.instructables.com/LCD-Invaders-a-Space-Invaders-Like-Game-on-16x2-LC/)
2021-07-14
- Updated program for ATtiny1614 and X-Pad
- Halved bullet vertical speed to make bullets more visible
- Add sound effects
- Made LCD backlight and power enabled via code
- Added processor sleep mode for battery powered systems
- Added EEPROM functions to store high score
**************************************************************************/
//game field size
#define WIDTH 16
#define HEIGHT 4
//custom sprites
#define SHIP 0
#define BULLET_UP 1
#define BULLET_DOWN 2
#define SHIP_BULLET 3
#define ALIEN1 4
#define ALIEN2 5
#define ALIEN1BULLET 6
#define ALIEN2BULLET 7
#define GAME_STEP 100 //Delay (ms) between game steps
#define ALIENS_NUM 8 //Number of aliens
byte animationStep; //Number of game step
char screenBuffer[HEIGHT/2][WIDTH+1]; //Characters to be displayed on the screen
byte alienStep = 5; //The number of game steps between alien's movements
byte fireProbability = 20; //Probability of alien to shoot
byte level = 1; //Game level
byte aliensLeft = 0; //Number of aliens left on current level
//Define custom characters for game sprites
byte ship_sprite[] = {
B00000,
B00000,
B00000,
B00000,
B00000,
B00100,
B01110,
B11011
};
byte ship_bullet_sprite[] = {
B00000,
B00100,
B00100,
B00000,
B00000,
B00100,
B01110,
B11011
};
byte bullet_down_sprite[] = {
B00000,
B00000,
B00000,
B00000,
B00000,
B00100,
B00100,
B00000
};
byte bullet_up_sprite[] = {
B00000,
B00100,
B00100,
B00000,
B00000,
B00000,
B00000,
B00000
};
byte alien1_1_sprite[] = {
B01010,
B10101,
B01110,
B10001,
B00000,
B00000,
B00000,
B00000
};
byte alien1_2_sprite[] = {
B01010,
B10101,
B01110,
B01010,
B00000,
B00000,
B00000,
B00000
};
byte alien1_1_bullet_sprite[] = {
B01010,
B10101,
B01110,
B10001,
B00000,
B00100,
B00100,
B00000
};
byte alien1_2_bullet_sprite[] = {
B01010,
B10101,
B01110,
B01010,
B00000,
B00100,
B00100,
B00000
};
//-------------------------------------------------------------------------
//Base class for game objects
class GameObject
{
//Object's coordinates and speed
protected:
int8_t _x;
int8_t _y;
int8_t _speed;
public:
GameObject():_x(0),_y(0),_speed(0){}
GameObject(int8_t x, int8_t y): _x(x), _y(y), _speed(0){}
GameObject(int8_t x, int8_t y, int8_t speed): _x(x), _y(y), _speed(speed){}
//Getters and setters
int8_t x() const
{
return _x;
}
int8_t y() const
{
return _y;
}
int8_t speed() const
{
return _speed;
}
bool setX(int8_t x)
{
if (x<0||x>WIDTH)
{
return false;
}
_x = x;
return true;
}
bool setY(int8_t y)
{
if (y<0||y>HEIGHT)
{
return false;
}
_y = y;
return true;
}
bool setSpeed(int8_t speed)
{
_speed = speed;
return true;
}
//Collision detection
bool collides(const GameObject& o)
{
return (_x==o.x() && _y==o.y()) ? true : false;
}
};
//-------------------------------------------------------------------------
//Bullet class
class Bullet:public GameObject
{
#define BULLET_DELAY 3 //This slows the bullets down so that are more visible on the LCD screen
private:
bool _active; //Bullet is active while it is within game field
bool _half; //Used to half the speed of the bullet due to limitations of LCD
int _delay;
public:
Bullet():GameObject(), _active(false), _half(false), _delay(0) {}
void setActive(bool active)
{
_active = active;
}
bool active()
{
return _active;
}
// Moving bullet. Returns true if successful
bool move()
{
_delay++;
if (_delay == BULLET_DELAY)
{
_delay = 0;
_y+=_speed;//for bullets speed is always vertical
if (_y<0||_y>=HEIGHT) //if bullet leaves the field
{
if (_y<0)
{
playMissTone();
}
_y-=_speed;
_active = false;
return false;
}
else
{
return true;
}
}
return true;
}
} shipBullet, alienBullets[ALIENS_NUM]; //bullet objects for ship and aliens
//-------------------------------------------------------------------------
// Ship class
class Ship: public GameObject
{
public:
//Moving right. Returns true if successfull
bool moveRight()
{
_x++;
if (_x>=WIDTH)
{
_x--;
return false;
}
else
{
return true;
}
}
//Moving left. Returns true if successfull
bool moveLeft()
{
_x--;
if (_x<0)
{
_x++;
return false;
}
else
{
return true;
}
}
} ship;
//-------------------------------------------------------------------------
// Alien class
class Alien: public GameObject
{
private:
bool _alive;//shows wether alien is alive
bool _state;//alien's current state for animation purpose
public:
Alien():GameObject(), _alive(false), _state(false){}
void setAlive(bool alive)
{
_alive = alive;
}
bool alive()
{
return _alive;
}
void setState(bool state)
{
_state = state;
}
bool state()
{
return _state;
}
//Moving alien. Returns true if successfull
bool move()
{
_x+=_speed;
_state = !_state;
if (_x<0||_x>=WIDTH)
{
_x-=_speed;
return false;
}
else
{
return true;
}
}
} aliens[8];
//-------------------------------------------------------------------------
// Update LCD screen
// First drawing in a character buffer, then print it to the screen to avoid flickering.
// Note: we have to draw ship separately since it has char code 0 and lcd.print() processes it like EOL*/
void updateScreen()
{
bool shipDisplayed = false; //shows whether ship have been displayed with SHIP_BULLET sprite
//Clearing the buffer
for (byte i = 0; i < HEIGHT/2; i++)
{
for (byte j = 0; j < WIDTH; j++)
{
screenBuffer[i][j] = ' ';
}
screenBuffer[i][WIDTH] = '\0';
}
//Drawing ship's bullet
if (shipBullet.active())
{
if ((ship.x()==shipBullet.x()) && (shipBullet.y()==2))
{
screenBuffer[shipBullet.y()/2][shipBullet.x()] = SHIP_BULLET;
shipDisplayed = true;
}
else
{
screenBuffer[shipBullet.y()/2][shipBullet.x()] = shipBullet.y()%2 ? BULLET_DOWN : BULLET_UP;
}
}
//Drawing aliens
for (byte i = 0; i<ALIENS_NUM; i++)
{
if(aliens[i].alive())
{
screenBuffer[aliens[i].y()/2][aliens[i].x()] = aliens[i].state() ? ALIEN1 : ALIEN2;
}
}
//Drawing aliens and bullets
bool bulletDisplayed = false;
for (byte i = 0; i < ALIENS_NUM; i++)
{
if(alienBullets[i].active())
{
bulletDisplayed = false;
for (int j = 0; j < ALIENS_NUM; j++)
{
if ((aliens[j].x()==alienBullets[i].x()) && (alienBullets[i].y()==1) && (aliens[i].alive()))
{
screenBuffer[alienBullets[i].y()/2][alienBullets[i].x()] = aliens[i].state() ? ALIEN1BULLET : ALIEN2BULLET;
bulletDisplayed = true;
}
}
if (!bulletDisplayed)
{
if ((ship.x()==alienBullets[i].x()) && (alienBullets[i].y()==2))
{
screenBuffer[alienBullets[i].y()/2][alienBullets[i].x()] = SHIP_BULLET;
shipDisplayed = true;
}
else
{
screenBuffer[alienBullets[i].y()/2][alienBullets[i].x()] = alienBullets[i].y()%2 ? BULLET_DOWN : BULLET_UP;
}
}
}
}
//Sending the buffer to the screen
for (byte i = 0; i < HEIGHT/2; i++)
{
lcd.setCursor(0,i);
lcd.print(screenBuffer[i]);
}
//After all, displaying the ship
if (!shipDisplayed)
{
lcd.setCursor(ship.x(), ship.y()/2);
lcd.write(byte(SHIP));
}
}
//-------------------------------------------------------------------------
// Reset all the objects before easch level
void initLevel(byte l)
{
level = l;
if (level>42)//Easter egg: 42 is the ultimate level :)
{
level = 42;
}
//Reset ship object
ship.setX(WIDTH/2);
ship.setY(3);
shipBullet.setX(WIDTH/2);
shipBullet.setY(3);
shipBullet.setActive(false);
//Reset aliens objects
for (byte i = 0; i<ALIENS_NUM; i++)
{
aliens[i].setX(i);
aliens[i].setY(0);
aliens[i].setSpeed(1);
aliens[i].setAlive(true);
aliens[i].setState(false);
alienBullets[i].setActive(false);
}
//Reset the rest of the game variables
animationStep = 0;
alienStep = 6-level/2;//alien's speed depends on a level
if (alienStep < 1)
{
alienStep = 1;
}
fireProbability = 110-level*10; //alien's shoot probability depends on a level
if (fireProbability < 10)
{
fireProbability = 10;
}
aliensLeft = ALIENS_NUM;
//Displaying a number of level
lcd.clear();
lcd.print("Level ");
lcd.setCursor(8,0);
lcd.print(level);
delay(1000);
lcd.clear();
}
//-------------------------------------------------------------------------
// Initialise Hardware for space invaders
void invaderSetup()
{
//Define custom characters
lcd.createChar(SHIP, ship_sprite);
lcd.createChar(BULLET_UP, bullet_up_sprite);
lcd.createChar(BULLET_DOWN, bullet_down_sprite);
lcd.createChar(SHIP_BULLET, ship_bullet_sprite);
lcd.createChar(ALIEN1, alien1_1_sprite);
lcd.createChar(ALIEN2, alien1_2_sprite);
lcd.createChar(ALIEN1BULLET, alien1_1_bullet_sprite);
lcd.createChar(ALIEN2BULLET, alien1_2_bullet_sprite);
invaderInitialise();
}
//-------------------------------------------------------------------------
// Initialise Hardware for space invaders
void invaderInitialise()
{
score = 0;
initLevel(1);
}
//-------------------------------------------------------------------------
// Release Hardware used for space invaders
void invaderShutDown()
{
}
//-------------------------------------------------------------------------
// Main program loop
bool invaderLoop()
{
//Processing the buttons
if (rightButton->State() == HIGH)
{
ship.moveRight();
}
else if (leftButton->State() == HIGH)
{
ship.moveLeft();
}
else if (downButton->State() == HIGH)
{
//Game pause
while (downButton->State() == HIGH);
while (downButton->State() == LOW);
while (downButton->State() == HIGH);
}
else if (fireButton->State() == LOW && !shipBullet.active())
{
shipBullet.setX(ship.x());
shipBullet.setY(ship.y());
shipBullet.setSpeed(-1);
shipBullet.setActive(true);
}
//Moving all the objects
if(shipBullet.active()) //Moving the ship bullet
{
shipBullet.move();
}
//Moving the aliens and their bullets
for (byte i = 0; i<ALIENS_NUM; i++)
{
if (alienBullets[i].active())
{
alienBullets[i].move();
if (alienBullets[i].collides(ship)) //Ship destruction
{
invaderGameOver();
return true;
}
}
if (!(animationStep % alienStep))
{
aliens[i].move();
}
if (aliens[i].collides(shipBullet) && shipBullet.active() && aliens[i].alive()) //Alien dies
{
aliens[i].setAlive(false);
score += 10*level;
aliensLeft--;
playHitTone();
shipBullet.setActive(false);
}
if (!random(fireProbability) && !alienBullets[i].active() && aliens[i].alive()) //Alien shoots
{
alienBullets[i].setX(aliens[i].x());
alienBullets[i].setY(aliens[i].y()+1);
alienBullets[i].setSpeed(1);
alienBullets[i].setActive(true);
}
}
if ( !(animationStep % alienStep) && (aliens[0].x()==0 || aliens[ALIENS_NUM-1].x() == WIDTH-1)) //Changing the aliens'move direction
{
for (byte i = 0; i<ALIENS_NUM; i++)
{
aliens[i].setSpeed(-aliens[i].speed());
}
}
//Refresh screen
updateScreen();
animationStep++;
delay (GAME_STEP);
//If no aliens left, starting next level
if (!aliensLeft)
{
initLevel(level+1);
}
return false;
}
//--------------------------------------------------------------------
// Display Opening animation, instructions and "Press button to start" message
void displayInvaderInitialScreens()
{
lcd.clear();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(ALIEN1 + (x & 1)));
delay(100);
lcd.setCursor(x, y);
if (y==0)
{
lcd.print(" SPACE "[x]);
}
else
{
lcd.print(" INVADERS "[x]);
}
}
}
delay(1000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print(" PRESS BUTTON ");
lcd.setCursor(0,1);
lcd.print(" TO START ");
}
//---------------------------------------------------------------
//Handle game over
void invaderGameOver()
{
lcd.clear();
lcd.setCursor(6, 0);
lcd.print("GAME");
lcd.setCursor(6, 1);
lcd.print("OVER");
playLoseSound();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(ALIEN1 + (x & 1)));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SCORE:");
lcd.setCursor(7, 0);
lcd.print(score);
lcd.setCursor(0, 1);
lcd.print("BEST:");
lcd.setCursor(7, 1);
lcd.print(EepromData.invaderHighScore);
if (score > EepromData.invaderHighScore)
{
EepromData.invaderHighScore = score;
writeEepromData();
playWinSound();
}
delay(1000);
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(ALIEN1 + (x & 1)));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
delay(1000);
}
/**************************************************************************
Space Impact Game
Author: John Bradnam (jbrad2089@gmail.com)
Modified code from Space Impact LCD game by MOHD SOHAIL
(https://www.youtube.com/channel/UCaXI2PcsTlH5g0et67kdD6g)
(https://www.hackster.io/mohammadsohail0008/space-impact-lcd-game-ce5c74)
2021-07-14
- Create program for ATtiny1614
- Replaced joystick with X-Pad
- Made fire and movement processing interrupt driven
- Made LCD backlight and power enabled via code
- Added processor sleep mode for battery powered systems
- Added more sounds
- Added EEPROM functions to store high score
**************************************************************************/
//Logical screen
volatile uint8_t area[4][15] =
{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
#define CUSTOM_CHARACTERS 8
const byte cc[CUSTOM_CHARACTERS][8] =
{
{B11100, B01111, B11100, B00000, B00000, B00000, B00000, B00000},
{B00000, B00000, B00000, B00000, B11100, B01111, B11100, B00000},
{B11100, B01111, B11100, B00000, B11100, B10100, B11100, B00000},
{B11100, B10100, B11100, B00000, B11100, B01111, B11100, B00000},
{B11100, B10100, B11100, B00000, B11100, B10100, B11100, B00000},
{B00000, B00000, B00000, B00000, B00100, B10010, B01000, B00000},
{B00100, B10010, B01000, B00000, B11100, B10100, B11100, B00000},
{B11100, B10100, B11100, B00000, B00100, B10010, B01000, B00000}
};
#if (F_CPU == 1000000L)
//A 1MHz clock uses less battery power when running
#define MAX_GAME_DELAY 50
#define MIN_GAME_DELAY 10
#define STEP_GAME_DELAY 5
#else
//Assume 8MHz if running via external power
#define MAX_GAME_DELAY 200
#define MIN_GAME_DELAY 50
#define STEP_GAME_DELAY 5
#endif
volatile uint8_t fireLoad = 0;
volatile uint8_t fireConsumption = 0;
volatile bool xPadButtonDown = false;
//-------------------------------------------------------------------------
// Initialise Hardware
void impactSetup()
{
attachInterrupt(SW_FIRE, fireButtonInterrupt, CHANGE); //Used to wake up processor and to fire bullet
//Define custom characters
for (int i = 0; i < CUSTOM_CHARACTERS; i++)
{
lcd.createChar(i, &cc[i][0]);
}
lcd.home();
displayImpactInitialScreens();
//Set up background player control
TCB1.CCMP = 10000;
TCB1.INTCTRL = TCB_CAPT_bm;
TCB1.CTRLA = TCB_ENABLE_bm;
}
//-------------------------------------------------------------------------
// Initialise Hardware for space invaders
void impactInitialise()
{
}
//-------------------------------------------------------------------------
// Release Hardware used for space invaders
void impactShutDown()
{
TCB1.CTRLA = TCB1.CTRLA & ~TCB_ENABLE_bm;
detachInterrupt(SW_FIRE);
}
//--------------------------------------------------------------------
// Handle pin change interrupt when SW_FIRE is pressed
void fireButtonInterrupt()
{
if (!gameover && fireButton->State() == LOW && fireLoad >= fireConsumption) {
fireLoad -= fireConsumption;
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<14; x++)
{
if (area[y][x]==1) // spaceship
{
area[y][x+1] += 4;
}
}
}
}
}
//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each mS - output segments
ISR(TCB1_INT_vect)
{
if (xPadButtonDown)
{
xPadButtonDown = (leftButton->State() == HIGH || rightButton->State() == HIGH || upButton->State() == HIGH || downButton->State() == HIGH);
}
else if (!gameover)
{
bool doBreak = false;
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==1)
{
doBreak = true;
if (leftButton->State() == HIGH && x > 0)
{
area[y][x] = 0;
area[y][x-1] += 1;
xPadButtonDown = true;
}
else if (rightButton->State() == HIGH && x < 14)
{
if (area[y][x+1]!=4)
{
area[y][x] = 0;
area[y][x+1] += 1;
xPadButtonDown = true;
}
}
else if (upButton->State() == HIGH && y > 0)
{
area[y][x] = 0;
area[y-1][x] += 1;
xPadButtonDown = true;
}
else if (downButton->State() == HIGH && y < 3)
{
area[y][x] = 0;
area[y+1][x] += 1;
xPadButtonDown = true;
}
}
if (doBreak)
{
break;
}
}
if (doBreak)
{
break;
}
}
}
//Clear interrupt flag
TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}
//-------------------------------------------------------------------------
// Main program loop
bool impactLoop()
{
//Setup the game
lcd.clear();
delay(500);
playStartRound();
//Clear playing field
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
area[y][x] = 0;
}
}
area[0][0] = 1; //Put ship in top left corner
uint8_t sleep = MAX_GAME_DELAY;
uint8_t junkRisk = 10;
fireLoad = 0;
fireConsumption = 9;
uint8_t life = 3;
unsigned long count = 0;
lcd.setCursor(0,0);
lcd.print(life);
lcd.setCursor(0,1);
lcd.print(fireLoad);
draw();
gameover = false;
while (!gameover)
{
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=15; x>0; x--)
{
if (area[y][x-1]==4)
{
area[y][x-1] = 0;
if (x<15)
{
area[y][x] += 4;
}
}
}
}
if (fireLoad<9)
{
fireLoad++;
lcd.setCursor(0,1);
lcd.print(fireLoad);
}
draw();
delay(sleep);
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==2)
{
area[y][x] = 0;
if (x>0)
{
area[y][x-1] += 2;
}
}
}
}
for (uint8_t y=0; y<4; y++) {
if (random(100) < junkRisk) {
area[y][14] += 2;
}
}
draw();
delay(sleep);
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==3) // collided ship
{
area[y][x] = 1;
blinkShip();
life--;
lcd.setCursor(0, 0);
lcd.print(life);
if(life==0)
{
impactGameOver();
gameover = true;
}
}
else if (area[y][x]>4)
{
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(3);
digitalWrite(SPEAKER,LOW);
delay(3);
}
score+=10;
area[y][x] -= 6;
}
}
}
score++;
count++;
if (count % 100 == 0)
{
sleep = max(MIN_GAME_DELAY, sleep - STEP_GAME_DELAY);
junkRisk +=3;
fireConsumption--;
}
}
return gameover;
}
//-------------------------------------------------------------------------
// Transfer the logical screen to the physical screen
void draw()
{
for (uint8_t y=0; y<4; y+=2)
{
for (uint8_t x=0; x<15; x++)
{
lcd.setCursor(x+1, y/2);
if (area[y][x]==1)
{
if (area[y+1][x]==0)
{
lcd.print(char(0));
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10) // down obstacle
{
lcd.print(char(2));
}
} else if (area[y][x]==2 || area[y][x]==6 || area[y][x]==10)
{
if (area[y+1][x]==0)
{
lcd.write(0b11011111); //upper Small box
}
else if (area[y+1][x]==1)
{
lcd.print(char(3));
}
else if (area[y+1][x]==2)
{
lcd.print(char(4));
}
else if (area[y+1][x]==4 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.print(char(7));
}
}
else if (area[y][x]==4)
{
if (area[y+1][x]==0)
{
lcd.write(0b11011110); //bullet
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.print(char(6)); //bullet + junk
}
}
else if (area[y][x]==0) // above nothing
{
if (area[y+1][x]==0) // nothing below
{
lcd.print(" ");
}
else if (area[y+1][x]==1) // below ship
{
lcd.print(char(1));
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.write(0b10100001); //lower Small box
}
else if (area[y+1][x]==4)
{
lcd.print(char(5));
}
} else {
lcd.print(" ");
}
}
}
}
//-------------------------------------------------------------------------
// Flash the ship
void blinkShip()
{
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==1) //Found ship
{
for (uint8_t i=0; i<3; i++)
{
lcd.setCursor(x+1, y>1);
if (y==0 || y==2)
{
if (area[y+1][x]==0)
{
lcd.print(" ");
}
else
{
lcd.write(0b10100001);
}
}
else
{
if (area[y-1][x]==0) {
lcd.print(" ");
}
else
{
lcd.write(0b11011111);
}
}
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(25);
digitalWrite(SPEAKER,LOW);
delay(5);
}
lcd.setCursor(x+1, y>1);
if (y==0 || y==2)
{
if (area[y+1][x]==0)
{
lcd.print(char(0));
}
else
{
lcd.print(char(2));
}
}
else
{
if (area[y-1][x]==0)
{
lcd.print(char(1));
}
else
{
lcd.print(char(3));
}
}
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(25);
digitalWrite(SPEAKER,LOW);
delay(5);
}
}
}
}
}
}
//--------------------------------------------------------------------
// Display Opening animation, instructions and "Press button to start" message
void displayImpactInitialScreens()
{
lcd.clear();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
if (y==0)
{
lcd.print(" SPACE "[x]);
}
else
{
lcd.print(" IMPACT "[x]);
}
}
}
delay(1000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("3 -> LIFE POINTS");
lcd.setCursor(0,1);
lcd.print("9 -> WEAPON LOAD");
delay(3000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print(" PRESS BUTTON ");
lcd.setCursor(0,1);
lcd.print(" TO START ");
}
//---------------------------------------------------------------
//Handle game over
void impactGameOver()
{
//Kill interrupts
impactShutDown();
lcd.clear();
lcd.setCursor(6, 0);
lcd.print("GAME");
lcd.setCursor(6, 1);
lcd.print("OVER");
playLoseSound();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SCORE:");
lcd.setCursor(7, 0);
lcd.print(score);
lcd.setCursor(0, 1);
lcd.print("BEST:");
lcd.setCursor(7, 1);
lcd.print(EepromData.impactHighScore);
if (score > EepromData.impactHighScore)
{
EepromData.impactHighScore = score;
writeEepromData();
playWinSound();
}
delay(1000);
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
delay(1000);
}
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#include "Button.h"
Button::Button(int pin)
{
_name = pin;
_pin = pin;
_range = false;
_low = 0;
_high = 0;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT_PULLUP);
}
Button::Button(int name, int pin)
{
_name = name;
_pin = pin;
_range = false;
_low = 0;
_high = 0;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT_PULLUP);
}
Button::Button(int name, int pin, int analogLow, int analogHigh, bool activeLow)
{
_name = name;
_pin = pin;
_range = true;
_low = analogLow;
_high = analogHigh;
_activeLow = activeLow;
_backgroundCallback = NULL;
_repeatCallback = NULL;
pinMode(_pin, INPUT);
}
//Set function to invoke in a delay or repeat loop
void Button::Background(void (*pBackgroundFunction)())
{
_backgroundCallback = pBackgroundFunction;
}
//Set function to invoke if repeat system required
void Button::Repeat(void (*pRepeatFunction)())
{
_repeatCallback = pRepeatFunction;
}
bool Button::IsDown()
{
if (_range)
{
int value = analogRead(_pin);
return (value >= _low && value < _high);
}
else
{
return (digitalRead(_pin) == LOW);
}
}
//Tests if a button is pressed and released
// returns true if the button was pressed and released
// if repeat callback supplied, the callback is called while the key is pressed
bool Button::Pressed()
{
bool pressed = false;
if (IsDown())
{
unsigned long wait = millis() + DEBOUNCE_DELAY;
while (millis() < wait)
{
if (_backgroundCallback != NULL)
{
_backgroundCallback();
}
}
if (IsDown())
{
//Set up for repeat loop
if (_repeatCallback != NULL)
{
_repeatCallback();
}
unsigned long speed = REPEAT_START_SPEED;
unsigned long time = millis() + speed;
while (IsDown())
{
if (_backgroundCallback != NULL)
{
_backgroundCallback();
}
if (_repeatCallback != NULL && millis() >= time)
{
_repeatCallback();
unsigned long faster = speed - REPEAT_INCREASE_SPEED;
if (faster >= REPEAT_MAX_SPEED)
{
speed = faster;
}
time = millis() + speed;
}
}
pressed = true;
}
}
return pressed;
}
//Return current button state
int Button::State()
{
if (_range)
{
int value = analogRead(_pin);
if (_activeLow)
{
return (value >= _low && value < _high) ? LOW : HIGH;
}
else
{
return (value >= _low && value < _high) ? HIGH : LOW;
}
}
else
{
return digitalRead(_pin);
}
}
//Return current button name
int Button::Name()
{
return _name;
}
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#pragma once
#include "Arduino.h"
#define DEBOUNCE_DELAY 10
//Repeat speed
#define REPEAT_START_SPEED 500
#define REPEAT_INCREASE_SPEED 50
#define REPEAT_MAX_SPEED 50
class Button
{
public:
//Simple constructor
Button(int pin);
Button(int name, int pin);
Button(int name, int pin, int analogLow, int analogHigh, bool activeLow = true);
//Background function called when in a wait or repeat loop
void Background(void (*pBackgroundFunction)());
//Repeat function called when button is pressed
void Repeat(void (*pRepeatFunction)());
//Test if button is pressed
bool IsDown(void);
//Test whether button is pressed and released
//Will call repeat function if one is provided
bool Pressed();
//Return button state (HIGH or LOW) - LOW = Pressed
int State();
//Return button name
int Name();
private:
int _name;
int _pin;
bool _range;
int _low;
int _high;
bool _activeLow;
void (*_repeatCallback)(void);
void (*_backgroundCallback)(void);
};
Comments