🧰

【Unity】Tilemap分割ツールを開発したよ、見てみて【OSS】

に公開


この画像、Clipchamp を使って数分でできました ありがとう Microsoft

  • リポジトリ(良ければ Star, PR お願いします🙇‍♂️)

https://github.com/SunagimoOisii/TilemapSplitter

どんなツールなのか一言で


分割(分類)対象Tilemap をセットし、プレビュー表示後、実際に分割する例

  • Tilemap を接続関係から分類し、複数の Tilemap として再構成する
  • 分割 Tilemap の Tag, Layer 設定もこれでできる
  • MIT LICENSE

何故作ったのか

  • あるゲーム開発で必要だった
  • タイルごとに Layer 等を設定したいが、設定毎に Tilemap を分けて作るのは面倒
  • ならば、
    1. Tilemap を1つ作成
    2. 複数の Tilemap(を持つ GameObject) に分割
    3. 分割 Tilemap の設定を行う
      という作業の流れにすれば楽になるのではないか…というのがきっかけ

誰に, どんなときに有用なのか

  • 誰に → 2D ゲーム開発者, レベルデザイナー

    • Tilemap 機能を活用してステージやフィールドを作る方
  • どんなときに → タイルの形状によって異なる演出をしたいとき

    • エッジ部分だけにラインを描くなどなど

導入方法

A:UPM を利用する場合

  • Unity エディタ上で Window → Package Manager を開く
  • 左上 + ボタンで Add package from git URL… を選択
  • 次の URL を入力して Add を押す
https://github.com/SunagimoOisii/TilemapSplitter.git?path=/Packages/com.sunagimo.tilemapsplitter

B:手動インストールの場合

  • TilemapSplitter フォルダを Assets 下へ配置

使い方

1:Tilemap を1つ用意する

2:本ツールのウィンドウを開き、設定を行う


Tools → TilemapSplitter を選択でこのウィンドウが開く

  • 以降はこのウィンドウを上から解説する

分割 Tilemap 設定

  • 分割対象をアタッチするだけ

分割後の Tilemap 設定

  • 各項目を分割種類ごとに設定できる
  • 分割種類は縦エッジ, 横エッジ, 交差, T字, 角, 孤立が存在する
具体的にどの種類がどれなのか

縦エッジ(VerticalEdge)

横エッジ(HorizontalEdge)

交差(Cross)

T字(TJunction)

角(Corner)

孤立(Isolate)

各項目の解説

  • Which obj to add to
    • VerticalEdge, HorizontalEdge 以外の種類のみある設定
    • 分割後、どの Tilemap に組み込むか Nothing, Everything, Vertical, Horizontal, Independent から選択可能
    • Nothing はその種類の分割を行わない
    • Everything は Vertical, Horizontal, Independent の全選択
    • Vertical, Horiontal はそれぞれ縦エッジ, 横エッジの Tilemap に組み込まれる
    • Independent はその種類で Tilemap(GameObject) を独立させる
  • Tag:分割後のタグ設定
  • Layer:分割後のレイヤー設定
  • Preview:シーンビューで分割プレビューを適用するかどうか
  • PreviewColor:分割プレビュー時の色の設定

3:分割



設定通りの Tilemap(を持つ GameObject)が生成される

  • ボタンを押して分割実行
  • 処理の間はプログレスバーが表示される
その他 細かい設定

  • Reset Settings:PreviewColor 等を初期状態に戻す
  • Attach Colliders:分割 Tilemap の GameObject に Rigidbody2D, TilemapCollider2D, CompositeCollider2D をアタッチする
  • Merge VerticalEdge, HorizontalEdge:縦, 横エッジを同じ Tilemap に組み込む

使われているプログラム

本ツールを使用するだけであれば、本章より前の解説のみ読むだけでも十分です
本ツールは以下のプログラムで構成されている(2025/07/16 時点)

TilemapSplitterWindow.cs

  • EditorWindow 派生クラスとしてメニュー登録, ウィンドウ表示を担当
  • UIElements を用いて ObjectField, Foldout 等のコントロールを生成, 配置
  • RefreshPreview(), SplitCoroutine() によるプレビュー更新, 分割処理を実装
実際のコード

出典:TilemapSplitterWindow.cs

