Unity Gyroscope Controller

by mandarinCapybara in Circuits > Arduino

204 Views, 1 Favorites, 0 Comments

Unity Gyroscope Controller

Instructables_Demonstration

In this project, I'll be making a controller for Unity3D with a gyroscope to control a playable character. My goal for this project was to make a different control scheme than usual to play a video game.

This project is the final result of "If This Then That", a project I had to do for school where we had to use an Arduino to make an interaction happen.

Before coming to the final product, I had several iterations. These are listed in Step 1. If you're only interested in following this project without the past iterations, you can start the construction at Step 2.

Supplies

Hardware

  • 1x Arduino UNO
  • 1x MPU 6050
  • 4x Jumper cable

For this project, I used M/M cables, but you can also connect M/F cables straight to the MPU 6050.

  • 1x Breadboard
  • 1x USB cable (A to B)

Please consider using a longer cable than the one that comes with the Arduino UNO. This cable will be used to control the game and should be long enough for comfort.

  • 1x Soldering set (optional)
  • 3D printer (optional)

Software

  • Arduino IDE

I'm using V2.3.2, but any version should work.

  • Unity3D

I'm using V2022.3.17f1, but any version should work.

  • Cura or any other 3D-printing software (optional)

Earlier Iterations

image_2024-03-28_191621978.png
image_2024-03-28_200155753.png

Iteration 1

My first idea for this project was to use the HC-05 Bluetooth module to make the controller wireless. I've listed the diagram for connecting the parts to the Arduino above this step.

The reason that I ultimately decided against using this method, and instead opted for a wired controller was because my Windows 11 laptop could not detect the HC-05. I tried several methods of resetting my Bluetooth but I simply could not get it to work. If you're able to get it working in Windows 11, please let me know how.

However, this module does work with the Android and Windows 10 devices I tested it on, so if you want to go for a wireless controller instead of a wired one, you can. Please note, however, that this Instructable is made assuming that you are using a wired controller. If you're with a wireless controller, you will need a different method of getting the Arduino data at Step 4.

Extra parts for this to work

  • HC-05 Bluetooth module
  • 9V battery
  • 9V battery clip

Iteration 2

My second idea for this project was to use two controllers instead of one; One for the left hand, and one for the right. This iteration was meant to be the final product, but I messed up one of the Arduinos during the soldering stage and thus I ended up having to settle with one controller instead of two.

My initial idea with this iteration was to let the player control the character as if it's a marionette on strings using inverse kinematics instead of the movement you'd usually see inside a video game.

If you want to make this iteration yourself, you can simply do the steps up to Step 4 twice, with two Arduinos instead of one. On the Unity side of things, you'll want to open another serial and use the same logic for reading the rotation for both serials. Doing this allows you to use one controller for movement, while the other can be used for the camera or to use items, just to name a few examples.

For this iteration, I also wanted to use gloves as controllers instead of the simple 3D-printed box I provided in a later step. I discovered, however, that this was extremely uncomfortable to wear and thus I scrapped that idea to the more simple approach.

Extra parts for this to work

  • 1x Arduino UNO
  • 1x MPU 6050
  • 4x Jumper cable
  • 1x Breadboard
  • 1x USB cable (A to B)


Arduino Setup

20240324_144023.jpg
20240324_134132.jpg
image_2024-03-27_183825923.png

What is the MPU-6050?

The MPU-6050 is a triple-axis accelerometer and gyroscope. This means it measures both acceleration and rotation in a three-dimensional space. For this project, we will only be using the rotation variable. This is because we want to measure how much the controller is tilted for the player to move in the desired direction.

Construction

To connect the MPU-6050 to the Arduino UNO, you need to connect four wires.

I used a breadboard for this, but you could also use M/F cables and connect those directly to the MPU-6050.

You want to connect the VCC pin of the MPU to the 3.3V pin of the Arduino. The VCC, or "Voltage Common Collector" pin is the positive pole of the MPU-6050. Connect the GND pin to one of the grounds in the Arduino, which are also labeled with "GND". The GND pin, or Ground pin, is the negative pole in the circuit. You always want to have a pin in both the positive- and negative poles to create a closed circuit, or else the electronic won't get power.

