i-school - KeyCode をマッピングする移動方法の実装例
 移動用のキー入力に矢印キーを利用する場合、Input.GetAxis メソッドの活用が一般的ですが、KeyCode に合わせて if 文や switch 文を利用した分岐処理も実装可能です。
ただし分岐の数が多くなってしまう上に、キーの追加や変更の都度、分岐を書き直す必要があるため、全体的に冗長的なソースコードになり、保守が困難になります。

 そういった処理を軽減する方法として事前に Dictionary などを利用して、KeyCode とそれに紐づく処理を登録しておくアプローチの方法を紹介します。
こういった紐付けのことをマッピングと呼びます。



 ここでは実装例として、3D空間内の移動と回転について KeyCode をマッピングしています。
実際の移動と回転部分については、LINQ を利用して実装しています。



1.物理演算を利用しないケース


 Transform を利用した移動のケースです。


using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class KeyCodeMappingTransformController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 1.0f;  // 移動速度

    private Vector3 prevPosition;              // 前回の座標
  public Vector3 PrevPosition => prevPosition;      // プロパティ


    // KeyCode と移動方向をマッピング
    private Dictionary<KeyCode, Vector3> directionMappings = new ()
    {
        { KeyCode.UpArrow, Vector3.up },
        { KeyCode.DownArrow, Vector3.down },
        { KeyCode.LeftArrow, Vector3.left },
        { KeyCode.RightArrow, Vector3.right }
    };

    // KeyCode と回転方向をマッピング
    private Dictionary<KeyCode, Quaternion> rotationMappings = new ()
    {
        { KeyCode.UpArrow, Quaternion.Euler(0, 0, 0) },
        { KeyCode.DownArrow, Quaternion.Euler(0, 0, 180) },
        { KeyCode.LeftArrow, Quaternion.Euler(0, 0, 90) },
        { KeyCode.RightArrow, Quaternion.Euler(0, 0, 270) }
    };


    void Update()
    {
        Move();
    }

    /// <summary>
    /// 移動
    /// </summary>
    private void Move()
    {
        Vector3 position = transform.position;

	// 移動前の位置を保存
        prevPosition = position;

        // キー入力による移動方向の確認
        KeyCode keyCode = directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();

        if (keyCode != KeyCode.None)
        {
      // 移動
            position += directionMappings[keyCode] * moveSpeed;
            transform.position = position;

      // 回転
            Turn(keyCode);
        }
    }

    /// <summary>
    /// 回転
    /// </summary>
    /// <param name="keyCode"></param>
    private void Turn(KeyCode keyCode)
    {
        if (rotationMappings.TryGetValue(keyCode, out Quaternion rotation))
        {
            // 回転
            transform.rotation = rotation;
        }
    }
}


<マッピングと LINQ を利用した処理>


 下記の処理の部分において、KeyCode とそれに対応する方向、および回転角度を紐づけしています。

    // KeyCode と移動方向をマッピング
    private Dictionary<KeyCode, Vector3> directionMappings = new ()
    {
        { KeyCode.UpArrow, Vector3.up },
        { KeyCode.DownArrow, Vector3.down },
        { KeyCode.LeftArrow, Vector3.left },
        { KeyCode.RightArrow, Vector3.right }
    };

    // KeyCode と回転方向をマッピング
    private Dictionary<KeyCode, Quaternion> rotationMappings = new ()
    {
        { KeyCode.UpArrow, Quaternion.Euler(0, 0, 0) },
        { KeyCode.DownArrow, Quaternion.Euler(0, 0, 180) },
        { KeyCode.LeftArrow, Quaternion.Euler(0, 0, 90) },
        { KeyCode.RightArrow, Quaternion.Euler(0, 0, 270) }
    };

 Dictionary では Key を指定することで Value を参照することが可能です。
今回の場合では、Key を KeyCode、Value を Vector3 として1セットに設定しています。
それを各方向分用意しておくことにより、矢印キーのキー入力と方向とを1対1で紐づけています。

 回転の場合も同様です。



 このようにして事前に情報の紐付けしておくことをマッピングといいます。

 実際には以下の部分で利用されています。
こちらの処理には LINQ を利用し、省略記法によって処理を実装しています。

        // キー入力による移動方向の確認
        KeyCode keyCode = directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();

 Where メソッドの条件式として Input.GetKey(kvp.Key) メソッドを実行しています。
