質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.37%

UnityのTileMapをJSONデータから作成した時の不具合

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 1,445

score 27

UnityでTileMapのエディターもどきを制作しております。

JSONの文字列データからマップの生成を行っており、数値ごとにセルにスプライトを割り当ててます。

例)-1ならタイルなし、0なら0番目のスプライトを使用

PlayMode(再生ボタン)中なら問題なく動作するのですが、
再生中以外に生成してしまうとTileMapの保存情報が壊れているみたいです。

具体的にはタイルを置いていない場所に0番目ののスプライトタイルがおかれてしまいます。
再生していない際でも、正常に動作させる方法はないでしょうか。

特定のGameObject下のgameObjectを全削除するプログラムを書いているつもりなのですが、
こちらも再生中以外はうまく動作しないようです。

3~4件削除した後に処理がとまってしまいます。

"listMap": [
            "0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,4, 4, 4, 3",
            "2, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,4, 4, 4, 3",
            "2, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3",
            "2, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 3",
            "2, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 3",
            "7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7"
        ],
[CustomEditor(typeof(TileMapManager))] //拡張するクラスを指定
public class CustomAddButton : Editor {

    /// <summary>
      /// InspectorのGUIを更新
      /// </summary>
      public override void OnInspectorGUI(){

        //ボタンを表示
        if (GUILayout.Button("Json Load")){
            // エディタが再生しているか or しようとしてるかどうか
            if(EditorApplication.isPlayingOrWillChangePlaymode){
                tileMapManager.JsonLoad();
                Debug.Log("Json Load");
            } else {
                Debug.Log("Not Play Mode");
            }
        }  
    }

    //ボタンを表示
    if (GUILayout.Button("Map Delete")){
        // エディタが再生しているか or しようとしてるかどうか
        if(EditorApplication.isPlayingOrWillChangePlaymode){
            tileMapManager.MapDelete();
            Debug.Log("Map Delete");
        } else {
            tileMapManager.MapDelete();
            Debug.Log("Map Delete");
            Debug.Log("Not Play Mode");
        }
    }  

}
public class TileMapManager : MonoBehaviour {

    [ContextMenu("Method")]
    private void Method () {

    }

    public Tilemap tilemap;