If you've done this correctly, the MPU-6050 should give a green light to indicate that it's being powered. If this isn't the case, you might have to solder the header pins to the MPU-6050.

After connecting the cables and the MPU turns on, you want to connect the SCL and SDA pins. The SCL pin is the Serial Clock pin, which sends pulses at regular intervals. The SDA pin is the Serial Data pin, which sends data between the MPU-6050 and the Arduino.

These two pins are found above the AREF pin on the Arduino. The SDA pin is the one directly above it, and the SCL pin is the uppermost pin of the Arduino.

If you want to make sure you have these pins in the correct place, please refer to the diagram above.

Arduino Code

image_2024-03-28_195629238.png
image_2024-03-28_195657494.png

For the MPU-6050 to give values, you have to give the Arduino Uno some code. For this project, I used a tutorial by Superb Tech.

This code takes the rotations of the MPU-6050 in three dimensions (x, y, and z) and turns them into a value between 0 and 255. These values are printed into the Serial, which will be used inside Unity to read the rotation and convert it into movement.

For this code to work, you'll have to install the MPU-6050 library, which can be found here.

Put this code in the Arduino IDE

#include "Wire.h"       
#include "I2Cdev.h"    
#include "MPU6050.h"    

MPU6050 mpu;
int16_t ax, ay, az;
int16_t gx, gy, gz;

struct MyData {
  byte X;
  byte Y;
  byte Z;
};

MyData data;

void setup()
{
  Serial.begin(19200);
  Wire.begin();
  mpu.initialize();
}

void loop()
{
  Serial.flush();
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
  data.X = map(ax, -17000, 17000, 0, 255 ); // X axis data
  data.Y = map(ay, -17000, 17000, 0, 255);
  data.Z = map(az, -17000, 17000, 0, 255);  // Y axis data
  delay(100);
  Serial.print(data.X);
  Serial.print(" ");
  Serial.print(data.Y);
  Serial.print(" ");
  Serial.print(data.Z);
  Serial.print(" ");
  Serial.println("");
}

If you've done everything correctly, you should see 3 digits in the Serial Monitor. If you can't find the Serial Monitor, press the icon in the top right corner of the screen, as seen in the image above.

After testing inside the Arduino IDE, close the Serial Monitor again. If you don't do this, it might give errors inside Unity.

Setting Up Unity for Arduino

image_2024-03-28_194907478.png
image_2024-03-28_194924349.png
image_2024-03-28_195243158.png

Adding the script

Before anything, we have to create a script in Unity and add it to an empty GameObject. A GameObject in Unity is essentially an empty vessel we can use to create logic. For this step, we will use one to store the script, but in a later step, we will use another to create the actual player.

To create a new GameObject, press the plus icon in the "hierarchy" tab and select "Create Empty". This will make a new empty GameObject. Rename it to "Gyroscope".

Inside this object, press the "Add Component" button and type "GyroscopeInput". In my case, it will show the script, since I've already made it but for you, it only gives the option to create a new script. Click this option and open the script after Unity is done compiling. This will be the script we use to read the MPU-6050's gyroscope values and store them inside Unity.

Setting up the serial port

To connect the Arduino to Unity, you need to open the serial port to which the Arduino is connected. I'm doing this in a c# script called "GyroscopeInput", as I will use the same script to read the values from the Serial.

This is the code to open the serial:

    private SerialPort serial;
    [SerializeField] private string comName = "COM3"; // change this to the com to which the Arduino is connected.

    private void Start()
    {
        serial = new SerialPort("\\\\.\\" + comName, 19200); // the 19200 is for the baudrate
        if (!serial.IsOpen)
        {
            serial.Open();
            serial.ReadTimeout = 100;
            serial.Handshake = Handshake.None;
            if (serial.IsOpen) { print("Opened serial"); }
        }
}

