【Unity】矢印キーで2Dプレイヤーを1マスずつ移動

当サイトで紹介する商品・サービス等の外部リンクは、アフィリエイト広告を含む場合があります。
スポンサーリンク
本記事を読むと以下の実行ができます

矢印キーで2Dプレイヤーをタイル1マスずつ動かし、長押しでの連続移動と、進む方向に合わせた向きの画像の切り替えができるようになります。

昔のRPGのように、キャラクターがマス目にそってカクカクと動く移動するゲームは3Dゲームよりも制作の負担もなく現在もレトロゲームや同人ゲームとして人気が強いです。

Tilemapでマップを作ったあと、「プレイヤーを1マスずつ動かすにはどうすればいいの?」とつまずく人も多いはずです。

本記事では、2dキャラクターの実装として、Input.GetKeyを使ったキー入力から1マスずつの移動・向きの切り替えまでを解説します。キーを押しっぱなしにしたときの連続移動や、2つのキーを同時に押したときの優先順位など、実際に遊ぶと気になる動きにも対応しています。

3Dのキャラクター移動を解説しています。
本記事は次の人におすすめ
  • 矢印キーで2Dキャラクターを1マスずつ動かしたい人
  • Tilemapで作ったマップの上をプレイヤーに歩かせたい人
  • unity キー 入力(GetKey・GetKeyDown)の違いと使い分けを知りたい人
  • キーを押しっぱなしにしたときに、ちょうど良い速さで連続移動させたい人
  • 進む方向に合わせてキャラクターの画像を上下左右に切り替えたい人
スポンサーリンク

Input.GetKey(キー入力)

Input.GetKeyは、キーボードのキーが押されているかどうかを調べる関数です。矢印キーでキャラクターを動かす、スペースキーでジャンプする、Escキーでメニューを開くなど、キーボードで操作するゲームのほとんどで使われます。

使い方は、調べたいキーをKeyCodeで指定するだけです。矢印キーはそれぞれ次のKeyCodeに対応しています。

キーKeycode
↑ 上KeyCode.UpArrow
↓ 下KeyCode.DownArrow
→ 右KeyCode.RightArrow
← 左KeyCode.LeftArrow
if (Input.GetKey(KeyCode.RightArrow))
{
    // →キーが押されている間、毎フレーム実行される
}

キー入力の処理は、毎フレーム呼ばれるUpdate()の中に書きます。

参考:Input.GetKey – Unity スクリプトリファレンス

GetKey・GetKeyDown・GetKeyUpの違い

キー入力を調べる関数は3種類あり、trueを返すタイミングが違います。

関数trueになるタイミング主な用途
Input.GetKeyキーを押している間ずっと(毎フレーム)押しっぱなしで動き続ける移動・ダッシュ
Input.GetKeyDownキーを押した瞬間の1フレームだけジャンプ・決定・1回だけの移動
Input.GetKeyUpキーを離した瞬間の1フレームだけため攻撃の発射・長押しの終了

1マスずつ動かしたい場合、GetKeyだけを使うと毎フレーム1マス進んでしまい、一瞬で画面の端まで飛んでいってしまいます。反対にGetKeyDownだけを使うと、押すたびに1マスしか進まず、長い距離を歩くときにキーを何度も叩くことになります。

そこで本記事のコードでは、2つを組み合わせています。

  • GetKeyDown: どのキーが「新しく押されたか」を調べる
  • GetKey: そのキーが「まだ押され続けているか」を調べ、一定の間隔で連続移動する