    // ステージを生成する
    public void JsonLoad() {

        // タイルマップのリセット
        ResetTileMap(tilemap);

        // 保存
        jsonStage.SetFileName(fileNameJson);

        // リセット
        jsonStage.stage.Reset();

        // 読み込み
        jsonStage.Load();

        Sprite sprite;
        var bound = tilemap.cellBounds;
        Tile tile = ScriptableObject.CreateInstance<Tile>();

        // マップの生成
        for(int y=0; y<jsonStage.stage.listMap.Count; y++){

            // マップデータ文字を分解
            String width = jsonStage.stage.listMap[y];
            width = width.Replace(" ", ""); // 空白を削除

            // 文字列を分解してlistに追加
            string[] arrayWidth =  width.Split(',');
            List<int> listWidth = new List<int>();
            for(int i=0; i<arrayWidth.Length; i++){
                int num = int.Parse( arrayWidth[i] );
                listWidth.Add(num);
            }

            // タイルマップのセット
            for(int x=0; x<listWidth.Count; x++){

                // -1なら次へ
                int spriteNum = listWidth[x];
                if( spriteNum < 0) continue;

                // タイルマップのポジションの取得
                //int posX = bound.position.x + x + 1;
                int posX = bound.position.x + x;
                int posY = (bound.position.y + jsonStage.stage.listMap.Count) - y - 1;
                //int posY = (bound.position.y + jsonStage.stage.listMap.Count) - y;

                // タイルマップのセット
                string spriteName = jsonStage.stage.listSpriteName[spriteNum];
                sprite = GetSprite("Sprites/test", spriteName);

                tile.sprite = sprite;

                // 角度の設定
                Quaternion rot = Quaternion.Euler(0.0f, 0.0f, 0.0f);

                // 右上タイル反転
                if(spriteName == CORNER_1_SPRITE_NAME){
                    if(cornerCount1 == CORNER_1_FLIP){
                        rot = Quaternion.Euler(0.0f, 180.0f, 0.0f);
                    }
                    cornerCount1++;
                }

                // 左下タイル反転
                if(spriteName == CORNER_2_SPRITE_NAME){
                    if(cornerCount2 == CORNER_2_FLIP){
                        rot = Quaternion.Euler(0.0f, 180.0f, 0.0f);
                    }
                    cornerCount2++;
                }

                // タイルの設定と回転
                SetTile(new Vector3Int( posX, posY, 0 ), rot, tilemap, tile);

            }
        }

        // マップを全削除
        public void MapDelete(){

            // 子オブジェクトを全削除
            AllDestroy(stageObject);        
            ResetTileMap(tilemap);

        }

        // サイズを変えずに全てのタイルをリセット
        public void ResetTileMap(Tilemap tilemap){

            var bound = tilemap.cellBounds;
            // 全て削除
            for (int y = bound.max.y - 1; y >= bound.min.y; --y){
                for (int x = bound.min.x; x < bound.max.x; ++x){                
                    Tile tile = ScriptableObject.CreateInstance<Tile>();
                    Quaternion rot = Quaternion.Euler(0.0f, 0.0f, 0.0f);
                    //tilemap.SetTile(new Vector3Int(x, y, 0), tile);
                    SetTile(new Vector3Int( x, y, 0 ), rot, tilemap, tile);
                }
            }
        }

        // タイルをセット
        private void SetTile(Vector3Int pos, Quaternion rot, Tilemap tilemap, Tile tile) {
            tilemap.SetTile(pos, tile);
            tilemap.SetTransformMatrix(pos, Matrix4x4.TRS(Vector3.zero, rot, Vector3.one));
        }

        // 子オブジェクトを全削除
        public void AllDestroy(GameObject gameobject){

            // PlayModeでなければ
            if(EditorApplication.isPlayingOrWillChangePlaymode){
                foreach ( Transform n in gameobject.transform ) {
                    GameObject.Destroy(n.gameObject);
                }
            } else {
                foreach ( Transform n in gameobject.transform ) {
                    GameObject.DestroyImmediate(n.gameObject);
                }
            }
        }
    }

}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+1

Object Delete Flagがオンの状態でMap Deleteボタンを押しても一度に全部消えない件について

たとえばオブジェクトを8個作った状態だと、Map Deleteボタンを押すたびに8個→4個→2個→1個→0個といった具合にオブジェクトが半々に減っていきます。この奇妙な挙動はTransformが子を列挙する仕組みのせいのような気がします。

子の列挙が開始されると、まず内部的なインデックスが0に初期化された状態で、0番目の子が返されます。ここでDestroyImmediateによりオブジェクトが削除されると、子の総数は7個に減って、削除前は1番だった子が0番に変わるはずです。
ですがTransformの列挙子はそれにはおかまいなく、内部的なインデックスをインクリメントして1にします。結果として次は1番の子...つまり初期状態では2番だった子が返されることになり、0番の子...つまり初期状態では1番だった子は列挙から漏れて生き残ります。
Transformの列挙子はループのたびにその時点の子の数と内部的なインデックスを比較し、インデックスが子の数以上になったところで列挙を終了します。結果として初期状態で0、2、4、6番だった子は削除され、1、3、5、7番だった子は生き残るのでしょう。

ゲーム実行中だとちゃんと全件削除されるのは、おそらくDestroyImmediateではなくDestroyを使っているためでしょう。
こちらでは、オブジェクトが実際に削除されるのはレンダリング直前まで遅延されるので、子の列挙作業中には子の数は変化しないはずです。ですのでTransformの列挙子がすべての子を漏れなく列挙できたのだろうと思われます。

