JoyStrickの入力をビジュアルで確認してみよう

2022年9月4日

アナログ入力の様子をビジュアルに確認することを通してUIの学習と依存性を下げるためのオブジェクト指向、イベント学習を進めます

UI画面の構成

入力の様子を十字表示で確認できるようにしたいです
十字表示を担当するコントローラ(書籍では監督)とアナログ入力を担当するコントローラから構成します

これら2つの機能は、入出力を一手に引き受けるViewコントローラオブジェクトにアタッチします

CanvasにSliderを追加

2つのSliderを作成します。それぞれの名前をSliderX, SliderYとします

Sliderの調整

2つのスライダーのハンドルの位置を-1から+1までの範囲で調整できるようにします
SliderXの位置、サイズは、キャプチャを参考にしてください

2つのスライダーのFillオブジェクトのチェックを外して、表示を無効にします

SliderYのみ、縦表示するようにします
SliderXの位置、サイズは、キャプチャを参考にしてください

コードの作成

スライダーの値を更新するコード

Sliderの値を取得するのは、次のコードになります

GameObject.Find("SliderX").GetComponent<Slider>()

今回は、これを一気に代入する仕組みを採用します

インスペクターで代入できるようにする

Public Slider sliderX;でもいいです
違いは、アクセス修飾子をPublicにするかPrivateにするかです(修飾子を省略するとPrivateになります)

[SerializeField]
Slider sliderX;

スライダーに値を代入する

sliderX.value = position.x;

全体のコード

using UnityEngine;
using UnityEngine.UI;

public class SliderController : MonoBehaviour
{
    [SerializeField]
    Slider sliderX;
    [SerializeField]
    Slider sliderY;

    public void SetPosition(Vector2 position)
    {
        sliderX.value = position.x;
        sliderY.value = position.y;
    }
}

アタッチする

使えるようにするため、ゲームオブジェクトにアタッチします

ViewController ゲームオブジェクトを作成する

空のGameObjectを作成して、名前をViewControllerに変更します

スクリプトをアタッチ

スクリプトをアタッチしたら、それぞれのスライダーをドラッグ&ドロップ(アウトレット接続)します
これで、GameObject.FindとGetComponentを同時に処理したのと同じになります

JoyStick入力

アナログ入力の仕組みについてよくわからない方は、リンクで先に確認しておいてください

上記で作成したコードを次のように更新すると、これだけで動きます
練習、簡易確認用であれば十分です
今回は、実用に向いている(仕様追加・変更に強い、修正に強い)仕組みの構築の学習を目指します
なので、次からのコード作成は、このコードに比べると冗長(無駄に長い)ように感じるかもしれませんが、そうとしか思えないとそこでスキルの向上が止まってしまいますので、ご注意ください(趣味の範囲を出ようとは思っていないのであれば特に気にしなくていいです)


using UnityEngine;
using UnityEngine.UI;

public class Simple : MonoBehaviour
{
    [SerializeField]
    Slider sliderX;
    [SerializeField]
    Slider sliderY;

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float y = Input.GetAxis("Vertical");

        sliderX.value = x;
        sliderY.value = y;
    }
}

スクリプトの作成

入力処理を専門に処理するスクリプトを作成します
変更、修正に強いプロジェクトとするため、さらにイベントを取り入れます

イベントについて

コード中には、どのイベントハンドラ(メソッド)を呼び出すかを記述しません
これにより依存性を下げています(依存とは、特定の相手を必ず必要とする状態。しがらみ。強いほど抜け出せなくなる。自由度が減り、変更、追加が困難になります)

// イベントをインスペクタで登録できるようにしておく
public UnityEvent<Vector2> inputEvent;

// イベントハンドラをまとめて呼び出すコード
inputEvent.Invoke(position);

全体のコード

イベントを組み込んだコードになります

using UnityEngine;
using UnityEngine.Events;

public class InputController : MonoBehaviour
{
    public UnityEvent<Vector2> inputEvent;

