Contents


Building an Arduino-based laser game, Part 2

Firing the gun

Jump-start your electronics projects with the open source hardware and software platform Arduino

Comments

Content series:

This content is part # of # in the series: Building an Arduino-based laser game, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Building an Arduino-based laser game, Part 2

Stay tuned for additional content in this series.

Before you start

Whether you're new to Arduino or a seasoned builder, this project has something for you. There's nothing quite as satisfying as creating an interactive physical object, knowing that if it breaks or needs modification, you know where all the parts go and how everything works. The 'Duino tag gun is a great project to work on by yourself or with friends. To complete this project, you should at least have a basic understanding of electronics (you should know what a resistor is, but you don't need to know the science behind one) and have an understanding of programming (you should know what loops and variables are, but you don't need to be able to parse Big O Notation). Don't be afraid to jump right in.

About this series

In this series, you use Arduino technology to create a basic interactive laser game called 'Duino tag:

  • Part 1: Learn some Arduino basics, lay out the project, and do an experiment that will help you understand how infrared works.
  • Part 2: Build and test the receiver part of the 'Duino Tag gun, including the testing.
  • Part 3: Build the transmitter and complete the 'Duino Tag gun.

About this tutorial

To follow along, you don't need any electronics experience, although experience working with electronic components can certainly serve you well. If you've worked with microcontrollers, you'll have an edge, but keep in mind that the Arduino platform is well suited for people without that experience. Above all, you should be willing to stretch your skills. Working with electronics and microcontrollers can be rewarding. Most software engineers don't get a chance to write code for devices that interface with the physical world, and Arduino provides a low-cost entry point into working with interactive devices.

This tutorial focuses on shaping the game. Now that you know the Arduino basics, you'll spend most of this tutorial building and testing a receiver for the 'Duino Tag gun. You'll get your hands dirty working with the code. We've got a bit of a chicken-and-egg problem: We can't actually test the receiver without having something to transmit, and we can't actually test transmission without having something to receive it. Fortunately, you'll learn a pretty easy solution for that.

System requirements

For this tutorial, you need a few tools and supplies. See Part 1 for a list, including the Arduino hardware and software. Following are the basic items needed.

Infrared LED
Just about any would do, but the brighter the better.
Infrared sensor
This series was written using a TSOP2138YA Infrared Sensor (from All Electronics).
100-ohm resistor
The markings for this resistor are brown-black-brown.
0.1uF capacitor
You will need a capacitor of 0.1uF.
Switch
You need a single pole momentary switch.
A piezo element
You want one with leads already soldered.
Wire
Get 22 gauge, solid or stranded.

You also need a way to connect your components, either using a breadboard or soldering things together.

To get around the chicken-and-egg problem, you need either a universal remote or a Sony Electronics-brand remote of some kind. The example in this series used a remote from a Sony combo VHS/DVD player and was successfully tested with a universal remote from a cable box.

As mentioned in Part 1, this project is more fun if you're working with other people building their own 'Duino Tag guns. It is also much easier to test your work if you have two guns to work with. If you decide to work alone, you may find Parts 2 and 3 easier if you build two guns yourself. This is especially true for Part 3, when we pull the whole thing together.

For now, let's focus on getting a basic receiver up and running. First, you need to learn about our infrared sensor.

Infrared sensor basics

There are many kinds of infrared (IR) sensors on the market. The TSOP2138YA was chosen primarily because of its low cost and availability from a great dealer.

The sensor

The sensor, shown below, combines a photo sensor and a preamp. Most IR sensors of this type combine these two features into one component for convenience. The housing acts as an IR filter. As discussed in Part 1, IR is everywhere. The housing of this sensor and other sensors of this type serve to filter out background IR. The sensor uses a carrier frequency of 38 kHz. This is a commonly used IR frequency and means that our guns will need to transmit at 38 kHz, as well. This sensor has a simple 3-pin interface:

  • Pin 1 provides a signal when a 38-kHz IR signal is sensed.
  • Pin 2 connects to a supply voltage.
  • Pin 3 connects to a ground.

As far as sensors go, it doesn't get much easier than this.

Figure 1. The TSOP2138TA sensor
The TSOP2138TA sensor

Now you'll connect the few components and get the sensor working.

Wiring the sensor

These instructions assume you're using a breadboard to connect the components. If you're soldering, you can adjust the instructions as necessary. The figures below feature a mini-breadboard attached to an Arduino proto-shield purchased from NKC Electronics. This is a great setup if you work with Arduino.

For wiring the IR sensor, you need the resistor and the capacitor. The spec sheet for the sensor suggests adding a capacitor and a resistor to "suppress power supply disturbances." When you finalize your gun configuration, you will want these additional components wired as close to the sensor as possible. Usually, people do this by mounting the sensor, capacitor, and resistor by themselves on a small printed circuit board or PCB.

The output from the sensor comes from pin 1, which should be wired to pin 2 on the Arduino board.

The supply voltage, which comes into the sensor on pin 2, should come through the 100-ohm resistor. Run a line from the +5v pin on the Arduino board to the resistor, and connect the resistor to pin 2 on the IR sensor.

The IR sensor is grounded through pin 3. The 0.1uF capacitor should be connected between pins 2 and 3 on the sensor, as shown below.

Figure 2. Sensor wiring
The sensor wiring
The sensor wiring

Now that you have the sensor wired, let's set up a couple of support components. This is where the piezo element and red LED come in. (That's the same red LED you used in the experiment from Part 1.)