TileMapManager2の全削除メソッドを(ついでに追加メソッドも)下記のようにしてみるとどうでしょうか。

    // 子オブジェクトを全削除
    public void AllDestroy(GameObject gameobject) {
        // PlayModeでなければ
        if(EditorApplication.isPlayingOrWillChangePlaymode){
            foreach ( Transform n in gameobject.transform ) {
                GameObject.Destroy(n.gameObject);
            }
        } else {
            // 子を列挙しながら削除していくのではなく、まず子を配列にして
            // 処理対象を確定させてしまってから...
            // (Cast<Transform>を使うには冒頭にusing System.Linq;が必要です)
            Transform[] children = gameobject.transform.Cast<Transform>().ToArray();

            // それらを削除していく
            foreach ( Transform n in children ) {
                // 削除によってヒエラルキーの状態が変わっても、ヒエラルキービュー上のシーン名部分に
                // 要保存を示す「*」マークが付かないことにお気付きかもしれません
                // DestroyImmediateの代わりにUndo.DestroyObjectImmediateを使うと、シーン状態の変化が
                // ちゃんと認識されて「*」マークが付くかと思います
                // GameObject.DestroyImmediate(n.gameObject);
                Undo.DestroyObjectImmediate(n.gameObject);
            }
        }
    }
    // 追加
    public void Add() {
        GameObject obj = Instantiate(
                    prefab,
                    Vector3.zero,
                    Quaternion.identity,
                    stageObject.transform
                ) as GameObject;

        // 上記と同じく、こちらについても生成したオブジェクトをUndoレコードに
        // 登録してやることで変更が認識され、「*」マークが付くと思います
        Undo.RegisterCreatedObjectUndo(obj, $"Create {prefab.name}");
    }

タイルの外観が急に全部同じになってしまう件について

どうやら先の回答で申し上げた、一つのタイルオブジェクトを使い回すことによる弊害が現れたような感じですね...
対処案として、まずJSON_PARENTには後のコード中コメントで言及しました「スプライトと対応する既存のタイルを探す」という作業に失敗した場合の保険として、下記のようなプロパティとメソッドを追加しました。

    // このマップ用のタイルアセット保存フォルダーパス
    private string TilesDirectoryPath => (Path.GetDirectoryName(filePath) == "StreamingAssets" ? "Assets/" : filePath) + Path.GetFileNameWithoutExtension(fileName) + "Tiles";

    // フルパスをAssets起点のパスにする
    private static string GetAssetPath(string fullPath)
    {
        // fullPathの「Assets」で始まる以降の部分を取り出して返す
        var root = Application.dataPath;
        return fullPath.StartsWith(root) ? fullPath.Substring(root.Length - 6) : null;
    }

    // このマップ用に作成されたタイルアセットをすべて削除
    public void DeleteTileAssets(){
        #if UNITY_EDITOR
        string directoryPath = TilesDirectoryPath;
        if (Directory.Exists(directoryPath))
        {
            foreach (string path in Directory.EnumerateFiles(directoryPath))
            {
                string assetPath = GetAssetPath(path);
                if (AssetDatabase.DeleteAsset(assetPath))
                {
                    Debug.Log($"Delete {assetPath}");
                }
                else
                {
                    File.Delete(path);
                    Debug.Log($"Delete {path}");
                }
            }
            Directory.Delete(directoryPath);
            Debug.Log($"Delete {directoryPath}");
        }
        #endif
    }

    // タイルアセットをプロジェクト内に保存
    public void SaveTileAsset(UnityEngine.Tilemaps.Tile tile){
        #if UNITY_EDITOR
        string directoryPath = TilesDirectoryPath;
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
            Debug.Log($"Create directory {directoryPath}");
        }

        string filePath = directoryPath + "/" + tile.name + ".asset";
        if (File.Exists(filePath))
        {
            Debug.LogError($"{filePath} exists.");
        }
        else
        {
            string assetPath = GetAssetPath(filePath);
            AssetDatabase.CreateAsset(tile, assetPath);
            Debug.Log($"Create {assetPath}");
        }
        #endif
    }

そしてTileMapManager2の変更点として、まず宣言部分を

public class TileMapManager2 : MonoBehaviour, ISerializationCallbackReceiver {

とし、

    // ゲーム実行時にタイルを得るため、タイルへの参照をシリアライズして持っておく
    [SerializeField, HideInInspector] private Sprite[] tileKeys;
    [SerializeField, HideInInspector] private Tile[] tileValues;
    private Dictionary<Sprite, Tile> tiles = new Dictionary<Sprite, Tile>();

