Introduction: Star Wars Themed Retro Arcade Game

About: I may be an electrical engineer by trade but that won't stop me from tinkering in the domain of mechanical engineers and artists:P

One button. LCD. Procedurally generated terrain. A dash of Star Wars. An addicting game.

Not sure why, but I have been obsessed with art style of those pixelated retro arcade games. I just couldn't get enough and so I decided to make a simple arcade game for Arduino that would display graphics on a standard 16x2 character LCD.

This game was designed to be simple and not resource intensive but don't let that fool you. You can learn a lot from a challenge like this. I recommend downloading source code

I didn't use an Arduino board but a Arduino compatibel LinkIt ONE board because I don't have an Arduino.

Step 1: The Idea

Limitations presented by 16x2 LCD were pretty tough. The driver allows for a total of 8 custom sprites (and you need at least 2 for an animated sprite) + all standard characters (which are in most part useless). The 2 row LCD basically allows only two games: jumping over obstacles and and dodging obstacles. Jumping over obstacles requires better animation to look natural - something that can not be done with only 8 custom characters available (not to mention those huge gaps between them!). This is how I decided on making an obstacle dodger game.

And now for the setting. I could pretend space theme was chosen after a thorough thinking what would look best but... You know how Star Wars VII was just released... and I managed to make a character that looks somewhat like an X-wing fighter... Must I go on? :)

Now the controls. I could go with 4 buttons (go up, go down, go left, go right) but through some testing I figured out left and right movement doesn't add much to the gaming experience and so I dumped it but. Now up and down movement... You can either go up or go down. There is only one way you can go so I dumped one more button, leaving me one buttonfor toggling which row space ship is in.

To sum it up: this will be a retro Star Wars themed one button controlled obstacle dodger game.

Now enough intro, lets get started.

Step 2: The Hardware

Hardware won't be discussed much here as this was mainly a programming challenge but I will tell you enough for you to make it work.

For setting up the display, take a look at the diagram above or take a look at my other 'ible, which you can see here. Note that I've switched pin 12 for pin 7 and pin 11 for pin 6. Also make sure your LCD libraries are up to date as older ones came with an error.

Button should be connected as seen in second diagram above. You can't just connect the button so it makes connection between +5V and pin. You must follow diagram above or it won't work. The only thing you can change is the resistor's resistance. The bigger the better but anything above 100 Ohm should do (this only affects power consumption).

Step 3: Game Mechanics

This game would suck if you couldn't interact with it. If you couldn't die, if it was too easy or if it was repetitive you would get bored fast and dump it. I will list game elements in order I implemented them.

Procedural level generation:

Game will generate the terrain on the fly. Since this is a big part, it deserves it's own step.

Button

Detecting button click is simple. Wait for button press, wait for button release, carry on with the code. But what about if you want to animate things while waiting for button release? A bit harder but not a problem.

bool buttonReady = 1;
//buttonif (buttonReady){
  if (!digitalRead(buttonPin)){
    y=!y;
    buttonReady = 0;
    lcd.setCursor(x,y);
    lcd.write(ship);
    lcd.setCursor(x,!y);
    lcd.write(' ');
    }
    //lcd.clear();
  }
else{
  if (digitalRead(buttonPin)){
    buttonReady = 1;
  }
}

As you can see in code above, I did that by adding a buttonReady variable. Naming might not be the best but functionality remains the same. While buttonReady is 1 (true), program waits for button press. After it detects button press, buttonReady toggles state to 0 (false) and program starts waiting for button release. On button release spaceship moves and button ready is set back to 1. This way game can run while button is being pressed.

Collision:

If you want to check if you hit something, you must check if any object is in the same place as you are. Same goes for this. See code below to see how this works.

//collision detectionif(y){ //bottom rowif ((b>>15-x)%2){
    goto boom;
  }
}
else{  //top rowif ((a>>15-x)%2){
    goto boom;
  }
}

