John Bradnam
Published © GPL3+

Tiny Space Invaders Game

A battery or external powered LCD 1602 game console with built in Space Invaders and Space Impact games.

IntermediateFull instructions provided8 hours1,521

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
Standard LCD - 16x2 White on Blue
Adafruit Standard LCD - 16x2 White on Blue
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
7mm Shaft
×1
Passive Components
3 x 10K, 1 x 22K, 1 x 220R 0805 Resistors 2 x 0.1uF 0805 ceramic capacitors 1 x 10uF 1206 ceramic capacitor 1 x 47uF/10V 3528 tantalum capacitor
×1
LM1117-33
SOT223 3.3V regulator
×1
DC POWER JACK 2.1MM BARREL-TYPE PCB MOUNT
TaydaElectronics DC POWER JACK 2.1MM BARREL-TYPE PCB MOUNT
SMD variant
×1
Single Turn Potentiometer- 10k ohms
Single Turn Potentiometer- 10k ohms
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

STL Files for 3D printing

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

SpaceGamesV1.ino

C/C++
Main file
/**************************************************************************
 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();
  }
}

SpaceInvaders.ino

C/C++
/**************************************************************************
 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);
}

SpaceImpact.ino

C/C++
/**************************************************************************
 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);
}

Button.cpp

C/C++
/*
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;
}

Button.h

C Header File
/*
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);
};

Credits

John Bradnam

John Bradnam

141 projects • 167 followers

Comments