    public void OnBeforeSerialize()
    {
        tileKeys = tiles.Keys.ToArray();
        tileValues = tiles.Values.ToArray();
    }

    public void OnAfterDeserialize()
    {
        foreach ((Sprite key, Tile value) in tileKeys.Zip(tileValues, (key, value) => (key, value)))
        {
            tiles.Add(key, value);
        }
    }

    // スプライトと対応するタイルを得る
    // tiles内に存在しなければアセットデータベース内を探し、それでも見つからなければ新規にタイルを生成する
    private Tile GetTileForSprite(Sprite sprite) {
        if (!tiles.TryGetValue(sprite, out Tile tile))
        {
            #if UNITY_EDITOR
            string guid = AssetDatabase.FindAssets($"{sprite.name} t:Tile").FirstOrDefault();
            if (string.IsNullOrEmpty(guid))
            {
                tile = ScriptableObject.CreateInstance<Tile>();
                tile.name = sprite.name;
                tile.sprite = sprite;
                jsonStage.SaveTileAsset(tile);
            }
            else
            {
                tile = AssetDatabase.LoadAssetAtPath<Tile>(AssetDatabase.GUIDToAssetPath(guid));
            }
            #else
            // UNITY_EDITORでない状態でこのメソッドが実行されたということは
            // 実機上でのプレイ時であると考えられるため、タイルをアセットデータベースに
            // 保存してやるといった考慮は不要なはず
            // そのため、単純にタイルを生成するだけとする
            tile = ScriptableObject.CreateInstance<Tile>();
            tile.name = sprite.name;
            tile.sprite = sprite;
            #endif
            tiles[sprite] = tile;
        }

        return tile;
    }

を追加、GenerateMapは下記のようにして(大部分は変更なしですので、字数節約のためばっさり省略しました)...