Wire the positive leg of the LED to pin 13 on the Arduino board and the negative leg of the LED to the ground. Normally, you would need to add a resistor to keep the LED from burning out, but the Arduino board includes a resistor wired to pin 13. You'll use this LED to indicate that a signal is being received.

Likewise, you will use the piezo element to produce a sound when a signal is received. Wire the positive lead from the piezo to pin 12 on the Arduino and wire the negative lead to a ground pin.

That's it! Your sensor should be completely wired, and should look like the sensor in Figure 3.

Figure 3. The completed wiring
The completed wiring
The completed wiring

Now you can start putting together the code for the sensor. But first, to establish how data will be transmitted, you need to establish a simple protocol.

Setting up the 'Duino tag protocol

When a shot is fired, the IR LED in the transmitter broadcasts a small piece of encoded data. Before we can really decode this data, we need to know what the data will be. Doing so will require making some assumptions that should be spelled out clearly.

'Duino Tag is not a game for cheaters.
Meaning that everyone plays by the rules. We could establish a lot of protocol overhead to make sure nobody could cheat. If you want such a protocol, feel free to modify the code to your needs. The best way to prevent cheating, however, is to upload the same software to each player's gun just before the game starts. This tutorial does not cover encrypting or decrypting signals, or trying to prevent people from cheating.
Teams are organic in nature, not enforced by the hardware.
This allows for all sorts of behavior, such as making verbal alliances, betraying your allies, and accidentally shooting your friends.
Different types of ammunition, and different levels of players may be desirable.
You should leave some room in the protocol for this.
A referee may sometimes be necessary.
The referee should be able to change a player's level or ammo, reset a gun, etc.

The assumptions are outlined, and we're transmitting data in binary type, so the only remaining question is how many bits do we need for the protocol? If you break the protocol roughly in half, the first half represents "Who" and the second half represents "What." For example, Player 1 (who) fired a shot of strength 2 (what), or Referee 2 (who) resurrected you (what).

Let's start by assuming that for every six players, you need a referee, and that 12 players is a pretty big game. Using four bits, you could transmit 16 unique codes — more than enough to start. Likewise, 16 actions would be more than enough to start for a player or a referee. You could fire a range of 16 kinds of shots, or perform 16 different administrative actions.

You need eight bits total — the first four will correspond to a player's number, and the last four will correspond to an action of some kind (shot strength, or referee action). Now that you know approximately what to expect, let's start writing some code for the sensor.

Coding the sensor

