[Unity][C# Script]「新しい」INPUT SYSTEMによるコントローラの統合(PC版)(仮公開)

18 min read読了の目安(約16500字

本記事は検証中のものが含まれています。追って修正、追記します。
rev 2/27 一部修正

現行のunity標準のInput Managerは少々設計が古いため、これからPCでゲームを作ろうという方にはすでにリリースされている New Input System(新しいINPUT SYSTEM)をお勧めします。
 理由はいくつかありますが、おそらく今後の主流はNew Input Systemとなること、古いInput Managerは標準では様々なゲームパッドを一意に扱えないことが主な理由です。
また、ゲームパッドのボタンアサインが直感的ではないため、ボタンアサインを確認するためにテストスクリプトを作り、実行してはManagerを直すということをプロジェクトを作るたびに行っていたと思いますが、New Input System では実際にゲームパッドのボタンを押すことによってボタンを検出でき、そのまま定義ができます。さらにその定義を簡単にプロジェクト間で使い回すことができるようになります。

New Input Systemの欠点がいくつかあります。まず、以前のInput Managerにはあったデフォルトの定義がないため、最初はサクッと使うことができません。
 また、今までのスクリプトと互換性がないため、スクリプトの置き換えが必要になります。一応、古いのImput Managerと共存するモードもありますが、そのモードでよいかは保守を考えると悩ましいところです。

今回はとりあえず、キャラクタ移動とカメラ移動をこの新しいinput systemで作ってみることを目的とします。
 そのうえで、今までのスクリプトのどこを修正していけばよいか探って生きたいと思います。

1. New Input Systemのセットアップ

New Input Systemをインストールします。
 Unityのパッケージマネージャ(Menu: Window > Package Manager)を開き、リストからInput Systemパッケージを選択し、「install」をクリックします。

Package ManagerのカテゴリはUnity Registryです。
検索🔍に input と入力して検索するのが早いかもしれません。

デフォルトでは、Unityの古典的な入力マネージャ(UnityEngine.Input)がアクティブになっており、新しい入力システムのサポートは非アクティブになっています。
 Input Systemパッケージをインストールすると、Unityは新しいバックエンドを有効にするかどうかを尋ねてきます。Yesをクリックすると、Unityエディタを再起動します。新しいバックエンドが有効になり、古いバックエンドは無効になります。

2. 標準のバインドを利用して使用する(試験用)

とりあえず、以前のInput Managerのようにすぐ使えないのはすごくマイナスです。
ですが、テラシュールブログさんに、「デバッグ目的等でサクッと特定のキー入力を使用したい場合」と限定的ながらすぐに使える方法がありましたので引用しておきます。キーバインドを設定せず、すぐに試したいときなどに便利そうです。

テラシュールブログ【Unity】出来るだけ簡単にNew Input Systemを使いたい

http://tsubakit1.hateblo.jp/entry/2019/10/14/215312
テラシュールブログさんの記事より引用
// マウスの左クリックを検出
Mouse.current.leftButton.wasPressedThisFrame;
// キーボードのスペースキーを検出
Keyboard.current.spaceKey.wasPressedThisFrame;
// ゲームパッドの左スティックを検出
Gamepad.current.leftStick.ReadValue();

3. キーバインドを設定して使用する

New Input Systemは、InputActionという概念をもちいています。
これは現行のInput Manager で、"Jump"というNameがキーボードスペースキーとゲームパッドのbutton 3というように、アクション名に複数のボタン割り当てているのと同じように、たとえばInputActionエディタで"JUMP"というAction名のInputActionをつくり、そこにキーボードのスペースキーとゲームパッドのボタンなどをアサインできます。

そしてスクリプトからはその"JUMP"というAction名をつかって入力を検知し処理していきるようになります。これも現行のInput Managerと似ていますね。

InputActionを複数まとめたものは、InputActuinsファイルとして保管されます。このファイルは、New Input System標準のInputActionエディタで作ったり、カスタマイズすることでできますので、いろんな一度作ったこのアクションに対するキーやボタンの割当を、プロジェクトでキーバインド設定を使いまわせるようになります。

ちなみに現行のInput Managerは最初からFire1~3やJump、HorizontalやVerticalが定義されていますが、New Input Systemの定義はまっさらです。
ですから、まずはinputactuinsファイルを作ります。

1) .inputactionsファイルを作成

Projectの任意の場所(ここではAssets直下)で、右クリック > Create > Input Action にて.inputactionsファイルを作成します。

そして名前を任意に変更します。ここでは「StandardKeyBind」に変更しました。

2) StandardKeyBindの定義を設定する

続いて、このStandardKeyBindを設定していきます。
 StandardKeyBindをダブルクリックすると、.inputactions ファイルのエディタウィンドウが表示されます。

Action Maps、Actions、Properties の3ペイン構成です。

