Introduction: Arduino-Controlled Platformer Game With Joystick and IR Receiver

Today, we're going to use an Arduino microcontroller to control a simple C#-based platformer game. I am using the Arduino to take input from a joystick module, and send that input to the C# application which listens and decodes input over a Serial connection. Although you don't need any previous experience in building video games to complete the project, it may require some time to absorb some of the things going on in the "game loop," which we will discuss later.

To complete this project, you will need:

  • Visual Studio Community
  • An Arduino Uno (or similar)
  • A joystick controller module
  • Patience

If you're ready to begin, carry on!

Step 1: Hook Up the Joystick and IR LED

Here, the hookup is fairly simple. I have included diagrams showing only the joystick hooked up, as well as the setup I am using, which includes the joystick plus an infrared LED for controlling the game with a remote control, which comes with many Arduino kits. This is optional, but it seemed like a cool idea to be able to do wireless gaming.

The pins used in the setup are:

  • A0 (analog) <- Horizontal or X-axis
  • A1 (analog) <- Vertical or Y-axis
  • Pin 2 <- Joystick Switch input
  • Pin 2 <- Infrared LED input
  • VCC <- 5V
  • Ground
  • Ground #2

Step 2: Create a New Sketch

We will start with creating our Arduino sketch file. This polls the joystick for changes, and sends those changes to the C# program every several milliseconds. In an actual video game, we would check the serial port in a game loop for input, but I began the game as an experiment, so the framerate is actually based on the number of events on the serial port. I had actually begun the project in the Arduino sister project, Processing, but it turns out it was much, much slower and could not handle the number of boxes on the screen.

So, first create a new Sketch in the Arduino code editor program. I will show my code and then explain what it does:

#include "IRremote.h"

// IR variables
int receiver = 3;             // Signal Pin of IR receiver
IRrecv irrecv(receiver);      // create instance of 'irrecv'
decode_results results;       // create instance of 'decode_results'

// Joystick/game variables
int xPos = 507;
int yPos = 507;
byte joyXPin = A0;
byte joyYPin = A1;
byte joySwitch = 2;
volatile byte clickCounter = -1;
int minMoveHigh = 530;
int minMoveLow = 490;
int currentSpeed = 550;         // Default = an average speed
int speedIncrement = 25;        // Amount to increase/decrease speed with Y input
unsigned long current = 0;      // Holds current timestamp
int wait = 40;                  // ms to wait between messages [Note: lower wait = faster framerate]
volatile bool buttonPressed = false; // Gauge if the button is pressed

void setup()
{
  Serial.begin( 9600 );
  pinMode( joySwitch, INPUT_PULLUP );
  attachInterrupt( 0, jump, FALLING );
  current = millis();           // Set up the current time
  // Set up infrared receiver:
  irrecv.enableIRIn(); // Start the receiver
  
} // setup


void loop()
{
  int xMovement = analogRead( joyXPin );
  int yPos = analogRead( joyYPin );
  
  // Handle the Joystick X movement regardless of timing:
  if ( xMovement > minMoveHigh || xMovement < minMoveLow )
  {
    xPos = xMovement;
  }

  // Only handle the IR movement if input has been received:
  if ( irrecv.decode( &results ) ) // have we received an IR signal?
  {
    translateIR( results ); 
    irrecv.resume();              // receive the next value
  }

  // Either output a "Jump" message or a movement message this frame.
  if( buttonPressed )
  {
    delay( 50 );                // Put this in the main loop to stop
                                //    the message from being erased
    buttonPressed = false;      // After the delay, continue the loop
    Serial.println( "Jump" );
  }
  else if( millis() > current + wait )
  {
    currentSpeed = yPos > minMoveLow && yPos < minMoveHigh // If only moved a little...
      ? currentSpeed              //    ...just return the current speed
      : getSpeed( yPos );         // Only change yPos if joystick moved significantly
    //int distance = ;
    Serial.print( (String) xPos + "," + (String) yPos + ',' 
        + (String) currentSpeed + '\n' );
    current = millis();
    
  }
  
} // loop


int getSpeed( int yPos )
{
  // Negative values indicate Joystick moved up
  if( yPos < minMoveLow )     // Interpreted "UP"
  {
    // Protect from going over maximum value
    return currentSpeed + speedIncrement > 1023 ? 1023 : currentSpeed + speedIncrement;
  }
  else if( yPos > minMoveHigh )  // Interpreted "Down"
  {
    // Protect from going under 0
    return currentSpeed - speedIncrement < 0 ? 0 : currentSpeed - speedIncrement;
  }
  
} // getSpeed


void jump()
{
  buttonPressed = true;         // Indicate button was pressed.
  
} // jump


// When a button is pressed on the remote, handle the proper response
void translateIR( decode_results results ) // takes action based on IR code received
{
    switch( results.value )
    {
        case 0xFF18E7:
            //Serial.println("2");
            currentSpeed += speedIncrement * 2;
            break;
        case 0xFF10EF:
            //Serial.println("4");
            xPos = -900;
            break;
        case 0xFF38C7:
            //Serial.println("5");
            jump();
            break;
        case 0xFF5AA5:
            //Serial.println("6");
            xPos = 900;
            break;
        case 0xFF4AB5: //Serial.println("8");
            currentSpeed -= speedIncrement * 2;
            break;
      
        default: 
          //Serial.println(" other button   ");
          break;
  
    }// End switch

} //END translateIR

I tried to create the code to be mostly self-explanatory, but there are a few things worth mentioning. One thing I tried to account for was in the following lines:

int minYMoveUp = 520;
int minYMoveDown = 500;

