Serial Communication between Arduino and Unity 3D

In-Editor and PC/OSX Standalone.

Overview

The purpose of this tutorial is to show you the basics of how you can use a serial connection over USB to communicate between an Arduino board and Unity 3D.

Prerequisites

Before you start make sure you have the following prerequisites.

Knowledge

  • Unity (basic)
  • Arduino (basic)

Software

Hardware

Any Arduino compatible board with a USB IO Peripheral that we can use for Serial communication. I'm using a SparkFun Pro nRF52840 Mini for this tutorial. SparkFun Pro Mini

References

Arduino Sketch

Let's begin by setting up our Arduino sketch first. Open the Arduino IDE and create a new project.

Setup

In your setup function set the baud rate you want to use with your Serial connection. In this case we will use 9600 as our baud rate. Then wait for the serial port to connect. You can modify this to timeout if you like. Here I am waiting indefinitely until a connection is made before moving on to the loop.

void setup()
{
    // Set your baud rate for your Serial connection
    Serial.begin(9600);

    // Wait for the serial port to connect.
    while (!Serial) {}
}

Reading

Now to setup a way for us to read some input. In your loop function we will first listen to see if there is data available in the read buffer of our serial connection. If there is, Serial.available() will return the number of bytes that are available to read. In this case we are just going to read one byte at a time, although you could read all the data into a byte buffer if you wanted.

Once we read our byte we check to see if we successfully read anything (-1 indicates that nothing was read). If we did read something, print it out to the Serial connection.

void loop()
{
    // Check if there is data available in the read buffer
    if (Serial.available() > 0)
    {
        // Got something
        int read = Serial.read();
        if (read >= 0)
        {
            // Print out what we read
            Serial.print(read);
        }
    }
}

Build and deploy this to your Arduino board now.

Testing using the Serial Monitor

Now let's test what we have written so far. Open the Serial Monitor in your Arduino IDE (the little magnifying glass button on the top right of the IDE). Now type 1 in the input field and press the Send button.

You will see 4910 appear in the output window. Why wasn't it 1? Because Serial.print prints data to the serial port as human-readable ASCII text followed by a newline character (ASCII 10) at the end. So 1 is 49 in ASCII and a new line is 10. If you type 1234 in the input field and press the send button, you will get 4950515210 . Which is 49 (1), 50 (2), 51 (3), 52 (4), and 10 (newline).

Writing

We may want to write out binary instead of ASCII data. In this case, we will replace Serial.print(read) with Serial.write(read).

void loop()
{
    if (Serial.available() > 0)
    {
        // Got something
        int read = Serial.read();
        if (read >= 0)
        {
            // Repeat back what we read in binary
            Serial.write(read);
        }
    }
}

Build and deploy this to your Arduino board now.

Testing using the Serial Monitor

Now let's test it by opening up the Serial Monitor again and typing 1 in the input field and pressing the Send button. This time we will get 1 in the output window. If we type 1234 and send it, we will get 1234 back in the output window.

Don't forget that if you have any Serial.print calls they will be sent over the Serial connection as well! I made this mistake once which caused me a lot of headaches as I was trying to figure out what all that extra data was that was coming out of my serial data stream!

Unity Code

Now that our Arduino board is setup to receive and send serial data, let's use Unity to send and receive data with instead of the Serial Monitor. Create a new empty project in Unity.

Getting a List of Ports

Let's create a script that will get a list of COM ports that we can connect to for our serial connection. In this example, I've called the script "SerialTest", but you can call it whatever you want.

Here we are going to use a dropdown from Unity's UI that the user can select from to get a list of COM ports that are available to connect over.

using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System;
using System.Linq;
using System.IO.Ports; // Requires .NET 4 in Project Settings

public class SerialTest : MonoBehaviour
{
    public Dropdown PortsDropdown;

    private List<string> _ports;

    void Start()
    {
        RefreshPortsDropdown();
    }

    public void RefreshPortsDropdown()
    {
        // Remove all the previous options
        PortsDropdown.ClearOptions();

        // Get port names
        string[] portNames = SerialPort.GetPortNames();
        _ports = portNames.ToList();

        // Add the port names to our options
        PortsDropdown.AddOptions(_ports);
    }
}

SerialPort.GetPortNames() will give us an array of COM ports that are currently available. We put this in a separate method called RefreshPortsDropdown() so that we can call it from a "refresh" button if we wanted to.

Note that you will need to set the Api Compatibility Level in your Project Settings to .Net 4 in order to use the SerialPort class, otherwise you may get errors in your project.

Connecting and Disconnecting