Run this code in Play mode. If everything goes correctly it should print "Opened Serial" in the console.

  • If you get an error saying the com does not exist, please make sure you entered the correct com number. You can find this in the Arduino IDE.
  • If you get an error saying that the com is busy, please make sure you closed the serial monitor in the Arduino IDE.
  • If Unity does not know what a SerialPort is, add the System.IO.Ports library to the script. This is done at the very top of the script.
using System.IO.Ports;
using UnityEngine;


Closing the com

After you close play mode, you want to close the com. This is simply done in the OnApplicationQuit() function inside Unity. This function gets called when the player quits the game or exits play mode inside the editor.

    private void OnApplicationQuit()
    {
        try
        {
            serial.Close();
            print("closed com");
        }
        catch
        {
        }
    }

I've used try/catch here to avoid getting errors when the serial does not exist. If the serial does exist, it will simply close itself after the game has concluded.

Reading the Serial in Unity

image_2024-03-28_185839925.png

Creating the Thread

To read the Arduino's serial data in Unity, we want to create a thread. Inside your global variables, you want to create a Thread variable. In my case, I've simply called it "thread"

private Thread thread;

Inside the start function, we want to create and start our new thread.

thread = new Thread(new ThreadStart(GetArduino));
thread.Start();
  • If Unity does not know what a thread is, add the library to the script. This is done on the very top of the script
    using System.IO.Ports;
using System.Threading;
using UnityEngine;


You will now get an error message saying that "GetArduino" does not exist yet. Don't worry, though since we will make it now.

Receiving the data

To receive the serial data, we want to create a string that contains this information. Inside the GetArduino() function, we will read this serial data as long as the thread is alive. Please make sure to put this in a try/catch structure as I've done below, since Unity tends to throw out Timeout exception errors otherwise. (Don't worry, the code still works even though Unity spits out these errors)

    string receivedData = "";

    private void GetArduino()
    {
        while (thread.IsAlive)
        {
            try
            {
                if (serial.IsOpen)
                    receivedData = serial.ReadLine();
            }
            catch { }
}
}

We now want to give these two variables a value. This is done by splitting the received data into an array. This array should, in theory, always be a length of 3. After doing that, we want to parse the values into 3 separate variables, namely gyroX, gyroY, and gyroZ.

            string[] elements = receivedData.Split(" ");
            try
            {
                int gyroX = int.Parse(elements[0]);
                int gyroY = int.Parse(elements[1]);
                int gyroZ = int.Parse(elements[2]);
}
catch{}


Please make sure the values actually correlate to the desired axes!

  • X should measure left/right tilt
  • Y should measure up/down tilt
  • Z should measure the rotation of your palm

For a clearer image of the rotational values of the MPU-6050, please refer to the image listed above. This is an image I used from RoboSync.

