UnityのTilemapで心折れかけたことと解決のためのプレビュー拡張

# はじめに

UnityのTilemapで2DのRPGを作ろうと試みて1ヶ月。
この1ヶ月の間、Tilemap作成時に心が折れそうになるときがあったので
自分なりに分解、考察してみました。

作業はUnity2018.2で行っています。

# Tilemap作成時に心が折れそうになるポイント

個人的にはこの3つでした。

 - 作業しているタイルマップをよく間違える
 - Inspectorにタイルマップの情報がない
 - Prefab化したタイルマップが見えない

詳細を以下に書きます。

## a. 作業しているタイルマップをよく間違える

タイルマップを置くレイヤーはTile Paletteビューの `Active Tilemap` で指定するのですが、
多層レイヤーでゲームを作成する場合、この指定ミスを多くやってしまいました。
ありがちなのはHierarchyビューでタイルマップを指定した気になるパターンです。
他のレイヤーのactiveを切り替えたときにうわああああああとなります。

↓間違えて床のタイルマップを手前に塗っていたの図
<img width="1203" alt="1-0.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/57c013a1-ee8b-9172-aec9-8f7523acafdb.png">

<img width="1204" alt="1-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/89f2212f-44e4-cdfd-6b02-48c84d05e486.png">


## b. Inspectorにタイルマップの情報がない

タイルデータはSceneビューにしか表示されないので、タイルマップのInspectorに表示される座標はAnchorのみです。
開発ではtransformの座標、タイルマップの座標が存在するため
タイルマップの{0,0}座標の位置がよくわからない状態です。
まれに作業後にtransformが数マス分ずれていたなんてこともありました。

↓Inspectorの座標情報はAnchorのみ。Sceneビューの青い四角はずれている?
<img width="1204" alt="1-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/3504f382-2755-7581-b682-ad5b556d4230.png">


## c. Prefab化したタイルマップが見えない

繰り返しになりますがタイルデータはSceneビューにしか表示されません。
作業単位を細かくするためにPrefab化を試みましたが、
Prefab化すると何のタイルマップかわからなくなります。

↓Prefab化したタイルマップ。なんのタイルマップかわからない。
<img width="1205" alt="1-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/2f257722-b2f7-74c9-6c30-5a632717cf06.png">

# 調査編 Tilemapの実体について

上記について、まず「Tilemapコンポーネントはどのようなデータか」をきちんと理解することで
折れない心のベースを作りました。

調査対象は11x11のTilemapコンポーネントを持つGameObjectです。

``` 
GameObject
  -Tilemap
  -Tilemap Renderer
  -Tilemap Collider2D
```

## Inspector編

通常のInspectorで見ると、Tilemapコンポーネントは、
Animation Frame Rate、Color、Anchor、Orientationの項目のみでした。
右上のメニューをNormalからDebugに変更すると、OriginとSizeが表示されます。

<img width="1204" alt="2-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/18efec3d-11df-fab2-22a0-6519fbd24f4d.png">
<img width="1203" alt="2-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/770a070d-7f1f-34de-8dab-d7dd622f43c5.png">

このOriginとSizeはSceneビューの青い領域を表していることがわかります。
また、この領域はタイルマップを敷くことで変化するようです。

OriginとSizeがあるだけでも座標の目安がつきます。
多層レイヤーでは領域を固定することによってわかりやすい構造になります。

ただ、敷き詰めたタイルのデータは見れなかったので以下YAML編に続きます。

## YAML編

用意したGameObjectをPrefab化してテキストエディタで見てみます。

すると、11x11という小さいタイルマップにもにもかかわらず1600行(！)もありました。
ほとんどがTilemapコンポーネント内の `m_Tiles` のものです。

<img width="483" alt="2-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/1ee1c67e-d76e-66f3-7938-8319c49f0e45.png">

`m_Tiles` は配列で各データは `first` と `second` を持っています。
キー`first`ではzが0のVector3Intで座標情報を持っていることがわかります。
座標はソートされておらず、タイルのないマスのデータはありません。
キー`second`ではindexで `Tile`の参照情報を持っています。

## 調査編のまとめ

とりあえず以下のことがわかりました。

- Sceneビューで表示されるタイルの実データはTilemapコンポーネントが保持している
- タイルマップの領域はOriginとSizeで、InspectorをDebugにすると確認可能
- タイルマップのデータはVector3Intの座標情報とTileの参照情報

これをもとに問題を解決します。

# 解決編 Tilemapのエディタ拡張

## Tilemapのエディタ拡張で領域を編集可能にする

まずTilemapのInspectorに必要な情報を表示できるようにしました。
エディタ拡張をしようと思ったのですが、
`OnInspectorGUI `を改めて呼び出すだけでOrigin, Sizeが表示されるようになりました。

``` TilemapInspector.cs
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEditor;

[CustomEditor(typeof(Tilemap))]
public class TilemapInspector : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
    }
}
```

After
<img width="1207" alt="3-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/6e943ee2-b4c3-9b5d-d08c-5fb9ffcf011a.png">
<img width="1201" alt="3-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/38d3cd10-3bac-5f81-9b24-8655532dc0ce.png">