namespace TilemapSplitter
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEditor.UIElements;
    using UnityEngine;
    using UnityEngine.Tilemaps;
    using UnityEngine.UIElements;

    internal class TilemapSplitterWindow : EditorWindow
    {
        private const string PrefPrefix = "TilemapSplitter.";
        private static string CreateKey(string name) => PrefPrefix + name;

        private Dictionary<ShapeType, ShapeSetting> settingsDict = CreateDefaultSettings();
        private static Dictionary<ShapeType, ShapeSetting> CreateDefaultSettings() => new()
        {
            [ShapeType.VerticalEdge]   = new() { flags = ShapeFlags.VerticalEdge,   previewColor = Color.green  },
            [ShapeType.HorizontalEdge] = new() { flags = ShapeFlags.HorizontalEdge, previewColor = Color.yellow },
            [ShapeType.Cross]          = new() { flags = ShapeFlags.Independent,    previewColor = Color.red    },
            [ShapeType.TJunction]      = new() { flags = ShapeFlags.Independent,    previewColor = Color.blue   },
            [ShapeType.Corner]         = new() { flags = ShapeFlags.Independent,    previewColor = Color.cyan   },
            [ShapeType.Isolate]        = new() { flags = ShapeFlags.Independent,    previewColor = Color.magenta },
        };

        private Foldout verticalEdgeFoldOut;
        private Foldout horizontalEdgeFoldOut;
        private Foldout crossFoldOut;
        private Foldout tJunctionFoldOut;
        private Foldout cornerFoldOut;
        private Foldout isolateFoldOut;

        private Tilemap source;
        private ShapeCells shapeCells = new();
        private readonly TilemapPreviewDrawer previewDrawer = new();

        private bool canMergeEdges       = false;
        private bool canAttachCollider   = false;
        private bool isRefreshingPreview = false;

        [MenuItem("Tools/TilemapSplitter")]
        public static void ShowWindow() => GetWindow<TilemapSplitterWindow>("Split Tilemap");

        private void OnEnable()
        {
            LoadPrefs();
            previewDrawer.Register();
        }

        private void OnDisable()
        {
            SavePrefs();
            previewDrawer.Unregister();
        }

        public void CreateGUI()
        {
            var root = rootVisualElement;

            //Create a ScrollView and a container VisualElement
            var scroll    = new ScrollView();
            var container = new VisualElement();
            container.style.flexDirection = FlexDirection.Column;
            container.style.paddingLeft   = 10;
            container.style.paddingRight  = 10;
            root.Add(scroll);
            scroll.Add(container);

            //Create an ObjectField and HelpBox for the user to select the source Tilemap asset
            var sourceF = new ObjectField("Split Tilemap");
            var hp      = new HelpBox("Select the subject of the division", HelpBoxMessageType.Info);
            hp.visible = (source == null);
            sourceF.objectType = typeof(Tilemap);
            sourceF.value      = source;
            sourceF.RegisterValueChangedCallback(evt =>
            {
                source = evt.newValue as Tilemap;
                hp.visible = (source == null);
                RefreshPreview();
            });
            container.Add(sourceF);
            container.Add(hp);

            AddHorizontalSeparator(container);

            //Create Split Settings Button
            var resetB = new Button(() =>
            {
                ResetPrefs();
                root.Clear();
                CreateGUI();
            });
            resetB.text            = "Reset Settings";
            resetB.style.marginTop = 5;
            container.Add(resetB);

            //Create Colliders Attach Button
            var attachT = new Toggle("Attach Colliders");
            attachT.value = canAttachCollider;
            attachT.RegisterValueChangedCallback(evt => canAttachCollider = evt.newValue);
            container.Add(attachT);

            //Create Vertical, Horizontal Edge Shape Settings UI
            var mergeT  = new Toggle("Merge VerticalEdge, HorizontalEdge");
            var mergeHB = new HelpBox("When merging, VerticalEdge shapeSettings take precedence",
                HelpBoxMessageType.Info);
            mergeT.value = canMergeEdges;
            mergeT.RegisterValueChangedCallback(evt => canMergeEdges = evt.newValue);
            container.Add(mergeT);
            container.Add(mergeHB);

            //Create Split Each Shape Settings UI
            var infos = new (ShapeType type, string title)[]
            {
                (ShapeType.VerticalEdge,   "VerticalEdge"),
                (ShapeType.HorizontalEdge, "HorizontalEdge"),
                (ShapeType.Cross,          "Cross"),
                (ShapeType.TJunction,      "T-Junction"),
                (ShapeType.Corner,         "Corner"),
                (ShapeType.Isolate,        "Isolate")
            };
            foreach (var info in infos)
            {
                var fold = CreateFoldout(container, info.type, info.title);
                switch (info.type)
                {
                    case ShapeType.VerticalEdge:   verticalEdgeFoldOut   = fold; break;
                    case ShapeType.HorizontalEdge: horizontalEdgeFoldOut = fold; break;
                    case ShapeType.Cross:          crossFoldOut          = fold; break;
                    case ShapeType.TJunction:      tJunctionFoldOut      = fold; break;
                    case ShapeType.Corner:         cornerFoldOut         = fold; break;
                    case ShapeType.Isolate:        isolateFoldOut        = fold; break;
                }
                AddHorizontalSeparator(container);
            }

            //Add the Execute Splitting button at the bottom of the UI
            var splitB = new Button(() =>
            {
                if (source == null)
                {
                    EditorUtility.DisplayDialog("Error", "The split target isn't set", "OK");
                    return;
                }
                StartCoroutine(SplitCoroutine());
            });
            splitB.text            = "Execute Splitting";
            splitB.style.marginTop = 10;
            container.Add(splitB);

            previewDrawer.Setup(source, settingsDict);
        }

        private static void AddHorizontalSeparator(VisualElement parentContainer)
        {
            var separator = new VisualElement();
            separator.style.borderBottomWidth = 1;
            separator.style.borderBottomColor = Color.gray;
            separator.style.marginTop         = 5;
            separator.style.marginBottom      = 5;

            parentContainer.Add(separator);
        }

        private Foldout CreateFoldout(VisualElement parentContainer, ShapeType type, string title)
        {
            var fold = new Foldout();
            fold.text                          = title;
            fold.style.unityFontStyleAndWeight = FontStyle.Bold;

            var setting = settingsDict[type];
            if (type == ShapeType.VerticalEdge ||
                type == ShapeType.HorizontalEdge)
            {
                AddShapeSettingControls(fold, setting);
            }
            else
            {
                var enumF = new EnumFlagsField("Which obj to add to", setting.flags);
                fold.Add(enumF);

                Toggle previewT    = null;
                ColorField colorF  = null;
                (previewT, colorF) = AddShapeSettingControls(fold, setting);

                enumF.RegisterValueChangedCallback(evt =>
                {
                    setting.flags = (ShapeFlags)evt.newValue;
                    RefreshPreview();
                    RefreshFoldoutUI(setting, fold, previewT, colorF);
                });

                RefreshFoldoutUI(setting, fold, previewT, colorF);
            }

            parentContainer.Add(fold);
            return fold;
        }

        private (Toggle previewToggle, ColorField colorField) AddShapeSettingControls(Foldout fold,
            ShapeSetting setting)
        {
            //Create controls and register callbacks
            var layerF   = new LayerField("Layer", setting.layer);
            var tagF     = new TagField("Tag", setting.tag);
            var previewT = new Toggle("Preview") { value = setting.canPreview };
            var colF     = new ColorField("Preview Color") { value = setting.previewColor };
            layerF.RegisterValueChangedCallback(evt => setting.layer = evt.newValue);
            tagF.RegisterValueChangedCallback(evt => setting.tag = evt.newValue);
            previewT.RegisterValueChangedCallback(evt =>
            {
                setting.canPreview = evt.newValue;
                RefreshPreview();
            });
            colF.RegisterValueChangedCallback(evt => setting.previewColor = evt.newValue);

            //Add creation controls to foldout
            fold.Add(layerF);
            fold.Add(tagF);
            fold.Add(previewT);
            fold.Add(colF);

            return (previewT, colF);
        }

        private void RefreshFoldoutUI(ShapeSetting setting, Foldout fold,
            Toggle previewToggle, ColorField colField)
        {
            var opt = setting.flags;

            var exist = fold.Q<HelpBox>();
            if (exist != null) fold.Remove(exist);

            string msg   = null;
            string helpV = "The canPreview complies with VerticalEdge shapeSettings.";
            string helpH = "The canPreview complies with HorizontalEdge shapeSettings.";
            if      (opt.HasFlag(ShapeFlags.VerticalEdge))   msg = helpV;
            else if (opt.HasFlag(ShapeFlags.HorizontalEdge)) msg = helpH;

            if (string.IsNullOrEmpty(msg) == false)
            {
                fold.Add(new HelpBox(msg, HelpBoxMessageType.Info));
            }

            bool isVisible = opt.HasFlag(ShapeFlags.Independent);
            previewToggle.visible = isVisible;
            colField.visible      = isVisible;
        }

        private static void StartCoroutine(IEnumerator e)
        {
            EditorApplication.update += Update;

            void Update()
            {
                if (e.MoveNext() == false) EditorApplication.update -= Update;
            }
        }

        private IEnumerator SplitCoroutine()
        {
            shapeCells = new ShapeCells();
            var  e = TileShapeClassifier.ClassifyCoroutine(source, settingsDict, shapeCells);

            while (e.MoveNext()) yield return null;

            TilemapCreator.GenerateSplitTilemaps(source, shapeCells, settingsDict,
                canMergeEdges, canAttachCollider);
            RefreshPreview();
        }

        private void RefreshPreview()
        {
            if (source == null || isRefreshingPreview) return;
            StartCoroutine(RefreshPreviewCoroutine());

            IEnumerator RefreshPreviewCoroutine()
            {
                isRefreshingPreview = true;

                shapeCells = new ShapeCells();
                var e = TileShapeClassifier.ClassifyCoroutine(source, settingsDict, shapeCells);
                while (e.MoveNext())
                {
                    yield return null;
                }

                previewDrawer.Setup(source, settingsDict);
                previewDrawer.SetShapeCells(shapeCells);
                SceneView.RepaintAll();
                UpdateFoldoutTitles();

                isRefreshingPreview = false;
            }
        }

        private void UpdateFoldoutTitles()
        {
            var list = new (Foldout f, string name, int count)[]
            {
                (verticalEdgeFoldOut,   "VerticalEdge",   shapeCells.VerticalCells.Count),
                (horizontalEdgeFoldOut, "HorizontalEdge", shapeCells.HorizontalCells.Count),
                (crossFoldOut,          "CrossCells",     shapeCells.CrossCells.Count),
                (tJunctionFoldOut,      "TJunctionCells", shapeCells.TJunctionCells.Count),
                (cornerFoldOut,         "CornerCells",    shapeCells.CornerCells.Count),
                (isolateFoldOut,        "IsolateCells",   shapeCells.IsolateCells.Count),
            };
            foreach (var (f, name, count) in list)
            {
                f.text = $"{name} (Count:{count})";
            }
        }

        #region Saving and Loading Split Settings using EditorPrefs

        private void SavePrefs()
        {
            if (source != null)
            {
                string path = AssetDatabase.GetAssetPath(source);
                EditorPrefs.SetString(CreateKey("SourcePath"), path);
            }
            else
            {
                EditorPrefs.DeleteKey(CreateKey("SourcePath"));
            }

            EditorPrefs.SetBool(CreateKey("CanMergeEdges"),  canMergeEdges);
            EditorPrefs.SetBool(CreateKey("AttachCollider"), canAttachCollider);

            foreach (var kv in settingsDict)
            {
                string name = kv.Key.ToString();
                var setting = kv.Value;
                EditorPrefs.SetInt(CreateKey($"{name}.Flags"), (int)setting.flags);
                EditorPrefs.SetInt(CreateKey($"{name}.Layer"), setting.layer);
                EditorPrefs.SetString(CreateKey($"{name}.Tag"), setting.tag);
                EditorPrefs.SetBool(CreateKey($"{name}.CanPreview"), setting.canPreview);
                EditorPrefs.SetString(CreateKey($"{name}.Color"),
                    ColorUtility.ToHtmlStringRGBA(setting.previewColor));
            }
        }

        private void LoadPrefs()
        {
            if (EditorPrefs.HasKey(CreateKey("SourcePath")))
            {
                var path = EditorPrefs.GetString(CreateKey("SourcePath"));
                source   = AssetDatabase.LoadAssetAtPath<Tilemap>(path);
            }

            canMergeEdges     = EditorPrefs.GetBool(CreateKey("CanMergeEdges"), canMergeEdges);
            canAttachCollider = EditorPrefs.GetBool(CreateKey("AttachCollider"), canAttachCollider);

            foreach (var kv in settingsDict)
            {
                var name    = kv.Key.ToString();
                var setting = kv.Value;
                setting.flags      = (ShapeFlags)EditorPrefs.GetInt(CreateKey($"{name}.Flags"), (int)setting.flags);
                setting.layer      = EditorPrefs.GetInt(CreateKey($"{name}.Layer"), setting.layer);
                setting.tag        = EditorPrefs.GetString(CreateKey($"{name}.Tag"), setting.tag);
                setting.canPreview = EditorPrefs.GetBool(CreateKey($"{name}.CanPreview"), setting.canPreview);
                string col = EditorPrefs.GetString(CreateKey($"{name}.Color"), ColorUtility.ToHtmlStringRGBA(setting.previewColor));
                if (ColorUtility.TryParseHtmlString("#" + col, out var c))
                {
                    setting.previewColor = c;
                }
            }
        }

        private void ResetPrefs()
        {
            EditorPrefs.DeleteKey(CreateKey("SourcePath"));
            EditorPrefs.DeleteKey(CreateKey("CanMergeEdges"));
            EditorPrefs.DeleteKey(CreateKey("AttachCollider"));

            foreach (ShapeType t in Enum.GetValues(typeof(ShapeType)))
            {
                string name = t.ToString();
                EditorPrefs.DeleteKey(CreateKey($"{name}.Flags"));
                EditorPrefs.DeleteKey(CreateKey($"{name}.Layer"));
                EditorPrefs.DeleteKey(CreateKey($"{name}.Tag"));
                EditorPrefs.DeleteKey(CreateKey($"{name}.CanPreview"));
                EditorPrefs.DeleteKey(CreateKey($"{name}.Color"));
            }

            settingsDict      = CreateDefaultSettings();
            source            = null;
            canMergeEdges     = false;
            canAttachCollider = false;
        }

        #endregion
    }
}

