🧰
【Unity】Tilemap分割ツールを開発したよ、見てみて【OSS】
この画像、Clipchamp を使って数分でできました ありがとう Microsoft
- リポジトリ(良ければ Star, PR お願いします🙇♂️)
どんなツールなのか一言で
分割(分類)対象Tilemap をセットし、プレビュー表示後、実際に分割する例
- Tilemap を接続関係から分類し、複数の Tilemap として再構成する
- 分割 Tilemap の Tag, Layer 設定もこれでできる
- MIT LICENSE
何故作ったのか
- あるゲーム開発で必要だった
- タイルごとに Layer 等を設定したいが、設定毎に Tilemap を分けて作るのは面倒
- ならば、
- Tilemap を1つ作成
- 複数の Tilemap(を持つ GameObject) に分割
- 分割 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()
によるプレビュー更新, 分割処理を実装
実際のコード
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
に書き込むロジックを提供
実際のコード
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()
で各タイルのプレビュー描画
実際のコード
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()
で全体の生成フロー統括
実際のコード
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 をセル数分使う代わりに、メッシュでまとめて描画してしまうとか…?
改めて
余談
- これっていわゆる OSS 活動にあたるのか
Discussion