    void GenerateMap(){

        cornerCount1 = 0;
        cornerCount2 = 0;

        // タイルマップのリセット
        // 古いタイルアセットは一旦削除する
        #if UNITY_EDITOR
        jsonStage.DeleteTileAssets();
        tiles.Clear();
        #endif
        ResetTileMap(tilemap);

        Sprite sprite;
        //var bound = tilemapTest.cellBounds;
        var bound = tilemap.cellBounds;

        // タイルをいきなり新規生成するのはやめる
        //Tile tile = ScriptableObject.CreateInstance<Tile>();

        // 省略...「マップの生成」から「タイルマップのセット」のsprite = GetSprite("Sprites/test", spriteName);まで変更なし

                // タイルのスプライトを切り替えながら塗るのではなく、スプライトと対応する
                // 既存のタイルを探して(なければ生成して)、それを使って塗る
                // tile.sprite = sprite;
                Tile tile = GetTileForSprite(sprite);

                // 省略...「角度の設定」以降は変更なし

ResetTileMapも下記のように変えました。

    // サイズを変えずに全てのタイルをリセット
    public void ResetTileMap(Tilemap tilemap){

        /*
        var bound = tilemap.cellBounds;
        var size = tilemap.editorPreviewSize;
        Debug.Log(bound);
        Debug.Log(size);
        tilemap.ClearAllTiles();
        tilemap.cellBounds = bound;
        Debug.Log(bound);
        Debug.Log(size);
        */
        // ここでもやはりUndoレコードにグリッドオブジェクト以下を登録しておき、
        // ダーティーフラグ周りの不整合を防止する(取り消しもできるようになる)
        #if UNITY_EDITOR
        Undo.RegisterFullObjectHierarchyUndo(tilemap.layoutGrid.gameObject, "Modify Map");
        #endif

        var bound = tilemap.cellBounds;
        // 全て削除
        // 「スプライトを持たない新規タイル」をセットするのではなく、単にnullをセットする
        for (int y = bound.max.y - 1; y >= bound.min.y; --y){
            for (int x = bound.min.x; x < bound.max.x; ++x){
                //Tile tile = ScriptableObject.CreateInstance<Tile>();
                Quaternion rot = Quaternion.Euler(0.0f, 0.0f, 0.0f);
                //tilemap.SetTile(new Vector3Int(x, y, 0), tile);
                SetTile(new Vector3Int( x, y, 0 ), rot, tilemap, null);
            }
        }
    }

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/04/26 14:39

    すみません、tilesをDictionary<Sprite, Tile>型にしましたが、これはシリアライズ対象外なのを忘れていました!
    代わりにキーと値を配列化してシリアライズするよう変更しました。

    キャンセル

  • 2020/04/27 15:10

    ご回答が遅くなってしまい申し訳ございません。
    わからない関数など調べながら進めていたため時間がかかってしまいました。
    簡潔な処理や手順までありがとうございます。

    ここまで丁寧にアドバイスもらえたことなかったので感謝しかないです。
    実際、原因だと思っていた場所とタイルがおかしくなる場所違ったので本当に助かりました。

    削除の件についても、参考がなくメチャクチャわかりやすかったです。
    試しにログを出してみたところ、その動きをしておりました。

    キャラの動きなどはほぼできているため、もう少しでリリースできそうです。
    ほとんふぉBongoさんのおかげです・・・。
    本当にありがとうございました。

    キャンセル

0

手作業でタイルマップを描くのを代替する目的でしたら、動的にTileオブジェクトを生成する方法をとるのであれば、そのTileオブジェクトを新規アセットとしてプロジェクト内に保存してやらないとまずいような気がします。
しかも、単一のTileオブジェクトを使い回してスプライトだけ切り替えながら配置していったのでは、タイルマップができあがった直後は正常そうに見えても、タイルマップのリフレッシュが発生した際には、その単一のTileオブジェクトが提示するスプライトが全タイルに供給されてしまうと思われ、勝手に見た目が差し替わってしまいそうです。

もし「すでに各種タイルアセットは作成済みであり、タイルパレットでタイルを選んでタイルマップを描いていくことはできる状態になっているが、手作業で描いていく代わりにJSONで記述した配置図に従ってボタン一発で描いてほしい」というようなシチュエーションならば、JSONに記載するのをスプライト名ではなくタイルアセット名にするというのはどうでしょう。

たとえば仮にJSONの作りを...

  • タイルアセット名の一覧表を記述する。タイルアセット名はマップ記述に使用する記号と対にして並べる(ご質問者さんのように整数値を使用してもかまわないのですが、「何番目が何のタイルなのか脳内置き換えするのはややこしそう」、「タイルのない部分が-1だと字面の自己主張が強くて、何もない感じが薄い」といった個人の感想からこんな風にしてしまいました...)。
  • 前述の記号を使ってマップを記述する。タイルのない部分は空白とし、タイルを裏返す必要がある場合は記号の前に*を付ける(これもやはりご質問者さんの好みに従って自由に決めてかまわないのですが、私の感覚だと「JSONを読んだときにどのタイルが裏返るか視覚的にわかりやすそう」なんて気分になり、こんな感じにしてみました...)。

とすることにして、

{
    "tiles" : [
        "c, TopCorner",
        "T, TopEdge",
        "L, LeftEdge",
        "R, RightEdge",
        "O, Stone",
        "w, Grass",
        "=, Underground",
        "C, BottomCorner",
        "B, BottomEdge"
    ],
    "listMap" : [
        " c, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,*c",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , O, O, O, R",
        " L,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , O, O, O, R",
        " L, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, w, R",
        " L, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, R",
        " L, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, =, R",
        "*C, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, C"
    ]
}

といった感じで記述したとします。
これに対して、マネージャースクリプトを...

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Tilemaps;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class TilemapManager : MonoBehaviour
{
    // 配置図JSONはプロジェクト内にインポートしておき、それをインスペクター上でセットする方式にしました
    // これもただの個人的な気まぐれですので、お好みの方法で読み込んでいいと思います
    public TextAsset sourceJson;

    // 編集対象のタイルマップをここにセットしておく
    public Tilemap tilemap;

    // 配置図上の記号をキーに、タイルアセットを選択するディクショナリー
    private Dictionary<string, Tile> tilePalette;

    // JSONから読み込んだ配置図は入れ子のリストの形にすることにしました
    private List<List<string>> tileKeys;

    // マップ上に配置されているタイルをすべて取り除く
    // クリア後に、クリア前のcellBoundsを復元する
    public bool Clear()
    {
        if (this.tilemap == null)
        {
            Debug.LogError("No tilemap.");
            return false;
        }

        var bounds = this.tilemap.cellBounds;
        this.tilemap.ClearAllTiles();
        this.tilemap.origin = bounds.position;
        this.tilemap.size = bounds.size;
        this.tilemap.ResizeBounds();
        return true;
    }

    // 現在のタイルマップはクリアし、JSONの内容を元にタイルを配置する
    public void PlaceTiles()
    {
        // まずJSONの配置図を入れ子リストに直してやる
        if ((this.tilePalette == null) || (this.tilePalette.Count <= 0) || !this.FillTileKeys() || !this.Clear())
        {
            return;
        }

        // tileKeysが構築されたはずなので、中のタイル記号を列挙して対応するタイルを置いていく
        var bounds = this.tilemap.cellBounds;
        var rowCount = this.tileKeys.Count;
        foreach (var (tileKeyF, x, y) in this.tileKeys.SelectMany(
            (row, y) => row.Select((tileKeyF, x) => (tileKeyF, x, y))))
        {
            // もしキーが*で始まるなら、タイルを裏返す必要がある
            // 裏返しフラグを立て、キーからは頭の*記号を取り除く
            var flip = false;
            var tileKey = tileKeyF;
            if (tileKey.StartsWith("*"))
            {
                flip = true;
                tileKey = tileKey.Substring(1);
            }

            // キーが空白、または未知の文字列なら(タイル名一覧に存在しない記号なら)スキップしてタイルを置かない
            // タイルがnull...つまりタイル名に対応するタイルアセットが見つからなかった場合もスキップ
            if (string.IsNullOrWhiteSpace(tileKey) || !this.tilePalette.TryGetValue(tileKey, out var tile) || (tile == null))
            {
                continue;
            }

            // タイルを置く
            // もし裏返しが必要なら、その座標に変換行列を仕込む
            var position = new Vector3Int(bounds.position.x + x, (bounds.position.y + rowCount) - 1 - y, 0);
            this.tilemap.SetTile(position, tile);
            if (flip)
            {
                this.tilemap.SetTransformMatrix(position, Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(0, 180, 0), Vector3.one));
            }
        }
    }

    #if UNITY_EDITOR
    // sourceJsonにセットされているJSONを読み取ってtilePaletteの内容を置き換える
    public bool LoadTilePalette()
    {
        if (this.sourceJson == null)
        {
            Debug.LogError("No source.");
            return false;
        }

        var tiles = JsonUtility.FromJson<TilemapData>(this.sourceJson.text).tiles;
        if (this.tilePalette == null)
        {
            this.tilePalette = new Dictionary<string, Tile>();
        }

        this.tilePalette.Clear();
        foreach (var pair in tiles.Select(keyAndName => keyAndName.Split(',')))
        {
            var tileKey = pair[0].Trim();
            var tileName = pair[1].Trim();
            var tileGuid = AssetDatabase.FindAssets($"{tileName} t:{typeof(Tile)}").FirstOrDefault();
            var tile = (Tile)null;
            if (string.IsNullOrEmpty(tileGuid))
            {
                Debug.LogError($"Tile '{tileName}' not found.");
            }
            else
            {
                tile = AssetDatabase.LoadAssetAtPath<Tile>(AssetDatabase.GUIDToAssetPath(tileGuid));
            }

            this.tilePalette.Add(tileKey, tile);
        }

        return true;
    }
    #endif

    // sourceJsonにセットされているJSONを読み取ってtileKeysを構築する
    private bool FillTileKeys()
    {
        if (this.sourceJson == null)
        {
            Debug.LogError("No source.");
            return false;
        }

        var listMap = JsonUtility.FromJson<TilemapData>(this.sourceJson.text).listMap;
        var rowCount = listMap.Length;
        if (this.tileKeys == null)
        {
            this.tileKeys = new List<List<string>>();
        }

        while (this.tileKeys.Count < rowCount)
        {
            this.tileKeys.Add(new List<string>());
        }

        if (this.tileKeys.Count > rowCount)
        {
            this.tileKeys.RemoveRange(rowCount, this.tileKeys.Count - rowCount);
        }

        for (var i = 0; i < rowCount; i++)
        {
            var row = this.tileKeys[i];
            row.Clear();
            row.AddRange(listMap[i].Split(',').Select(key => key.Trim()));
        }

        return true;
    }

    [Serializable]
    private class TilemapData
    {
        public string[] tiles;
        public string[] listMap;
    }
}

とし、エディタースクリプトを...

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TilemapManager))]
public class TilemapManagerEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        var tilemapManager = this.target as TilemapManager;
        using (new EditorGUI.DisabledScope(tilemapManager.tilemap == null))
        {
            using (new EditorGUI.DisabledScope(tilemapManager.sourceJson == null))
            {
                if (GUILayout.Button("Place Tiles") && tilemapManager.LoadTilePalette())
                {
                    RecordTilemap(tilemapManager.tilemap, "Place Tiles");
                    tilemapManager.PlaceTiles();
                }
            }

            if (GUILayout.Button("Clear All"))
            {
                RecordTilemap(tilemapManager.tilemap, "Clear All");
                tilemapManager.Clear();
            }
        }
    }

    private static void RecordTilemap(Tilemap tilemap, string name)
    {
        var grid = tilemap.GetComponentInParent<GridLayout>();
        if (grid == null)
        {
            Undo.RegisterCompleteObjectUndo(tilemap, name);
        }
        else
        {
            Undo.RegisterFullObjectHierarchyUndo(grid, name);
        }
    }
}