The code for the sensor is written in four parts. First, you need to set up the pins and run a brief test sequence in the setup function. Then you'll put together a function to decode incoming IR signals. After this function is put together, you set up the Loop function to listen for signals. Finally, you'll sort out a function to play some tones. After the code is written, you can begin testing.

Setting up the pins and variables

Before getting into the setup function, we should set out some variables. By using variables to hold your pin assignments, you can easily rewire your Arduino board without making a bunch of code changes. When you wired the components to the Arduino, you used pin 2 for the sensor, pin 12 for the speaker, and pin 13 for the feedback LED (as in Listing 1).

Listing 1. Variables to hold pin assignments
i
int sensorPin  = 2;      // Sensor pin 1
int speakerPin = 12;     // Positive Lead on the Piezo
int blinkPin   = 13;     // Positive Leg of the LED we will use to indicate
                         // signal is received

You'll want three more variables related to setting thresholds with the sensor. One variable sets the threshold for a "start bit" that will be sent by the transmitter, and two variables represent the values of zero and one in terms of sensor signal pulse length.

Listing 2. Variables for setting thresholds with the sensor
int startBit   = 2000;   // This pulse sets the threshold for a transmission start bit
int one        = 1000;   // This pulse sets the threshold for a transmission that 
                         // represents a 1
int zero       = 400;    // This pulse sets the threshold for a transmission that 
                         // represents a 0

And you'll need an array to hold the results of the IR decoding.

Listing 3. Array to hold results of IR decoding
int ret[2];              // Used to hold results from IR sensing.

These variables won't make much sense until you get to the function that's used to decode the incoming data. Let's put that aside now, and put together the setup function. This function needs to set the modes of the various pins, then it should provide feedback to indicate the system is ready — a blink and a beep or two.

Setting the pin modes

First you need to set the pin modes, as shown in Listing 4. There is one input pin (pin 2) and two output pins (pin 12 and pin 13).

Listing 4. Setting the pin modes
void setup() {
  pinMode(blinkPin, OUTPUT);
  pinMode(speakerPin, OUTPUT);
  pinMode(sensorPin, INPUT);

Let's blink the light three times, playing a tone each time.

Listing 5. Setting the lights and tones
  for (int i = 1;i < 4;i++) {
    digitalWrite(blinkPin, HIGH);
    playTone(900*i, 200);
    digitalWrite(blinkPin, LOW);
    delay(200);
  }

With the playTone function, the first number controls the tone and the second controls the duration. The end result is three notes played in different tones, making a nice little startup sequence. (More on this in the next section.) Finally, just for testing, let's set up the serial monitor so you can observe the results in the monitor.

Listing 6. Setting up the serial monitor
  Serial.begin(9600);
  Serial.println("Ready: ");
}

The setup function is complete. You've set the pin modes correctly, put together a brief startup sequence, and even initialized a serial connection. Now let's look at the function you'll use to decode incoming data from the IR sensor.

Decoding the IR

Before starting to decode the IR, we know we'll want two tiny arrays to hold the two halves of the data— the who and the what— and an array to hold the two return values.