Now we need a way to connect to the port that the user will select in the dropdown.

    public Text ConnectionText;
    private SerialPort _serial;

    public void ConnectToPort()
    {
        // Get the port we want to connect to from the dropdown
        string port = _ports[PortsDropdown.value];

        try
        {
            // Attempt to create our serial port using 9600 as our baud rate which matches the baud rate we set in the Arduino Sketch we created earlier.
            _serial = new SerialPort(port, 9600)
            {
                Encoding = System.Text.Encoding.UTF8,
                DtrEnable = true
            };

            // Open up our serial connection
            _serial.Open();

            ConnectionText.text = $"Connected to {port}";
            Debug.Log(ConnectionText.text);
        }
        catch (Exception e)
        {
            ConnectionText.text = e.Message;
        }
    }

Note that we are setting the DtrEnable property of our SerialPort instance to true. Without this you may not be able to read a response back from your Arduino board.

Now that we have a way to connect, it is important to also have a way to disconnect our serial connection. Otherwise the connection will remain open!

    public void Disconnect()
    {
        if (_serial != null)
        {
            // Close the connection if it is open
            if (_serial.IsOpen)
            {
                _serial.Close();
            }

            // Release any resources being used
            _serial.Dispose();
            _serial = null;

            if (ConnectionText != null)
            {
                ConnectionText.text = "";
            }
            Debug.Log("Disconnected");
        }
    }

    // Make sure to close our connection if the script is destroyed
    void OnDestroy()
    {
        Disconnect();
    }

Notice that we are calling Disconnect() in OnDestroy(). This is to make sure we close the connection we opened if the script is destroyed or if the app is closed down.

Writing

To write to our Arduino board from Unity, call the Write method of our SerialPort instance. In this case we are writing out a byte buffer of size 1 and setting the binary value 1 in it. You could write out more data if you wanted, but here we only need to pass 1 byte to illustrate our point.

    public void Ping()
    {
        byte[] outBuffer = new byte[1];
        outBuffer[0] = 1;
        _serial.Write(outBuffer, 0, 1);

        Debug.Log("Ping");
    }

Reading

To read our response from the Arduino board, we need to check our serial connection if data is available to read. We can do this by first making sure our connection is open and then checking to see how many bytes are available to read. If we have some data to read, we create a buffer of the appropriate size to read into and then print the results to the Unity console if we got anything. Note that we are reading the data in as binary data, not ASCII data.

    // For simplicity we are reading data in our Update loop of our script. However, if you have a lot of data to read, I would recommend creating a separate thread and reading the data from that instead of the main UI thread.
    void Update()
    {
        // Make sure we have a serial port and that the connection is open
        if (_serial != null && _serial.IsOpen)
        {
            // Check to see if there are any bytes available to read
            int bytesToRead = _serial.BytesToRead;
            if (bytesToRead > 0)
            {
                Debug.Log($"Bytes to read: {bytesToRead}");

                // Create our buffer to store the data
                byte[] buff = new byte[bytesToRead];

                // Read the available data into our buffer
                int read = _serial.Read(buff, 0, bytesToRead);

                // Check if we were able to read anything
                if (read > 0)
                {
                    Debug.Log($"Read {read} bytes");
                    PrintBytes(ref buff);
                }
                else
                {
                    Debug.Log("Didn't read anything.");
                }
            }
        }
    }

    // A convenience method for printing out bytes to the Unity console
    private void PrintBytes(ref byte[] bytes)
    {
        StringBuilder sb = new StringBuilder();
        foreach (byte b in bytes)
        {
            sb.Append($"{b} ");
        }
        Debug.Log(sb.ToString());
    }

Now when we send data to our Arduino board using the Ping method we wrote earlier the Arduino board will read it and then reply back with what we sent it. In this case, a 1.

Putting it all Together

To use the SerialTest script we wrote above, attach it to a component in your scene and create a Canvas that has a Dropdown, Button ("Connect" button) for connecting to the selected port, Button ("Ping" button) for calling our Ping method, and a Text component for showing the status of our connection. The output will just go to the Unity Console, but you could modify the code so it goes to a UI Text component instead. Reference the Dropdown and Text component in your SerialTest script. Connect the "Connect" button's OnClick event in the Inspector to the ConnectToPort() method of SerialTest. Connect the "Ping" button's OnClick event to the Ping() method of SerialTest.

Run the scene in the editor. Click on the dropdown to select a port that your Arduino board is attached to and then click on the "Connect" button. If all goes well, you should see your Text component show that the connection was successful. If not, check to make sure your Arduino device is turned on, connected to your PC via USB, and that the Serial Monitor is not open.

Once you have a successful connection, click on your "Ping" button and you should see a response in the console that was sent from your Arduino board.

You cannot run the Arduino IDE's Serial Monitor at the same time you are running your project in Unity as the Serial Monitor will take up all the resources for your serial connection. Make sure to close the Serial Monitor before you run your scene.

Conclusion

At this point you should be able to read and write between your Arduino board and Unity. Depending on the complexity of your project you may want to read and send data in a separate thread in Unity to prevent blocking the main UI thread.

Project Code