Colour Invaders

From Hamsterworks Wiki!

Jump to: navigation, search

This Arduino project was made during an evening of hacking around in Jan 2015.

Colour Invaders is a simple game I made up to play with a strip of WS2812B LEDs when I had a spare night - there is a video of it in action at http://youtu.be/uXn9usxy4AY. The idea of the game is to shoot the incoming Colour Invaders by matching their colour and then firing the 'photon torpedo' at them. It is pretty much the same game as was on the 1980's "Number Invaders" calculators by Casio.

It is a fun way to play with the LEDs and a rotary encoder. I've used my bit-banging WS2812B interface, so no additional software modules are required.

If you are using an Arduino that doesn't run at 16MHz (like the Uno), or is based around a different processor you will need to update the code to use a WS2812B library that works with your board.

Hardware required

  • An Arduino Uno - it must be a 5V, 16MHz ATmega based Arduino unless you want to rewrite some of the code.
  • A Rotary Encoder - I used the Digilent PMOD-ENC, which has on-board pull-up resistors
  • A switch for the fire button - The encoder has an pushbutton feature, so I used that.
  • Pull-up resistors, as required. I wired the switches to be 'pulled down' when the switches are open, and 5V when the switches are closed.
  • A strip of WS2812 LEDs - I used a strip from Seeed Studio that has 30 LEDs per meter
  • Wires to connect it all up - I used a breadboard and some jumpers

Assembly

  • Connect the +5V and GND signals on the WS2812B to the Arduino +5V and GND.
  • Connect the data signal to digital pin 6 on the arduino.
  • Connect the rotary encoder contacts to digital pins 3 and 4 on the Arduino. Have these pulled down to GND with a suitable resistor.
  • Connect the common connection on the encoder to +5V
  • Connect one contact on the push-button switch to digital pin 5 on the Arduino, and also add a pull down resistor if required.
  • Connect the other contact of the push-button switch to 5V
  • Check everything is wired correctly, then download the following sketch

Source File

Copy and paste this into a new Arduino Sketch.

/******************************************
* Colour Invaders!
*
* How long can you last against the relentless attack 
* of the COLOUR INVADERS!
*
* Author : Mike Field <hamster@snap.net.nz<
*
* A really simple game for a strip of 30  WS2812B LEDs,
* an Arduino Uno, and a rotary encoder with a switch-.
*
* The Colour Invaders are attacking. You have 
* To use the rotary encoder (connected to pins 3 and 4)
* and the fire button (on pin 5) to fend them off. Ensure
* that your engineer attaches the correct pull-down resistors
* after connecting the sensors to the 5V power bus. 
* 
* Your only tool is the 30-LED WS2812B scanner, connected to 
* pin 5 (as well as the 5V and GND power bus connections)
*
* To destroy the invaders you have to tune into the 
* colour of their deflector shields, then launch your 
* photon torpedo. Do not rush though, as your photon torpedo
* will take a second or so to recharge.
*
* If you have matched the colour they will explode in
* a blinding white light. 
*
*********************************************/

/***************************************
* Note: the output can not be changed 
* Without updating the in-line assembler
***************************************/
#define OUTPUT_PIN 6

/************************************
* Pins for the encoder and button 
************************************/
#define ENC_PIN_1  3
#define ENC_PIN_2  4
#define BTN_PIN    5
#define LED_COUNT 30

/* Speed the missile moves - lower is faster */
#define MISSILE_SPEED 5

/* How long the explosion lasts */
#define EXPLOSION_DELAY 300

/* How long it takes for the weapon to recharge */
#define RECHARGE_DELAY 300

/* Speed of the invader - gradually reduces */
static byte invader_speed = 200;

static unsigned char leds[LED_COUNT][3];
static byte invaders[LED_COUNT];
static byte invaders_colours[LED_COUNT];
static byte encoder_sw_last = 0;
static byte encoder = 0;
static byte missile = 0;
static word missile_delay = 1000;
static word missile_colour;
static word invader_delay;
static word recharge_delay;
static word explosion = 0;
static word explosion_count;
static byte random_colour = 0;