TileShapeClassifier.cs

  • 分割 Tilemap の各セルを縦エッジ, 横エッジ, 角, 交差, T 字, 孤立に分類
  • ClassifyCoroutine() で隣接セル判定を行い、ShapeFlags を適用
  • 分類結果を ShapeCells に書き込むロジックを提供
実際のコード

出典:TileShapeClassifier.cs

namespace TilemapSplitter
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.Tilemaps;

    [Flags]
    internal enum ShapeFlags
    {
        VerticalEdge   = 1 << 0,
        HorizontalEdge = 1 << 1,
        Independent    = 1 << 2,
    }

    internal enum ShapeType
    {
        VerticalEdge = 0,
        HorizontalEdge,
        Cross,
        TJunction,
        Corner,
        Isolate,
    }

    internal class ShapeSetting
    {
        public ShapeFlags flags;
        public int        layer;
        public string     tag        = "Untagged";
        public bool       canPreview = true;
        public Color      previewColor;
    }

    /// <summary>
    /// Store cell coordinates for each tile classification
    /// </summary>
    internal class ShapeCells
    {
        public readonly List<Vector3Int> VerticalCells   = new();
        public readonly List<Vector3Int> HorizontalCells = new();
        public readonly List<Vector3Int> CrossCells      = new();
        public readonly List<Vector3Int> TJunctionCells  = new();
        public readonly List<Vector3Int> CornerCells     = new();
        public readonly List<Vector3Int> IsolateCells    = new();
    }

    internal static class TileShapeClassifier
    {
        /// <summary>
        /// Compress the tilemap bounds to exclude empty rows and columns
        /// </summary>
        public static IEnumerator ClassifyCoroutine(Tilemap source, 
            Dictionary<ShapeType, ShapeSetting> settings, ShapeCells sc, int batch = 100)
        {
            sc.VerticalCells.Clear();
            sc.HorizontalCells.Clear();
            sc.CrossCells.Clear();
            sc.TJunctionCells.Clear();
            sc.CornerCells.Clear();
            sc.IsolateCells.Clear();

            //Compress the Tilemap’s cellBounds to skip empty rows and columns
            source.CompressBounds();

            //Get the bounding box in cell coordinates and retrieve all tiles inside it(empty slots = null)
            var cellBounds    = source.cellBounds;
            var tilesInBounds = source.GetTilesBlock(cellBounds);

            //Only cells containing tiles are stored in the collection
            int width  = cellBounds.size.x;
            int height = cellBounds.size.y;
            var occupiedCells = new HashSet<Vector3Int>();

            try
            {
                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                    {
                        int index = x + y * width;
                        if (tilesInBounds[index] == null) continue;

                        //Calculate the world-space offset from the lower-left cell to origin
                        var cell = new Vector3Int(cellBounds.xMin + x,
                                                  cellBounds.yMin + y,
                                                  cellBounds.zMin);
                        occupiedCells.Add(cell);
                    }

                    if (y % batch == 0)
                    {
                        float progress = (float)(y * width) / (width * height);
                        EditorUtility.DisplayProgressBar("Classify", "Collecting cells...", progress);
                        yield return null;
                    }
                }

                int total     = occupiedCells.Count;
                int processed = 0;
                foreach (var cell in occupiedCells)
                {
                    //Perform proximity determination for each cell
                    ClassifyCellNeighbors(cell, occupiedCells, settings, sc);

                    processed++;
                    if (processed % batch == 0)
                    {
                        float progress = (float)processed / total;
                        EditorUtility.DisplayProgressBar("Classify",
                            $"Classifying... {processed}/{total}", progress);
                        yield return null;
                    }
                }
            }
            finally
            {
                EditorUtility.ClearProgressBar();
            }
        }

        /// <summary>
        /// Classify the specified cell based on the four neighbouring cells
        /// </summary>
        private static void ClassifyCellNeighbors(Vector3Int cell, HashSet<Vector3Int> cells,
            Dictionary<ShapeType, ShapeSetting> settings, ShapeCells sc)
        {
            //Determine whether adjacent cells exist
            bool up    = cells.Contains(cell + Vector3Int.up);
            bool down  = cells.Contains(cell + Vector3Int.down);
            bool left  = cells.Contains(cell + Vector3Int.left);
            bool right = cells.Contains(cell + Vector3Int.right);
            bool anyV  = up || down;
            bool anyH  = left || right;

            //Add to collection by Classification
            int neighborCount  = (up ? 1 : 0) + (down ? 1 : 0) + (left ? 1 : 0) + (right ? 1 : 0);
            switch (neighborCount)
            {
                case 4: //Cross
                    ApplyShapeFlags(cell, settings[ShapeType.Cross].flags, sc, sc.CrossCells);
                    break;
                case 3: //TJunction
                    ApplyShapeFlags(cell, settings[ShapeType.TJunction].flags, sc, sc.TJunctionCells);
                    break;
                case 2 when anyV && anyH: //Corner
                    ApplyShapeFlags(cell, settings[ShapeType.Corner].flags, sc, sc.CornerCells);
                    break;
                default:
                    if (anyV && anyH == false) //Vertical
                    {
                        sc.VerticalCells.Add(cell);
                    }
                    else if (anyH && anyV == false) //Horizontal
                    {
                        sc.HorizontalCells.Add(cell);
                    }
                    else if (neighborCount == 0) //Isolate
                    {
                        ApplyShapeFlags(cell, settings[ShapeType.Isolate].flags, sc, sc.IsolateCells);
                    }
                    break;
            }
        }

        /// <summary>
        /// Add to each collection according to the settings
        /// </summary>
        private static void ApplyShapeFlags(Vector3Int cell, ShapeFlags flags,
            ShapeCells sc, List<Vector3Int> indepCells)
        {
            if (flags.HasFlag(ShapeFlags.VerticalEdge))   sc.VerticalCells?.Add(cell);
            if (flags.HasFlag(ShapeFlags.HorizontalEdge)) sc.HorizontalCells?.Add(cell);
            if (flags.HasFlag(ShapeFlags.Independent))    indepCells?.Add(cell);
        }
    }
}