At the end of this stage, your GetArduino() function should look something like this:

    private void GetArduino()
    {
        while (thread.IsAlive)
        {
            try
            {
                if (serial.IsOpen)
                    receivedData = serial.ReadLine();
            }
            catch { }


            string[] elements = receivedData.Split(" ");


            try
            {
                int gyroX = int.Parse(elements[0]);
                int gyroY = int.Parse(elements[1]);
                int gyroZ = int.Parse(elements[2]);
catch{}
}
}

Converting the Data Into Values

image_2024-03-28_201933565.png

After reading the data, we want to create two more global variables, namely two Vector3s to store the gyroscope data. If you're unfamiliar with Unity, a Vector3 is merely a fancy array of 3 float values.

[SerializeField] private Vector3 gyroscopeOrigin = Vector3.zero;
[SerializeField] private Vector3 gyroscope;

"gyroscope" is the active rotation of the MPU, while "gyroscopeOrigin" is the position the MPU had when starting the game. In my case, I added a small reset function in the Update function so that I can reset this origin value while playing the game.

   private void Update()
    {
        if (Input.GetKey(KeyCode.R))
        {
            gyroscopeOrigin = Vector3.zero;
        }
    }


Back inside the GetArduino() function, we now want to give the gyroscope variable its values. We will do this inside the try{} statement where we created the gyroX, gyroY, and gyroZ variables.

We want to make a new gyroscope variable and give it the variables we created.

gyroscope = new()
{
    x = gyroX,
    y = gyroY,
    z = gyroZ
};

After assigning the gyroscope with its value, we want to see if the origin has been created yet. If it hasn't yet or is reset, we want to set the origin to the current gyroscope value. To check whether or not the origin is assigned already, we will compare it to a Vector3.zero. This is an empty Vector3 where all axes are assigned a value of 0. I added this line immediately after assigning the variables to "gyroscope".

gyroscope = new()
{
    x = gyroX,
    y = gyroY,
    z = gyroZ
};

if (gyroscopeOrigin == Vector3.zero)
{
    gyroscopeOrigin = gyroscope;
}

We can now test this inside Play mode. Connect the Arduino to the com and move it around. If you've done everything correctly, the "Gyroscope" value of the script will move according to how you tilt the MPU-6050, while the "GyroscopeOrigin" will only change at the start, or when you press the R button.

  • Please make sure to play on "focused" mode instead of "maximized", so that you can see the variables inside the inspector.
  • If variables do not change according to how you want them to move, you might need to reorder the x- and y-axes or make them negative. This is fully dependent on what direction your MPU-6050 is facing when testing.

As a general rule of thumb, you want to be able to read the pin names while facing the screen.

GetArduino() function code

private void GetArduino()
{
    while (thread.IsAlive)
    {
        try
        {
            if (serial.IsOpen)
                receivedData = serial.ReadLine();
        }
        catch { }

        string[] elements = receivedData.Split(" ");

        try
        {
            int gyroX = int.Parse(elements[0]);
            int gyroY = int.Parse(elements[1]);
            int gyroZ = int.Parse(elements[2]);


            gyroscope = new()
            {
                x = gyroX,
                y = gyroY,
                z = gyroZ
            };

            if (gyroscopeOrigin == Vector3.zero)
            {
                gyroscopeOrigin = gyroscope;
            }
        }
        catch
        { 
        }
    }
}

Calculating the Controls

image_2024-03-28_202645002.png

To use the values of the MPU-6050 in Unity, we need to convert them into smaller, more readable values. To achieve this, we want to create a new function inside the script and call it "GetAdjustedVectors". We want to call this function at the end of the GetArduino() function (This is the last line of code in that function, I swear!)

This new function will get code that turns the "Gyroscope" and "GyroscopeOrigin" into a new Vector3, called "controls". Before we add anything to this function, we want to add two more extra global variables. One for the gyroscope difference, and one for the controls. Your global variables should now look something like this;

private SerialPort serial;
[SerializeField] private string comName = "COM3";

[SerializeField] private Vector3 gyroscopeOrigin = Vector3.zero;
[SerializeField] private Vector3 gyroscope;
[SerializeField] private Vector3 gyroscopeDifference;
public Vector3 controls;
private Thread thread;

Note; gyroscopeDifference can be called inside GetAdjustedVectors(), but I decided to make it a global variable to check it inside the inspector.


Calculating the difference between the origin and the current tilt of the MPU-6050

To calculate the difference between the origin and the current tilt, we can use a simple subtraction. If you subtract the origin from the current position, you will get the difference between the two.

gyroscopeDifference = gyroscope - gyroscopeOrigin;

However, this will not account for the case where the gyroscope value goes to 255, while the origin is at 0. This is a very small tilt in theory, but since the difference looks to be very high inside the code, we want to make it smaller.

For this, we want to check whether or not the difference is bigger than 122, which is around half of the total number it can be. If it is, we want to subtract 255 from the difference variable to make it into a more realistic digit. This will make the GetAdjustedVectors() function look a bit like this;

private void GetAdjustedVectors()
{
        gyroscopeDifference = gyroscope - gyroscopeOrigin;
        #region x
        if (gyroscopeDifference.x > 122)
        {
            gyroscopeDifference.x -= 255;
        }
        else if (gyroscopeDifference.x < -122)
        {
            gyroscopeDifference.x += 255;
        }
        #endregion
        #region y
        if (gyroscopeDifference.y > 122)
        {
            gyroscopeDifference.y -= 255;
        }


        else if (gyroscopeDifference.y < -122)
        {
            gyroscopeDifference.y += 255;
        }
        #endregion
        if (gyroscopeDifference.z > 122)
        {
            gyroscopeDifference.z -= 255;
        }
        #region z
        if (gyroscopeDifference.z < -122)
        {
            gyroscopeDifference.z += 255;
        }
        #endregion
}

Now that we have the difference figured out, we want to turn it into a variable that can be used to control the player. To do this, we first want to divide the difference by a lot so that the value is smaller. I divided it by 50, but you should test to see what value works best for your purposes.

Vector3 controlsSmaller = gyroscopeDifference / 50;

After doing this, we want to make a slight correction so that we don't accidentally move the player at the slightest tilt of the controller. We do this by checking if the "controlsSmaller" variable is bigger or smaller than a certain value.

  • On the negative side, we want to see if it's smaller than -0.2.
  • On the positive side, we want to see if it's bigger than 0.2.
  • We want to do this for all 3 axes
if (controlsSmaller.x < -0.2f || controlsSmaller.x > 0.2f)
{

}

if (controlsSmaller.y < -0.2f || controlsSmaller.y > 0.2f)
{

}

if (controlsSmaller.z < -0.2f || controlsSmaller.z > 0.2f)
{

}

Inside these empty if statements, we want to set a value to a new Vector3, called "controlsAdjusted". First, we want to set the "controlsAdjusted"-axis to the "controlsSmaller"-axis.

In my case, I found out that I needed to get the negative "controlsSmaller"-axis. You should test at the end of this step to see if this is the case for you as well. If the controls are inverted, you should simply remove the "-" before the "controlsSmaller"-axis.

After setting the value to "controlsAdjusted", we will clamp it between -1 and 1 to make sure the variable can't exceed a maximum value. We want to do this for every axis.

controlsAdjusted.x = -controlsSmaller.x;
controlsAdjusted.x = Mathf.Clamp(controlsAdjusted.x, -1, 1);

After setting the controlsAdjusted values, we want to assign them to "controls".

controls = controlsAdjusted;

You should now have the full code inside GyroscopeInput. It should look like this;

using System.IO.Ports;
using System.Threading;
using UnityEngine;

public class GyroscopeInput : MonoBehaviour
{
    private SerialPort serial;
    [SerializeField] private string comName = "COM3";

    [SerializeField] private Vector3 gyroscopeOrigin = Vector3.zero;
    [SerializeField] private Vector3 gyroscope;
    [SerializeField] private Vector3 gyroscopeDifference;
    public Vector3 controls;
    private Thread thread;

    private void Start()
    {
        serial = new SerialPort("\\\\.\\" + comName, 19200);
        if (!serial.IsOpen)
        {
            serial.Open();
            serial.ReadTimeout = 100;
            serial.Handshake = Handshake.None;
            if (serial.IsOpen) { print("Opened serial"); }
        }

        thread = new Thread(new ThreadStart(GetArduino));
        thread.Start();

    }

    private void Update()
    {
        if (Input.GetKey(KeyCode.R))
        {
            gyroscopeOrigin = Vector3.zero;
        }
    }

    string receivedData = "";
    private void GetArduino()
    {
        while (thread.IsAlive)
        {
            try
            {
                if (serial.IsOpen)
                    receivedData = serial.ReadLine();
            }
            catch { }

            string[] elements = receivedData.Split(" ");

            try
            {
                int gyroX = int.Parse(elements[0]);
                int gyroY = int.Parse(elements[1]);
                int gyroZ = int.Parse(elements[2]);

                gyroscope = new()
                {
                    x = gyroX,
                    y = gyroY,
                    z = gyroZ
                };

                if (gyroscopeOrigin == Vector3.zero)
                {
                    gyroscopeOrigin = gyroscope;
                }
            }
            catch
            { 
            }

            GetAdjustedVectors();
        }
    }
    private void GetAdjustedVectors()
    {
            gyroscopeDifference = gyroscope - gyroscopeOrigin;
            #region x
            if (gyroscopeDifference.x > 122)
            {
                gyroscopeDifference.x -= 255;
            }
            else if (gyroscopeDifference.x < -122)
            {
                gyroscopeDifference.x += 255;
            }
            #endregion
            #region y
            if (gyroscopeDifference.y > 122)
            {
                gyroscopeDifference.y -= 255;
            }


            else if (gyroscopeDifference.y < -122)
            {
                gyroscopeDifference.y += 255;
            }
            #endregion
            if (gyroscopeDifference.z > 122)
            {
                gyroscopeDifference.z -= 255;
            }
            #region z
            if (gyroscopeDifference.z < -122)
            {
                gyroscopeDifference.z += 255;
            }
            #endregion


        Vector3 controlsSmaller = gyroscopeDifference / 50;


        Vector3 controlsAdjusted = Vector3.zero;
        if (controlsSmaller.x < -0.2f || controlsSmaller.x > 0.2f)
        {
            controlsAdjusted.x = -controlsSmaller.x;
            controlsAdjusted.x = Mathf.Clamp(controlsAdjusted.x, -1, 1);
        }


        if (controlsSmaller.y < -0.2f || controlsSmaller.y > 0.2f)
        {
            controlsAdjusted.y = -controlsSmaller.y;
            controlsAdjusted.y = Mathf.Clamp(controlsAdjusted.y, -1, 1);
        }


        if (controlsSmaller.z < -0.2f || controlsSmaller.z > 0.2f)
        {
            controlsAdjusted.z = -controlsSmaller.z;
            controlsAdjusted.z = Mathf.Clamp(controlsAdjusted.z, -1, 1);
        }


        controls = controlsAdjusted;
    }


    private void OnApplicationQuit()
    {
        try
        {
            serial.Close();
            print("closed com");
        }
        catch
        {
        }
    }
}


Setting Up the Player

image_2024-03-28_211521069.png
image_2024-03-28_211650148.png
image_2024-03-28_212619197.png
image_2024-03-28_212709974.png
image_2024-03-28_213836407.png

Importing the model

Click here to download the player model. This is an FBX file containing the player model and the animation data.

Drag this model into Unity and click on it. In the inspector menu, you will see that it has 4 menus. Open the Animation menu.

This model will have 3 animations with "Armature|" in front of it. You can simply delete these animations by pressing the "-" button below the clips. The animations are duplicates and will not be used in the project.

For the other animations, you will want to click on them and tick the box next to "Loop Time". This will ensure that the animation gets looped instead of only playing once.

After doing this, scroll down and click "Apply" to finalize the changes to the animations.

Scene Setup

After setting up the player asset, drag it into the scene and make sure the position is (0, 0, 0).

We now want to add two components to this player model, namely an Animator and a new script called "PlayerController". We will use the Animator later, but we should at least set it up now. To use the Animator, we will need to add an "Animator Controller" to it. To do this, right-click inside the Assets folder, which can be found in the Project tab, and under Create, search for the Animator Controller. I named mine "Player". Drag this Animator Controller in the "Controller" box in the Player's Animator component.

You also want to drag the "Main Camera" object into the "Instructables_UnityCharacter" object. This will ensure that the camera follows the player. I gave the camera a local position of (0, 13, -15) and a rotation of (15, 0, 0), but you should play with these values until you get a look you're happy with.

I also gave the player a blue material by right-clicking in the Assets folder and adding a Material under the Create menu. After making the material and giving it a color by changing the Albedo (the color value) in the Inspector, I simply dragged it onto the model to give it a different color. This is entirely optional, however.

Programming the Player

image_2024-03-28_213455183.png

Inside the PlayerController script, we want to create a few global variables. We want to get a reference to the GyroscopeController we made earlier, a float for the walk speed, and a Vector3 for the controls. We also want an Update function, with a single line of code that calls the Move() function, which is the function we're going to use to move the player.

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private GyroscopeInput gyroscope;
    [SerializeField] private float walkSpeed = 10;
    private Vector3 controls;

    private void Update()
    {
        Move();
    }

private void Move()
{
}
}