void outputWS2812Bbytes(unsigned char (*leds)[3], unsigned char length);

/************************************
* Configure the I/O ports
************************************/
void setup() {
  // put your setup code here, to run once:
  pinMode(OUTPUT_PIN,OUTPUT);
  pinMode(ENC_PIN_1, INPUT);
  pinMode(ENC_PIN_2, INPUT);
  pinMode(BTN_PIN,   INPUT);
  encoder_sw_last = read_encoder();
}

/************************************
* Read the rotary encoder position
************************************/
int read_encoder(void)
{
  if(digitalRead(ENC_PIN_1))
  {
    if(digitalRead(ENC_PIN_2))
      return 3;
    return 1;
  }

  if(digitalRead(ENC_PIN_2))
     return 2;
  return 1;
}

/**************************************
* Update the value for 'encoder', and
* clamp it between 0 and 47.
**************************************/
void process_encoder(void)
{
  byte i = read_encoder();

  /* pseudo random way to generate the next invader colour */
  if(i != encoder_sw_last)
        random_colour = millis()%6;
  
  /* Update the encoder value */
  switch(encoder_sw_last) {
      case 0:
        switch(i) {
          case 1: encoder++; break;
          case 3: encoder--; break;
        }
        break;
      case 1:
        switch(i) {
          case 2: encoder++; break;
          case 0: encoder--; break;
        }
        break;
      case 2:
        switch(i) {
          case 3: encoder++; break;
          case 1: encoder--; break;
        }
        break;
      default:
        switch(i) {
          case 0: encoder++; break;
          case 2: encoder--; break;
        }
        break;
  }
  encoder_sw_last = i;
  
  /* Clamp the encoder between 0 and 48 */
  if(encoder == 0xFF)
    encoder = 47;
  if(encoder == 48) 
    encoder = 0;
}

/************************************
* Test to see if a missle has hit, and
* if so take the required actions
*
* This may need to be called twice from 
* loop(), as the missile and the invaders may 
* both move in the same pass
***********************************/
void test_for_missile_hit(void) {
  if(missile > 0 && invaders[missile]) {
      if(invaders_colours[missile] == missile_colour) {
        invaders[missile] = 0;
        explosion = missile;
        explosion_count = EXPLOSION_DELAY;
        if(invader_speed > 10) 
          invader_speed -= 4;
      }
      missile = 0;
  }
}

/***********************************
* Paint the colour of an LED, and 
* also clamp the value.
***********************************/
void add_colour(byte led, byte colour, byte intensity)
{
  byte r,g,b;
  
  /* Validate the array index */
  if(led >= LED_COUNT)
    return;

  /* Select the colour - the six invader colours and white */
  switch(colour) {
   case 0:  r = 0x80; g = 0x00; b = 0x00; break;
   case 1:  r = 0x50; g = 0x50; b = 0x00; break;
   case 2:  r = 0x00; g = 0x80; b = 0x00; break;
   case 3:  r = 0x00; g = 0x50; b = 0x50; break;
   case 4:  r = 0x00; g = 0x00; b = 0x80; break;
   case 5:  r = 0x50; g = 0x00; b = 0x50; break;
   default: r = 0x30; g = 0x30; b = 0x30; break;
  }

  /* Scale the brightness to three levels */  
  switch(intensity) {
    case 0: r >>= 4; g >>=2; b >>=2; break;
    case 1: r >>= 2; g >>=1; b >>=1; break;
  }
  
  /*******************************************
  * Add the intensity to what is already there, 
  * and clamp the brightness of the LED to stop 
  * odd stuff happening.
  ********************************************/
  if(leds[led][1] > 0xFF-r) 
    leds[led][1] = 0xFF;
  else
    leds[led][1] += r;
    
  if(leds[led][0] > 0xFF-g) leds[led][0] = 0xFF;
  else
    leds[led][0] += g;

  if(leds[led][2] > 0xFF-b) leds[led][2] = 0xFF;
  else
    leds[led][2] += b;
    
}