TilemapPreviewDrawer.cs

  • シーンビュー上にプレビューを描画
  • Setup() で分割 Tilemap と ShapeSetting を受け取る
  • Register(), Unregister() で描画ハンドラを管理
  • SetShapeCells() で分類結果を保持、OnSceneGUI() で各タイルのプレビュー描画
実際のコード

出典:TilemapPreviewDrawer.cs

namespace TilemapSplitter
{
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.Tilemaps;

    /// <summary>
    /// Draws color-coded previews of classified tiles on the SceneView
    /// </summary>
    internal class TilemapPreviewDrawer
    {
        private Tilemap tilemap;
        private Dictionary<ShapeType, ShapeSetting> shapeSettings;
        private ShapeCells shapeCells;

        public void Setup(Tilemap source, Dictionary<ShapeType, ShapeSetting> settings)
        {
            tilemap       = source;
            shapeSettings = settings;
        }

        public void SetShapeCells(ShapeCells sc) => shapeCells = sc;

        public void Register() =>   SceneView.duringSceneGui += OnSceneGUI;
        public void Unregister() => SceneView.duringSceneGui -= OnSceneGUI;

        private void OnSceneGUI(SceneView sv)
        {
            if (tilemap == null || shapeCells == null) return;

            //Read preview settings(color, visibility) for each tile classification
            var v       = shapeSettings[ShapeType.VerticalEdge];
            var h       = shapeSettings[ShapeType.HorizontalEdge];
            var cross   = shapeSettings[ShapeType.Cross];
            var t       = shapeSettings[ShapeType.TJunction];
            var corner  = shapeSettings[ShapeType.Corner];
            var isolate = shapeSettings[ShapeType.Isolate];

            //Draw each cell only if its preview flag is enabled, using the specified preview color
            var previewSettings = new (List<Vector3Int> cells, Color c, bool canPreview)[]
            {
                (shapeCells.VerticalCells,   v.previewColor,       v.canPreview),
                (shapeCells.HorizontalCells, h.previewColor,       h.canPreview),
                (shapeCells.CrossCells,      cross.previewColor,   cross.canPreview),
                (shapeCells.TJunctionCells,  t.previewColor ,      t.canPreview),
                (shapeCells.CornerCells,     corner.previewColor,  corner.canPreview),
                (shapeCells.IsolateCells,    isolate.previewColor, isolate.canPreview)
            };
            foreach (var (cells, c, canPreview) in previewSettings)
            {
                if (canPreview) DrawCellPreviews(cells, c);
            }
        }

