Post

Project 02: Alternative Controller

Alt Controller Project

This project was pretty fun to do, and I got to work more with Unity and make an actual game that used an alternative controller that I designed.

The Circuit

The circuit for this was pretty simple, it is just a Piezo, potentiometer, and a button. I ended up designing it this way because I designed the kind of game I wanted to make, and then designed the controller around that idea.

Originally, I hadn’t planned on adding in the LED or the button, but the game needed a way to pause it and I didn’t want to involve needing another device or input to play the game, so I added it in. Additionally, I forgot about adding in Actuators, so I just put in the quickest thing I could, which was an LED. All the LED does is turn on when the game starts, then when the game loop starts it turns off, then when the player dies it turns on again.

Circuit

Parts List

NameQuantityComponent
U11Arduino Uno R3
PIEZO11Piezo
R111 MΩ Resistor
Rpot11250 kΩ Potentiometer
S11Pushbutton
R2, R32220 Ω Resistor
D11Red LED

Parts List CSV

Schematics

Schematics Schematics pdf

Building the Circuit

Putting together the circuit was pretty simple, though I did have to make slight modifications to the way I wired things, like moving the Piezo to use both sides of the breadboard, and adding additional smaller wires to bridge the connections instead of just having longer ones across to make using the device as a controller more ergonomic, other than that, it is the same as the schematic I created. This is the final result:

Modified circuit

Programming the Arduino

After constructing the circuit, I wrote the code for the Arduino. All the Arduino needed to do for this project was to communicate its data to the SerialPort so that I could have full control over how to interpret it in Unity, and the LED output just needs to read a 0 or 1 when available.

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const int potPin = A0;
const int piezoPin = A1;
const int buttonPin = 7;
const int ledPin = 4;

int potData = -1;
int piezoData = -1;
int buttonState = -1;

void setup() {
  // initialize serial communication
  Serial.begin(9600);
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
}

// data processing wll be done by Unity
// just bus raw values to serial port as soon as possible
void loop() {
  // read data
  potData = analogRead(potPin);
  piezoData = analogRead(piezoPin);
  buttonState = digitalRead(buttonPin);

  // basic csv format since it's only 3 values
  Serial.print(potData);
  Serial.print(",");
  Serial.print(piezoData);
  Serial.print(",");
  Serial.print(buttonState);
  Serial.println();

  if(Serial.available()){
    int data = Serial.read();

    // turn on/off LED depending on what Unity says
    if(data == '1') digitalWrite(ledPin, HIGH);
    else digitalWrite(ledPin, LOW);
  }
}

Programming the Game

After I finished programming the Arduino, it was time to make the game in Unity. This didn’t go as well as I expected it to, since I started over-scoping and trying to add in all sorts of features, most of which I had to cut since there just wasn’t enough time with all the work from other classes.

Challenges

Aside from over-scoping, the main issues I ran into are as follows:

  1. I forgot to change the build profile to .Net Framework, so trying to use Ports caused compile errors. (To fix it just go to Edit > Project Settings > BuildProfiles > Player Settings > Other Settings > Configuration > API Compatibility Level > .NET Framework )

  2. Time management, since for whatever reason, all my other classes had projects and deadlines specifically during these 2 weeks.

  3. For some reason, when I was making test builds for the project, there was lots of lag between Arduino input and the game even though it ran completely fine in the Unity Editor. I have no idea what caused it, nor do I know what fixed it, it just started working properly after some point.

Code

The game repo and the rest of the code can be found here: https://github.com/SamayRShah/Diver

Since there’s lots of code that went into making this game, I won’t go over everything, but I will be explaining the more relevant Arduino related things below:

