M5Stack + Unity 『草刈りゲーム』の作り方



この記事は プロトアウトスタジオの[アドベントカレンダー2020](https://qiita.com/advent-calendar/2020/protoout) の7日目の記事です！

##草刈りゲーム
実は、今日2020年12月７日はプロトアウトスタジオの卒業制作のクラウドファンディングのスタート日です。

私のは、これです。
[ハッカソンから生まれたアイデア『草刈りゲーム』のeスポーツイベントを開催したい](https://camp-fire.jp/projects/view/325175)
![f47c456f-17db-4cee-9986-d3279300b696.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/e874f2be-8708-7ca7-57aa-e9f27a33d62c.jpeg)

リアルの草刈りとバーチャル空間を連動するゲームです。
[![Image from Gyazo](https://i.gyazo.com/80293d3b4c8c32bbbd5502080f1d8364.gif)](https://gyazo.com/80293d3b4c8c32bbbd5502080f1d8364)






##必要なもの
###ハード
- 草刈り機
- [M5Stack Gray](https://www.switch-science.com/catalog/3648/)
- [３G拡張ボード](https://soracom.jp/products/kit/3g_module_m5stack/)
- ソラコムのSIMカード (ナノサイズ）<br>※プランは plan01s , plan-D

###事前準備
・SORACOMのアカウントをとること
・Unityをインストールすること


##システム構成図
![草刈りゲームシステム構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/bb5803b7-8c30-a5c9-b3df-ca91cec4544f.png)


- 草刈り機にM5StackGrayをつけて、ジャイロセンサーの値をSORACOMの3G回線でクラウド（SORACOM HARVEST）に上げます。<br>（詳細記事）[M5StackGRAYのジャイロセンサの値を3G拡張ボードを使ってSORACOM Harvestに送ってみた](https://qiita.com/tatsuya1970/items/b25881ddf11ae8cd6409)<br><br>
- 定期的にUnity側からクラウドを参照し、ジャイロセンサーの値によってゲーム内キャラクターの挙動を決める
<br>（詳細記事）[UnityからSORACOM Harvestにあるデータをゲットする方法](https://qiita.com/tatsuya1970/items/a7884c2dc9786928eb04)




##コード

###M5Stack
M5StackGrayのジャイロセンサーの値をSORACOMの3G回線でクラウド（SORACOM HARVEST）に上げるコードです。
（詳細記事）[M5StackGRAYのジャイロセンサの値を3G拡張ボードを使ってSORACOM Harvestに送ってみた](https://qiita.com/tatsuya1970/items/b25881ddf11ae8cd6409)

```main.cpp
//<M5Stack.h>をincludeする前に、IMUモジュールを#defineしておく
#define M5STACK_MPU6886 
#include <M5Stack.h>

//3G通信に必要
#define TINY_GSM_MODEM_UBLOX
#include <TinyGsmClient.h>

float gyroX, gyroY, gyroZ;

TinyGsm modem(Serial2); /* Serial2 is Modem of 3G Module */
TinyGsmClient ctx(modem);

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

  M5.begin();
  M5.Power.begin(); 

  M5.IMU.Init();

  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.println(F("M5Stack + 3G Module"));

  M5.Lcd.print(F("modem.restart()"));
  Serial2.begin(115200, SERIAL_8N1, 16, 17);
  modem.restart();
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("getModemInfo:"));
  String modemInfo = modem.getModemInfo();
  M5.Lcd.println(modemInfo);

  M5.Lcd.print(F("waitForNetwork()"));
  while (!modem.waitForNetwork()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("gprsConnect(soracom.io)"));
  modem.gprsConnect("soracom.io", "sora", "sora");
  M5.Lcd.println(F("done"));

  M5.Lcd.print(F("isNetworkConnected()"));
  while (!modem.isNetworkConnected()) M5.Lcd.print(".");
  M5.Lcd.println(F("Ok"));

  M5.Lcd.print(F("My IP addr: "));
  IPAddress ipaddr = modem.localIP();
  M5.Lcd.print(ipaddr);
  delay(2000);
}


void loop() {
  M5.update();

  M5.Lcd.clear(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(F("Uptime post to SORACOM Harvest"));

  /* HTTP GET example */
  if (!ctx.connect("uni.soracom.io", 80)) {
    Serial.println(F("Connect failed."));
    return;
  }
  Serial.println(F("connected."));

  /* build payload */

  M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
  M5.Lcd.setCursor(0, 10);
  M5.Lcd.printf("gyro=(%5.1f, %5.1f, %5.1f)", gyroX, gyroY, gyroZ);

  char payload[1024];


  M5.Lcd.setCursor(0, 15);
  /* send request */
  ctx.println("POST / HTTP/1.1");
  ctx.println("Host: uni.soracom.io");
  ctx.println("Content-Type: application/json");
  char content_length_hdr[32];
  sprintf(content_length_hdr, "Content-Length: %lu", strlen(payload));
  ctx.println(content_length_hdr);
  ctx.println();
  ctx.println(payload);
  Serial.println("sent.");

  /* receive response */
  while (ctx.connected()) {
    String line = ctx.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      Serial.println("headers received.");
      break;
    }
  }
  char buf[1 * 1024] = {0};
  ctx.readBytes(buf, sizeof(buf)); /* body */
  ctx.stop();
  M5.Lcd.println(buf);

  delay(1000 * 10);
}

```


###Unity
定期的にUnity側からクラウドを参照し、ジャイロセンサーの値によってゲーム内キャラクターの挙動を決めます。
（詳細記事）[UnityからSORACOM Harvestにあるデータをゲットする方法](https://qiita.com/tatsuya1970/items/a7884c2dc9786928eb04)

```kusakari.cs
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;

public class UnityChanGame : MonoBehaviour
{
    //Soracom Harvest 設定
    private string APIKEY = "APIKEY";
    private string APITOKEN = "APITOKEN"
    public float gyroX, gyroY, gyroZ;
    private Animator animator;

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

    // Update is called once per frame
    void Update()
    {
       //ソラコムHarvest           
       StartCoroutine(GetText("https://g.api.soracom.io/v1/subscribers/#####/data?sort=desc"));
       //各ジャイロセンサーの閾値。ここの数値を実際の草刈機の振動で調整する。
       if ( Mathf.Abs(gyroX) > 30 || Mathf.Abs(gyroY) > 30 || Mathf.Abs(gyroZ) > 30)
       {
       // 座標を取得
       Vector3 unitychanPos = transform.position;
       unitychanPos.z -= 0.005f;
       transform.position = unitychanPos;
       //歩くアニメーション（アニメーションの設定が必要です）
       animator.SetBool("is_walking", true);
       }
       else
       {
        animator.SetBool("is_walking", false);
       }           
    }

    IEnumerator GetText(string url)
    {
       using (UnityWebRequest www = UnityWebRequest.Get(url))
       {
        www.SetRequestHeader("Accept", "application/json");
        www.SetRequestHeader("X-Soracom-API-Key", APIKEY);
        www.SetRequestHeader("X-Soracom-Token", APITOKEN);

        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
            {
            Debug.Log(www.error);
            }
            else
                {
                //JSON Objectというアセットのインポート要             
                JSONObject json = new JSONObject(www.downloadHandler.text);
                JSONObject content = json[0].GetField("content");
                string content2 = content.str;
                string str = System.Text.RegularExpressions.Regex.Unescape(content2);
                JSONObject jsonObj = new JSONObject(str);
                gyroX = jsonObj.GetField("gyroX").n;
                gyroY = jsonObj.GetField("gyroY").n;
                gyroZ = jsonObj.GetField("gyroZ").n;

                Debug.Log("gyroX:" + gyroX + "\t gyroY:" + gyroY + "\t gyroZ:" + gyroZ)                     

                }

        }
    }

```

（参考記事）
- [ユニティちゃんを動かす](https://qiita.com/tatsuya1970/items/5dea86208951574fcc9c)
- [リアルアバター（FBXファイル）をUnityで動かす](https://qiita.com/tatsuya1970/items/b9af491ef4102b388124)




##今後の課題

- <b>クラウドの負荷</b><br>
今は、都度ジャイロセンサーの値をあげ、Unity側から取得にいっていますが、すごく非効率です。クラウドの利用制限にひっかかる懸念がありますし、貴重なIoTのクラウドのリソースをそんな使い方するのはあまりよろしくないと思います。<br>クラウドを介せず、ジャイロセンサーの閾値が変化したときだけ、webhookでPCに接続したマイコンに伝え、そこから制御する方法とか考えられそうです。<br><br>
- <b>実際に草刈りをしているかの判定</b><br>今は、ジャイロセンサーで取得したエンジンの振動によりゲーム内キャラクターの動きを決めていますが、実際に草を刈らなくても、ゲーム内キャラクターは動くという欠点があります。<br>草刈りをしているか、していないかを音から判定する方法ができないか検討中です。


##最後に


2020年12月12日に[オンラインピッチイベント](https://mashupawards.connpass.com/event/196634/)で草刈りゲームのことをピッチします。
ぜひ応援にきてください！
[ヒーローズリーグ オンライン 決勝・BINGO大会 & ProtoOut DEMO DAY](https://mashupawards.connpass.com/event/196634/)

<br>