UnityとArduinoで簡単なゲームを作成してみた。

こんにちは。ファブテラスいわて役員の川田将宏です。
今回は、個人的に興味があった、UnityとArduinoを利用したゲームづくりを紹介します。

ゲームアイデア

Unityのチュートリアルにある「Roll a Ball」を利用しました。
チュートリアルでは、十字キーを利用して球を動かしますが、今回はArduinoを利用して土台を動かして球を動かしていき、マップのキューブをすべて獲得するというゲームを作成しました。

用意するもの

  • Unity バージョン2019.4.17f1
  • Arduino Uno R3 (今回使ったのはELEGOO製の互換機)
  • GY-BNO055 (加速度センサ)
  • Autodesk Fusion360 (ケース作成用)
  • ELEGOO Mars 2

1. Unity側での準備

「Roll a Ball」の基礎となるゲームオブジェクトを設置していきます。
はじめに、Cubeを設置して
Positionを X: 0 Y: -5 Z: 0に設定し、Scaleを X: 30 Y: 3 Z: 30に設定します。
さらにRigidbodyコンポーネントを追加します。
ここで、Rigidbodyのところにある「Use Gravity」のチェックを外し、「Is Kinematic」にチェックを入れます。
また、「Collision Detection」を「Continuous Dynamic」に設定します。
名前はわかりやすく「Stage」と設定します。

次に、Sphereを設置してRigidbodyコンポーネントを追加します。
ここで、Rigidbodyのところにある「Collision Detection」を「Continuous Dynamic」に設定します。

次に、ステージの壁を設置していきます。
Cubeを4つ配置していき、それぞれに「wallF」「wallB」「wallR」「wallL」と名前をつけ、下記のような値に設定します。

wallF:
Position X: 0 Y: -3 Z: -14.5
Scale X: 30 Y: 1 Z: 1

wallB:
Position X: 0 Y: -3 Z: 14.5
Scale X: 30 Y: 1 Z: 1

wallR:
Position X: 14.5 Y: -3 Z: 0
Scale X: 1 Y: 1 Z: 30

wallL:
Position X: -14.5 Y: -3 Z: 0
Scale X: 1 Y: 1 Z: 30

すると、以下のような配置になります。

先程作成した壁をまとめてStageの子要素にし、それぞれのオブジェクトにマテリアルで色付をしたら前準備は終わりです。

2. Arduino側の準備

今回は、Arduinoの開発環境のインストールが済んでいる状態で話を進めていきます。
ArduinoIDEを起動したら、ツール→ライブラリを管理…を開きます。

検索欄に「Adafruit」と入力し、以下の2つのライブラリをインストールします。

  • Adafruit BNO055
  • Adafruit Unified Sensor

インストールが完了したら、以下のようなソースコードをArduinoに書き込みます。
このソースコードは、BNO055の「rawdata」からUnityに送る必要があるデータだけを残したものです。

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>

/* This driver reads raw data from the BNO055

   Connections
   ===========
   Connect SCL to analog 5
   Connect SDA to analog 4
   Connect VDD to 3.3V DC
   Connect GROUND to common ground

   History
   =======
   2015/MAR/03  - First release (KTOWN)
*/

/* Set the delay between fresh samples */
#define BNO055_SAMPLERATE_DELAY_MS (100)

// Check I2C device address and correct line below (by default address is 0x29 or 0x28)
//                                   id, address
Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x29);

void setup(void)
{
  Serial.begin(115200);

  /* Initialise the sensor */
  if(!bno.begin())
  {
    while(1);
  }

  delay(1000);
  
  bno.setExtCrystalUse(true);
}
void loop(void)
{
  // Quaternion data
  imu::Quaternion quat = bno.getQuat();
  Serial.print("x");
  Serial.print(quat.x(), 4);
  Serial.print("y");
  Serial.print(quat.y(), 4);
  Serial.print("z");
  Serial.println(quat.z(), 4);
  
  delay(BNO055_SAMPLERATE_DELAY_MS);
}

ArduinoとBNO055を以下のように接続します。
SCL – ANALOG 5
SDA – ANALOG 4
VDD – POWER 5V
GND – GND

ツール→シリアルモニタを選択し、きちんと値が取れているか確認しましょう。

少し見づらいですが、x, y, zそれぞれの角度を取れていることがわかります。
以上でArduino側での準備は終わりになります。

