【Unity】Tilemapで作られたマップをグリッドベースで移動させる方法

本記事では、Unityでのタイルマップで作成されたマップの上を、グリッドベースで移動させるキャラクターの作り方を説明したいと思います。

## タイルマップとは？

「タイルマップくらい知ってるよ！」という方は読み飛ばしてください。

Unityにおけるタイルマップは、簡単に説明すると、RPGツクール（RPGMaker）や、ウディタに搭載されているマウス操作でタイルを配置できる機能だと思って差し支えありません。詳細についての概要については、以下の公式マニュアルをご参照ください。

https://docs.unity3d.com/ja/2019.4/Manual/class-Tilemap.html

上記のタイルマップで作成したマップの上を、「グリッドベースで移動する」ということですが、タイルマップはグリッド状に配置されているため、各グリッドの当たり判定を考慮した上で、移動を行う方法となります。

## 本記事で説明を省略する部分

他のチュートリアル系の記事で多く触れられている部分であるため、本記事では、以下の説明については省略させていただきます。

- スプライトの読み込み・分割、タイルマップパレットの作成
- 操作キャラクターのアニメーション

## Sceneの説明

Unity上のヒエラルキーでは、以下のような階層でGameObjectを配置しています。

```
- MainCamera
- Grid
    - WallFloatTilemap
    - GroundTilemap
- Player
```

### 1. MainCamera
MainCameraについては、Sceneの作成時にデフォルトで存在しているものから変更していないので、特に説明はありません。

### 2. Grid(WallFloatTilemap, GroundTilemap)
Grid配下に当たり判定を有するGroundTilemap、プレイヤーと被さって表示されるWallFloatTilemapを配置しています。

ヒエラルキーウィンドウを右クリックまたは、左上の＋ボタンを押し、`2D オブジェクト` > `タイルマップ` > `矩形`からタイルマップを追加することができます。これで、`Grid`とネストされた`Tilemap`がヒエラルキーウィンドウに追加されたと思います。

今回は、プレイヤーと重複するタイルも表示させたいため、ネストされている`Tilemap`をコピーし、コピー元と同じ`Grid`配下にペーストします。

配置したタイルマップの、プレイヤーとの衝突判定を行わせたいGameObjectに`TilemapCollider2D`コンポーネントを追加しましょう。このコンポーネントが、（後述する）Playerの持つColliderとの間で当たり判定を行い、移動を制限します。

### 3. Player
Playerは、その名前の通り、プレイヤーが操作するオブジェクトとなります。
Playerについては、当たり判定を行うScriptを自作する必要があるので、次の説明で詳しく見ていきましょう。

## Playerの説明

先に実装の全体像をご覧ください。続けて、各メソッドの動きについて細かく説明していきます。

```c#
using System.Collections;
using UnityEngine;

public class Player : MonoBehaviour
{
    private bool isMoving;
    public float moveSpeed;

    BoxCollider2D boxCollider;
    Rigidbody2D rb2D;
    int blockingLayer;

    private void Start()
    {
        boxCollider = GetComponent<BoxCollider2D>();
        rb2D = GetComponent<Rigidbody2D>();
        blockingLayer = LayerMask.GetMask("BlockingLayer");
    }

    void Update()
    {
        // 移動中は処理しない
        if (isMoving) return;

        // 縦横の入力を受け取る
        int horizontal = (int)Input.GetAxisRaw("Horizontal");
        int vertical = (int)Input.GetAxisRaw("Vertical");

        if (horizontal != 0)
            vertical = 0;
        if (horizontal != 0 || vertical != 0)
        {
            // 移動を開始する
            Move(horizontal, vertical, out RaycastHit2D hit);
        }
    }

    protected bool Move(int xDir, int yDir, out RaycastHit2D hit)
    {
        Vector2 start = transform.position;
        Vector2 end = start + new Vector2(xDir, yDir);

        // デバッグ用にRayを描画
        Debug.DrawRay(start, new Vector2(xDir, yDir), Color.white, 5);

        boxCollider.enabled = false;
        hit = Physics2D.Linecast(start, end, blockingLayer);
        boxCollider.enabled = true;

        // 衝突しておらず、移動中でなく
        if (hit.transform == null && !isMoving)
        {
            // 滑らかな移動を開始
            StartCoroutine(SmootMovement(end));
            return true;
        }
        return false;
    }

    protected IEnumerator SmootMovement(Vector3 end)
    {
        float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
        isMoving = true;

        while (sqrRemainingDistance > float.Epsilon)
        {
            Vector3 newPosition = Vector3.MoveTowards(rb2D.position, end, moveSpeed * Time.deltaTime);
            rb2D.MovePosition(newPosition);
            sqrRemainingDistance = (transform.position - end).sqrMagnitude;
            yield return null;
        }
        isMoving = false;
    }
}
```