Inside Unity, we want to drag the Gyroscope object we made (the one with the GyroscopeController) into the "gyroscope" variable. After doing this, it should show the GyroscopeController in the inspector.

Now that we have the gyroscope reference, we can look at its control values and store them locally in the player. This is done with a single line of code inside the Move() function.

private void Move()
{
    controls = gyroscope.controls;
}

In order to make the player move, we will need to add two more lines of code.

First, we want to make a Vector3 for the move direction. We want to take the x-axis of the controls and put it in the x-axis of this Vector3, and take the y-axis of the controls and put it in the z-axis of the Vector3. The reason we want to use the z-axis instead of the y-axis is because Unity uses this third dimension for depth, which is what we want for a 3-dimensional game.

If you're making a 2D project, you want to use the y-axis instead.

Secondly, we want to update the position of the player. This is done by adding the move direction multiplied by Time.deltaTime and the "walkSpeed" variable. Time.deltaTime is simply used to make the movement smoother since it looks at the time between frames and normalizes the speed based on it.

You can now test and see that the player is moving based on how you tilt the MPU-6050. If the direction does not correspond with how you are moving, you need to go to the GyroscopeController for a few fixes.

  • If left and right are inverted: multiply "gyroX" by -1 before assigning it to "gyroscope".
  • If up and down are inverted: multiply "gyroY" by -1 before assigning it to "gyroscope".
  • If the X and Y axes are flipped, assign gyroscope.x to "gyroY" and gyroscope.y to "gyroX".