この部分が Dictionary としてマッピングされている KeyCode が参照されています。

 マッピング済のいずれかの KeyCode に該当した場合、 Select メソッドを利用して KeyCode が含まれている Dicitionary の Key を取得します。
最後に SingleOrDefault メソッドを利用して、取得した情報を1つ取り出し、最初に = の左辺に用意した keyCode 変数に代入することで、今回キー入力された KeyCode を特定しています。



 その後、その下に続く処理で KeyCode をハンドリングし、
マッピング されている KeyCode を取得できている場合のみ、移動の処理と回転の処理につながるようになっています。

    if (keyCode != KeyCode.None)
        {
      // 移動
            position += directionMappings[keyCode] * moveSpeed;
            transform.position = position;

      // 回転
            Turn(keyCode);
        }



 移動の処理には Dictionary の Value の情報を利用しています。

// 移動
position += directionMappings[keyCode] * moveSpeed;

 directionMappings[keyCode] のように Dictionary を Key で指定することにより、
その Key とセットになっている Value を取り出すことが可能です。

 仮に Key が KeyCode.UpArrow である場合、 Vector3.up の値が Value として取り出され、
先ほどの処理に適用されています。

 適用して次のように読み変えます。


// 移動(KeyCode.UpArrow のとき)
position += Vector3.up * moveSpeed;

 実際にプログラムは、このような値として処理しています。

 変数が出てきたときには、常にその値を見て、脳内で置き換えていくように心がけていきましょう。


<Linq の機能 〜Where メソッド、elect メソッド、SingleOrDefault メソッド〜>


 Linq(リンク)とは、コレクション(Dictionary や List など)の要素を操作して、検索したり集計する処理を簡潔に記述することができるライブラリ(複数の機能をまとめたもの)です。

 Linqを使用するためにはusing の宣言が必要になります。今回作成したメソッド内に登場している処理ですので、復習して読み解けるようにしていきましょう。

using System.Linq;



 Linqを記述する際にはラムダ式の記述を用います。ラムダ式についてはこちらをご確認ください。
SamuraiBlog様
【C#入門】LINQの使い方総まとめ(Select、Where、GroupByなど)
https://www.sejuku.net/blog/56519

 Linqには多くの機能がありますが今回利用している機能についてまとめておきます。
そのほかの機能については記事がたくさんありますが、こちらのサイトも参考になります。
地平線に行く様
LINQの拡張メソッド一覧と、ほぼ全部のサンプルを作ってみました。
https://yujisoftware.hatenablog.com/entry/20111031...


1.Where メソッド

 配列や List などのコレクションに対してフィルタリング処理を行い、指定した条件を満たす要素のみを取得することができる機能です。
引数にはフィルタリングを行う条件式を指定します。


        // キー入力による移動方向の確認
        KeyCode keyCode = directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();

 今回の実装例では Where メソッドの条件式に kvp => Input.GetKey(kvp.Key) を指定しています。
これは Dicitionary 型のコレクションである directionMappings 変数の要素を1つずつフィルタリングし、上記の条件に合う要素のみを取得しています。
kvp 変数には directionMappings 変数の要素である Dicitionary<KeyCode, Vector3> 型の情報が1つずつ順番に代入され、
Input.GetKey(kvp.Key) メソッドの実行結果である戻り値の情報と、Dicitionary<KeyCode, Vector3> 型 の Key である KeyCode とを照合しています。

 この処理は IEnumerable<T> の戻り値を持つため、メソッドチェーンを行うことで、この処理の結果を次の処理につなげていくことが可能です。
今回であれば、フィルタリングして照合の条件を満たした Dicitionary<KeyCode, Vector3> 型の情報を取得することが出来ます。


参考サイト
MicroSoft
Enumerable.Where メソッド
https://docs.microsoft.com/ja-jp/dotnet/api/system...
陰干し中のゲーム開発メモ
【C#,LINQ】Where〜配列やリストを指定した条件でフィルタリングしたいとき〜
https://www.urablog.xyz/entry/2018/06/27/070000
.NET Column 様
【LINQのメソッド紹介その3】Whereで条件に合うデータを取得する
https://www.fenet.jp/dotnet/column/language/1458/


2.Select メソッド

 射影処理と呼ばれる機能です。射影とはデータベース用の専門用語で、テーブル(実データ)から特定の列のデータのみを取り出すことを言います。

 Select メソッドでは引数にした条件を元に、照合できたデータのみを取り出す操作を行います。