event Action(C#のイベント)で処理をつなぐ

eventは、「何かが起きたこと」を他のスクリプトに知らせるためのC#の仕組みです。Actionと組み合わせると、知らせるときに値(移動方向など)も一緒に渡せます。

例えば「キーが押された」ことを入力のスクリプトが知らせ、それを受け取った移動のスクリプトがプレイヤーを動かす、という使い方をします。

// 知らせる側
public event Action<Vector3> MoveRequested;   // Vector3(方向)を一緒に渡すイベント
MoveRequested?.Invoke(Vector3.up);            // 登録されている処理をまとめて呼ぶ

// 受け取る側
input.MoveRequested += OnMoveRequested;       // 登録
input.MoveRequested -= OnMoveRequested;       // 解除
書き方意味
event Action値を渡さないイベント
event Action<T>値を1つ渡すイベント
event Action<T1, T2>値を2つ渡すイベント
?.Invoke(…)登録している処理があれば呼び出す(誰も登録していなければ何もしない)

イベントを使うと、入力・移動・画像の切り替えを別々のスクリプトに分けられます。それぞれのスクリプトはお互いの中身を知らなくてよいため、後から「NPCにも同じ移動を使いたい」「入力をゲームパッドに変えたい」となったときに、一部だけを差し替えられます。

なお、UnityにはInspectorから登録できるUnityEventもあります。本記事ではコードの中だけで完結させるため、C#のeventを使っています。

参考:イベント – C# プログラミング ガイド(Microsoft Learn)

SpriteRenderer.spriteで画像を差し替える

SpriteRendererは、2Dの画像(スプライト)を画面に表示するコンポーネントです。
spriteプロパティに別のスプライトを代入すると、表示している画像をその場で差し替えられます。

playerRenderer.sprite = spriteUp;   // 上向きの画像に切り替える

キャラクターの向きを変える方法には、ほかにAnimatorでアニメーションを切り替える方法もあります。

歩きのアニメーションまで作り込みたい場合はAnimatorが向いていますが、上下左右の4枚の画像を切り替えるだけなら、spriteを差し替える方法の方が設定が少なく済みます。

方法向いている場面
SpriteRenderer.spriteの差し替え上下左右の静止画を切り替えるだけのとき(本記事)
Animator(Blend Tree)方向ごとに歩きのアニメーションを付けたいとき
オブジェクトが表示されないエラーについて紹介しています。

参考:SpriteRenderer.sprite – Unity スクリプトリファレンス

矢印キーで1マスずつ動く2Dプレイヤー移動

ここまでに説明したInput.GetKey・event Action・SpriteRenderer.spriteを組み合わせて、Tilemapの上を1マスずつ歩く2Dプレイヤーを作ります。実装する動きは次のとおりです。

  • 1マスずつ移動: 矢印キーを押すと、上下左右に1マス分だけ移動する
  • 長押しで連続移動: キーを押しっぱなしにすると、少し待ってから一定の間隔で進み続ける
  • 同時押しの優先: 2つのキーを押したときは、後から押したキーの方向へ進む(斜めには進まない)
  • 向きの切り替え: 進む方向に合わせて、キャラクターの画像を上下左右に切り替える
  • 止まったときの表示: キーを離したら「待機画像」か「最後に向いた方向」のどちらかを表示する

処理は役割ごとに5つのスクリプトに分けています。

スクリプト役割
PlayerInput.cs矢印キーの入力から動く方向を決める
GridMover.cs対象を1マス動かし「動こうとした」「動いた」と知らせる
PlayerDirectionSprite.cs向きに合わせて画像を差し替える
PlayerSpawner.csゲーム開始時の位置と向きを決める
PlayerController.cs上の4つをイベントでつなぐ

ソースコード

PlayerInput.cs

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

/// <summary>
/// 矢印キーから移動の要求
/// </summary>
public class PlayerInput : MonoBehaviour
{
    [SerializeField] private float repeatDelay = 0.2f;     
    [SerializeField] private float repeatInterval = 0.1f;  
    private float repeatTimer;      

    public event Action<Vector3> MoveRequested; 
    public event Action Released;              
    //入力キーと方向を対応させる
    private static readonly (KeyCode key, Vector3 direction)[] bindings =
    {
        (KeyCode.UpArrow, Vector3.up),
        (KeyCode.DownArrow, Vector3.down),
        (KeyCode.RightArrow, Vector3.right),
        (KeyCode.LeftArrow, Vector3.left),
    };

    // 押された順に並べたbindingsの添字。最後が現在有効なキー
    private readonly List<int> heldKeys = new List<int>();
    private const int None = -1;  // キーを押していない
    private int CurrentIndex => heldKeys.Count > 0 ? heldKeys[heldKeys.Count - 1] : None;

    private void Update()
    {
        int previousIndex = CurrentIndex;

        heldKeys.RemoveAll(i => !Input.GetKey(bindings[i].key));
        for (int i = 0; i < bindings.Length; i++)
        {
            if (Input.GetKeyDown(bindings[i].key) && !heldKeys.Contains(i))
                heldKeys.Add(i);
        }
        
        int currentIndex = CurrentIndex;
        if (currentIndex == None)
        {
            if (previousIndex != None) Released?.Invoke();
            return;
        }

        if (currentIndex != previousIndex)
        {
            repeatTimer = repeatDelay;
            MoveRequested?.Invoke(bindings[currentIndex].direction);
            return;
        }

        repeatTimer -= Time.deltaTime;
        if (repeatTimer <= 0f)
        {
            repeatTimer = repeatInterval;
            MoveRequested?.Invoke(bindings[currentIndex].direction);
        }
    }
}

GridMover.cs

using System;
using UnityEngine;

/// <summary>
/// 対象を1マスずつ動かす。向き・移動の通知を行う
/// </summary>
public class GridMover : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private float gridSize = 1f;

    public event Action<Vector3> MoveAttempted;
    public event Action<Vector3, Vector3> Moved;

    //移動処理
    public bool TryMove(Vector3 direction)
    {
        if (direction == Vector3.zero || target == null) return false;

        MoveAttempted?.Invoke(direction);

        Vector3 nextPos = target.position + direction * gridSize; //移動先

        target.position = nextPos;  //位置更新
        Moved?.Invoke(nextPos, direction);
        return true;
    }
}