領域のOriginとSizeが指定できるようになり、必要領域のみのTilemapのデータとすることができました。
タイルマップ座標もわかりやすくなりました。

## Tilemapのプレビュー拡張で確認画面を作成する

いくつかのコンポーネントはInspectorの下部分にプレビュー機能がありますが、
2D系のSpriteやTilemapにはそれがありません。
Tilemapのプレビューを作成することにより以下のことができるようになりました。

- 各レイヤーごとのタイルマップ確認
- タイルマップ{0, 0}位置の確認
- プレビューを確認しながらのタイルマップ作成
- Prefabのタイルマップ確認

<img width="1204" alt="4-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/475988b9-2ea7-fba3-b5e1-ab06acbc410c.png">
<img width="1203" alt="4-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/ac3d691c-d0da-e9b2-1b6d-76108887ebe3.png">
<img width="1204" alt="4-3.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/c4d304ec-b0f2-a047-4d82-30eb8b66f05d.png">

以下がソースコードです。
個人使いに作成したもののため、
10x10程度の大きさのタイルマップにしか対応していません。
また、今回はタイルの表示に楽そうだった `GUI#SelectionGrid` を使用していますがもっといい方法があるかもしれません。


``` TilemapPreview.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEditor;

[CustomPreview(typeof(Tilemap))]
public class TilemapPreview : ObjectPreview
{
    //プレビューの1マスのサイズ
    private static readonly float PREVIEW_CELL_SIZE = 24.0f;
    //プレビューの1マスのマージン
    private static readonly int PREVIEW_MARGIN = 1;
    //タイルがない場合のSpriteのResources下パス
    private static readonly string NO_TILE_SPRITE_PATH = "TilemapPreview/red";
    //基準点の場合のSpriteのResources下パス
    private static readonly string BASE_POSITION_SPRITE_PATH = "TilemapPreview/green";
    //プレビューのタイトル
    private static readonly GUIContent previewTitle = new GUIContent("Tilemap");

    private Tilemap tilemap = null;

    public override bool HasPreviewGUI()
    {
        return true;
    }

    public override GUIContent GetPreviewTitle()
    {
        return previewTitle;
    }

    public override void Initialize(Object[] targets)
    {
        base.Initialize(targets);

        foreach (Tilemap target in targets) {
            tilemap = target;
            break;
        }
    }

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        Vector3Int origin = tilemap.origin;
        Vector3Int size = tilemap.size;
        List<GUIContent> contents = new List<GUIContent>();

        for (int y = size.y - 1; y >= 0; y--) { //3D座標からUI座標にするためyは逆
            for (int x = 0; x < size.x; x++) {
                Vector3Int gridPos = new Vector3Int(origin.x + x, origin.y + y, 0);
                Sprite sprite = tilemap.GetSprite(gridPos);

                //タイルが設定されていない場合
                if (sprite == null) {
                    sprite = Resources.Load<Sprite>(NO_TILE_SPRITE_PATH);
                }
                GUIContent content = new GUIContent(string.Format("{0},{1}", gridPos.x, gridPos.y),
                                                    AssetPreview.GetAssetPreview(sprite));
                contents.Add(content);
            }
        }

        GUIStyle style = new GUIStyle();
        style.fixedWidth = PREVIEW_CELL_SIZE;
        style.fixedHeight = PREVIEW_CELL_SIZE;
        style.margin = new RectOffset(PREVIEW_MARGIN, PREVIEW_MARGIN, PREVIEW_MARGIN, PREVIEW_MARGIN);
        style.imagePosition = ImagePosition.ImageOnly;

        GUI.SelectionGrid(r, -1, contents.ToArray(), size.x, style);

        Sprite basePositionSprite = Resources.Load<Sprite>(BASE_POSITION_SPRITE_PATH);
        Rect center = new Rect(r.x - origin.x * PREVIEW_CELL_SIZE - origin.x * PREVIEW_MARGIN,
                               r.y - origin.y * PREVIEW_CELL_SIZE - origin.y * PREVIEW_MARGIN,
                               PREVIEW_CELL_SIZE, PREVIEW_CELL_SIZE);
        GUI.DrawTexture(center, basePositionSprite.texture);
    }
}
```

# まとめ

タイルマップをエディタ拡張・プレビュー拡張することで心折れる出来事をある程度回避することができました。

- Inspector、プレビューでTilemap情報を確認しながらタイルマップ作業
  - Active Tilemapのうっかりミス防止  
- Prefab化したタイルマップの確認

Unity側にエディタ拡張の機能があったため、ストレスはアイデアへ昇華されました。。
というわけでTilemapでのゲーム開発をモチベーション高く再開することができました。
Tilemapシリーズとして[【水面タイルの作成方法】](https://qiita.com/keidroid/items/eda4b4233c578248fc13)や[【Tilemapでのタッチ方式の移動方法】](https://qiita.com/keidroid/items/c4c57ca4f99e021e6ce1)の記事もQiitaに投稿済みです。
合わせてどうぞ。

エディタ拡張には以下を参考とさせていただきました。

エディタ拡張入門 第22章　SpriteAnimationPreview(スプライト一覧の表示)
https://anchan828.github.io/editor-manual/web/spriteanimationpreview1.html