3. ケースづくり

先程の写真のままだと、どうも扱いづらいので、簡単にケースを作成していきます。

Arduinoの公式サイトにある、Arduino UNO R3のページ下部からボードのdxfファイルをダウンロードします。
クリックしてもダウンロードされない場合は、デバッグツールから直接リンクを開くとダウンロードできます。
URL:https://store.arduino.cc/usa/arduino-uno-rev3

ダウンロードしたファイルを利用して、以下のような形になりました。

今回はケースというより、入れ物に近い形にしました。
以下が完成品になります。

4. UnityとArduinoのシリアル通信

ここから、実際にArduinoで取得した値をUnityに送るプログラムを書いていきます。

本記事では、C#のSystem.IO.Portsを利用してシリアル通信をおこないます。
UnityのFile→Build Settings→Player Settings→Playerの下の方にある、「Api Compatibillity Level*」を「.NET 4.x」してください。

ここを忘れるとSystem.IO.Portsによるシリアル通信ができない


「Player」という名前のC#スクリプトを作成し、以下のように記述します。
あとでわかりやすくするために、「Script」というフォルダを作成して、その中に保存しておきましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;

public class Player : MonoBehaviour
{
    private SerialPort serialPort;
    // Start is called before the first frame update
    void Start()
    {
        // SerialPortの第1引数はArduinoIDEで設定したシリアルポートを設定
        // ArduinoIDEの右下から確認できる
        serialPort = new SerialPort("/dev/cu.usbmode...", 115200); // ここを自分の設定したシリアルポート名に変えること
        serialPort.Open();
    }

    // Update is called once per frame
    void Update()
    {
        if (serialPort.IsOpen)
        {
            string data = serialPort.ReadLine();
            Debug.Log(data);
        }
    }
}

できたスクリプトをStageに追加し、ゲームを実行します。
すると、先程シリアルモニタに出力されたデータがUnity上で出力されるようになります。

では実際に角度を変えるプログラムを作成していきましょう。
先程取得したデータをもとに、X, Y, Zの値を取得していきます。
SubStringとIndexOfを利用して文字を切り取ります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;

public class Player : MonoBehaviour
{
    private SerialPort serialPort;
    // Start is called before the first frame update
    void Start()
    {
        // SerialPortの第1引数はArduinoIDEで設定したシリアルポートを設定
        // ArduinoIDEの右下から確認できる
        serialPort = new SerialPort("/dev/cu.usbmode...", 115200); // ここを自分の設定したシリアルポート名に変えること
        serialPort.Open();
    }

    // Update is called once per frame
    void Update()
    {
        if (serialPort.IsOpen)
        {
            string data = serialPort.ReadLine();
            string dx = data.Substring(1, data.IndexOf("y") - 1); // 追加
            string dy = data.Substring(data.IndexOf("y") + 1); // 追加
            dy = dy.Substring(0, dy.IndexOf("z") - 1); // 追加
            string dz = data.Substring(data.IndexOf("z") + 1); // 追加
            Debug.Log("X:" + dx);
            Debug.Log("Y:" + dy);
            Debug.Log("Z:" + dz);
        }
    }
}

すると、以下のようにX, Y, Zの値がそれぞれ出力されるようになります。

それぞれの値が切り取れたので、実際に回転させていきます。
今回は、UnityのQuaternion.Lerpを利用して、現在のStageの角度と、取得した値の2点をスムーズに回転させるようにします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;

public class Player : MonoBehaviour
{
    private SerialPort serialPort;

    public float rSpeed = 50; // 追加

    // Start is called before the first frame update
    void Start()
    {
        // SerialPortの第1引数はArduinoIDEで設定したシリアルポートを設定
        // ArduinoIDEの右下から確認できる
        serialPort = new SerialPort("/dev/cu.usbmode...", 115200); // ここを自分の設定したシリアルポート名に変えること
        serialPort.Open();
    }