After this, your player controller should look a little bit like this:

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private GyroscopeInput gyroscope;
    [SerializeField] private float walkSpeed = 10;
    private Vector3 controls;

    private void Update()
    {
        Move();
    }

private void Move()
{
controls = gyroscope.controls;
Vector3 moveDir = new(controls.x, 0, controls.y);
transform.position += moveDir * Time.deltaTime * walkSpeed;
}
}


Animation

image_2024-03-28_220525685.png
image_2024-03-28_220914316.png
image_2024-03-28_221130257.png
image_2024-03-28_221438570.png
image_2024-03-28_221841477.png
image_2024-03-28_221949246.png
image_2024-03-28_222458984.png
image_2024-03-28_222758640.png

Animator setup

In the inspector, go to the player and double-click the controller in the Animator Component. This will open a new tab. This tab will be where we control the animations of the player model. In the project tab, find the asset that contains the player with the animations. Drag the triangle called "Idle" into the Animator tab. This is the Idle animation. If done correctly, this should create a yellow/orange box called "Idle" in the Animator. Press play inside the Scene tab and see if the animation is playing correctly.

If it is not, make sure you have Loop Time enabled in the Animation settings of the asset.

Now we want to make a boolean called "Walking" in the Animator Parameters. This is done by pressing the plus icon and selecting Bool. This boolean will be what we'll use to switch between the idle and walk animations.