PlayerDirectionSprite.cs

using UnityEngine;

/// <summary>
/// プレイヤーの進行で画像差し替え
/// </summary>
public class PlayerDirectionSprite : MonoBehaviour
{
    //止まっているときの状態
    private enum IdleFacingMode
    {
        IdleSprite, //待機
        LastDirection //最後に向いている方向
    }

    [SerializeField] private SpriteRenderer playerRenderer;
    [SerializeField] private Sprite spriteIdle;
    [SerializeField] private Sprite spriteUp;
    [SerializeField] private Sprite spriteDown;
    [SerializeField] private Sprite spriteLeft;
    [SerializeField] private Sprite spriteRight;
    [SerializeField] private IdleFacingMode idleFacingMode = IdleFacingMode.LastDirection;

    private Vector3 lastFacingDirection = Vector3.zero;

    //指定した方向を向く
    public void Face(Vector3 direction)
    {
        if (direction == Vector3.zero) return; 

        lastFacingDirection = direction;
        SetSprite(GetSpriteForDirection(direction));
    }

    //止まったときの画像にする
    public void ShowIdle()
    {
        bool hasLastFacing = lastFacingDirection != Vector3.zero;
        bool keepLastFacing = idleFacingMode == IdleFacingMode.LastDirection && hasLastFacing;

        SetSprite(keepLastFacing ? GetSpriteForDirection(lastFacingDirection) : null);
    }

    //画像の差し替え
    private void SetSprite(Sprite sprite)
    {
        if (playerRenderer == null) return;

        Sprite nextSprite = sprite ?? spriteIdle;
        if (nextSprite != null && playerRenderer.sprite != nextSprite)
            playerRenderer.sprite = nextSprite;
    }

    //対応する画像を呼び出す
    private Sprite GetSpriteForDirection(Vector3 direction)
    {
        if (direction == Vector3.up && spriteUp != null) return spriteUp;
        if (direction == Vector3.down && spriteDown != null) return spriteDown;
        if (direction == Vector3.left && spriteLeft != null) return spriteLeft;
        if (direction == Vector3.right && spriteRight != null) return spriteRight;
    
        return null;
    }
}

PlayerSpawner.cs

using UnityEngine;

/// <summary>
/// シーン開始時のプレイヤーの位置と向きを決める
/// </summary>
public class PlayerSpawner : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private Vector3 startPosition = new Vector3(0.5f, 0.5f, 0f); 

    // 開始位置に置き、開始時の向きを startFacing に入れる
    public void Spawn(out Vector3 startFacing)
    {
        if (target == null) target = transform;

        target.position = startPosition;
        startFacing = Vector3.zero;
    }
}

PlayerController.cs

using UnityEngine;

/// <summary>
/// プレイヤーの各コンポーネントの接続
/// 入力 → 移動 → 向きの画像
/// </summary>
public class PlayerController : MonoBehaviour
{
    [SerializeField] private PlayerInput input;
    [SerializeField] private GridMover mover;
    [SerializeField] private PlayerSpawner spawner;
    [SerializeField] private PlayerDirectionSprite directionSprite; // 向きスプライト制御

    // イベントの登録/解除
    private void OnEnable()
    {
        if (input != null)
        {
            input.MoveRequested += OnMoveRequested; 
            input.Released += OnReleased;
        }
        if (mover != null)
        {
            mover.MoveAttempted += OnMoveAttempted;
        }
    }

    private void OnDisable()
    {
        if (input != null)
        {
            input.MoveRequested -= OnMoveRequested;
            input.Released -= OnReleased;
        }
        if (mover != null)
        {
            mover.MoveAttempted -= OnMoveAttempted;
        }
    }

    private void Start()
    {
        Vector3 startFacing = Vector3.zero;
        if (spawner != null) spawner.Spawn(out startFacing);

        if (directionSprite == null) return;
        if (startFacing == Vector3.zero) directionSprite.ShowIdle();
        else directionSprite.Face(startFacing);
    }

    ///イベントを受け取る
    private void OnMoveRequested(Vector3 direction) => mover?.TryMove(direction);
    private void OnReleased() => directionSprite?.ShowIdle();
    private void OnMoveAttempted(Vector3 direction) => directionSprite?.Face(direction);
}

解説

  • 名前空間(モジュール)