Communicating with the Arduino is done through a Singleton that opens the connection, reads values every frame, and writes values back to the port, and holds public accessors to the data. Reading data is also done on a separate thread that runs in the background, so that it isn’t blocking anything in the main thread and introducing lag. *This could also be done through Unity’s co-routines, or by registering the Arduino controller as a custom device input and using Unity’s input actions, though, making the input device is a little complicated and requires lots of extra work that wouldn’t be necessary for a game as small as Diver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    private void OpenStream()
    {
        // open communication with Arduino
        _running = true;
        _dataStream = new SerialPort(_serialPort, _baud);
        _dataStream.Open();

        // Start reading serial data on a background thread
        _readThread = new Thread(ReadSerial);
        _readThread.Start();
    }

    private void SerialUpdate()
    {
        // read latest values from Arduino
        string receivedString;
        lock (_dataLock)
        {
            receivedString = _latestData;
        }

        // process data
        ProcessData(receivedString);

        // write data back
        if (_shouldWriteData) WriteData();
    }

    void ReadSerial()
    {
        while (_keepReading)
        {
            try
            {
                // read latest data
                string line = _dataStream.ReadLine();
                lock (_dataLock)
                {
                    _latestData = line;
                }
            }
            catch (System.Exception)
            {
                // ignore timeout or disconnection errors
            }
        }
    }

    // writes whatever data class has back to the data stream
    private void WriteData()
    {
        _dataStream.Write(_outData);
        _shouldWriteData = false;
    }

    public void CleanUp()
    {
        // prevent double entry
        if (!_keepReading || !_running) return;
        _keepReading = false;
        _running = false;

        // close reading thread
        if (_readThread != null && _readThread.IsAlive)
        {
            try { _readThread.Join(100); } catch { }
        }

        // close serial port
        if (_dataStream != null && _dataStream.IsOpen)
        {
            try { _dataStream.Close(); } catch { }
        }

        Debug.Log("Serial connection closed safely.");
    }

I also have a way for the user to calibrate their input through a menu, which just captures the highest ‘raw’ vibration value, and sets it as the baseline in the main ArduinoInput singleton.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    [SerializeField] private TextMeshProUGUI _maxValueText;
    private int _maxValue = 0;

    // Update is called once per frame
    void Update()
    {
        int piezoVal = ArduinoInput.Instance.RawPiezoValue;

        if (piezoVal > _maxValue)
        {
            _maxValueText.text = piezoVal.ToString();
            _maxValue = piezoVal;
        }
    }
    public void OnConfirm() 
    {
        ArduinoInput.Instance.CalibratedMaxVibration = _maxValue;
        GameManager.Instance.QuitState();
    }

    public void OnReset()
    {
        _maxValue = 0;
    }

    public void OnCancel()
    {
        GameManager.Instance.QuitState();
    }

Other than that, I also managed to link the Arduino to Unity’s UI navigation, which made it possible to interact with the game solely through the controller.

Basically, I just got the input values from the Arduino singleton and then called Unity’s built in UI events and functions which simulate interacting with UI the normal way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    // difference in input at which to set next item
    [SerializeField] private float _navSpeed = 0.2f;
    [SerializeField] private Button[] _buttons;

    private float _prevRotation = 0;
    private int _currentButtonIndex = 0;

    private void OnEnable()
    {
        ArduinoInput.Instance.ButtonPressed += TriggerSelectedButton;
        SetSelectedButton(_currentButtonIndex);
    }

    private void OnDisable()
    {
        ArduinoInput.Instance.ButtonPressed -= TriggerSelectedButton;
        _currentButtonIndex = 0;
    }

    private void Update()
    {
        // Get difference in rotation to determine selection direction
        float rotation = ArduinoInput.Instance.TwistValue;
        float diff = rotation - _prevRotation;

        if (Mathf.Abs(diff) >= _navSpeed)
        {
            _prevRotation = rotation;

            // +- difference => +- index
            if (diff < 0) _currentButtonIndex--;
            else if (diff > 0) _currentButtonIndex++;

            // loop around
            if (_currentButtonIndex >= _buttons.Length) _currentButtonIndex = 0;
            else if (_currentButtonIndex < 0) _currentButtonIndex = _buttons.Length - 1;

            SetSelectedButton(_currentButtonIndex);
        }
    }

    private void SetSelectedButton(int index)
    {
        // guard against bad input
        if (index >= _buttons.Length) index = 0;

        // de-select other UI objects
        EventSystem.current.SetSelectedGameObject(null);

        // Unity UI select
        _buttons[index].Select();
        EventSystem.current.SetSelectedGameObject(
            _buttons[index].gameObject, new BaseEventData(EventSystem.current));
    }

    // Simulate a button click event for the selected button
    private void TriggerSelectedButton()
    {
        _buttons[_currentButtonIndex].onClick.Invoke();
    }
}

Game Demo

References

This post is licensed under CC BY 4.0 by the author.