As you see this is done with help of bitshift and modulo operator. This is part of my "optimize the code" minigame I played through programming this entire thing. What bitshift does is it shifts bits of variable left or right by set amount. This makes it easy to see whether there is a 1 at certain position or not. After bitshifting modulo (&2) returns division remainder and if that equals 1, there was a 1 in set position otherwise there was 0.

If you don't get bitshift operator (don't worry you'll get it some day), you can use array functions instead (your data variables must be arrays and you will have to make a function for shifting data).

Scoring
Game is not a game if it doesn't show you score you achieved. But how to score such game? By amount of obstacles dodged! But does that mean a pint should be added every time you click a button and not die? That would make levels with many obstacles in different lanes in early game unfairly easier and user could cheat by clicking multiple times when game starts and there are no obstacles... Adding a point for every block travelled without hitting anything is a better way to score players. I could actually name it "distance travelled" but "score" sounds better.

Because spaceship starts at position 4 and terrain is all blank game starts, player starts with negative 12 points. This way he will have 0 score if he hits the first obstacle.

Scaling difficulty

But even scoring can't make you stick with the game if it is too easy. You get bored and stop playing it. By default game updates screen every 0.7 seconds (stepTime variable). That is fine to start with but becomes boring quite soon. Obviously there must be some scaling difficulty implemented. I have decided to do that by speeding up the game - by lowering the stepTime variable. On each step it is reduced by difficulty amount. I found that difficulty between 1 and 3 work best. If you set it higher, game becomes too hard very quickly. If you want bigger challenge, I recommend setting stepTime variable lower and leaving difficulty low. That will get you fun fast paced game.

We don't need to worry about limiting stepTime to be always greater than 0 as player can not possibly go far when he has to make decisions in tenth of a second as even button press takes longer.

Step 4: Procedural Level Design

Designing levels for infinitely scrolling games in advance is obviously impossible. You have to come up with algorithm for generating terrain.

Here are demands for my algorithm I came up with:

Screen must start blank (with nothing but player on it) and obstacles must start scrolling from the right. Every time screen scrolls, a new obstacle must be added. It may be added in either top or bottom row but player must be able to avoid collision so whenever obstacle row shifts from one row to another, one blank column must appear. Because we want player to react immediately, he is always given only one cell of time to move thus no more than one blank column must appear one after another.

I first coded this algorithm in python as it is very easy to debug and a lot easier to understand when compared to C code. Besides if you understand how and why it works in python, you can write it in C as well.

import random
last = 0
skipped = 0
sym=[" ","X"]
a=""
b=""
for i in range(100):
    if skipped:
        a+=sym[last]
        b+=sym[not(last)]
        skipped = 0
    elif random.randint(0,10)%2 != last:
        last = not(last)
        skipped = 1
        a+=" "
        b+=" "
    else:
        a+=sym[last]
        b+=sym[not(last)]
        
print(a)
print(b)

If you don't know what above python script does, try running it and see what happens. It's simple and you should be able to figure it out quickly.

I then translated this code to C and used some bit operations as optimization. You don't have to use those, you can easily omit that and stick with array operations.

unsignedint last = 0;
bool skipped = 0;
unsignedint a = 0b0000000000000000;
unsignedint b = 0b0000000000000000;

a = a << 1;
b = b << 1;
//this moves averything one place to the leftif(skipped){
  a = a | last;
  b = b | !last;
  skipped = 0;
}