このメソッドの戻り値は IEnumerable<TResult> であり、その情報を取得して利用したい場合には、
メソッドチェーンを利用して、ToList メソッドや ToArray メソッドを利用して複数の情報を List や配列として扱える状態か
SingleOrDefault メソッドや FirstOrDefault メソッドを利用して1つだけ扱える状態にします。


        // キー入力による移動方向の確認
        KeyCode keyCode = directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();

 こちらの実装例では、Where メソッドの処理の結果に対して、Select メソッドを利用しています。
.Select(kvp => kvp.Key) の部分になります。

 Where メソッドの戻り値の結果の各要素を kvp 変数に代入し、その中から Key 変数の情報を取り出します。
今回の場合、Where メソッドの戻り値は1つだけですので、その戻り値に対して Key 変数の情報を取り出しています。


参考サイト
MicroSoft
Enumerable.Select メソッド
https://docs.microsoft.com/ja-jp/dotnet/api/system...
.NET Column 様
【LINQのSelectメソッドの書き方3選|LINQについてなどを紹介
https://www.fenet.jp/dotnet/column/language/1454/
Qiita @t_takahari 様
LINQのそのForEach、実はSelectで書き換えられるかも
https://qiita.com/t_takahari/items/6dc72f48b1ebdfe...


3.SingleOrDefault メソッド

 SingleOrDefault() は、IEnumerable<T> シーケンスから単一の要素を返す LINQ メソッドです。
IEnumerable<T> が空であるか、複数の要素を持っている場合や要素が1つも見つからない場合には null を戻します
(例外エラーは出ませんので、正しく動作します。)


        // 移動方向の確認
        KeyCode keyCode = directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();

 今回の場合、directionMappings から Input.GetKey() に合致する KeyCode を取得する目的で使われています。
Select(kvp => kvp.Key) で KeyCode が1つの IEnumerable<KeyCode> に絞り込まれているので、
SingleOrDefault() を使うことで直接単一の KeyCode を取得できます。

 また今回は利用していませんが、SingleOrDefault() は引数を指定することもでき、
その場合には、複数の要素から条件に合致する要素を1つだけ取り出すことが出来ます。


参考サイト
MicroSoft
Enumerable.SingleOrDefault メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/syste...
Dawn Coding Academy 様
【C#入門】SingleOrDefaultの使い方を解説【LINQ】
https://pa-n-da-blog.com/c_linq_singleordefault/



 なお、上記の3つの LINQ を利用せずに書いた場合は、以下のような処理になります。


  // 移動方向の確認
    KeyCode pressedKeyCode = KeyCode.None;

    // 全てのキーと対応する方向を確認
    foreach (var keyMapping in directionMappings)
    {
        // キーが押されているかを確認
        if (Input.GetKey(keyMapping.Key))
        {
            // キーが押されている場合、そのキーコードを記録
            pressedKeyCode = keyMapping.Key;

            // 一つ見つかったらループを終了
            break;
        }
    }

 LINQ の処理がどのように動いているのか確認できます。
同時に、どれだけの処理が省略されているかも分かります。


2.物理演算を利用するケース


 Rigidbody を利用した移動のケースです。
キー入力と回転処理は Update メソッド、移動処理は FixedUpdate メソッドと分けて活用します。


using System.Collections.Generic;
using UnityEngine;
using System.Linq;

