3 min read

Week 8: Serial Communication Between Arduino and Unity

Inspired by the possibilities I saw using p5.js, I thought I would try serial communication between the Arduino and Unity. I found this tutorial which helped me get started, but there were some missing pieces I had to add to accomplish my goal, which was to use four pushbuttons to maneuver a character up, down, left, right, and diagonally.

The Arduino side isn't complicated: I needed a strategy for sending relevant information to Unity so that the character would move only for as long as I pressed the pushbuttons. At first, I sent single bytes with Serial.write() corresponding to the directions I wanted the character to move. But I learned I risked overflowing the buffer because I was writing a byte every loop as long as I was pressing the corresponding button.

On the Unity side, I came across a peculiar problem as well. While I was able to read the bytes from the Arduino and convert them into character movements, I had trouble moving the character at a predictable speed. In Unity, it's typical to increment the position of an object in response to player commands using something like yPos += speed * Time.deltaTime, where  Time.deltaTime is the time that passes between each frame in a single second. In other words, if the game runs at 60fps, Time.deltaTime is 1/60s.

But in my code, I was updating the position of the character every frame, and for every byte read by the buffer. In other words, given a buffer either filled with hundreds of bytes that each increment position or nothing at all, it was difficult to scale the speed parameter appropriately.  

Handshaking seemed to fix issues on both sides. I moved the button digital reads inside a conditional checking Serial.available() > 0 on the Arduino, and added a ping function that writes a byte to serial in the beginning of each Update() loop in Unity. With these additions, the frequency at which the buffer is filled and read is controlled to the rate which the frame updates, meaning I can use Time.deltaTime and speed to scale movement.  

Another improvement I tried making after discussing with Tom was to deliver the button states in 4-byte arrays and calculating the appropriate movement in Unity. This way, I can better control what happens when the user presses, for example, the up and down buttons at the same time. Instead of privileging one direction over the other, I can just "cancel" the two opposing inputs out.

The Arduino code and an excerpt of the Unity code are shared below.

int upButton = 2;
int downButton = 5;
int rightButton = 4;
int leftButton = 3;

void setup() {
  Serial.begin(9600);
  pinMode(upButton, INPUT);
  pinMode(downButton, INPUT);
  pinMode(rightButton, INPUT);
  pinMode(leftButton, INPUT);
}

void loop() {
  
  if (Serial.available() > 0) {
    int inByte = Serial.read(); 
    char commands[4];
    if (digitalRead(upButton) == HIGH) commands[0] = 'W'; else commands[0] = 'O';
    if (digitalRead(downButton) == HIGH) commands[1] = 'S'; else commands[1] = 'O';
    if (digitalRead(rightButton) == HIGH) commands[2] = 'D'; else commands[2] = 'O';
    if (digitalRead(leftButton) == HIGH) commands[3] = 'A'; else commands[3] = 'O';
    Serial.write(commands);
  }
}
In each loop, if Serial.available(), we create a 4-byte array we fill with the characters W, S, A, or D, corresponding to up, down, left, or right, or the character O, which I've designated as empty. The byte array is then written to serial.
private void Update()
    {
        if (_serial != null && _serial.IsOpen)
        {
            Ping(); // write a single char to serial
            int bytesToRead = _serial.BytesToRead; 
            if (bytesToRead >= 4) // only do things if receiving at least a full 4-byte array
            {
                byte[] buff = new byte[bytesToRead]; 
                int read = _serial.Read(buff, 0, bytesToRead);

                if (read > 0)
                {
                    for (int i = 0; i < buff.Length - 4 ; i+=4)
                    {
                        Vector2 direction = new Vector2(0, 0);
                        // if opposing direction buttons are pressed
                        // the value in that axis is 0
                        if (buff[i] == 'W') { direction.y += 1; }
                        if (buff[i+1] == 'S') { direction.y -= 1; }
                        if (buff[i+2] == 'D') { direction.x += 1; }
                        if (buff[i+3] == 'A') { direction.x -= 1; }
                        direction.Normalize(); // normalize the x, y directions

                        yPos += direction.y * speed * Time.deltaTime; 
                        xPos += direction.x * speed * Time.deltaTime;

						// restrict the player to the screen
                        player.transform.position = new Vector2(Mathf.Clamp(xPos, -8f, 8f), Mathf.Clamp(yPos, -4f, 4f));

                    }
                }
            }
        }
    }
We check if we have at least enough bytes to map a command in each direction, then cancel any out, and finally normalize across the two axes before applying a motion to the character.
A quick demonstration of a character in Unity responding to directional button presses from a set of push buttons connected to an Arduino.