名前空間使っている理由
UnityEngineMonoBehaviour・Input・Vector3・SpriteRendererなど、Unityの基本機能を使うため
Systemイベントの型Actionを使うため(PlayerInput・GridMover)
System.Collections.Generic押しているキーを順番に覚えるList<int>を使うため(PlayerInput)
  • イベント
スクリプトイベント知らせる内容
PlayerInputMoveRequested「この方向に動きたい」(方向)
PlayerInputReleased「矢印キーがすべて離された」
GridMoverMoveAttempted「この方向に動こうとした」(方向)
GridMoverMoved「動いた」(移動後の位置・方向)

処理の流れをまとめると、次のようになります。

[PlayerInput] 矢印キーを押す → MoveRequested
      ↓
[PlayerController] OnMoveRequested → GridMover.TryMove()
      ↓
[GridMover] MoveAttempted → 1マス移動 → Moved
      ↓
[PlayerController] OnMoveAttempted → PlayerDirectionSprite.Face()(画像を切り替え)

[PlayerInput] キーを離す → Released → PlayerDirectionSprite.ShowIdle()

キー長押しで連続移動する(repeatDelay・repeatInterval)

PlayerInputのUpdate()では、キーを押したときと、押し続けているときで処理を分けています。

// 有効なキーが変わった:即移動
if (currentIndex != previousIndex)
{
    repeatTimer = repeatDelay;
    MoveRequested?.Invoke(bindings[currentIndex].direction);
    return;
}

//同じキーを押し続ける
repeatTimer -= Time.deltaTime;
if (repeatTimer <= 0f)
{
    repeatTimer = repeatInterval;
    MoveRequested?.Invoke(bindings[currentIndex].direction);
}

キーを押した瞬間はすぐに1マス進み、タイマーにrepeatDelay(0.2秒)をセットします。そのまま押し続けると、タイマーが0になったところで次の1マスに進み、以降はrepeatInterval(0.1秒)ごとに進みます。

パソコンでキーを押しっぱなしにしたとき、1文字目のあとに少し間が空いてから文字が連続で入力されるのと同じ仕組みです。最初に少し待つことで、「1マスだけ動きたかったのに2マス進んでしまった」という誤操作を防げます。

Time.deltaTime(前のフレームからの経過時間)を引いているため、フレームレートが変わっても同じ速さで進みます。歩く速さを変えたいときは、InspectorでRepeat Intervalの値を調整してください。値を小さくするほど速く歩きます。

キー同時押しの優先度

2つの矢印キーを同時に押したときにどちらへ進むかは、heldKeysリストで決めています。

//リスト更新(離されたキーを外し、新しく押されたキーを末尾に追加)
heldKeys.RemoveAll(i => !Input.GetKey(bindings[i].key));
for (int i = 0; i < bindings.Length; i++)
{
    if (Input.GetKeyDown(bindings[i].key) && !heldKeys.Contains(i))
        heldKeys.Add(i);
}
  1. RemoveAllで、もう押されていないキーをリストから外す
  2. GetKeyDownで新しく押されたキーを見つけ、リストの末尾に追加する

リストの末尾が「一番最後に押したキー」になり、CurrentIndexはこの末尾のキーを返します。

private int CurrentIndex => heldKeys.Count > 0 ? heldKeys[heldKeys.Count - 1] : None;

例えば、→キーを押しながら↑キーを押すと↑方向へ進みます。そのあと↑キーだけを離すと、リストには→キーが残っているため、すぐに→方向へ戻って進みます。

入力をif文で上から順に調べる書き方だと、「↑と→を同時に押すと、いつも↑が勝つ」「同時に押すと斜めに進む」といった動きになりがちです。押した順番を覚えておくことで、プレイヤーの操作に素直に反応する移動になります。

実装

5つのスクリプトは、プレイヤーのオブジェクトと、画像を表示するオブジェクトにアタッチします。

各スクリプトは[RequireComponent]を使っていないため、必要なコンポーネントは自動で追加されません。SpriteRendererの追加と、PlayerControllerの参照の設定は、忘れずに手動で行ってください。

まとめ

本記事では、Tilemapの上で2Dプレイヤーを矢印キーで1マスずつ動かす方法として、次の内容を解説しました。

  • Input.GetKeyとGetKeyDownを組み合わせて、押した瞬間の移動と長押しの連続移動を作る
  • 押したキーの順番をListで覚えて、同時押しでは後から押したキーを優先する
  • SpriteRenderer.spriteを差し替えて、進む方向に合わせて画像を切り替える

スクリプトを役割ごとに分けているため、ここから機能を足していくのも簡単です。次のステップとして、GridMoverに壁の判定を加えて「壁の方を向くだけで進めない」動きにしたり、Movedイベントを使ってマップの端で別のシーンへ切り替えたりすると、RPGのマップ移動らしくなります。

タイトルとURLをコピーしました