        private void DrawCellPreviews(List<Vector3Int> cells, Color c)
        {
            if (cells == null || cells.Count == 0) return;

            Handles.color = new Color(c.r, c.g, c.b, 0.4f);
            var cellSize  = tilemap.cellSize;
            foreach (var cell in cells)
            {
                var worldPos = tilemap.CellToWorld(cell) + new Vector3(cellSize.x / 2f, cellSize.y / 2f);
                var rect = new Rect(
                    worldPos.x - cellSize.x / 2f,
                    worldPos.y - cellSize.y / 2f,
                    cellSize.x,
                    cellSize.y);
                Handles.DrawSolidRectangleWithOutline(rect, Handles.color, Color.clear);
            }
        }
    }
}

TilemapCreator.cs

  • 分類済みセル情報に基づき、各形状ごとに新規 Tilemap(と GameObject) 生成
  • CreateTilemapObjForCells() でタイル転写, レイヤーとタグ設定, コライダー付与実装
  • GenerateSplitTilemaps() で全体の生成フロー統括
実際のコード

出典:TilemapCreator.cs

namespace TilemapSplitter
{
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.Tilemaps;

    internal static class TilemapCreator
    {
        private const string VerticalObjName   = "VerticalEdge";
        private const string HorizontalObjName = "HorizontalEdge";
        private const string MergeObjName      = "EdgeTiles";
        private const string CrossObjName      = "CrossTiles";
        private const string TJunctionObjName  = "TJunctionTiles";
        private const string CornerObjName     = "CornerTiles";
        private const string IsolateObjName    = "IsolateTiles";