Action Mapsの右の「+」をクリックし、新規マップを追加します。

そして新しく追加したAction Mapをダブルクリックし、Action Map名を変更します。ここでは Player と変更しました。

ここでは Player操作のための入力なので Map名を Player としています。
あとで Camera操作のための入力用に Cameraという Map名の定義も作成します。

次に、New action をダブルクリックし、アクション名を変更します。ここでは、Playerの移動に使うアクションを定義するため、Move に変更しました。

Moveの左の△をクリックし、Moveにアサインされているバインドを展開します。

このMoveには以下の3つをバインドします。
(a) コントロールパッドの左スティック
(b) キーボードの WASDキー
(c) キーボードのカーソルキー

まずは、(a)コントロールパッドの左スティックをバインドします。
Moveをクリックし、PropertiesのActionにある Action Typeを Valueに、Control Typeを Vector2に変更します。

続いてMoveの下にある、<No Binding>を設定します。

Path → Game Pad → Left Stick を選択し、バインドします。

Move に Left Stick [Gamepad]がバインドされました。

続いて、(b)WASDキーをアサインします。Moveの右の[+.]をクリックし、Add 2D Vector Compositeでバインド枠を追加し、名前をWASDに変更します。


続いて、Upをクリックし、Pathの右をクリックします。

キーアサインウィンドウが表示されるので、Listenをクリックし、Wキーを押下します。押したキーが表示されるので、それをクリックします。


WASD - Up に Wキーがバインドされました。

同様に、Down、Left、Rightもアサインします(行程は省略します
無事アサインできると、下のようになります。

続いて、(c)カーソルキーもアサインしていきます。
Moveの右の[+.]をクリックし、Add 2D Vector Compositeでバインド枠を追加し、名前をCURSORに変更します。カーソルキーもアサインすると、以下のようになります。

Moveはこれでおわりです。続いて、各ボタンをアサインします。
ボタンのアサインは、私は以下のようにしましたが、ゲームのコンセプトにあわせお好みでアサインしてみてください。

  1. XBOXコントローラのY = Tabキー
  2. XBOXコントローラのB = Eキー
  3. XBOXコントローラのX = Qキー
  4. XBOXコントローラのA = SPACEキー

ボタンYを定義するやりかた
Action[+] → ButtonY に名称変更、 Action Typeを Buttonにします。

バインディングは Path > Listen を押下し、アサインしたいコントローラのボタンを押すと、候補がリストに出てくるのでそれを選びます。

この、対応するボタンを押せばいという直感的な操作が、今までのInput Managerに比べ親切ですね。


ゲームパッドのButton Northがアサインされました。
「Button North」はゲームパッドの上ボタン、つまりXBOXコントローラのYボタンのことのようですね。

同様に4つのボタンすべてをアサインします。終わった状態は、以下のようになるはずです。

カメラ操作のアサイン
最後に、カメラビューの変更のアクション定義します。
Acton Maps に Cameraという名前のAction Mapを追加し、右スティックとマウスを登録します。右スティックは入力の上下反転を設定してみますね。

まずは Action map "Camera"を追加します。
Action Maps[+] して New action map を追加。その名前を Camera に変更します。

まずは右スティックを登録します。
Actions の New action を Camera に名称変更、 Action Typeを Value に、Control TypeをVector2に変更します。

Bindingは、Path > Game Pad > Right Stick を選択します。
これで右スティックの登録ができます。

続いてマウス移動を登録します。
Camera[+] > Add Binding > <No Binding>を追加します。
Bindingは、Path > Mouse > Delta を選択します。


必要に応じて、右スティックの上下入力値を反転します。
Right Stick のProcessorsの[+.]を押して、Invert Vector2を追加します。
invert X のチェックを外し、Y軸(上下軸)のみ反転するよう設定します。

これでStandardKeyBindの設定は完了です。

.inputactions ファイルのエディタウィンドウを閉じると、Saveするか確認されます。間違わずSaveしておきましょう。

ここで作成した.inputactions ファイルは、バックアップしておくとよいでしょう。

4. 基本的な使い方

1) 入力コンポーネントの追加

New Input Systemを使いたい GameObject に、Add Compornet > Input > Player Inputで入力コンポーネントを追加します。

2) Input Actions の登録

Actions に先に作成した Input Actions を設定し、Default Map に Player を指定します。

先程作ったキーバインドがPlayerでしたので、Playerを指定します。
別名で作った場合は、その名前を指定します。

  1. スクリプトに以下を追加します。
    先に設定した Player にキーバインドされた Move と ButtonA を利用するサンプルです。

・先頭ブロック

using UnityEngine.InputSystem;

・MonoBehaviourのprivate定義領域