    void Update()
    {
        float x = Input.GetAxis("Horizontal");
        float y = Input.GetAxis("Vertical");

        Vector2 position = new Vector2(x, y);

        inputEvent.Invoke(position);
    }
}

イベントのインスペクタでの表示

イベントハンドラ(実行するスクリプト)を登録する場所ができています

イベントハンドラを追加しましょう

オブジェクト図

コード中に参照するオブジェクトが入っていない状態が「依存性が低い」状態です
Unityに限らないプログラミングの定石になります

実行結果

ジョイスティックが用意できなくても、キーボードの上下矢印キーで動作できます
単純なON,OFFではなく、徐々に数値が変化しているのを確認します

効果

次のような機能を追加する要望があった場合、登録するイベントハンドラの差し替え、または追加で楽に対応ができます

  • 別々になったスライダーにしたい
  • 数値で表示したい
  • キャラクタの移動に使いたい

これくらい、1つのスクリプトで追加できると思われるかもしれません。ただ、プロジェクトはこれだけが目的ではないですよね。UIには、もっと多くの表示が必要でしょうし、ゲーム本体も実装されていません。1つのスクリプトにこだわると、どんどん複雑性が増してきます。つまり、変更・機能追加に弱い、つまり依存性が強いよくないプロジェクトが完成してしまいます。どのようにうまく辻褄を合わせなくてはいけないかに頭を使って悩むことになります

もう拡張や更新をしないと割り切っているのであれば、短いコードの方がいいこともあります
バランス(粒度)を見極めていきましょう

機能追加してみる

スライダーの位置を右上に移動してみる

Cubeの移動スクリプト

Cubeを1つ追加して、次のコードをアタッチします

using UnityEngine;

public class MoveController : MonoBehaviour
{
    // 秒速(1秒間に進むマス)
    [SerializeField]
    float speed;

    public void Move(Vector2 diff)
    {
        transform.Translate(diff * speed * Time.deltaTime);
    }
}

Cubeオブジェクトを作成、スクリプトのアタッチ

イベントハンドラにこの登録を追加

実行

オブジェクト図

機能を追加しましたが、変更点は最小限に留まっています
拡張性に強いコードになっています。また、バグがあっても特定のクラスだけをメンテナンスすればいいので保守性も同時に向上します

実用に向いていないオブジェクト図

すべての機能を1つのクラスに詰め込むとバグ修正が別のバグを生むことになりますし、拡張にも多くの手間がかかります。また、if文などの分岐も入ってきてどうにもならなくなります

初級レベルの練習、また、ちょっとした機能の確認であれば効率的に使えるので大丈夫です

応用(ローカル空間:ローカル座標系での移動)

JoyStick入力で前(z軸)に進むことを考えてみましょう
この練習では、機能確認になります。粒度が細かい(細かくスクリプト分けをしている)ので、実際のコードではもう少しまとめることが多いですね。ただ、これが良くないというわけではないので、理解ができる範囲で粒度を考えましょう

実行結果

どのように動作させるのか見ていきましょう

移動は、ローカル座標系(自分から見て前方をz軸とします)になります
ラジコンの操作がこのようになりますね

MoveControllerの更新

MoveメソッドをMoveSelfメソッドに名前を変更します
VisualStudioで「名前の変更」を選択して作業をしましょう。これは、利用されている箇所を判断して同時に変更してくれる機能です。一般的な置換が同じ名前であれば全て変更してしまうのに対して、スコープを考慮して置換してくれますので、逆に変更による予期しないエラーの発生を防いでくれます

この変更に伴い、イベントハンドラの登録(インスペクターでのドラッグ&ドロップするところ)がMissとなりますので再度ドラッグ&ドロップしましょう

using UnityEngine;

public class MoveController : MonoBehaviour
{
    // 秒速(1秒間に進むマス)
    [SerializeField]
    float fwdSpeed;

    // 1秒間の回転角度(オイラー角:分度器の角度のこと)
    [SerializeField]
    float rotSpeed;

