UnityのTilemapでマップタッチ方式の移動を実装

スマホ向けにアドベンチャーゲームを作成しており、
そこで気づいたことを ~~定期的に~~ **不定期に**書いています。

# はじめに
## スマホの2D RPGで適切な移動手段を考えた:thinking:

![IMG_1448.jpg](https://qiita-image-store.s3.amazonaws.com/0/19692/c2d06acd-3b4e-bf87-3f31-39c48e664b5c.jpeg)

ほとんどのスマホはゲームパッドがないため、スマホゲームは操作自体から考える必要があります。。
RPG系でよく使われているものを以下に挙げます。

|名称?|処理|
|:--|:--|
|バーチャルキー|専用の領域を設けてタッチしたキー方向に移動させる|
|バーチャルパッド(スティック)|バーチャルスティックをずらした方向(ドラッグ方向)に移動させる|
|マップタッチ|マップをタッチさせてそのマスにキャラクターを移動させる|

自分なりにメリット・デメリットを考えてみました。

### バーチャルキー

|項目||
|:--|:--|
|メリット| キー領域の工夫で操作ミスをある程度軽減できる。|
|デメリット| キーの大きさ、数などから領域を大きく取る必要がある。|
|考察| ローグ系などの1歩に重要な意味があるゲームに良さげ|

### バーチャルパッド(スティック)

|項目||
|:--|:--|
|メリット| 移動の強弱をつけることができる。場合によっては全方向に移動できる。|
|デメリット|グリッド状のマップでは上記メリットの恩恵がほぼない。|
|考察| 3Dアクションなど移動の速度や方向が重要なゲームに良さげ|

### マップタッチ

|項目||
|:--|:--|
|メリット|目的地への操作回数が少ない|
|デメリット|操作性がマップの1マスの大きさに依存する。移動途中の経路を考える必要がある|
|考察|目的地が明確など、移動自体に重要性がないゲームに良さげ|

今回はストレスない移動を目的としたため、マップタッチ方式を実装してみました。
以下が実装した手順を書きます。

# UnityのTilemapでマップタッチ方式の移動を実装

## やること

以下方式でTilemapでの移動を実現しました。

- タッチを検出して目的地のセル座標を取得する
- 移動するキャラクターのセル座標を取得する
- 各マスが通行可能か検知する
- 障害物をよけて目的地まで経路検索する
- 移動させる

ちなみにUnityにはNavMeshという経路検索AIの機能がありますが
Tilemapには現状対応していないようです。。

## タッチを検出して目的地のセル座標を取得する

まず、タッチ操作を検出してTilemapの座標を取得する必要がありました。
Cameraの `Camera#ScreenToWorldPoint(Vector3)`でタッチ座標をワールド座標に変換します。
次に `Grid#WorldToCell(Vector3)`でワールド座標からセル座標を取得します。

＃UnityEditorでも動作するように`Input.mousePosition`などマウス系操作で取得しています。

```
[SerializeField]
private Grid grid;

[SerializeField]
private Camera camera;

:

Vector3 touchPos = Input.mousePosition;
Vector3Int gridPos = grid.WorldToCell(camera.ScreenToWorldPoint(touchPos));

if (Input.GetMouseButtonUp(0)) {
  //タッチ操作で手を離したときの処理
}
  
```

## 移動するキャラクターのセル座標を取得する

`Grid#cellSize`でTilemapの1タイルあたりのサイズが取得できます。
指定のGameObjectについて、transformから現在のセル座標を取得できます。

(以下は一例です)

```
[SerializeField]
private Grid grid;

:

int tileX = (int) (gameObject.transform.position.x / grid.cellSize.x);
int tileY = (int) (gameObject.transform.position.y / grid.cellSize.y);
```

## 各マスが通行可能か検知する

UnityのTileオブジェクトはColliderTypeとして{None, Grid, Sprite}を選択できます。
簡易な方法として、`ColliderType`が`None`であれば通行可とみなすことができました。
`Tilemap#GetColliderType`で取得可能です。

画像: 池・木をColliderType:Gridとして、通行不可と検出したマスに赤を重ねたところ。
<img width="605" alt="スクリーンショット 2018-09-30 14.32.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/9b897428-49d3-23ac-b6d2-83cc498c62ed.png">

```
[SerializeField]
private Tilemap collisionTarget;

:

Vector3Int gridPos = new Vector3Int(x, y, 0); //指定のタイルマップ位置が

//Tile.ColliderType.Noneであれば通行可
Tile.ColliderType colliderType = collisionTarget.GetColliderType(gridPos);

```

## 障害物をよけて目的地まで経路検索する

NavMeshが使用できないためGridに対して8方向のA*アルゴリズムを使用しました。

<img width="1014" alt="スクリーンショット 2018-09-30 15.39.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/52e619ce-db31-6cdf-318a-b5cc6a8c4ead.png">

A*アルゴリズム自体については割愛しますが、
この時点でTilemapから障害物の情報を抽出できているので好きなアルゴリズムに代替可能です。

Keith Petersさんの以下書籍4章ではグリッド形式のA-Starについて詳細が書かれています。
(サンプルコードは付属していますがもちろんActionScriptなのでC#に移植する必要があります)
[詳解 ActionScript 3.0アニメーション ―衝突判定・AI・3DからピクセルシェーダまでFlash上級テクニック ](http://amzn.asia/d/aJxdiXo)

Qiita内だと以下記事を参考にさせていただきました。
- [よくわかるA*(A-star)アルゴリズム (Unity2Dのサンプルコードつき)](https://qiita.com/2dgames_jp/items/f29e915357c1decbc4b7)
- [経路検索して歩くドット絵RPG風キャラ](https://qiita.com/smith/items/62a08feec2ea4465e990)

経路は8方向に対応させているので、通行可能な経路から
縦横方向の距離(1)と斜め方向の距離(√2≒1.414)の和が小さいものを評価しています。

![IMG_1449.jpg](https://qiita-image-store.s3.amazonaws.com/0/19692/bc197029-7ad8-908a-a495-edbabf0a33bb.jpeg)

```
public readonly static float STRAIGHT_COST = 1.0f;
public readonly static float DIAG_COST = 1.414f; //sqrt(2)

:

public static float Heuristic(bool allowdiag, Vector2Int current, Vector2Int goal)
{
  float dx = Mathf.Abs(goal.x - current.x);
  float dy = Mathf.Abs(goal.y - current.y);
  float diag = Mathf.Min(dx, dy);
  float straight = dx + dy;
  return DIAG_COST * diag + STRAIGHT_COST * (straight - 2 * diag);
}

```



# 成果物

指定マスにキャラクターが障害物を避けて最短経路で移動できるようになりました。

![Sep-30-2018 16-08-46.gif](https://qiita-image-store.s3.amazonaws.com/0/19692/5745a8ed-4dbc-8d20-becd-377725e1ed20.gif)