Unityで簡単なTPS操作をサクッと実装【キーボードWASD】

# はじめに
初心者にとってUnityで**キャラクターを操作する方法**は結構複雑だなぁと感じたので、今回は**簡単な例をスクリプトとして**まとめました。InputSystemの登場によって入力とアクションを疎結合にしやすくなるなど、便利な機能が増える一方、**その場限りでちょっと試したい程度**のときの実装コストは高くなりがちな印象です。

### 完成イメージ

|WASD-移動|
|---|
|<img width="400" alt="wasd-image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/c3e2b51c-e404-fa5a-d5f3-3a31a8e253d6.gif">|

|IJKL-視点|Space-ジャンプ|
|---|---|
|<img width="250" alt="wasd-image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/d3f6d6f7-7c59-471b-4522-5da8b11d56c9.gif">|<img width="250" alt="wasd-image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/80807f96-c5a8-6885-5b7f-83a136c36c46.gif">|

# 結論
**以下のファイル(C#)をつくって、動かしたいキャラクターに貼り付ければ完成**。
実際のコードは長くなるので、この記事の一番下の方に置いておきます。DefaultPlayer.cs

```DefaultPlayer.cs
public class DefaultPlayer
{...}
```

# 準備と前提
キーボードからの入力を受け付ける手段として、新しい機能である**InputSystem**を採用しています。
これは**Unity2019.4以降**を主な対象とした、従来の**InputManager等(UnityEngine.Input)**の代替手段です。
2021年1月現在では**デフォルトでは有効になっていないプロジェクトファイルが多い**ため、先にこれを有効にしておきます。

[1.Window - Package Manager] > [2.Unity Resistry] > [3."inputsystem"と検索] > [4.install]
![inputsystem.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/a8ee9348-f745-75c4-b6b9-1685d5bf16d1.png)
インストールしたあと、誘導に従っていくことでUnityが再起動する場合があります。

# (応用) キーによる操作を追加したい場合
移動速度やジャンプの高さを変更したり、任意のキーにオリジナルな操作を割り当てることができます。 
先述の**DefaultPlayer.cs**をプロジェクト内に置いたまま、**CustomPlayer.cs**を作りましょう。
DefaultPlayerではなくCustomPlayerの方をキャラクターに貼り付けます。

 - onStart() -- 押下開始
 - onMiddle() -- 押下中
 - onEnd() -- 押下終了
 
メソッドの中で**player**インスタンスに対して操作ができます。
操作できること一覧は **IPlayer**インターフェースの定義元を参照してください。

```CustomPlayerテンプレ.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using static UnityEngine.InputSystem.Keyboard;

// *****************************************************************************
// CustomPlayer.cs
// *****************************************************************************
// カスタム設定のプレイヤー 例
public class CustomPlayer : Player
{
    // 前に動くスピード
    override public float moveSpeed ()
    {
        return 2.0f;
    }
    // プレイヤーの高さ
    override public float height () {
        return 2.0f;
    }

    // デフォルト値で良いときは省略可
    // ジャンプの高さ
    // override public float jumpHeight ()
    // 重力の大きさ
    // virtual public float gravity ()
    // カメラ回転の速さ
    // virtual public float lookSpeed ()

    // 入力に使うキーを列挙
    override public Input[] keyInputs ()
    {
        return new Input[]
        {
            ...
            ここに使用したいカスタムキーを追加していく
            (例)
            new T (), // T
            new E (), // E
            new S (), // S
            new Space () // スペース
            ...
        };
    }
}

// *****************************************************************************
// T.cs ファイルに分ても良い
// *****************************************************************************
// キーボードの「T」を押して入力テストをする例
public class T : Input
{
    public KeyControl key ()
    {
        return current.tKey; // 入力に使うキーを指定: キーボードの「T」は"tKey"
    }

    public void onStart (IPlayer player)
    {
        Debug.Log ("テスト 開始"); // キーを押し始めた時: ログ出力
    }

    public void onMiddle (IPlayer player, float deltaTime)
    {
        Debug.Log ("テスト 実施中"); // キーを押している間: ログ出力
    }

    public void onEnd (IPlayer player)
    {
        Debug.Log ("テスト 終了"); // キーを押し終わった時: ログ出力
    }
}

...
〜(省略) E, S, スペースキーのクラス〜
...

```

# 実装の説明
さて、ここで少しだけ実装に触れておきます。キーワードを箇条書き。

 - **CharacterController**
   - Unityでキャラクターを移動操作する方法は大きく分けて3つ
     - 1. transformを変更 
     - 2. RigidBodyで物理演算
     - 3. CharacterControllerを使う
   - 3を採用、**物理演算を無視することで処理自体を軽くし、シンプルでかつ軽快なキャラクター操作ができる**反面、氷で滑って移動(摩擦)や壁に押し出される(外部からの応力)ことは自動ではできなくなる。重力もコードによって再現。
 - **ポーリング**
   - 一般的な入力処理の分類
     - 1. イベント通知型
     - 2. ポーリング型
   - 例えばボタンが押された時に、ボタンさんが「押されたよ」とわざわざ教えに来てくれるのが1、ボタンが押されていないかどうかをこちらから定期的に確認しにいくのが2。
   - イベントの方が無駄な処理は減らしやすいが、今回はキャラクターを毎フレーム移動させるなどの処理をどのみち行うため、**フレームごとにキー押下を確認する 2 で実装。**特に重い処理はないので誤差の範囲内。
 - **アニメーション**
   - キャラクターの体が固まったまま動いてしまうのをなんとかする
   - 今後コードを追記予定
   - 別の記事にするかも 
 - **マウス入力**
   - 複雑化するので今回は見送り
   - 外付けコントローラーも同様 

# DefaultPlayer.cs
最後にコピペ用のスクリプトを貼っておきます。
```なお筆者は、趣味でC#コードを書くときは一般的な命名ルールやC#特有機能をガン無視すると誓っておりますので、気になる方は大文字小文字や動詞名詞、コメント記法を調整してください。```


```DefaultPlayer.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using static UnityEngine.InputSystem.Keyboard;

//***************************************************************
// DefaultPlayer.cs
//***************************************************************
// デフォルト設定のプレイヤー
public class DefaultPlayer : Player
{ }

//***************************************************************
// IPlayer.cs
//***************************************************************
// プレイヤーに対する操作一覧
public interface IPlayer
{
    // 足が地面についているかどうかを取得
    public bool isGrounded ();
    // どちらを見ているかを取得
    public To direction ();
    // to方向 に deltaTime秒間 移動する
    public void move (To to, float deltaTime);
    // ジャンプ
    public void jump ();
    // to方向 に 体を向ける
    public void turn (To to);
    // 上方を見る deltaTime秒間
    public void lookU (float deltaTime);
    // 左方を見る deltaTime秒間
    public void lookL (float deltaTime);
    // 下方を見る deltaTime秒間
    public void lookD (float deltaTime);
    // 右方を見る deltaTime秒間
    public void lookR (float deltaTime);
}

public enum To
{
    // Forward
    F,
    // Left
    L,
    // Right
    R,
    // Back
    B
}

//***************************************************************
// Input.cs
//***************************************************************
public interface Input
{
    // key for input
    public KeyControl key ();
    // start of pushing
    public void onStart (IPlayer player);
    // pushing
    public void onMiddle (IPlayer player, float deltaTime);
    // end of pushing
    public void onEnd (IPlayer player);
}

//***************************************************************
// Player.cs
//***************************************************************
// abstract Player
public abstract partial class Player : MonoBehaviour
{
    // CACHE
    CharacterController controller;
    Input[] inputs;
    float ySpeed = 0;
    Camera mainCamera;
    Transform fAnchorTransform;
    Animator animator; // ToDo: - enable to access animator

    // SETTING
    virtual public float moveSpeed ()
    {
        return 5.0f;
    }
    virtual public float jumpHeight ()
    {
        return 1.0f;
    }
    virtual public float gravity ()
    {
        return 9.8f;
    }
    virtual public float lookSpeed ()
    {
        return 50.0f;
    }
    virtual public float height ()
    {
        return 1.0f;
    }

    // keys to input
    virtual public Input[] keyInputs ()
    {
        return new Input[]
        {
            new DefaultW (),
                new DefaultA (),
                new DefaultS (),
                new DefaultD (),
                new DefaultSpace (),
                new DefaultI (),
                new DefaultL (),
                new DefaultJ (),
                new DefaultK ()
        };
    }
    // anchor object for definition the forward derection
    virtual public GameObject forwardAnchor ()
    {
        return Camera.main.gameObject;
    }

    /// MonoBehaviour Start()
    void Start ()
    {
        this.controller = this.gameObject.AddComponent<CharacterController> ();
        this.controller.center = new Vector3 (0, this.height () / 2, 0);
        this.controller.height = this.height ();
        this.inputs = this.keyInputs ();
        this.fAnchorTransform = this.forwardAnchor ().transform;
        this.mainCamera = Camera.main;
        this.animator = this.GetComponent<Animator> ();
    }

    /// MonoBehaviour Update()
    void Update ()
    {

        // refresh ySpeed
        if (this.isGrounded () && this.ySpeed < 0)
        {
            this.ySpeed = 0;
        }
        else
        {
            this.ySpeed -= this.gravity () * Time.deltaTime;
        }

        // use gravity
        var v3 = new Vector3 (0, this.ySpeed, 0);
        this.controller.Move (v3 * Time.deltaTime);

        // check input
        foreach (var input in this.inputs)
        {
            if (input.key ().wasPressedThisFrame)
            {
                input.onStart (this);
            }
            if (input.key ().isPressed)
            {
                input.onMiddle (this, Time.deltaTime);
            }
            if (input.key ().wasReleasedThisFrame)
            {
                input.onEnd (this);
            }
        }
    }
}

//***************************************************************
// Player+IPlayer.cs
//***************************************************************
public abstract partial class Player : IPlayer
{
    public bool isGrounded ()
    {
        return this.controller.isGrounded;
    }
    public To direction ()
    {
        var form = this.transform;
        var diffF = (-(this.fAnchorTransform.forward) - form.forward).sqrMagnitude;
        var diffL = (this.fAnchorTransform.right - form.forward).sqrMagnitude;
        var diffB = (this.fAnchorTransform.forward - form.forward).sqrMagnitude;
        var diffR = (-(this.fAnchorTransform.right) - form.forward).sqrMagnitude;
        var minDiff = diffF;
        var minTo = To.F;
        if (diffL < minDiff)
        {
            minDiff = diffL;
            minTo = To.L;
        }
        if (diffB < minDiff)
        {
            minDiff = diffB;
            minTo = To.B;
        }
        if (diffR < minDiff)
        {
            minDiff = diffR;
            minTo = To.R;
        }
        return minTo;
    }
    public void move (To to, float dTime)
    {
        var direction = Vector3.zero;
        switch (to)
        {
            case To.F:
                direction = this.fAnchorTransform.forward;
                break;
            case To.L:
                direction = -(this.fAnchorTransform.right);
                break;
            case To.B:
                direction = -(this.fAnchorTransform.forward);
                break;
            case To.R:
                direction = this.fAnchorTransform.right;
                break;
        }
        var v3 = direction * this.moveSpeed ();
        this.controller.Move (v3 * dTime);
    }
    public void jump ()
    {
        if (this.isGrounded ())
        {
            this.ySpeed += Mathf.Sqrt (this.jumpHeight () * 3.0f * this.gravity ());
        }
    }
    public void turn (To to)
    {
        var direction = Vector3.zero;
        switch (to)
        {
            case To.F:
                direction = this.fAnchorTransform.forward;
                break;
            case To.L:
                direction = -(this.fAnchorTransform.right);
                break;
            case To.B:
                direction = -(this.fAnchorTransform.forward);
                break;
            case To.R:
                direction = this.fAnchorTransform.right;
                break;
        }
        var v3 = new Vector3 (direction.x, 0, direction.z);

        this.mainCamera.transform.parent = null;
        this.transform.forward = -(v3);
        this.mainCamera.transform.parent = this.transform;
    }
    public void lookL (float deltaTime)
    {
        this.mainCamera.transform.RotateAround (this.transform.position, -(Vector3.up), this.lookSpeed () * deltaTime);
    }
    public void lookR (float deltaTime)
    {
        this.mainCamera.transform.RotateAround (this.transform.position, Vector3.up, this.lookSpeed () * deltaTime);
    }
    public void lookU (float deltaTime)
    {
        this.mainCamera.transform.RotateAround (this.transform.position, -(this.fAnchorTransform.right), this.lookSpeed () * deltaTime);
    }
    public void lookD (float deltaTime)
    {
        this.mainCamera.transform.RotateAround (this.transform.position, this.fAnchorTransform.right, this.lookSpeed () * deltaTime);
    }
}

//***************************************************************
// DefaultW.cs
//***************************************************************
// W (default)
public class DefaultW : Input
{
    public KeyControl key ()
    {
        return current.wKey;
    }
    public void onStart (IPlayer player)
    {
        player.turn (To.F);
    }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.move (To.F, deltaTime);
    }
    public void onEnd (IPlayer player)
    {

    }
}

//***************************************************************
// DefaultA.cs
//***************************************************************
// A (default)
public class DefaultA : Input
{
    public KeyControl key ()
    {
        return current.aKey;
    }
    public void onStart (IPlayer player)
    {
        player.turn (To.L);
    }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.move (To.L, deltaTime);
    }
    public void onEnd (IPlayer player)
    {

    }
}

//***************************************************************
// DefaultS.cs
//***************************************************************
// S (default)
public class DefaultS : Input
{
    public KeyControl key ()
    {
        return current.sKey;
    }
    public void onStart (IPlayer player)
    {
        player.turn (To.B);
    }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.move (To.B, deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultD.cs
//***************************************************************
// D (default)
public class DefaultD : Input
{
    public KeyControl key ()
    {
        return current.dKey;
    }
    public void onStart (IPlayer player)
    {
        player.turn (To.R);
    }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.move (To.R, deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultSpace.cs
//***************************************************************
// Space (default)
public class DefaultSpace : Input
{
    public KeyControl key ()
    {
        return current.spaceKey;
    }
    public void onStart (IPlayer player)
    {
        player.jump ();
    }
    public void onMiddle (IPlayer player, float deltaTime)
    { }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultI.cs
//***************************************************************
// I (default)
public class DefaultI : Input
{
    public KeyControl key ()
    {
        return current.iKey;
    }
    public void onStart (IPlayer player)
    { }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.lookU (deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultK.cs
//***************************************************************
// K (default)
public class DefaultK : Input
{
    public KeyControl key ()
    {
        return current.kKey;
    }
    public void onStart (IPlayer player)
    { }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.lookD (deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultL.cs
//***************************************************************
// L (default)
public class DefaultL : Input
{
    public KeyControl key ()
    {
        return current.lKey;
    }
    public void onStart (IPlayer player)
    { }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.lookR (deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

//***************************************************************
// DefaultJ.cs
//***************************************************************
// J (default)
public class DefaultJ : Input
{
    public KeyControl key ()
    {
        return current.jKey;
    }
    public void onStart (IPlayer player)
    { }
    public void onMiddle (IPlayer player, float deltaTime)
    {
        player.lookL (deltaTime);
    }
    public void onEnd (IPlayer player)
    { }
}

```

以上です。
最後まで読んでくれてありがとう！