/*******************************************
* Main loop, implemented as without blocking
*******************************************/
void loop() {
  int i;

  /* Clear all the LEDS */
  for(i = 0; i < LED_COUNT; i++)
  {
    leds[i][0] = 0;
    leds[i][1] = 0;
    leds[i][2] = 0;
  }

  /* Game over - red glow */ 
  if(invaders[0]) {
    leds[0][1] = 0xFF;
    leds[1][1] = 0x3F;
    leds[2][1] = 0x1F;
    leds[3][1] = 0x0F;
    outputWS2812Bbytes(leds,30);
    return;
  }
  
  /* Read the encoder */
  process_encoder();

  /* Show the current colour choice, either dull or
   * bright depending on recharge */
  if(recharge_delay == 0)
    add_colour(0,encoder>>3,2);
  else
    add_colour(0,encoder>>3,0);
  
  /* Keep track of the recharge */
  if(recharge_delay > 0)
    recharge_delay--;
    
  /* Is a missile in flight? */
  if(missile == 0)
  {
      /* Are we recharged? Is the button pushed? */
      if(recharge_delay == 0 && digitalRead(BTN_PIN)) {
        /* Fire missile! */
        missile_colour = encoder >>3;
        missile = 1;
        missile_delay = MISSILE_SPEED;
        recharge_delay = RECHARGE_DELAY;
        
        /* Use timer for a pseudo-random colour */
        random_colour = millis()%6;
      }
  }
  else
  {
    /* Is it time to update the missile location? */
    if(missile_delay == 0)
    {
      /* Yep, check for the missile being off the end of the strip */
      if(missile == LED_COUNT-1)
         missile = 0;
      else 
      {
         missile++;
         test_for_missile_hit();
         missile_delay = MISSILE_SPEED;
      }
    }
    else
      missile_delay--;
  }
  
  /* Is it time to move the invaders? */
  if(invader_delay == 0) {
     /* Move them along one */
     for(i = 0; i < LED_COUNT-1; i++)
     {
        invaders[i]         = invaders[i+1];
        invaders_colours[i] = invaders_colours[i+1];
     }
     
     /* Has one moved into the missile */
     test_for_missile_hit();
     invaders[LED_COUNT-1] = 0;
     
     /* Is it time to send a new invader? */
     if(invaders[LED_COUNT-2] == 0 && invaders[LED_COUNT-3] == 0 && invaders[LED_COUNT-4] == 0) {
       /* Colour is randomly choosen when a button is
       * pushed or encoder is turned - this gives cheat! */
       invaders[LED_COUNT-1] = 1;
       invaders_colours[LED_COUNT-1] = random_colour;
     }
     invader_delay = invader_speed;
  }  else
      invader_delay--;  
  
  /* Display the missile */
  if(missile > 0) add_colour(missile,   missile_colour, 2);
  if(missile > 1) add_colour(missile-1, missile_colour, 1);
  if(missile > 2) add_colour(missile-2, missile_colour, 2);

  /* Display the invaders */
  for(i = 0; i < LED_COUNT; i++)
  {
     if(invaders[i] != 0)
        add_colour(i, invaders_colours[i], 2);
  }
  
  /*********************************************
  * We love explosions! Do we have one to paint?
  *********************************************/
  if(explosion_count > EXPLOSION_DELAY*3/4) {
    explosion_count--;
    add_colour(explosion,    9, 2);
  }
  else if(explosion_count > EXPLOSION_DELAY/2) {
    explosion_count--;
    if(explosion > 1) add_colour(explosion-1, 9, 1);
    add_colour(explosion,    9, 2);
    if(explosion < LED_COUNT-1) add_colour(explosion+1, 9, 1);
  }
  else if(explosion_count > 0) {
    explosion_count--;
    if(explosion > 1) add_colour(explosion-1, 9, 0);
    add_colour(explosion,    9, 1);
    if(explosion < LED_COUNT-1) add_colour(explosion+1, 9, 0);
  }

  /**********************************
  * Finally update the display 
  **********************************/
  outputWS2812Bbytes(leds,30);
  
  /* Pause a while */
  delay(1);
}