Right-click on the grey space in the Animator view and under "Create State", choose "From New Blend Tree". This will add a Grey box called "Blend Tree" and a value called "Blend". This Blend Tree will be what we use for the walking animations.

Right-click the Idle animation and click "Make Transition" and drag the arrow into the blend tree. Do the same in the opposite direction so that you have two arrows pointing between the Blend Tree and the Idle Animation. Click on both these arrows and untick "Has Exit Time". Under "Conditions", you will want to click the plus icon and make sure it shows the "Walking" boolean.

  • From Idle to Blend Tree, Walking = true
  • From Blend Tree to Idle, Walking = false

Now double-click on the Blend Tree. This will show you a near-empty space with only the blend tree inside. If you want to return to the menu with the Idle animation, simply click "Base Layer" at the top of the tab. To start adding movement to this blend tree, click the plus icon under "Motion" five times and choose "Add Motion Field" so that you have five empty animations. This will be where we drag the walk animations into. From top to bottom drag the following animations from the Player asset into these empty animations.

  1. WalkForward
  2. WalkLeft
  3. WalkBackwards
  4. WalkRight
  5. WalkForward

Yes, WalkForward is in here twice, that is done so that the left and right animations can be affected by both backward- and forward animations.

Programming the Animation

Now that we've set up the Animator, we want to program it so that the Animator does what we want. First, we want a reference to the Animator component, which we will assign in the start function.

    private Animator anim;