InputAction move,fire;
 
 void Start()
 {
   // ...
   move = playerInput.actions["Move"]; // ← "Move" Actionを利用する。
   fire = playerInput.actions["ButtonA"]; // ← "ButtonA" Actionを利用する。
 } 	

・Update()内

void Update()
 {
     // キー入力を取得
     var inputMoveAxis = move.ReadValue<Vector2>(); // "Move" Action は Vector2型
     
     inputDirection.z = inputMoveAxis.x;
     inputDirection.x = inputMoveAxis.y;
     // inputDirection.z = Input.GetAxis("Horizontal"); // 旧システム
     // inputDirection.x = Input.GetAxis("Vertical"); // 旧システム
     
     if (fire.ReadValue<float>() >0f ) // "ButtonA" Action は float型
     {
         // fire!
         Debug.Log("Fire");
     }
 }

5. サンプルスクリプト

1. キャラクタ移動

MoveWithControlerNIS.cs
// 2021.02 K1Togami MoveWithControler with New Input System

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

public class MoveWithControlerNIS : MonoBehaviour
{
    private Vector3 moveDirection = Vector3.zero;
    private CharacterController controller;
    public float maxspeed = 3.0f;
    public float rotatespeed = 360.0f;

    //private SimpleAnimation _simpleAnimation = null; // Unity製 SimpleAnimationシステムを使用する場合に使う
    private Camera mainCamera = null;

    // 入力保持用
    private Vector3 inputDirection;
    private Vector3 lookingDirection;

    InputAction move, fire;


    // Start は最初のフレーム更新の前に呼び出されます。
    void Start()
    {
        controller = GetComponent<CharacterController>();
        //_simpleAnimation = GetComponent<SimpleAnimation>();
        lookingDirection = new Vector3(1, 0, 1);
        //_simpleAnimation.GetState("WAIT").speed = 0.7f; // アニメーションクリップの再生速度を0.7倍
        //_simpleAnimation.GetState("RUN").speed = 1.2f;  // アニメーションクリップの再生速度を1.2倍

        var playerInput = GetComponent<PlayerInput>();
        move = playerInput.actions["Move"];
        fire = playerInput.actions["ButtonA"];

        mainCamera = Camera.main;

    }
    // Update is called once per frame
    void Update()
    {
        // キー入力を取得
        var inputMoveAxis = move.ReadValue<Vector2>();
        
        inputDirection.z = inputMoveAxis.x;
        inputDirection.x = inputMoveAxis.y;
        // inputDirection.z = Input.GetAxis("Horizontal");
        // inputDirection.x = Input.GetAxis("Vertical");

        // メインカメラの向きによって入力を調整
        Vector3 cameraForward = Vector3.Scale(mainCamera.transform.forward, new Vector3(1, 0, 1)).normalized;
        inputDirection = cameraForward * inputDirection.x + mainCamera.transform.right * inputDirection.z;

        moveDirection = inputDirection * maxspeed;
        // いずれかの方向に入力がある場合。
        if (inputDirection != Vector3.zero)
        {
            // 回転!
            lookingDirection = inputDirection;
            // 走行アニメーションに遷移
            //_simpleAnimation.CrossFade("RUN", 0.2f);
        }
        else
        {
            // コントローラからの入力がなく、停止アニメーションに遷移。
            //_simpleAnimation.CrossFade("WAIT", 0.2f);
        }

        if (fire.ReadValue<float>() >0f )
        {
            // fireボタンの処理はここに記載します。
            Debug.Log("Fire");
        }

        // 方向転換処理(スムーズに回転するよう、若干ディレイをかけています)
        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(lookingDirection), (rotatespeed * Time.deltaTime));
        controller.Move(moveDirection * Time.deltaTime);
    }

}

2. カメラ制御

カメラ制御のスクリプトにAdd Compornentする Player Input には、Default Map "Camera"を選択します。

FollowCameraNIS.cs
// 2021.02 K1Togami Follow Camera with New Input System
// Base on CameraControllercsv2.1.cs in https://ruhrnuklear.de/fcc/

using UnityEngine;
using UnityEngine.InputSystem;

public class FollowCameraNIS : MonoBehaviour
{

   public Transform target; // ターゲット
   private float targetHeight = 5.0f;

   public float distance = 10.0f;
   public float horizontalAngle = 0.0f;
   public float verticalAngle = 10.0f;

   // カメラの移動限界
   public float verticalAngleMinLimit = -30f; // 見上げ限界角度
   public float verticalAngleMaxLimit = 80f; // 見下ろし限界角度 
   public float maxDistance = 20f; // 最大ズーム距離 
   public float minDistance = 0.6f; // 最小ズーム距離 

   //public Vector3 offset = Vector3.zero; // ターゲットとカメラのオフセット