    public void MoveSelf(Vector2 diff)
    {
        transform.Translate(Vector3.forward * diff.y * fwdSpeed * Time.deltaTime);

        transform.Rotate(Vector3.up * diff.x * rotSpeed * Time.deltaTime);
    }
}

上ではなく、前へ進むようにする

transform.Translateメソッドの引数について見ていきます
Vector3.forwardは、ローカル座標系(自分の向き)を指しています。

new Vector3(0, 0, 1)と同じです

これにyつまり上下キーの値(0から1)をかけます
speed * Time.deltaTimeで、speedを秒速とすることができます

transform.Translate(Vector3.forward * diff.y * speed * Time.deltaTime);

これは、次と同じになります(オーバーロードで、x, y, zでも良いため)

transform.Translate(0, 0, diff.y * fwdSpeed * Time.deltaTime);

回転できるようにする

transform.Rotateメソッドの引数について見ていきます
Vector3.upは、new Vector3(0, 1, 0)と同じです
引数は、回転軸とその大きさ(ベクトルの長さ)を代入します
speed * Time.deltaTimeで、speedを回転速度(秒速)とすることができます

transform.Rotate(Vector3.up * diff.x * speed * Time.deltaTime);

これは、次と同じになります(オーバーロードで、x, y, zでも良いため)

transform.Rotate(0, diff.x * rotSpeed * Time.deltaTime, 0);

応用(ワールド空間:ワールド座標系での移動)

JoyStick入力でXZ平面での移動になります
自分から見て前後ではなく、固定されたXZ平面での移動になります
東西南北の考え方が、自分の向きに関与しないとの同じことです
UFOキャッチャーのような動きですね

実行結果

どのように動作させるのか見ていきましょう

移動は、ローカル座標系(自分から見て前方をz軸とします)になります
ラジコンの操作がこのようになりますね

表示をローカル座標系にして確認しましょう

表示をワールド座標系(グローバル座標系)にして確認しましょう

MoveControllerの更新

MoveWorldメソッドを追加します

イベントハンドラの登録をMoveWorldメソッドへ変更する必要もあります

using UnityEngine;

public class MoveController : MonoBehaviour
{
    // 秒速(1秒間に進むマス)
    [SerializeField]
    float fwdSpeed;

    // 1秒間の回転角度(オイラー角:分度器の角度のこと)
    [SerializeField]
    float rotSpeed;


    public void MoveSelf(Vector2 diff)
    {
        transform.Translate(Vector3.forward * diff.y * fwdSpeed * Time.deltaTime);

        transform.Rotate(Vector3.up * diff.x * rotSpeed * Time.deltaTime);
    }

    public void MoveWorld(Vector2 diff)
    {
        // XZ平面に座標を変更
        Vector3 moveXZ = new Vector3(diff.x, 0, diff.y);

        // ワールド空間(ワールド座標系)での移動
        transform.Translate(moveXZ * fwdSpeed * Time.deltaTime, Space.World);
        // 移動方向に向きを変える
        transform.LookAt(transform.position + moveXZ);
    }
}

XZ平面に座標を変更

引数が、XY(JoyStickの入力)平面なので、XZ平面に変換します

Vector3 moveXZ = new Vector3(diff.x, 0, diff.y);

Tranlateメソッドは、第2引数にSpace.Worldを追加するとワールド空間での移動になります
省略するか、Space.Selfを追加するとローカル空間になります

transform.Translate(moveXZ * fwdSpeed * Time.deltaTime, Space.World);

進行方向を見るようにする

進行方向に向きを変えます
LookAtメソッドは、引数のベクトル方向にローカル座標系でのz軸を向けます
transform.positionは現在のCubeの位置ですね
moveXZはJoyStickの入力でXZ平面に変換されていますので、この2つを足すと移動したい方向が取得できることになります。つまり進行方向を取得できることになります

transform.LookAt(transform.position + moveXZ);

Unity

Posted by hidepon