【Unity】前後移動はボタン、横移動はスマホの傾きでtoioを動かす

## 本記事について

toio SDKのチュートリアルを一通りこなした後まず最初に作るべきものは、トイオコレクションに付属しているフリームーブのように自由に移動できるものが難易度がちょうど良いのではないかと考えた。

そこで以下のようなコントローラーを作成した。

![スクリーンショット_2022-12-18_11_51_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/1e61b5e3-5f8f-1a52-f3e8-13dc6bf8ce32.png)
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/8202d1d5-a348-8484-8d0d-5ba52bcc653a.gif)


本記事ではこの機能の実装方法を説明する。

## 環境

mac os 12.5 silicon
Unity 2021.3.7f1
Input System 1.3.0
iphone
toio SDK for Unity v1.5.1

## 本記事の対象者
Unityの基本的なことができる
toio SDKのチュートリアルができる
ビルドができる
Input Systemは使ったことがなくても大丈夫


## コンセプト
![スクリーンショット 2022-12-18 13.44.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/b22b93b3-0eab-ef50-01b1-08b906f36fd5.png)

本記事のコードでは3つのクラスを定義している。より複雑なことをするならもっと細かく役割を分けた方がいいと思うが、シンプルさを重視した設計。


## 手順
toio SDKのインストール等は省略

### Input Systemのインストール

Package Managerからインストール
![スクリーンショット_2022-12-18_14_03_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/fb07019b-e40f-a788-dfb8-d3ce3f7d13b7.png)

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.3/manual/Installation.html

Scene上のEvent Systemのconvert。スクショはないが、convertのようなボタンが表示されてるはずなので押す。

![スクリーンショット 2022-12-18 14.07.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f9742dce-4d33-7347-3e56-27587f690c93.png)
こうなってればおそらく大丈夫

Project Settings > Player > Other Settings > Configuration

にあるActive Input Handling をBothにして古いInput Managerが動くようにする。

![スクリーンショット 2022-12-18 14.09.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/ebd44069-6da3-e988-9efa-466137b02014.png)

toio SDKをそのまま使う場合にはSimulatorが古いInput Managerを使用しているため。


### ToioScene作成

![スクリーンショット 2022-12-18 14.15.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/fcbcc6bd-38cb-bc4e-f88f-e4fee13fb587.png)

Canvas以下は前後用のボタンとDebug用のtextを置いているだけ。

#### ToioScene

記事にするにあたって読みやすくするために実際に動かしたコードから変更している。

```ToioScene.cs
...
namespace Hoge
{
    public class ToioScene : MonoBehaviour
    {
        public ConnectType connectType;
        CubeManager cm;
        [SerializeField] FreeRunCubeController freeRunCubeController;

        void Start
        {
            if (GravitySensor.current != null)
            {
                InputSystem.EnableDevice(GravitySensor.current);
            }

            InitAsync().Forget();
        }

        async UniTask InitAsync()
        {
#if UNITY_EDITOR
            await SimulationCubeScene.LoadAsync();
#endif
            cm = new CubeManager(connectType);
            var cube = await cm.SingleConnect();
            var freeRunCube = new FreeRunCube(cm, cube);
            freeRunCubeController.Activate(freeRunCube);
        }
    }
}

```

動きをSimulatorで確認するためにUNITY_EDITORの場合にのみ別シーンでSimulatorを読み込んでいる。

https://qiita.com/Teach/items/20bc8145254fc057ed50

#### FreeRunCubeController

主に入力に対するハンドラを記載。

```FreeRunCubeController.cs
...
namespace Hoge.Toio
{
    public class FreeRunCubeController : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI sensorLabel;
        FreeRunCube freeRunCube = null;
        public void Activate(FreeRunCube freeRunCube)
        {
            this.freeRunCube = freeRunCube;
        }

        public void OnMoveForwardButtonDown()
        {
            if (freeRunCube == null)
            {
                return;
            }

            freeRunCube.StartMoveForward();
        }

        public void OnMoveForwardButtonUp()
        {
            if (freeRunCube == null)
            {
                return;
            }
            freeRunCube.EndMoveForward();
        }

        public void OnMoveBackButtonDown()
        {
            if (freeRunCube == null)
            {
                return;
            }
            freeRunCube.StartMoveBack();
        }

        public void OnMoveBackButtonUp()
        {
            if (freeRunCube == null)
            {
                return;
            }
            freeRunCube.EndMoveBack();
        }

        void Update()
        {
            var gravitySensor = GravitySensor.current;
            if (gravitySensor != null)
            {
                var enabled = gravitySensor.enabled;
                Vector3 gravity = gravitySensor.gravity.ReadValue();
                sensorLabel.text = $"enabled: {enabled}\n" +
                    $"Gravity\nX={gravity.x:#0.00} Y={gravity.y:#0.00} Z={gravity.z:#0.00}";

                freeRunCube?.SetHandleRotation(gravity.x);
            }else
            {
                freeRunCube?.SetHandleRotation(0);
            }

            freeRunCube?.OnUpdate();
        }
    }
}

```

傾きの取得方法については別記事にしたのでそちらを参考にどうぞ。