When the program is running, the analog input from the joystick tends to jump around, usually staying at around 507. To correct for this, the input doesn't change unless it is larger than minYMoveUp, or smaller than minYMoveDown.

pinMode( joySwitch, INPUT_PULLUP );
attachInterrupt( 0, jump, FALLING );

The attachInterrupt() method allows us to interrupt the normal loop at any time, so that we can take input, like the button press when the joystick button is clicked. Here, we have attached the interrupt in the line before it, using the pinMode() method. An important note here is that to attach an interrupt on the Arduino Uno, you have to use either pin 2 or 3. Other models use different interrupt pins, so you may have to check which pins your model uses on the Arduino website. The second parameter is for the callback method, here called an ISR or an "Interrupt Service Routine." It should not take any parameters or return anything.

Serial.print(...)

This is the line which will send our data to the C# game. Here, we send the X-axis reading, the Y-axis reading, and a speed variable to the game. These readings can be expanded to include other inputs and readings to make the game more interesting, but here, we will only use a couple.

If you are ready to test your code, upload it to the Arduino, and press [Shift] + [Ctrl] + [M] to open the serial monitor and see if you are getting any output. If you are receiving data from the Arduino, we are ready to move along to the C# portion of the code...

Step 3: Create the C# Project

To display our graphics, I initially started a project in Processing, but later decided it would be too slow to show all of the objects we need to display. So, I chose to use C#, which turned out to be much smoother and more responsive when handling our input.

For the C# part of the project, it is best to simply download the .zip file and extract it to its own folder, then modify it. There are two folders in the zip file. To open the project in Visual Studio, enter the RunnerGame_CSharp folder in Windows Explorer. Here, double-click the .sln (solution) file, and VS will load the project.

There are a few different classes I created for the game. I won't go into all the details about each class, but I will give an overview of what the main classes are for.

The Box Class

I created the box class to allow you to create simple rectangle objects that can be drawn on-screen in a windows form. The idea is to create a class that can be extended using other classes that may want to draw some kind of graphics. The "virtual" keyword is used so that other classes may override them (using the "override" keyword). That way, we can get the same behavior for the Player class and the Platform class when we need to, and also modify the objects however we need to.

Don't worry too much about all of the properties and draw calls. I wrote this class so that I could extend it for any game or graphics program I might want to make in the future. If you need to simply draw a rectangle on the fly, you don't have to write out a large class like this. The C# documentation has good examples of how to do this.

However, I will lay out some of the logic of my "Box" class:

public virtual bool IsCollidedX( Box otherObject ) { ... }

Here we check for collisions with objects in the X-direction, because the player only needs to check for collisions in the Y direction (up and down) if he is lined up with it on the screen.

public virtual bool IsCollidedY( Box otherObject ) { ... }

When we are over or under another game object, we check for Y collisions.

public virtual bool IsCollided( Box otherObject ) { ... }

This combines X and Y collisions, returning whether any object is collided with this one.

public virtual void OnPaint( Graphics graphics ) { ... }

Using the above method, we pass any graphics object in and use it as the program is running. We create any rectangles which might need to be drawn. This could be used for a variety of animations, though. For our purposes, rectangles will do fine for both the platforms and the player.

The Character Class

The Character class extends my Box class, so we have certain physics out of the box. I created the "CheckForCollisions" method to quickly check all the platforms we have created for a collision. The "Jump" method sets the player's upward velocity to the JumpSpeed variable, which is then modified frame-by-frame in the MainWindow class.

Collisions are handled slightly differently here than in the Box class. I decided in this game that if jumping upwards, we can jump through a platform, but it will catch our player on the way down if colliding with it.

The Platform Class

In this game, I only use the constructor of this class which takes an X-coordinate as an input, calculating all of the platforms' X locations in the MainWindow class. Each platform is set up at a random Y-coordinate from 1/2 the screen to 3/4 of the screen's height. The height, width, and color are also randomly generated.

The MainWindow Class

This is where we put all of the logic to be used while the game is running. First, in the constructor, we print all of the COM ports available to the program.

foreach( string port in SerialPort.GetPortNames() )
	Console.WriteLine( "AVAILABLE PORTS: " + port );

We choose which one we will accept communications on, according to which port your Arduino is already using:

SerialPort = new SerialPort( SerialPort.GetPortNames()[2], 9600, Parity.None, 8, StopBits.One );

Pay close attention to the command: SerialPort.GetPortNames()[2]. The [2] signifies which serial port to use. For example, if the program printed out "COM1, COM2, COM3", we would be listening on COM3 because the numbering begins at 0 in the array.

Also in the constructor, we create all of the platforms with semi-random spacing and placement in the Y direction on the screen. All of the platforms are added to a List object, which in C# is simply a very user-friendly and efficient way of managing an array-like data structure. We then create the Player, which is our Character object, set the score to 0 and set GameOver to false.

private static void DataReceived( object sender, SerialDataReceivedEventArgs e )

This is the method which is called when data is received on the Serial port. This is where we apply all of our physics, decide whether to display game over, move the platforms, etc. If you have ever built a game, you generally have what is called a "game loop", which is called each time the frame refreshes. In this game, the DataReceived method acts as the game loop, only manipulating the physics as data is received from the controller. It might have worked better to set up a Timer in the main window, and refresh the objects based on the data received, but as this is an Arduino project, I wanted to make a game that actually ran based on the data coming in from it.

In conclusion, this setup gives a good basis for expanding the game into something usable. Although the physics aren't quite perfect, it works well enough for our purposes, which is to use the Arduino for something everyone likes: playing games!