else{
  randomSeed(millis());
  if (random(10)%2){
    last = !last;
    skipped = 1;
    //a = a<<1;//b = b<<1
  }
  else{
    a = a | last;
    b = b | !last;
  }

That is it regarding level generation. This function (without variable declaration) is called every time screen scrolls. After terrain generation screen is redrawn.

Step 5: Character Creation

Now is the time you start looking at the code. If you want to make your own character, you must create a byte array. You can help yourself with this website, which makes character creation a lot easier. Click on boxes and copy text on the right when satisfied. This byte array is then sent to LCD through createChar(n,byte array) function. Time to take a look at the code below.

#include <LiquidCrystal.h>
byte spaceShip1[8] = {
	0b00000,
	0b11110,
	0b01000,
	0b01100,
	0b10111,
	0b01100,
	0b01000,
	0b11110
};

LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
lcd.createChar(0,spaceShip1);

You have to create every character the same way. If you take a look at the actual code, you will see there are two spaceship characters for both X-Wing and TIE fighter. The difference between them is that one is one pixel above the second one. Alternation between them gives a feeling of "living" spaceship as a post to a dead object.

I also made two different obstacles - one for top row and one for bottom one. I tried animating them (opposing faction's vessel) but that didn't turn out as good as entire screen became confusing + I would be left with only 2 characters for explosion animation. Also turrets didn't work out too well as they were really under defined with 5x8 px resolution... Rocks hanging from the ceiling and poking from the ground worked best.

Explosion consists of three random clumps of pixels, each one being bigger than the last one.

When you want to display your custom character, you can do that by calling lcd.write(n) where n is number you passed as first parameter of lcd.createChar(n,byteArray) function. In above example that would be done by calling lcd.write(0).

Step 6: Animation

If you want to make ship wiggle with set frequency, you can do it like this: draw ship, delay(time), draw other ship, delay(time) and repeat. This works. But what happens when you try to press a button? Nothing. Why? During delay() function, Processor "sleeps" and does nothing else. This becomes terribly noticeable when you want to animate things at different frequencies.

Don't worry, there is a solution to this problem. You have to keep track of when you last updated your spaceship and comparing current time to it. If difference is big enough, you update spaceship's sprite. This is done by storing system time in a variable (timeA = millis();) and comparing it to update cycle variable (if millis()-timeA > animationTime {update sprite}). If you want to animate different things with different frequencies, you have to store multiple time variables as I did in the game - one for spaceship animation and one for terrain animation

Some more animation techniques will be discussed in next step at end game splash screen part.

Step 7: Welcome and End of Game Splash Screen

When you first start this game, you are welcomed with text saying you must press the button to start and just after that, you are given the option to select your faction (light or dark side). This not only looks good, it also lets a factor of randomness to get into the game. Before I implemented welcome screen, LinkIt ONE would always generate the exact same terrain, making this game boring. Now that user decides when game starts, start time is different and thus random values actually become random. That is due to random's seed being a little bit different each time. Choosing your side will be discussed a bit more in next step.

End game splash screen however is there only for show. When you hit an obstacle, 3 frame explosion animation is displayed twice and screen is wiped in black. "GAME OVER!" is displayed, it flickers a little bit and score shows up in bottom row.

Explosion is ultra simple. You set cursor at spaceship's position, display first frame, wait a bit, display second screen, wait some more and display the last frame of explosion animation. Repeat once and start wiping it all in black.

Black cell is last character LCD has in it's ROM, thus callable by lcd.write(0b11111111). Parameter of function is biggest possible 8 bit number. That said, for loop iterates through 1st row, each time rendering one black cell than waiting a bit before moving on. Second row is done the same just from right to left. This produces a beautiful wipe animation.

After some time, "GAME OVER" is displayed in 1st row. Flicker animation is done by filling 1st row with black cells again and rewriting "GAME OVER" soon after.

Score is the easiest. Cursor is set at second position in second row and function prints "Score:_ _ _ _" with enough spaces to reach all but last character in that row. Cursor is then set after first space and score is displayed.

This is likely very confusing to you so let's take a look at the code as it is much, much easier to understand that what I just wrote.

//endgame screenboom://explosion
lcd.setCursor(x,y);
lcd.write(4);
delay(200);
lcd.setCursor(x,y);
lcd.write(5);
delay(200);
lcd.setCursor(x,y);
lcd.write(6);
delay(200);
lcd.setCursor(x,y);
lcd.write(4);
delay(200);
lcd.setCursor(x,y);
lcd.write(5);
delay(200);
lcd.setCursor(x,y);
lcd.write(6);
delay(200);

//black wipefor(int i = 0; i<16; i++){
  lcd.setCursor(i,0);
  lcd.write(0b11111111);
  delay(100);
}
for(int i = 15; i>-1; i--){
  lcd.setCursor(i,1);
  lcd.write(0b11111111);
  delay(100);
}

//game over
lcd.setCursor(3,0);
lcd.print("GAME OVER!");
delay(1000);

//flicker
lcd.setCursor(3,0);
for(int i = 3;i<12;i++){
  lcd.write(0b11111111);
}
delay(100);
lcd.setCursor(3,0);
lcd.print("GAME OVER!");
delay(1000);

//score
lcd.setCursor(1,1);
lcd.print("Score:        ");
lcd.setCursor(7,1);
lcd.print(score);

//wait for buttonwhile(!digitalRead(buttonPin)){;}
while(digitalRead(buttonPin)){;}

//reset game
a = 0b0000000000000000;
b = 0b0000000000000000;
score = 0;
stepTime = 700;
goto charSelect;

End game screen lasts until user clicks the button again. Then stats are reset, level is cleared and you are given option to select your faction once again.

Step 8: Choose Your Side

Whenever you start a new round, game asks you to to choose your side. You can step in shoes of rebel fighters and command an x-wing fighter to bring help light side bring balance to the force or you can play as an imperial clone trooper, supporting the empire in his Tie fighter.

That little bit of code makes the game so much more immersive and boosts it's replay potential. It makes it more personal and lets player support his favourite side.

Even though it is easy to script it hides a few little tricks. The first is how to differ button click from button hold. As you will see, click switches side and hold starts the game. That can be easily done by storing time button is first clicked and comparing that time to current time. If difference is big enough, game must start. If button is released before game must start, faction is changed. If you check the code, you will see that changing faction is nothing but storing different sprites in first two character spaces (locations 0 and 1).

To let user switch faction as many times as he wants before game starts, entire function is enclosed in "while 1" loop, which runs forever and button hold breaks that loop.

charSelect:while(1){
  if (buttonReady){  //onclickif (!digitalRead(buttonPin)){
      buttonReady = 0;
      timeS = millis();
    }
      //lcd.clear();
  }
  else{  //on releaseif(millis()-timeS>700){
      break;  //start game
    }
    if (digitalRead(buttonPin)){
      buttonReady = 1;  
      y = !y;
      
      if(y){
        lcd.createChar(0,tieFighter1);
        lcd.createChar(1,tieFighter2);
        lcd.setCursor(0,1);
        lcd.print(" dark side     ");
      }
      else{
        lcd.createChar(0,xWing1);
        lcd.createChar(1,xWing2);
        lcd.setCursor(0,1);
        lcd.print(" light side    ");
      }
    }
    
  }
  
  //animationif(millis()-timeA > animationTime){
    timeA = millis();
    ship=!ship;
    //goto redraw;
    lcd.setCursor(14,1);
    lcd.write(ship);
    //lcd.setCursor(x,!y);//lcd.write(' ');
  }
}

Step 9: Conclusion

If you managed to read through this entire text, congratulations! I know it was a lot of content to swallow but I hope you learned something from it. Please give me some feedback on this entire thing as this was one of my more extensive instructables and it would help me a lot when making next one if you told me what you liked about it and what bothered you. Also please give me a vote in Sci-Fi and Arduino all the things contests. Favourite this 'ible if you liked it and follow me for more content.

Choose your side and may the force be with you!

P.S. Sorry so much for puting in such long chunks of code... Instructables is having some issues and I will make them collapse as soon as that feature will work properly.

Sci-Fi Contest

Participated in the
Sci-Fi Contest

Make It Glow! Contest

Participated in the
Make It Glow! Contest

Arduino All The Things! Contest

Participated in the
Arduino All The Things! Contest