Listing 7. Two arrays to hold data
void senseIR() {
  int who[4];
  int what[4];

We set up three variables earlier that will be used in this function: startBit, one, and zero. These variables will set the thresholds for interpreting the behavior of the sensor pin when using the pulseIn function.

The pulseIn function, native to the Arduino language, is used to measure how long a specific pin (in this case, the sensor pin) is set to HIGH or LOW. Measurements from pulseIn are returned in microseconds. Using this function, you can decode what bits are being sent. However, you need to set the thresholds for how much elapsed time will represent a zero or a one. You also need some indication that you are at the beginning of a data set — thus, the startBit. The startBit variable indicates that:

  • A pulse of 2,000 microseconds will be used to start a data set.
  • A pulse of more than 1,000 microseconds is used to represent a one.
  • A pulse between 400 and 1,000 microseconds will be used to represent a zero.

The next thing to do is wait indefinitely for a startBit. While we wait for the startBit, make sure the feedback LED is off. But, as soon as we get a startBit, turn on the feedback LED.

Listing 8. Turning on the feedback LED
  while(pulseIn(sensorPin, LOW) < startBit) {
    digitalWrite(blinkPin, LOW);
  }

  digitalWrite(blinkPin, HIGH);

Now that you've received a startBit, you need to read the next eight pulses, assigning them to the proper arrays.

Listing 9. Assigning the pulses to the proper arrays
  who[0]   = pulseIn(sensorPin, LOW);
  who[1]   = pulseIn(sensorPin, LOW);
  who[2]   = pulseIn(sensorPin, LOW);
  who[3]   = pulseIn(sensorPin, LOW);
  what[0]  = pulseIn(sensorPin, LOW);
  what[1]  = pulseIn(sensorPin, LOW);
  what[2]  = pulseIn(sensorPin, LOW);
  what[3]  = pulseIn(sensorPin, LOW);

Once you have the data in the arrays, normalize the data to a simple set of zeros and ones, and send the binary number off for conversion. Along the way, you'll print out helpful messages to the serial monitor. Listing 10 is for the who array, though the what array is nearly identical.

Listing 10. The who array
  Serial.println("---who---");
  for(int i=0;i<=3;i++) {
    Serial.println(who[i]);
    if(who[i] > one) {
      who[i] = 1;
    } else if (who[i] > zero) {
      who[i] = 0;
    } else {
      // Since the data is neither zero or one, we have an error
      Serial.println("unknown player");
      ret[0] = -1;
      return;
    }
  }
  ret[0]=convert(who);
  Serial.println(ret[0]);

You can decode the data in any way you see fit. This function is abstracted to the convert function shown below.

Listing 11. Decoding the data
int convert(int bits[]) {
  int result = 0;
  int seed   = 1;
  for(int i=3;i>=0;i--) {
    if(bits[i] == 1) {
      result += seed;
    }
    seed = seed * 2;
  }
  return result;
}

By abstracting the functions, you make it easier for yourself down the line, should you decide to change the protocol in any way. Now that we have the code in place to decode the infrared signals, you need to set up the loop function to listen for the signals and behave accordingly.

The loop function

For now, the loop function is very simple. You mainly throw the loop function off to the senseIR function, where the while loop waits for a start bit. But, once the senseIR function returns, you look at the results in the ret array declared earlier and act on those results. At this time, that means ignoring invalid values, playing a tone when a valid value is returned, and dumping that value to the serial monitor.

Listing 12. The loop function
void loop() {
  senseIR();
 
  if (ret[0] != -1) {
    playTone(1000, 50);
    Serial.print("Who: ");
    Serial.print(ret[0]);
    Serial.print(" What: ");
    Serial.println(ret[1]);
  }
}

Once you have more components in place, you can take actions based on the return values. For now, this is enough testing. Well, almost. You still need to put together that playTone function mentioned earlier.

The playTone function

The playTone function, shown in Listing 13, was lifted directly from the Melody tutorial on the Arduino site (see Related topics). It is public domain code and serves to send a square wave to our piezo element, creating tones for us. This function is simple, accepting two arguments: the tone (a numeric value) and the length of time to play the tone (in microseconds).

Listing 13. playTone function
void playTone(int tone, int duration) {
  for (long i = 0; i < duration * 1000L; i += tone * 2) {
    digitalWrite(speakerPin, HIGH);
    delayMicroseconds(tone);
    digitalWrite(speakerPin, LOW);
    delayMicroseconds(tone);
  }
}

You don't need to dig too deep into this function for now. If you want your 'Duino Tag gun to play melodies or more interesting sounds, you can go through the Audio tutorials on the Arduino site. For now, you've got everything you need to start testing.

Testing the sensor

Once you've completed your code (or uploaded the Duino_Tag_Sensor file from the code archive for this tutorial), you are ready to test. At this point, you need that remote control mentioned earlier:

  1. Turn on the serial monitor, which will reset your Arduino board. You should see the startup sequence, with the blinking LED and associated beeping sounds. If you do not get the startup sequence, double-check your wiring.
  2. Point the testing remote at your IR sensor and start pressing buttons. You should hear the speaker beep and see the feedback LED blinking. You should also get some feedback in your serial monitor window, as shown below. The monitor output shows that one particular button press resulted in a who of 4 (player 4), and a what of 9 (action 9).
    Figure 4. Feedback in the serial monitor window
    Feeback in the serial monitor window
    Feeback in the serial monitor window
  3. Experiment with your remote control and see what other values you can get to return. If you are not getting any readings from your IR sensor, double-check your wiring and make sure your remote has good batteries.

Now that you've got a sensor working (and tested), take a step back and think about how cool it is that you've gotten this far. When you started, you might not have had any idea how you were going to do this, and you probably never worked with IR before. Congratulations!

In the next section, you set up the sensor so you're prepared for Part 3. Let's establish some basic behaviors for players and referees so that when the transmitter is done, you're ready to play. The code in the next section will prove a little difficult to test without finishing Part 3, but you may find that if you play with your remote, you can find some good codes to transmit. For example, on my remote, Volume Up always gives me a who/what of 4/9, while Volume Down always gives me a who/what of 12/9.

Handling decoded results

Now that you can decode these tiny datasets that are being transmitted, you need to do something with them.

Establishing players and referees

Start by establishing who the players are, who the referees are, and your player number and starting level. You should also set some variables to hold the number of shots and hits allowed, and the number of shots and hits you have. You won't be using the shots yet, but you can define them anyway. All of this gets defined at the top of the script.

Listing 14. Setting variables to hold number of shots and hits
int playerLine = 14;     // Any player ID >= this value is a referee, 
                         // <= this value is a player;
int myCode     = 1;      // This is your unique player code;
int myLevel    = 1;      // This is your starting level;
int maxShots   = 6;      // You can fire this many safe shots;
int maxHits    = 6;      // After this many hits you are dead;
int myShots   = maxShots;// You can fire 6 safe shots;
int myHits    = maxHits; // After 6 hits you are dead;

When uploading the code to multiple guns, all you need to do is change the value of the myCode variable for each new gun. You can leave the rest of the code untouched. You could do this with DIP switches, making it a hardware problem, instead. But the temptation to recode your gun on the fly would be overwhelming.

Now let's figure out what the actions themselves are. This will be easier to define if you start with the referee, for reasons that will shortly become obvious. Start by defining four actions a referee can take:

  • Promote (action 0)
  • Demote (action 1)
  • Reset Ammo (action 2)
  • Revive (action 3)

Because Promote and Demote could get out of hand, you should define a couple more variables to hold the maximum and minimum levels for a player.

Listing 15. Variable to hold maximum and minimum levels for a player
int maxLevel   = 9;      // You cannot be promoted past level 9;
int minLevel   = 0;      // You cannot be demoted past level 0

Next, define the actions a player can take. There are really only two actions: Hit and Reply. Since we allowed so much room, you can pass the player's level information along as the Hit action; you would have Hit From Level 0 (action 0), Hit From Level 1 (action 1), etc. until you reached the max, which in this case is Hit From Level 9 (action 9). By passing the level information along with the hit, you can make interesting modifications later to incorporate the level into the 'Duino Tag gun behavior.

As for Reply, replies come only in two forms: Success and Fail. Success means your hit landed or your referee task worked. Fail means the hit was ineffective or the referee task could not be completed, such as promoting someone past the available rank. Go ahead and designate these as Reply Success (action 14) and Reply Failure (action 15).

All of these actions can be stored as variables, as shown below.

Listing 16. Storing actions in variables
int refPromote = 0;     // The refCode for promotion;
int refDemote  = 1;     // The refCode for demotion;
int refReset   = 2;     // The refCode for ammo reset;
int refRevive  = 3;     // The refCode for revival;

int replySucc  = 14;   // the player code for Success;
int replyFail  = 15;   // the player code for Failed;

Once you have the actions defined, it's time to handle the actions as they come back from the senseIR function. Start by checking to see if the player code is above the line dividing referees from players: if (ret[0] >= playerLine) {.

Since you're in Referee territory, look at the return codes. For Promote, if the player is not maxed out, promote them and play a jingle.

Listing 17. Promote and play a jingle if the player is not maxed out
      if (ret[1] == refPromote) {
        // Promote
        if (myLevel < maxLevel) {
          Serial.println("PROMOTED!");
          myLevel++;
          playTone(900, 50);
          playTone(1800, 50);
          playTone(2700, 50);
        }

For Demote, if the player is not in the basement, demote him and play a different jingle.

Listing 18. Demote and play a different jingle if the player is not in the basement
      } else if (ret[1] == refDemote) {
        // demote
        if (myLevel > minLevel) {
          Serial.println("DEMOTED!");
          myLevel--;
        }
          playTone(2700, 50);
          playTone(1800, 50);
          playTone(900, 50);

If this is an ammo reset, set the player's shots to the max and play a little jingle.

Listing 19. Set player's shots to the max and play jingle on an ammo reset
      } else if (ret[1] == refReset) {
        Serial.println("AMMO RESET!");
        myShots = maxShots;
        playTone(900, 50);
        playTone(450, 50);
        playTone(900, 50);
        playTone(450, 50);
        playTone(900, 50);
        playTone(450, 50);

Finally, if the player is being revived, reset his shots, hits, and level, and play a snazzy tune.

Listing 20. Reset shots, hits, and level, play a tune if player is being revived
      } else if (ret[1] == refRevive) {
        Serial.println("REVIVED!");
        myShots = maxShots;
        myHits = maxHits;
        myLevel = 1;
        playTone(900, 50);
        playTone(1800, 50);
        playTone(900, 50);
        playTone(1800, 50);
        playTone(900, 50);
        playTone(800, 50);
      }

Player actions

You've covered all of the Referee actions. But what about player actions? They are easier to handle because there are fewer cases. There is a Success reply, which should play a successful-sounding jingle.

Listing 21. Success reply
    } else {
      if (ret[1] == replySucc) {
        playTone(9000, 50);
        playTone(450, 50);
        playTone(9000, 50);
        Serial.println("SUCCESS!");

A Failure reply should play an unsuccessful-sounding jingle.

Listing 22. Failure reply
      } else if (ret[1] == replyFail) {
        playTone(450, 50);
        playTone(9000, 50);
        playTone(450, 50);
        Serial.println("FAILED!");        
      }

A Hit reply should play a jingle that reminds you that you're not as good as the player who just nailed you when you were trying to be sneaky.

Listing 23. Hit reply
      if (ret[1] <= maxLevel && ret[1] >= myLevel && myHits > 0) {
        Serial.println("HIT!");
        myHits--;
        playTone(9000, 50);
        playTone(900, 50);
        playTone(9000, 50);
        playTone(900, 50);
      }
    }

Don't get too wrapped up in the tones for this example; they are mainly for testing and to act as placeholders. Once you have a viable transmitter for testing, you can tweak the tones until they fit a more cohesive sound font.

For now, if your remote is returning usable test codes, you can change the value of the variables to try the different parts of the code used for decoding the infrared. Having a fully coded receiver is nice, but having a transmitter would really be great right about now, huh? That will come in Part 3.

Summary

In this tutorial, you built and tested a receiver for the 'Duino Tag gun. You worked with the code and started shaping the game.

You're now ready for the transmitter. Line up your components and get ready for Part 3. You'll build the transmitter pretty quickly, then learn how to extend the range and look at some casing options. You'll explore ways to improve the system and possible avenues for future 'Duino Tag projects. If you have all of the components listed in Part 1, you should be in good shape. Just in case, for Part 3, make sure you have a single-pole momentary switch, an infrared LED, an 82-ohm resistor, and a 1k-ohm resistor. You already have everything else you'll need.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=368213
ArticleTitle=Building an Arduino-based laser game, Part 2: Firing the gun
publish-date=02102009