   public float rotationSpeed = 180.0f; // 画面の横幅分カーソルを移動させたとき何度回転するか.
   public float rotationDampening = 0.5f; // 回転の減衰速度 (higher = faster) 
   public float zoomDampening = 5.0f; // Auto Zoom speed (Higher = faster) 

   // 衝突検知用
   public LayerMask collisionLayers = -1; // What the camera will collide with 
   public float offsetFromWall = 0.1f; // 衝突する物体からカメラを遠ざけるときのオフセット 


   private float currentDistance; // 現在のカメラ距離
   private float desiredDistance; // 目標とするカメラ距離
   private float correctedDistance; // 矯正後のカメラ距離

   private float CameraFollowDelay; // カメラ回転後のカメラフォローまでの遅延

   // ユーザによる回転の許可
   public bool allowMouseInput = true; // カメラの方向をマウスでコントロールすることを許可するか。
   
  InputAction CameraRotate;

   void Start()
   {
       Vector3 angles = transform.eulerAngles;
       horizontalAngle = angles.x;
       verticalAngle = angles.y;

       currentDistance = distance;
       desiredDistance = distance;
       correctedDistance = distance;

       CameraFollowDelay = 0f;

        var playerInput = GetComponent<PlayerInput>();
        CameraRotate = playerInput.actions["Camera"];
   }

   void LateUpdate()
   {
       // ターゲットが定義されていない場合は何もしない
       if (target == null)
           return;

       Vector3 vTargetOffset; // ターゲットからのオフセット

       if (GUIUtility.hotControl == 0)
       {
           // コントローラの右スティックによる回転を入れる場合は、ここに入れる。
           // 回転速度はお好みに調整してください。
           var inputMoveAxis = move.ReadValue<Vector2>();
           horizontalAngle += inputMoveAxis.x * rotationSpeed * 0.001f;
           verticalAngle += inputMoveAxis.y * rotationSpeed * 0.001f;
           if (inputMoveAxis != 0f ) CameraFollowDelay = 1.0f;


           if (CameraFollowDelay > 0f)
           {
               CameraFollowDelay -= Time.deltaTime;
           } else
           {
               // マウスによる回転が無効の場合、カメラ視線をターゲットの視線にじわじわあわせる
               RotateBehindTarget();
           }

           verticalAngle = ClampAngle(verticalAngle, verticalAngleMinLimit, verticalAngleMaxLimit);

           // カメラの向きを設定
           Quaternion rotation = Quaternion.Euler( verticalAngle,horizontalAngle, 0);

           // 希望のカメラ位置を計算
           vTargetOffset = new Vector3(0, -targetHeight, 0);
           Vector3 position = target.transform.position - (rotation * Vector3.forward * desiredDistance + vTargetOffset);

           // 高さを使ってユーザーが設定した真のターゲットの希望の登録点を使って衝突をチェック
           RaycastHit collisionHit;
           Vector3 trueTargetPosition = new Vector3(target.transform.position.x,
               target.transform.position.y + targetHeight, target.transform.position.z);

           // 衝突があった場合は、カメラ位置を補正し、補正後の距離を計算
           var isCorrected = false;
           if (Physics.Linecast(trueTargetPosition, position, out collisionHit, collisionLayers))
           {
               // 元の推定位置から衝突位置までの距離を計算し、衝突した物体から安全な「オフセット」距離を差し引く
               // このオフセットは、カメラがヒットした面の真上にいないよう逃がす距離
               correctedDistance = Vector3.Distance(trueTargetPosition, collisionHit.point) - offsetFromWall;
               isCorrected = true;
           }

           // スムージングのために、距離が補正されていないか、または補正された距離が現在の距離より
           // も大きい場合にのみ、距離を返す。
           currentDistance = !isCorrected || correctedDistance > currentDistance
               ? Mathf.Lerp(currentDistance, correctedDistance, Time.deltaTime * zoomDampening)
               : correctedDistance;

           // 限界を超えないようにする
           currentDistance = Mathf.Clamp(currentDistance, minDistance, maxDistance);

           // 新しい currentDistance に基づいて位置を再計算する。
           position = target.transform.position - (rotation * Vector3.forward * currentDistance + vTargetOffset);

           // 最後にカメラの回転と位置を設定。
           transform.rotation = rotation;
           transform.position = position;

       }

   }


   // カメラを背後にまわす。
   private void RotateBehindTarget()
   {
       float targetRotationAngle = target.transform.eulerAngles.y;
       float currentRotationAngle = transform.eulerAngles.y;
       horizontalAngle = Mathf.LerpAngle(currentRotationAngle, targetRotationAngle, rotationDampening * Time.deltaTime);
   }

   // 角度クリッピング
   private float ClampAngle(float angle, float min, float max)
   {
       if (angle < -360f)
           angle += 360f;
       if (angle > 360f)
           angle -= 360f;
       return Mathf.Clamp(angle, min, max);
   }

}