[RequireComponent(typeof(Rigidbody))]
public class KeyCodeMappingRigidbodyController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 1.0f;  // 移動速度

    private Rigidbody rb;
    private KeyCode currentKeyCode = KeyCode.None;   // 現在押されているキー

    // KeyCode と移動方向をマッピング
    private Dictionary<KeyCode, Vector3> directionMappings = new()
    {
        { KeyCode.UpArrow, Vector3.up },
        { KeyCode.DownArrow, Vector3.down },
        { KeyCode.LeftArrow, Vector3.left },
        { KeyCode.RightArrow, Vector3.right }
    };

    // KeyCode と回転方向をマッピング
    private Dictionary<KeyCode, Quaternion> rotationMappings = new()
    {
        { KeyCode.UpArrow, Quaternion.Euler(0, 0, 0) },
        { KeyCode.DownArrow, Quaternion.Euler(0, 0, 180) },
        { KeyCode.LeftArrow, Quaternion.Euler(0, 0, 90) },
        { KeyCode.RightArrow, Quaternion.Euler(0, 0, 270) }
    };

    private void Start()
  {
      if (TryGetComponent(out rb))
      {
          rb.freezeRotation = true; // 回転を物理エンジンに任せないようにする
      }
  }

    private void Update()
    {
        // キー入力による移動方向の確認
        currentKeyCode = GetPressedKeyCode();

      if (currentKeyCode == KeyCode.None)
      {
          return;
      }

        // 回転は物理演算ではないため、Updateで呼び出す
        Turn(currentKeyCode);
    }

    /// <summary>
    /// キー入力による移動方向の確認
    /// </summary>
    /// <returns></returns>
    private KeyCode GetPressedKeyCode()
    {
        return directionMappings
            .Where(kvp => Input.GetKey(kvp.Key))
            .Select(kvp => kvp.Key)
            .SingleOrDefault();
    }

    /// <summary>
    /// 回転
    /// </summary>
    /// <param name="keyCode"></param>
    private void Turn(KeyCode keyCode)
    {
        if (rotationMappings.TryGetValue(keyCode, out Quaternion rotation))
        {
            // 回転
            transform.rotation = rotation;
        }
    }

  private void FixedUpdate()
  {
      if (currentKeyCode == KeyCode.None)
      {
          return;
      }

      // 物理演算に必要な情報を引数として渡す
      Move(currentKeyCode);
  }

  /// <summary>
  /// 移動
  /// </summary>
  private void Move(KeyCode keyCode)
  {
      // 移動
      Vector3 velocity = directionMappings[keyCode] * moveSpeed;
      rb.velocity = velocity;
  }
}

 早期 return 処理により、if 文によるネストを無くして可読性の高い処理を実現しています。

    /// <summary>
    /// 移動
    /// </summary>
    private void Move(KeyCode keyCode)
    {
        if (keyCode != KeyCode.None)
        {
            // 移動
            Vector3 velocity = directionMappings[keyCode] * moveSpeed;
            rb.velocity = velocity;
        }
    }

 このような書式ではなくて、事前に移動可能であることを精査した上、Move メソッドでは実行命令だけを記述しています。


  private void FixedUpdate()
  {
      if (currentKeyCode == KeyCode.None)  // ← Move できるかチェックし、できないなら、そもそも Move しない
      {
          return;
      }

      // 物理演算に必要な情報を引数として渡す
      Move(currentKeyCode);
  }

  /// <summary>
  /// 移動
  /// </summary>
  private void Move(KeyCode keyCode)
  {
      // 移動
      Vector3 velocity = directionMappings[keyCode] * moveSpeed;
      rb.velocity = velocity;
  }

 ネットの記事や、ChatGPT の回答例の多くは if 文によるネスト処理が多くなりがちです。

 早期 return を活用し、「if ならば 〜 する」という書式だけではなく、「if ならば 〜 しない」という書式の書き方と活用法を覚えていきましょう。


3.Input.GetAxis メソッドを利用した物理演算による移動処理の例


 上記の2つ以外に利用されている、一般的な実装例も提示しておきます。


using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class AxisMappingRigidbodyController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 1.0f;  // 移動速度

    private Rigidbody rb;
    private float horizontalInput;
    private float verticalInput;

    private void Start()
    {
        if (TryGetComponent(out rb))
        {
            rb.freezeRotation = true; // 回転を物理エンジンに任せないようにする
        }
    }

    private void Update()
    {
        // 水平方向と垂直方向の入力を取得
        horizontalInput = Input.GetAxis("Horizontal");
        verticalInput = Input.GetAxis("Vertical");

        // 回転は物理演算ではないため、Updateで呼び出す
        Turn();
    }

    private void FixedUpdate()
    {
        // 物理演算に必要な情報を引数として渡す
        Move(horizontalInput, verticalInput);
    }

    /// <summary>
    /// 移動
    /// </summary>
    private void Move(float horizontal, float vertical)
    {
        // 移動
        Vector3 movement = new Vector3(horizontal, 0, vertical) * moveSpeed;
        rb.velocity = movement;
    }

    /// <summary>
    /// 回転
    /// </summary>
    private void Turn()
    {
        // 方向ベクトルを求め、その方向を向くように回転
        if (horizontalInput != 0 || verticalInput != 0)
        {
            Vector3 direction = new Vector3(horizontalInput, 0, verticalInput);
            Quaternion toRotation = Quaternion.LookRotation(direction, Vector3.up);
            transform.rotation = toRotation;
        }
    }
}