としてみました。

下図の画像をベースに...

図1

9つのスプライトを作り...

図2

タイルパレットを作ったところ...

図3

プロジェクト中にタイルアセットが生成され...

図4

この状態で「Place Tiles」ボタンを押したところ、下図の形に配置されました。

図5

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/04/25 22:57

    回答が遅くなってしまい申し訳ございません。
    Bongoさんのプログラムで正常に動きました。

    手順まで丁寧にありがとうございました。

    TileMapを使うのが初めてで、書きたいコードが簡潔にかかれてたので非常に勉強になりました。
    本当に感謝しかないです。

    BongoさんのものをSample、私が書いたものをOriginalとしてミニマムプロジェクトを作ってみました。
    こちらで検証したところ、やはり私のソースではタイルマップが壊れてしまうようです。
    この辺、Tileの初期化方法が違ったため、このあたりに不具合の原因がありそうです。

    ①TileMapManager2のJsonLoadを再生時以外に呼ぶと、再生、停止するとTileMapがすべて0番目のものになる
    ※ TileMapManager2はMainCameraにアタッチ

    ②MapDeleteを再生以外で呼ぶと全件消えてくれない、Addでオブジェクトを追加できるように修正
    foreachを使って全件検索しているつもりなのですが、再生していない際はダメなようです。

    流石に甘えすぎているので、お時間があればで構いませんので、ご確認いただけないでしょうか。

    サンプルPG
    https://37.gigafile.nu/0502-d2ce9138fef59c3aca04fe8c4c4e45707

    DLコード
    1234

    キャンセル

  • 2020/04/26 12:11

    プロジェクトご提示ありがとうございます。対処法を検討してみましたがいかがでしょうか(字数制限のため別回答になってしまいましたがご容赦ください...)。
    動作確認をそこまでしっかりやったわけではありませんので、何か見落としがあるかもしれません。変な挙動に気付きましたら、その現象が発生する手順をコメントいただければ調べてみようと思います。

    キャンセル

  • 2020/04/27 15:11

    回答遅くなって申し訳ないです。
    わかりやすいプログラムで助かりました。
    TileMapについては参考記述がほとんど海外なので勉強になりました。
    最後までありがとうございました。

    キャンセル

0

おーー!!!メッチャ詳細な回答ありがとうございます!
前回も回答いただいたのに諸々ありがとうございます。説明省くために抜けてて申し訳ないです。
タイルアセット名は実は吐き出しておりまして。ミニマムのプロジェクトを作って再度貼らせてくださいませ。

ただ頂いたスクリプトに大ヒントがありそうでして、調査させてくださいませ。
ご丁寧に本当にありがとうございます!

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.37%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る