        public static void GenerateSplitTilemaps(Tilemap source, ShapeCells sc,
            Dictionary<ShapeType, ShapeSetting> settings, bool mergeEdges, bool canAttachCollider)
        {
            if (mergeEdges)
            {
                var mergedCells = new List<Vector3Int>(sc.VerticalCells);
                var v           = settings[ShapeType.VerticalEdge];
                v.flags         = ShapeFlags.Independent;
                mergedCells.AddRange(sc.HorizontalCells);
                CreateTilemapObjForCells(source, mergedCells, v, MergeObjName, canAttachCollider);
            }
            else
            {
                var v = settings[ShapeType.VerticalEdge];
                var h = settings[ShapeType.HorizontalEdge];
                CreateTilemapObjForCells(source, sc.VerticalCells,   v, VerticalObjName,   canAttachCollider);
                CreateTilemapObjForCells(source, sc.HorizontalCells, h, HorizontalObjName, canAttachCollider);
            }

            var cross   = settings[ShapeType.Cross];
            var t       = settings[ShapeType.TJunction];
            var corner  = settings[ShapeType.Corner];
            var isolate = settings[ShapeType.Isolate];
            CreateTilemapObjForCells(source, sc.CrossCells,     cross,   CrossObjName,     canAttachCollider);
            CreateTilemapObjForCells(source, sc.TJunctionCells, t,       TJunctionObjName, canAttachCollider);
            CreateTilemapObjForCells(source, sc.CornerCells,    corner,  CornerObjName,    canAttachCollider);
            CreateTilemapObjForCells(source, sc.IsolateCells,   isolate, IsolateObjName,   canAttachCollider);
        }