    // Update is called once per frame
    void Update()
    {
        if (serialPort.IsOpen)
        {
            string data = serialPort.ReadLine();
            string dx = data.Substring(1, data.IndexOf("y") - 1);
            string dy = data.Substring(data.IndexOf("y") + 1);
            dy = dy.Substring(0, dy.IndexOf("z") - 1);
            string dz = data.Substring(data.IndexOf("z") + 1);

            float x = float.Parse(dx) * rSpeed; // 追加
            float y = float.Parse(dy) * rSpeed; // 追加
            float z = float.Parse(dz) * -rSpeed; // 追加

            Quaternion rotation = Quaternion.Euler(x, z, y); // 追加
            gameObject.transform.rotation = Quaternion.Lerp(gameObject.transform.rotation, rotation, .25f); // 追加
        }
    }
}

スタートを押し、BNO055を動かしたときにStageが動けば、接続成功となります。

5. ゲームの仕上げ

ゲームの要素であるアイテムを設定していきます。

Cubeを設置し、名前は「Item」とし、以下のスクリプトを追加します。
スクリプト名は「RotateItem」とします。これをランダムな場所に5つ設置します。
ここで、1つ作ったらPrefabとして作成しておくと設置が楽になります。
この際、タグに「Item」を追加し、設定します。
InspectorからTag→Add Tag…→Tagsの+→名前を設定してSaveで追加できます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RotateItem : MonoBehaviour
{
    private float rSpeed = 1f;

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(new Vector3(0, 90, 0) * Time.deltaTime, Space.Self);
    }
}

以下のようなItemが作成されます。

続いて、球がItemに接触したらアイテムが消えるようにします。
Unityの接触判定を利用します。先程設定した「Item」タグを持つオブジェクトに触れたら、そのオブジェクトを削除するようにします。今回はOnTriggerを利用するため、ItemのBox ColliderのIs Triggerにチェックを入れます。

下記のようなスクリプトをSphereに追加します。名前は「DestroyItem」とします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DestroyItem : MonoBehaviour
{
    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Item")
        {
            Destroy(other.gameObject);
        }
    }
}

続いて、ゲームのシステム部分を作成していきます。

今回のゲームはStage上にあるItemをすべて取ることができたらゲームクリアなので、はじめにStage上のItemがいくつあるかを把握しなくてはなりません。UnityのUIを利用してStage上のItemの数を表示します。

空のゲームオブジェクトを作成して、以下の「GameSystem」スクリプトを追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 追加

public class GameSystem : MonoBehaviour
{
    public Text sum;

    public Text count;
    // Start is called before the first frame update
    void Start()
    {
        sum.text = GameObject.FindGameObjectsWithTag("Item").Length.ToString();
    }

    // Update is called once per frame
    void Update()
    {
        count.text = GameObject.FindGameObjectsWithTag("Item").Length.ToString();
    }
}

UI.Textオブジェクトをそれぞれ「Sum」, 「Count」として作成します。
2つのオブジェクトは以下の部分を共通の値にします。Positionや色は好みの設定にしてください。

  • Text – 空白にする
  • Font Size – 96
  • Alignment – 両方中心
  • Horizontal Overflow – Overflow
  • Vertical Overflow – Overflow

ゲームを実行して以下のようになればきちんと処理されています。

左が現在の数、右がもとの数

次に、現在のItem数が0になったとき、「Game Clear」と表示し、ゲームが終了する処理をGameSystemスクリプトに追加します。Invokeを利用して、10秒後にゲームが終了するようにします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameSystem : MonoBehaviour
{
    public Text sum;

    public Text count;

    public Text clear;

    private int cnt;
    // Start is called before the first frame update
    void Start()
    {
        sum.text = GameObject.FindGameObjectsWithTag("Item").Length.ToString();
    }

    // Update is called once per frame
    void Update()
    {
        count.text = GameObject.FindGameObjectsWithTag("Item").Length.ToString();

        cnt = int.Parse(count.text);

        if (cnt == 0)
        {
            sum.text = "";
            count.text = "";
            clear.text = "Game Clear";

            Invoke("ExitGame", 10.0f);
        }
    }

    void ExitGame()
    {
        Application.Quit();
    }
}

6. 完成

以上で、簡単なゲームをUnityとArduinoを使って作成しました。
初めてArduinoを利用したゲーム制作ということもあり、つまずいたりうまくいかないことが多々ありました。今後もUnity、Arduinoを利用したゲームづくりや面白いことを記事に書いていこうと考えております。

今回作成したゲームは、ファブテラスいわてに直接来ていただいた際に試せるようにしたいと考えておりますので、ぜひファブテラスいわてにお越し下さい。

それでは次の記事で会いましょう。