/*******************************************************************
* Ew! My yeach in-line assembler stuff to send the colours out to 
* the WS2812B strip. Here be dragons, as well as terrible assembler.
*
* Please submit any patches to me! :-)
******************************************************************/
void outputWS2812Bbytes(unsigned char (*leds)[3], unsigned char length)
{
delay(1);
  asm volatile(
  " cli \n\t" // Disable interrupts
  " mov 18,%1\n\t"  // Copy length
  " add %1, 18\n\t" // Add it back
  " add %1, 18\n\t" // Add it back, so it is now x3 what it was
  "L_next%=:" "\n\t"
  // Bit 7
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 128 \n\t" // Test the bit - one cycle
  " BRNE L_bit7_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit7_1_%=  \n\t"
  "L_bit7_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit7_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause

  ///////////////////////////////////////////////////////////
  // Bit 6 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 64 \n\t" // Test the bit - one cycle
  " BRNE L_bit6_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit6_1_%=  \n\t"
  "L_bit6_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit6_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  
  ///////////////////////////////////////////////////////////
  // Bit 5 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 32 \n\t" // Test the bit - one cycle
  " BRNE L_bit5_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit5_1_%=  \n\t"
  "L_bit5_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit5_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  
  ///////////////////////////////////////////////////////////
  // Bit 4 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 16 \n\t" // Test the bit - one cycle
  " BRNE L_bit4_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit4_1_%=  \n\t"
  "L_bit4_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit4_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  
  
  ///////////////////////////////////////////////////////////
  // Bit 3 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 8 \n\t" // Test the bit - one cycle
  " BRNE L_bit3_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit3_1_%=  \n\t"
  "L_bit3_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit3_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  
  ///////////////////////////////////////////////////////////
  // Bit 2 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 4 \n\t" // Test the bit - one cycle
  " BRNE L_bit2_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit2_1_%=  \n\t"
  "L_bit2_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit2_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause

  ///////////////////////////////////////////////////////////
  // Bit 1 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z \n\t" // Load and post increment - two cycles
  " ANDI 18, 2 \n\t" // Test the bit - one cycle
  " BRNE L_bit1_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit1_1_%=  \n\t"
  "L_bit1_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit1_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  
  
  ///////////////////////////////////////////////////////////
  // Bit 0 - Verified
  " SBI 11, 6 \n\t" // Set port b bit 4 Arduino pin 12
  " NOP \n\t" // A pause
  " LD 18, Z+ \n\t" // Load and post increment - two cycles
  " ANDI 18, 1 \n\t" // Test the bit - one cycle
  " BRNE L_bit0_0_%= \n\t" // Skip the clear if the bit is set
  " CBI 11, 6 \n\t" // clear port b bit 4
  " JMP L_bit0_1_%=  \n\t"
  "L_bit0_0_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  "L_bit0_1_%=:" "\n\t"
  " NOP \n\t" // A pause
  " NOP \n\t" // A pause
  " cbi 11, 6 \n\t" // clear port b bit 4
  " SUBI %1,1 \n\t" // Decrement the count
  " BREQ L_done%= \n\t" // Jump back if we still have more to do
  " JMP L_next%= \n\t" // Jump back if we still have more to do

  // All Finshed
  "L_done%=: \n\t"
  " sei \n\t"
    : : "z" (leds),  "d" (length));
}

Personal tools