### フィールド定義

当たり判定を行うので、プレイヤーのGameObjectの持つBoxCollider2D、Rigidbody2Dを保持する変数を定義しています。
また、名前の通りですが、移動中の判定用のフラグでありisMoving、移動速度を規定するmoveSpeedを定義しています。
int型のblockingLayerについてですが、これはUnityのインスペクターで設定した、レイヤーの番号を保持しています。

### Startメソッド

Startメソッドは、Unityで予め用意されているメソッドです。オブジェクトがインスタンス化されたときに一度だけ呼び出される処理です。

boxCollider、rb2Dをアタッチ元のPlayerのGameObjectから取得しています。blockingLayerについては、予めTilemapに設定したものと同じレイヤーを取得し、保持しておきます。

### Updateメソッド

Startメソッドも、Startメソッドと同様に、Unityで予め用意されているメソッドです。毎フレーム呼び出され、オブジェクトが破棄されるまで呼び出され続けます。

ここでは、移動中でない場合にキー操作を受け取り（GetAxisRawで縦軸、横軸を個別に取得）、入力があった場合にMoveメソッドを呼び出すようになっています。

### Moveメソッド

上述の通り、フレーム毎の入力判定の結果、移動のキー操作を受け取った場合に動作します。

Linecastについては「特定の方向と距離（つまりはベクトル）に線分を引き、衝突する物体が存在するかを判定するメソッド」という風に理解していただければ問題ありません。プレイヤーのGameObjectが持つコライダーが干渉するため、当たり判定（`Physics2D#Linecast`）の実施時は一時的にコライダーを無効化しています。

ここで重要となるのは、Linecastメソッドの第3引数に`blockingLayer`を渡している点です。Linecastメソッドの第3引数には、

## SmootMovementメソッド

このメソッドは、Moveメソッドから呼び出され、キャラクターを少しずつ移動させます。

移動には、終了地点と現在地点との差分を利用します。差分の値（ここでは`sqrRemainingDistance`として扱っている）が、極小さな値になるまで、移動処理および終了地点との差分計算の処理を行います。

https://docs.microsoft.com/ja-jp/dotnet/api/system.single.epsilon?view=net-6.0

なお、移動の開始/終了時に`isMoving`フラグを操作します。

## あとがき

以上がUnityでのグリッドベースでの移動方法になります。いかがでしたでしょうか。

このやり方にたどり着くまでは、公式チュートリアルと同様に配置するタイルをすべてPrefabとして扱い、スクリプトから自動的に配置するという手法を取っていました。

しかし...

- 単純に当たり判定を行うだけならPrefabではなくTilemapで十分なこと
- タイル数が増えるとPrefabも増え、管理が難しくなること
- Tilemapを使った手法でも自動生成スクリプトからマップを生成できること

等の点から、今回のやり方に変更しました。

皆様のゲーム開発の参考になれば幸いです。

## 参考

https://learn.unity.com/project/2d-roguelike-tutorial

Unity公式のローグライク系チュートリアルになります。今回ご紹介したPlayerのスクリプトの大部分は、こちらのチュートリアルの`Unit Mechanics` > `4.Writing the Player Script`で紹介されているものになります。全編英語ですが、Youtubeに解説動画もありますので、ぜひご参照ください。