M5Stack Core2内蔵のジャイロ加速度計「MPU6886」の入力」で出力する加速度・ジャイロデータを使って、Unityで作成したジェット機を飛行させてみます。M5Stack Core2とUnity間はシリアルインタフェースで接続し、加速度データと姿勢角を使用します。

・Unityバージョン:2022.3.11f
・プロジェクト名:GyroTest

シリアルインタフェイスの設定

Unityでシリアル通信を行うために、次のように設定します。

Editメニューから「ProjyectSettings」を選択します。

表示されたダイアログで「Player」→「Configuration」→「Api Compatibility Level」で次のように設定します。

背景のアセットの組み込み

「Polylised – Medieval Desert City」をアッセットストアからダウンロードしてプロジェクトにインポートします。

「Assets/Polylised – Medieval Desert City/Demo」の「Demo_01.unity」をダブルクリックして、シーンを設定します。

ジェット機のアセットの組み込み

「Fighter Jet Low Poly」をアッセットストアからダウンロードしてプロジェクトにインポートします。

「Assets/Raptor3D/FA_N26/0_Prefabs」の「FA_N26_Color_2_Prefab.prefab」をシーン「Demo_01」にドラッグ&ドロップします。

Main Cameraの設置

ジェット機を後ろから追従するようにMain Cameraを設置するため、Main Cameraをジェット機の子オブジェクトとします。具体的には、FA_N26_Color_2_Prefab」にMain Cameraをドラッグ&ドロップします。

「FA_N26_Color_2_Prefab」の位置を次に示します。

「FA_N26_Color_2_Prefab」と「Main Camera」の位置合わせ

  1. 「FA_N26_Color_2_Prefab」と「Main Camera」との親子関係を解除します。
  2. 「Move Tool」ツールを選択し「FA_N26_Color_2_Prefab」をクリックして軸を表示し、右上の3Dアイコン(シーンギズモ)を用いて調整しやすい位置に設定します。X軸は左右、Z軸は進行方向、Y軸は上下方向を示します。シーンギズモは、「Sift+シーンギズモの□」をクリックすると初期状態に戻ります。
  3. 希望する位置に設定に設定したら、「Main Camera」の位置も同じにして、「FA_N26_Color_2_Prefab」の子として「Main Camera」を設定します。
  4. 「Main Camera」の位置を次のように変更します。

BGMの組み込み

BGMの音源をゲームに組み込みます。

  1. BGMの音源「effect.mp3」をAssets画面にドラッグ&ドロップします。
  2. 「Demo_01」を右クリックし、「GameObject」→「Create Empty」を選択します。
  3. 「Inspector」タブに表示された「Add Component」をクリックし、次のように「Audio Source」選択します。
  4. 表示されたAudio Sourceの「AudioClip」にAssets画面のBGMの音源「effect.mp3」をドラッグ&ドロップし、タイトルを「BGM」、「Loop」をチェックします。

スクリプトの作成

Unityの作成画面を次に示します。

作成したスクリプトを次に示します。姿勢角を使用する場合は、最初は机にM5Stack Core2を置いて、補正値を求めます。

  • 39-55行目で、’a’(加速度)の場合はOnAccelReceived()関数、 ‘p’(姿勢角)の場合はOnGyroReceived()関数をそれぞれ呼び出します。
  • 85行目で、’a’(加速度) ‘p’(姿勢角)のみデータを使用します。
  • 162行目で、姿勢角の平均値を求めます。
  • 200行目で、求めた姿勢角の平均値を入力した姿勢角に反映させます。

SerialHandler.cs

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System.IO.Ports;
using System.Threading;
using System;
using System.Runtime.InteropServices;
using System.Text;

public class SerialHandler : MonoBehaviour
{

    public string portName = "COM9";
    public int baudRate = 9600;

    private SerialPort serialPort_;
    private Thread thread_;
    private bool isRunning_ = false;
    private string lastrcvd = "";
    private byte dataid = 0x0;

    private string message_;
    private bool isNewMessageReceived_ = false;

    private int gyrocnt = 0;
    private float offsetpitch;
    private float offsetroll;
    private float offsetyaw;
    private float[] setpitch = new float[10];
    private float[] seteroll = new float[10];
    private float[] setseyaw = new float[10];

    void Start()
    {
        lastrcvd = "";
        Open();
    }

    void Update()
    {
        if (isNewMessageReceived_)
        {
            if (dataid == 'a')
            {
                gyrocnt = 0;
                OnAccelReceived(message_);

            }
            if (dataid == 'p')
            {
                OnGyroReceived(message_);

            }
        }
    }
    void OnDestroy()
    {
        Close();
    }