private void Start()
{
    anim = GetComponent<Animator>();
 }

Let's start with checking whether or not the player is walking. We want to use the "Walking" boolean inside the Animator to play the right type of animation based on whether the player is standing still or moving.

To do this, we will compare the controls to a Vector3.zero in the Move() function. If the controls return (0, 0, 0) it means the player is standing still. Otherwise, the player is moving.

if(controls == Vector3.zero)
    anim.SetBool("Walking", false);
else
    anim.SetBool("Walking", true);

With this, we can't control the walk animation's direction yet. For that, we need some more code. We will create a float called "blendVar" and assign a value of 0 inside the Move() function.

After that, we want to check both the x- and y-axis of "controls" to see what direction the player is walking. For this, I decided to make a bit of a buffer so that the animations don't immediately play at the slightest motion.

        float blendVar = 0;
        if(controls.y > 0.3) // forward
        {
            if (controls.x > 0.3) blendVar = 0.875f; // forward right
            else if (controls.x < -0.3) blendVar = 0.125f; // forward left
            else blendVar = 0;
        }
        else if(controls.y < -0.3) // backward
        {
            if (controls.x > 0.3) blendVar = 0.625f; // backward right
            else if (controls.x < -0.3) blendVar = 0.375f; // backward left
            else blendVar = 0.5f;
        }
        else
        { 
            if (controls.x > 0.3) blendVar = .75f; // right 
            else if (controls.x < -0.3) blendVar = .25f; // left
        }

After assigning the "blendVar" variable, we want to give this information to the animator.

 anim.SetFloat("Blend", blendVar);

After doing this, we finished the player script and it should look something like this;

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private GyroscopeInput gyroscope;
    [SerializeField] private float walkSpeed = 10;
    private Vector3 controls;

    private Animator anim;

    private void Start()
    {
        anim = GetComponent<Animator>();
    }

    private void Update()
    {
        Move();
    }

    private void Move()
    {
        controls = gyroscope.controls;

        float blendVar = 0;
        if(controls.y > 0.3) // forward
        {
            if (controls.x > 0.3) blendVar = 0.875f; // forward right
            else if (controls.x < -0.3) blendVar = 0.125f; // forward left
            else blendVar = 0;
        }
        else if(controls.y < -0.3) // backward
        {
            if (controls.x > 0.3) blendVar = 0.625f; // backward right
            else if (controls.x < -0.3) blendVar = 0.375f; // backward left
            else blendVar = 0.5f;
        }
        else
        { 
            if (controls.x > 0.3) blendVar = .75f; // right 
            else if (controls.x < -0.3) blendVar = .25f; // left
        }

        if(controls == Vector3.zero)
            anim.SetBool("Walking", false);
        else
            anim.SetBool("Walking", true);

        anim.SetFloat("Blend", blendVar);

        Vector3 moveDir = new(controls.x, 0, controls.y);
        transform.position += moveDir * Time.deltaTime * walkSpeed;
    }
}

Optional Casing and Soldering

20240328_094714.jpg
20240328_094718.jpg
20240328_094737.jpg
20240328_094752.jpg

For the sake of completion, I added another step for making a case and soldering the MPU-6050 to the Arduino. I created a simple box in Blender that can be imported into Cura or any other 3D-printing software. I made this box to fit the Arduino Uno, so if you're using another type of Arduino for this project, it might not fit. This model is made with the intent to solder the MPU-6050 directly to the bottom of the Arduino. I learned from experience that that is a pretty painful experience, however. As you can probably tell from the images above, I'm still quite a beginner at soldering (I'm proud to say I only burned myself 3 times).

Since I am by no means qualified to instruct how to use a 3D printer or how to solder, I decided to link two pages here that explain it way better than I ever could.

3D printing guide

Soldering guide

Click here to download the 3D model for the 3D Print