        private static void CreateTilemapObjForCells(Tilemap source,
            List<Vector3Int> cells, ShapeSetting setting, string name, bool canAttachCollider)
        {
            if (cells == null || cells.Count == 0) return;

            //Skip instantiating this tile collection when the Independent flag is not enabled in settings
            bool isRequiredIndependentFlag = name == CrossObjName  || name == TJunctionObjName ||
                                             name == CornerObjName || name == IsolateObjName;
            if (isRequiredIndependentFlag &&
                setting.flags.HasFlag(ShapeFlags.Independent) == false) return;

            //Instantiate a GameObject with Tilemap and TilemapRenderer components attached
            //Copy the source transform(position, rotation, scale) and apply the specified layer and tag
            var obj = new GameObject(name, typeof(Tilemap), typeof(TilemapRenderer));
            obj.layer                = setting.layer;
            obj.tag                  = setting.tag;
            obj.transform.localScale = source.transform.localScale;
            obj.transform.SetLocalPositionAndRotation(source.transform.localPosition,
                source.transform.localRotation);
            obj.transform.SetParent(source.transform.parent, false);

            //If there is a TilemapRenderer in the source, match its settings.
            var renderer = obj.GetComponent<TilemapRenderer>();
            if (source.TryGetComponent<TilemapRenderer>(out var oriRenderer))
            {
                renderer.sortingLayerID = oriRenderer.sortingLayerID;
                renderer.sortingOrder = oriRenderer.sortingOrder;
            }
            else
            {
                Debug.LogWarning("Since TilemapRenderer is not attached to the split target, " +
                    "the TilemapRenderer of the generated object was generated with the default shapeSettings.");
            }

            //Transfer tile data(sprite, color, transform matrix) from the original to the new tile
            var tm = obj.GetComponent<Tilemap>();
            foreach (var cell in cells)
            {
                tm.SetTile(cell,  source.GetTile(cell));
                tm.SetColor(cell, source.GetColor(cell));
                tm.SetTransformMatrix(cell, source.GetTransformMatrix(cell));
            }

            if (canAttachCollider)
            {
                var tmCol             = obj.AddComponent<TilemapCollider2D>();
                tmCol.usedByComposite = true;

                var rb = obj.AddComponent<Rigidbody2D>();
                rb.bodyType = RigidbodyType2D.Static;

                obj.AddComponent<CompositeCollider2D>();
            }

            Undo.RegisterCreatedObjectUndo(obj, "GenerateSplitTilemaps " + name);
        }
    }
}