    private void Open()
    {
        serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort_.ReadTimeout = 5000;

        serialPort_.Open();

        isRunning_ = true;

        thread_ = new Thread(Read);
        thread_.Start();
    }

    private void Read()
    {
        byte rcv;
        char tmp;

        while (isRunning_ && serialPort_ != null && serialPort_.IsOpen)
        {
            try
            {
                rcv = (byte)serialPort_.ReadByte();

                if ((rcv == 'a') || (rcv == 'p'))
                {
                    dataid = rcv;

                    message_ = lastrcvd;
                    Debug.LogFormat("textLine:{0}", message_);
                    lastrcvd = "";
                    isNewMessageReceived_ = true;
                }
                else
                {
                    tmp = (char)rcv;
                    Debug.LogFormat("rcv:{0}", tmp.ToString());
                    lastrcvd = lastrcvd + tmp.ToString();
                }
            }
            catch (System.Exception e)
            {
                Debug.LogError("Read:" + e.Message);
            }
        }
    }

    private void Close()
    {
        isRunning_ = false;

        if (thread_ != null && thread_.IsAlive)
        {
            thread_.Join();
        }

        if (serialPort_ != null && serialPort_.IsOpen)
        {
            serialPort_.Close();
            serialPort_.Dispose();
        }
    }

    void OnAccelReceived(string message_)
    {
        float x;
        float y;
        float z;

        float speed = 10.0f;

        var data = message_.Split(
                new string[] { "," }, System.StringSplitOptions.None);
        if (data.Length != 3) return;

        try
        {
            var data1 = data[0].Split(new string[] { ":" }, System.StringSplitOptions.None);
            x = float.Parse(data1[1]);
            data1 = data[1].Split(new string[] { ":" }, System.StringSplitOptions.None);
            y = float.Parse(data1[1]);
            data1 = data[2].Split(new string[] { ":" }, System.StringSplitOptions.None);
            z = float.Parse(data1[1]);
            Debug.LogFormat("Accel x:{0} y:{1} z:{2}", x, y, z);

            var dire = Vector3.zero;

            dire.z = x * -1;
            dire.x = y * -1;

            dire *= Time.deltaTime;
            transform.Rotate(dire * speed);
            this.transform.Translate(Vector3.forward * Time.deltaTime * speed);

        }
        catch (System.Exception e)
        {
            Debug.LogError("OnAccel:" + e.Message);
        }
    }

    float Average(float[] data)
    {
        float sum = 0f;

        //合計値計算
        for (int i = 0; i < data.Length; i++)
        {
            Console.WriteLine("Data[{0}] = {1}", i + 1, data[i]);
            sum += data[i];
        }
        int count = data.Length;
        //平均値計算
        return sum / count;
    }

    void OnGyroReceived(string message_)
    {
        float pitch;
        float roll;
        float yaw;
        float speed = 10.0f;


        var data = message_.Split(
                new string[] { "," }, System.StringSplitOptions.None);
        if (data.Length != 3) return;
        try
        {
            var data1 = data[0].Split(new string[] { ":" }, System.StringSplitOptions.None);
            pitch = float.Parse(data1[1]);
            data1 = data[1].Split(new string[] { ":" }, System.StringSplitOptions.None);
            roll = float.Parse(data1[1]);
            data1 = data[2].Split(new string[] { ":" }, System.StringSplitOptions.None);
            yaw = float.Parse(data1[1]);
            Debug.LogFormat("pitch:{0} roll:{1} yaw:{2}", pitch, roll, yaw);

            if (gyrocnt >= 10)
            {
                this.transform.rotation = Quaternion.Euler(roll - offsetroll,
                    yaw - offsetyaw, pitch - offsetpitch);
                //this.transform.rotation = Quaternion.Euler(pitch, yaw, roll);
                this.transform.Translate(Vector3.forward * Time.deltaTime * speed);
            }
            else
            {
                setpitch[gyrocnt] = pitch;
                seteroll[gyrocnt] = roll;
                setseyaw[gyrocnt] = yaw;
                if (gyrocnt == 9)
                {
                    offsetpitch = Average(setpitch);
                    offsetroll = Average(seteroll);
                    offsetyaw = Average(setseyaw);
                    Debug.LogFormat("offsetpitch:{0} offsetroll:{1} offsetyaw:{2}", offsetpitch, offsetroll, offsetyaw);
                }
                gyrocnt++;
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError("OnGyro:" + e.Message);
        }
    }
}

ゲームプレイ

M5Stack Core2を傾け、入力した加速度データを使って、Unityでジェット機を操作しました。