https://qiita.com/Teach/items/707377fff88dc3a81313

複雑なことをやりたい場合にはこのクラスをコマンドパターン等で書き換えるのが良いと思う。

![スクリーンショット 2022-12-18 14.52.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/a538b5de-29ab-5fbe-7d95-f6f48fa60d63.png)

ハンドラはButtonにEventTriggerをつけて呼び出している。

#### FreeRunCube

```FreeRunCube.cs
...
namespace Hoge.Toio
{
    public class FreeRunCube
    {
        public enum MoveType
        {
            Forward,
            Idle,
            Back
        }

        CubeManager cm;
        Cube cube;
        bool isForward = false;
        bool isBack = false;
        MoveType moveType = MoveType.Idle;

        public float forwardSpeed = 50f;
        public float backSpeed = 30f;
        public float forwardRotateBaseSpeed = 10f;
        public float backRotateBaseSpeed = 10f;
        public float rotateWeight = 1.0f;
        public float handleRotation = 0f;

        public FreeRunCube(CubeManager cm, Cube cube)
        {
            this.cm = cm;
            this.cube = cube;
        }

        void UpdateMoveType()
        {
            if (isForward && !isBack)
            {
                moveType = MoveType.Forward;
            }
            else if (!isForward && isBack)
            {
                moveType = MoveType.Back;
            }
            else
            {
                moveType = MoveType.Idle;
            }
        }

        public void StartMoveForward()
        {
            isForward = true;
            UpdateMoveType();
        }

        public void EndMoveForward()
        {
            isForward = false;
            UpdateMoveType();
        }

        public void StartMoveBack()
        {
            isBack = true;
            UpdateMoveType();
        }

        public void EndMoveBack()
        {
            isBack = false;
            UpdateMoveType();
        }

        public void SetHandleRotation(float value) => handleRotation = value;

        /// <param name="handleRotation">曲がる用のハンドルの回転 -1.0 ~ 1.0</param>
        public void OnUpdate()
        {
            if (moveType == MoveType.Idle)
            {
                return;
            }

            if (cm.IsControllable(cube))
            {
                Move();
            }
        }

        void Move()
        {
            var motorSpeed = CalcSpeed();
            cube.Move((int)motorSpeed.x, (int)motorSpeed.y, 100);
        }

        Vector2 CalcSpeed()
        {
            float moveSpeed = GetMoveSpeed();
            float rotationValue = Mathf.Clamp01(handleRotation * 1.5f); // 60度回転させたら最大の曲がりにする。
            float rotateSpeed = handleRotation * GetRotateBaseSpeed();

            return new Vector2(
                CalcSpeed(moveSpeed, rotateSpeed, rotateWeight),
                CalcSpeed(moveSpeed, -rotateSpeed, rotateWeight)
            );
        }

        float GetMoveSpeed()
        {
            switch (moveType)
            {
                case MoveType.Forward:
                    return forwardSpeed;
                case MoveType.Back:
                    return -backSpeed;
            }
            return 0;
        }

        float GetRotateBaseSpeed()
        {
            switch (moveType)
            {
                case MoveType.Forward:
                    return forwardRotateBaseSpeed;
                case MoveType.Back:
                    return -backRotateBaseSpeed;
            }
            return 0;
        }

        float CalcSpeed(float move, float rotation, float rotateWeight)
        {
            return move + rotation * rotateWeight;
        }
    }
}


```

モーターの回転速度を計算してCubeに渡している。

■ ポイント1　どのくらい傾けたら最大の横移動をするのかの調節

スマホを90度曲げないと最大の曲がり方をしないというのは大変なので調節した。

|before|after|
|:--:|:--:|
|90度傾けじゃないと最大カーブ速度でない|60~120度傾けで最大カーブ速度！|
|![スクリーンショット 2022-12-18 15.39.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/ed6942c9-379e-3e8c-3a5f-cd992eefeb2f.png)|![スクリーンショット 2022-12-18 15.39.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/35d65bdf-0f82-c95c-00c6-87784a28d511.png)|

図. スマホを傾けたときの「中心 -> ホームボタン」の向きにおける rotationValue。

```.cs
float rotationValue = Mathf.Clamp01(handleRotation * 1.5f); // 60度回転させたら最大の曲がりにする。
```

上記コードによってピンクの範囲が最大の曲がり速度となる。

■ ポイント2 IsControllable

[ドキュメント](https://github.com/morikatron/toio-sdk-for-unity/blob/main/docs/tutorials_basic.md#9-cubemanager%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E7%B0%A1%E7%95%A5%E5%8C%96)

```.cs
        /// <summary>
        /// 前回のCubeへの送信から45ミリ秒以上空いていた時にTrueが返ります.
        /// </summary>
        public bool IsControllable(Cube cube)
```

https://github.com/morikatron/toio-sdk-for-unity/blob/v1.5.1/toio-sdk-unity/Assets/toio-sdk/Scripts/Cube/CubeManager.cs#L171-L174

毎フレームtoioに送信したくないので一定間隔待つ。

## おわりに
toio動かすだけでとても楽しいのでぜひ試してみてください。