処理の流れ

1:起動とGUI

主に関係するクラス

  • TilemapSplitterWindow
    • ShowWindow():メニュー項目からウィンドウを起動
    • OnEnable(), OnDisable():プリファレンス読み込みとプレビュードロワー登録解除
    • CreateGUI():各種コントロールを生成・配置

2:プレビュー更新

主に関係するクラス

  • TilemapSplitterWindow

    • RefreshPreview(), RefreshPreviewCoroutine():セル分類, プレビュー再描画
    • StartCoroutine(…):エディタ更新ループにコルーチン登録
  • TileShapeClassifier

    • ClassifyCoroutine(Tilemap, …):セルごとに形状分類を行い ShapeCells を埋める
  • TilemapPreviewDrawer

    • Setup(Tilemap, Dictionary<ShapeType,ShapeSetting>):プレビュー設定受け取り
    • Register(), Unregister()SceneView.duringSceneGui に描画ハンドラ登録解除
    • SetShapeCells(ShapeCells):分類結果を保持
    • OnSceneGUI(…)DrawCellPreviews(…) 経由でセルを色塗り描画

3:分割処理

主に関係するクラス

  • TilemapSplitterWindow

    • SplitCoroutine():分割用コルーチン起動(分類 → 生成 → プレビュー更新)
  • TileShapeClassifier

    • ClassifyCoroutine(…):再度分類を実行
  • TilemapCreator

    • GenerateSplitTilemaps(…):各形状ごとに新規 Tilemap(とGameObject) を生成
    • CreateTilemapObjForCells(…):個別オブジェクトの設定, タイル転写, コライダー付与

このツールの伸びしろ

CellLayout が Rectangle 以外の対応(2025/08/06 対応済み)

  • 全 CellLayout に対応

  • しかし、Isometric, Isometric Z as Y だけは描画が分割前から変わってしまう

プレビュー描画の最適化

  • 現状、各セルの矩形を毎フレーム Handles.DrawSolidRectangleWithOutline で描画
  • ハンドル API をセル数分使う代わりに、メッシュでまとめて描画してしまうとか…?

改めて

https://github.com/SunagimoOisii/TilemapSplitter

余談

  • これっていわゆる OSS 活動にあたるのか

Discussion