[Add] WorldAutotile authoring pipeline

This commit is contained in:
2026-03-29 09:58:59 +07:00
parent 456fe76e86
commit e2dab2208a
9 changed files with 938 additions and 219 deletions
@@ -0,0 +1,75 @@
namespace InfiniteWorld.Editor
{
internal static class WorldAutotileEditorLabels
{
public static string GetShapeLabel(AutoTileShape shape)
{
switch (shape)
{
case AutoTileShape.Center:
return "Center";
case AutoTileShape.Top:
return "Top";
case AutoTileShape.Right:
return "Right";
case AutoTileShape.Bottom:
return "Bottom";
case AutoTileShape.Left:
return "Left";
case AutoTileShape.OuterTopLeft:
return "Outer Top Left";
case AutoTileShape.OuterTopRight:
return "Outer Top Right";
case AutoTileShape.OuterBottomRight:
return "Outer Bottom Right";
case AutoTileShape.OuterBottomLeft:
return "Outer Bottom Left";
case AutoTileShape.InnerTopLeft:
return "Inner Top Left";
case AutoTileShape.InnerTopRight:
return "Inner Top Right";
case AutoTileShape.InnerBottomRight:
return "Inner Bottom Right";
case AutoTileShape.InnerBottomLeft:
return "Inner Bottom Left";
default:
return shape.ToString();
}
}
public static string GetPropertyName(AutoTileShape shape)
{
switch (shape)
{
case AutoTileShape.Center:
return "center";
case AutoTileShape.Top:
return "top";
case AutoTileShape.Right:
return "right";
case AutoTileShape.Bottom:
return "bottom";
case AutoTileShape.Left:
return "left";
case AutoTileShape.OuterTopLeft:
return "outerTopLeft";
case AutoTileShape.OuterTopRight:
return "outerTopRight";
case AutoTileShape.OuterBottomRight:
return "outerBottomRight";
case AutoTileShape.OuterBottomLeft:
return "outerBottomLeft";
case AutoTileShape.InnerTopLeft:
return "innerTopLeft";
case AutoTileShape.InnerTopRight:
return "innerTopRight";
case AutoTileShape.InnerBottomRight:
return "innerBottomRight";
case AutoTileShape.InnerBottomLeft:
return "innerBottomLeft";
default:
return string.Empty;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7f9b9a3347cfc82428971840cb651f99
@@ -0,0 +1,642 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.Tilemaps;
namespace InfiniteWorld.Editor
{
[InitializeOnLoad]
public static class WorldAutotileProfilePipeline
{
private const string AuthoringLayoutFolder = "Assets/Editor/Generated/WorldAutotileAuthoring";
private static readonly AutoTileShape[] WallShapeOrder =
{
AutoTileShape.OuterTopLeft,
AutoTileShape.Top,
AutoTileShape.OuterTopRight,
AutoTileShape.Left,
AutoTileShape.Center,
AutoTileShape.Right,
AutoTileShape.OuterBottomLeft,
AutoTileShape.Bottom,
AutoTileShape.OuterBottomRight,
AutoTileShape.InnerTopLeft,
AutoTileShape.InnerTopRight,
AutoTileShape.InnerBottomLeft,
AutoTileShape.InnerBottomRight
};
private static readonly Vector3Int[] WallShapePositions =
{
new Vector3Int(0, 4, 0),
new Vector3Int(1, 4, 0),
new Vector3Int(2, 4, 0),
new Vector3Int(0, 3, 0),
new Vector3Int(1, 3, 0),
new Vector3Int(2, 3, 0),
new Vector3Int(0, 2, 0),
new Vector3Int(1, 2, 0),
new Vector3Int(2, 2, 0),
new Vector3Int(0, 1, 0),
new Vector3Int(2, 1, 0),
new Vector3Int(0, 0, 0),
new Vector3Int(2, 0, 0)
};
private static readonly HashSet<string> PendingProfilePaths = new HashSet<string>();
private static readonly MethodInfo SetIconForObjectMethod = typeof(EditorGUIUtility).GetMethod("SetIconForObject", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
private static bool refreshQueued;
static WorldAutotileProfilePipeline()
{
AssemblyReloadEvents.beforeAssemblyReload += ClearQueue;
EditorApplication.projectChanged += QueueAllProfilesRefresh;
}
public static void QueueProfileRefresh(WorldAutotileProfile profile)
{
if (profile == null)
{
return;
}
string path = AssetDatabase.GetAssetPath(profile);
if (string.IsNullOrEmpty(path))
{
return;
}
PendingProfilePaths.Add(path);
if (refreshQueued)
{
return;
}
refreshQueued = true;
EditorApplication.delayCall += ProcessPendingProfiles;
}
public static string GetAuthoringLayoutPath(WorldAutotileProfile profile)
{
return AuthoringLayoutFolder + "/" + profile.name + "_AuthoringLayout.prefab";
}
public static GameObject LoadAuthoringLayoutAsset(WorldAutotileProfile profile)
{
if (profile == null)
{
return null;
}
return AssetDatabase.LoadAssetAtPath<GameObject>(GetAuthoringLayoutPath(profile));
}
public static void GenerateAuthoringLayout(WorldAutotileProfile profile, bool pingAsset = true)
{
if (profile == null)
{
return;
}
EnsureFolderExists(AuthoringLayoutFolder);
string path = GetAuthoringLayoutPath(profile);
DeleteExistingAuthoringLayout(path);
GameObject root = BuildAuthoringLayoutRoot(profile);
PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
if (!pingAsset)
{
return;
}
GameObject layout = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (layout != null)
{
EditorGUIUtility.PingObject(layout);
Selection.activeObject = layout;
}
}
public static bool BuildProfileFromAuthoringLayout(WorldAutotileProfile profile, bool pingProfile = true)
{
if (profile == null)
{
return false;
}
string path = GetAuthoringLayoutPath(profile);
if (!System.IO.File.Exists(path))
{
Debug.LogWarning($"Authoring layout was not found for profile '{profile.name}' at '{path}'.");
return false;
}
GameObject prefabRoot = PrefabUtility.LoadPrefabContents(path);
try
{
Undo.RecordObject(profile, "Build World Autotile Profile From Layout");
if (!TryBuildProfileFromLayout(prefabRoot, profile, out string error))
{
Debug.LogError(error);
return false;
}
EditorUtility.SetDirty(profile);
AssetDatabase.SaveAssets();
if (pingProfile)
{
EditorGUIUtility.PingObject(profile);
Selection.activeObject = profile;
}
if (profile.autoRefreshGeneratedWorld && Application.isPlaying)
{
RefreshActiveGenerators(profile);
}
return true;
}
finally
{
PrefabUtility.UnloadPrefabContents(prefabRoot);
}
}
private static void ProcessPendingProfiles()
{
refreshQueued = false;
if (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
if (PendingProfilePaths.Count > 0)
{
refreshQueued = true;
EditorApplication.delayCall += ProcessPendingProfiles;
}
return;
}
string[] paths = new string[PendingProfilePaths.Count];
PendingProfilePaths.CopyTo(paths);
PendingProfilePaths.Clear();
for (int i = 0; i < paths.Length; i++)
{
WorldAutotileProfile profile = AssetDatabase.LoadAssetAtPath<WorldAutotileProfile>(paths[i]);
if (profile == null || !profile.autoUpdatePaletteLayout)
{
continue;
}
if (LoadAuthoringLayoutAsset(profile) == null)
{
GenerateAuthoringLayout(profile, false);
}
}
}
private static GameObject BuildAuthoringLayoutRoot(WorldAutotileProfile profile)
{
GameObject root = new GameObject(profile.name + "_AuthoringLayout", typeof(Grid), typeof(WorldAutotileAuthoringRoot));
WorldAutotileAuthoringRoot rootMarker = root.GetComponent<WorldAutotileAuthoringRoot>();
rootMarker.profile = profile;
Transform guidesRoot = new GameObject("Guides").transform;
guidesRoot.SetParent(root.transform, false);
CreateGuideLabel(guidesRoot, "Workflow Label", new Vector3(5f, 6.6f, 0f), "sv_label_0");
Transform wallSection = CreateSection(root.transform, "WallShapes", WorldAutotileAuthoringSectionType.WallShapes, new Vector3(0f, 0f, 0f), new Vector2Int(3, 5));
Tilemap wallBackground = CreateTilemap(wallSection, "Background", 0);
Tilemap wallTiles = CreateTilemap(wallSection, "Walls", 1);
PopulateWallSection(profile, wallBackground, wallTiles);
CreateGuideLabel(guidesRoot, "Wall Shapes Label", wallSection.localPosition + new Vector3(1f, 5.6f, 0f), "sv_label_3");
CreateWallShapeLabels(guidesRoot, wallSection.localPosition, "sv_label_6");
Transform backgroundSection = CreateSection(root.transform, "BackgroundSample", WorldAutotileAuthoringSectionType.BackgroundSample, new Vector3(5f, 0f, 0f), new Vector2Int(5, 3));
Tilemap backgroundTilemap = CreateTilemap(backgroundSection, "Background", 0);
PopulateBackgroundSection(profile, backgroundTilemap);
CreateGuideLabel(guidesRoot, "Background Sample Label", backgroundSection.localPosition + new Vector3(2f, 3.6f, 0f), "sv_label_4");
int environmentWidth = Mathf.Max(1, CountAssignedEnvironmentTiles(profile));
Transform environmentSection = CreateSection(root.transform, "EnvironmentPalette", WorldAutotileAuthoringSectionType.EnvironmentPalette, new Vector3(0f, -3f, 0f), new Vector2Int(environmentWidth, 1));
Tilemap environmentBackground = CreateTilemap(environmentSection, "Background", 0);
Tilemap environmentTilemap = CreateTilemap(environmentSection, "Environment", 1);
PopulateEnvironmentSection(profile, environmentBackground, environmentTilemap);
float environmentCenterX = Mathf.Max(0f, (environmentWidth - 1) * 0.5f);
CreateGuideLabel(guidesRoot, "Environment Palette Label", environmentSection.localPosition + new Vector3(environmentCenterX, 1.6f, 0f), "sv_label_2");
return root;
}
private static Transform CreateSection(Transform parent, string name, WorldAutotileAuthoringSectionType sectionType, Vector3 localPosition, Vector2Int size)
{
GameObject section = new GameObject(name, typeof(WorldAutotileAuthoringSection));
section.transform.SetParent(parent, false);
section.transform.localPosition = localPosition;
WorldAutotileAuthoringSection marker = section.GetComponent<WorldAutotileAuthoringSection>();
marker.sectionType = sectionType;
marker.size = size;
return section.transform;
}
private static void CreateGuideLabel(Transform parent, string labelName, Vector3 position, string iconName)
{
GameObject label = new GameObject(labelName);
label.transform.SetParent(parent, false);
label.transform.localPosition = position;
SetIconForObject(label, iconName);
}
private static void CreateWallShapeLabels(Transform parent, Vector3 sectionPosition, string iconName)
{
for (int i = 0; i < WallShapeOrder.Length; i++)
{
Vector3Int cell = WallShapePositions[i];
string label = WorldAutotileEditorLabels.GetShapeLabel(WallShapeOrder[i]) + " Label";
Vector3 position = sectionPosition + new Vector3(cell.x + 0.5f, cell.y + 0.5f, 0f);
CreateGuideLabel(parent, label, position, iconName);
}
}
private static void PopulateWallSection(WorldAutotileProfile profile, Tilemap backgroundTilemap, Tilemap wallsTilemap)
{
TileBase background = profile.baseGroundTile;
for (int i = 0; i < WallShapeOrder.Length; i++)
{
Vector3Int position = WallShapePositions[i];
if (background != null)
{
backgroundTilemap.SetTile(position, background);
}
TileBase wallTile = profile.wallTiles != null ? profile.wallTiles.GetAssignedTile(WallShapeOrder[i]) : null;
if (wallTile != null)
{
wallsTilemap.SetTile(position, wallTile);
}
}
if (background != null)
{
backgroundTilemap.SetTile(new Vector3Int(1, 1, 0), background);
backgroundTilemap.SetTile(new Vector3Int(1, 0, 0), background);
}
}
private static void PopulateBackgroundSection(WorldAutotileProfile profile, Tilemap backgroundTilemap)
{
if (profile.baseGroundTile == null)
{
return;
}
for (int x = 0; x < 5; x++)
{
for (int y = 0; y < 3; y++)
{
backgroundTilemap.SetTile(new Vector3Int(x, y, 0), profile.baseGroundTile);
}
}
}
private static void PopulateEnvironmentSection(WorldAutotileProfile profile, Tilemap backgroundTilemap, Tilemap environmentTilemap)
{
if (profile.environmentTiles == null)
{
return;
}
int x = 0;
for (int i = 0; i < profile.environmentTiles.Count; i++)
{
EnvironmentTileEntry entry = profile.environmentTiles[i];
if (entry == null || entry.tile == null)
{
continue;
}
Vector3Int position = new Vector3Int(x, 0, 0);
if (profile.baseGroundTile != null)
{
backgroundTilemap.SetTile(position, profile.baseGroundTile);
}
environmentTilemap.SetTile(position, entry.tile);
x++;
}
}
private static Tilemap CreateTilemap(Transform parent, string name, int sortingOrder)
{
GameObject child = new GameObject(name, typeof(Tilemap), typeof(TilemapRenderer));
child.transform.SetParent(parent, false);
TilemapRenderer renderer = child.GetComponent<TilemapRenderer>();
renderer.sortingOrder = sortingOrder;
return child.GetComponent<Tilemap>();
}
private static bool TryBuildProfileFromLayout(GameObject prefabRoot, WorldAutotileProfile profile, out string error)
{
error = null;
if (prefabRoot == null)
{
error = "Could not load authoring layout prefab contents.";
return false;
}
WorldAutotileAuthoringRoot rootMarker = prefabRoot.GetComponent<WorldAutotileAuthoringRoot>();
if (rootMarker == null)
{
error = $"Authoring layout '{prefabRoot.name}' is missing {nameof(WorldAutotileAuthoringRoot)}.";
return false;
}
if (rootMarker.profile != null && rootMarker.profile != profile)
{
error = $"Authoring layout '{prefabRoot.name}' is linked to profile '{rootMarker.profile.name}', not '{profile.name}'.";
return false;
}
WorldAutotileAuthoringSection wallSection = FindSection(prefabRoot, WorldAutotileAuthoringSectionType.WallShapes);
WorldAutotileAuthoringSection backgroundSection = FindSection(prefabRoot, WorldAutotileAuthoringSectionType.BackgroundSample);
WorldAutotileAuthoringSection environmentSection = FindSection(prefabRoot, WorldAutotileAuthoringSectionType.EnvironmentPalette);
if (wallSection == null || backgroundSection == null || environmentSection == null)
{
error = $"Authoring layout '{prefabRoot.name}' is missing one or more required section markers.";
return false;
}
Tilemap wallTilemap = FindTilemap(wallSection.transform, "Walls");
Tilemap backgroundTilemap = FindTilemap(backgroundSection.transform, "Background");
Tilemap environmentTilemap = FindTilemap(environmentSection.transform, "Environment");
if (wallTilemap == null || backgroundTilemap == null || environmentTilemap == null)
{
error = $"Authoring layout '{prefabRoot.name}' is missing one or more required tilemaps.";
return false;
}
profile.baseGroundTile = FindFirstTile(backgroundTilemap, backgroundSection.size);
profile.wallTiles = ExtractWallTiles(wallTilemap);
profile.environmentTiles = ExtractEnvironmentTiles(environmentTilemap, environmentSection.size, profile.environmentTiles);
return true;
}
private static WorldAutotileAuthoringSection FindSection(GameObject root, WorldAutotileAuthoringSectionType sectionType)
{
WorldAutotileAuthoringSection[] sections = root.GetComponentsInChildren<WorldAutotileAuthoringSection>(true);
for (int i = 0; i < sections.Length; i++)
{
if (sections[i].sectionType == sectionType)
{
return sections[i];
}
}
return null;
}
private static Tilemap FindTilemap(Transform root, string name)
{
Transform child = root.Find(name);
return child != null ? child.GetComponent<Tilemap>() : null;
}
private static TileBase FindFirstTile(Tilemap tilemap, Vector2Int size)
{
for (int y = 0; y < Mathf.Max(1, size.y); y++)
{
for (int x = 0; x < Mathf.Max(1, size.x); x++)
{
TileBase tile = tilemap.GetTile(new Vector3Int(x, y, 0));
if (tile != null)
{
return tile;
}
}
}
return null;
}
private static AutoTileDefinition ExtractWallTiles(Tilemap wallTilemap)
{
AutoTileDefinition definition = new AutoTileDefinition();
for (int i = 0; i < WallShapeOrder.Length; i++)
{
AssignWallTile(definition, WallShapeOrder[i], wallTilemap.GetTile(WallShapePositions[i]));
}
return definition;
}
private static void AssignWallTile(AutoTileDefinition definition, AutoTileShape shape, TileBase tile)
{
switch (shape)
{
case AutoTileShape.Center:
definition.center = tile;
break;
case AutoTileShape.Top:
definition.top = tile;
break;
case AutoTileShape.Right:
definition.right = tile;
break;
case AutoTileShape.Bottom:
definition.bottom = tile;
break;
case AutoTileShape.Left:
definition.left = tile;
break;
case AutoTileShape.OuterTopLeft:
definition.outerTopLeft = tile;
break;
case AutoTileShape.OuterTopRight:
definition.outerTopRight = tile;
break;
case AutoTileShape.OuterBottomRight:
definition.outerBottomRight = tile;
break;
case AutoTileShape.OuterBottomLeft:
definition.outerBottomLeft = tile;
break;
case AutoTileShape.InnerTopLeft:
definition.innerTopLeft = tile;
break;
case AutoTileShape.InnerTopRight:
definition.innerTopRight = tile;
break;
case AutoTileShape.InnerBottomRight:
definition.innerBottomRight = tile;
break;
case AutoTileShape.InnerBottomLeft:
definition.innerBottomLeft = tile;
break;
}
}
private static List<EnvironmentTileEntry> ExtractEnvironmentTiles(Tilemap tilemap, Vector2Int size, List<EnvironmentTileEntry> previousEntries)
{
List<EnvironmentTileEntry> entries = new List<EnvironmentTileEntry>();
for (int y = 0; y < Mathf.Max(1, size.y); y++)
{
for (int x = 0; x < Mathf.Max(1, size.x); x++)
{
TileBase tile = tilemap.GetTile(new Vector3Int(x, y, 0));
if (tile == null || ContainsEnvironmentTile(entries, tile))
{
continue;
}
EnvironmentTileEntry existing = FindEnvironmentEntry(previousEntries, tile);
entries.Add(new EnvironmentTileEntry
{
id = existing != null && !string.IsNullOrWhiteSpace(existing.id) ? existing.id : tile.name,
tile = tile,
weight = existing != null ? existing.weight : 1f
});
}
}
return entries;
}
private static bool ContainsEnvironmentTile(List<EnvironmentTileEntry> entries, TileBase tile)
{
for (int i = 0; i < entries.Count; i++)
{
if (entries[i] != null && entries[i].tile == tile)
{
return true;
}
}
return false;
}
private static EnvironmentTileEntry FindEnvironmentEntry(List<EnvironmentTileEntry> entries, TileBase tile)
{
if (entries == null)
{
return null;
}
for (int i = 0; i < entries.Count; i++)
{
if (entries[i] != null && entries[i].tile == tile)
{
return entries[i];
}
}
return null;
}
private static int CountAssignedEnvironmentTiles(WorldAutotileProfile profile)
{
if (profile.environmentTiles == null)
{
return 0;
}
int count = 0;
for (int i = 0; i < profile.environmentTiles.Count; i++)
{
if (profile.environmentTiles[i] != null && profile.environmentTiles[i].tile != null)
{
count++;
}
}
return count;
}
private static void DeleteExistingAuthoringLayout(string path)
{
if (!File.Exists(path))
{
return;
}
AssetDatabase.DeleteAsset(path);
}
private static void SetIconForObject(GameObject gameObject, string iconName)
{
if (gameObject == null || SetIconForObjectMethod == null)
{
return;
}
Texture2D icon = EditorGUIUtility.IconContent(iconName).image as Texture2D;
if (icon == null)
{
return;
}
SetIconForObjectMethod.Invoke(null, new object[] { gameObject, icon });
}
private static void RefreshActiveGenerators(WorldAutotileProfile profile)
{
InfiniteWorldGenerator[] generators = Object.FindObjectsByType<InfiniteWorldGenerator>(FindObjectsSortMode.None);
for (int i = 0; i < generators.Length; i++)
{
InfiniteWorldGenerator generator = generators[i];
if (generator != null && generator.UsesProfile(profile))
{
generator.EditorRefreshFromProfile();
}
}
}
private static void EnsureFolderExists(string assetFolder)
{
string[] parts = assetFolder.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, parts[i]);
}
current = next;
}
}
private static void ClearQueue()
{
PendingProfilePaths.Clear();
refreshQueued = false;
}
private static void QueueAllProfilesRefresh()
{
string[] guids = AssetDatabase.FindAssets("t:WorldAutotileProfile");
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
WorldAutotileProfile profile = AssetDatabase.LoadAssetAtPath<WorldAutotileProfile>(path);
if (profile != null)
{
QueueProfileRefresh(profile);
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 24d3fe24380362140ab8b0a89ecb61e4
+185 -219
View File
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@@ -5,22 +6,26 @@ namespace InfiniteWorld.Editor
{ {
public class WorldGeneratorEditorWindow : EditorWindow public class WorldGeneratorEditorWindow : EditorWindow
{ {
private enum BrushMode private static readonly AutoTileShape[] WallShapeOrder =
{ {
Erase, AutoTileShape.OuterTopLeft,
Wall, AutoTileShape.Top,
Environment AutoTileShape.OuterTopRight,
} AutoTileShape.Left,
AutoTileShape.Center,
AutoTileShape.Right,
AutoTileShape.OuterBottomLeft,
AutoTileShape.Bottom,
AutoTileShape.OuterBottomRight,
AutoTileShape.InnerTopLeft,
AutoTileShape.InnerTopRight,
AutoTileShape.InnerBottomLeft,
AutoTileShape.InnerBottomRight
};
private WorldAutotileProfile profile; private WorldAutotileProfile profile;
private ChunkTemplate template;
private SerializedObject serializedProfile; private SerializedObject serializedProfile;
private Vector2 profileScroll; private Vector2 scroll;
private Vector2 chunkScroll;
private BrushMode brushMode = BrushMode.Wall;
private bool isPainting;
private int pendingWidth = 16;
private int pendingHeight = 16;
[MenuItem("Tools/Infinite World/World Builder")] [MenuItem("Tools/Infinite World/World Builder")]
public static void Open() public static void Open()
@@ -33,101 +38,9 @@ namespace InfiniteWorld.Editor
DrawToolbar(); DrawToolbar();
EditorGUILayout.Space(6f); EditorGUILayout.Space(6f);
EditorGUILayout.BeginHorizontal();
DrawChunkEditor();
GUILayout.Space(8f);
DrawProfileEditor();
EditorGUILayout.EndHorizontal();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
template = (ChunkTemplate)EditorGUILayout.ObjectField(template, typeof(ChunkTemplate), false, GUILayout.Width(position.width * 0.38f));
profile = (WorldAutotileProfile)EditorGUILayout.ObjectField(profile, typeof(WorldAutotileProfile), false, GUILayout.Width(position.width * 0.38f));
if (GUILayout.Button("New Chunk", EditorStyles.toolbarButton, GUILayout.Width(80f)))
{
CreateChunkAsset();
}
if (GUILayout.Button("New Profile", EditorStyles.toolbarButton, GUILayout.Width(80f)))
{
CreateProfileAsset();
}
EditorGUILayout.EndHorizontal();
}
private void DrawChunkEditor()
{
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.52f));
EditorGUILayout.LabelField("Chunk Painter", EditorStyles.boldLabel);
if (template == null)
{
EditorGUILayout.HelpBox("Create or assign a ChunkTemplate asset. Paint walls and environment directly on the chunk grid and mark exits on each side.", MessageType.Info);
EditorGUILayout.EndVertical();
return;
}
template.EnsureCellData();
pendingWidth = EditorGUILayout.IntField("Width", pendingWidth == 0 ? template.width : pendingWidth);
pendingHeight = EditorGUILayout.IntField("Height", pendingHeight == 0 ? template.height : pendingHeight);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Resize"))
{
Undo.RecordObject(template, "Resize Chunk Template");
template.Resize(pendingWidth, pendingHeight);
EditorUtility.SetDirty(template);
}
if (GUILayout.Button("Clear"))
{
Undo.RecordObject(template, "Clear Chunk Template");
template.Clear();
EditorUtility.SetDirty(template);
}
if (GUILayout.Button("Border From Exits"))
{
Undo.RecordObject(template, "Apply Border Walls");
template.ApplyBorderWallsFromExits(3);
EditorUtility.SetDirty(template);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("Brush", EditorStyles.miniBoldLabel);
brushMode = (BrushMode)GUILayout.Toolbar((int)brushMode, new[] { "Erase", "Wall", "Environment" });
EditorGUILayout.Space(4f);
EditorGUILayout.LabelField("Exits", EditorStyles.miniBoldLabel);
EditorGUILayout.BeginHorizontal();
DrawExitToggle("Top", ChunkExit.Top);
DrawExitToggle("Right", ChunkExit.Right);
DrawExitToggle("Bottom", ChunkExit.Bottom);
DrawExitToggle("Left", ChunkExit.Left);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8f);
chunkScroll = EditorGUILayout.BeginScrollView(chunkScroll, GUILayout.ExpandHeight(true));
Rect gridRect = GUILayoutUtility.GetRect(template.width * 24f, template.height * 24f, GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(false));
DrawGrid(gridRect);
EditorGUILayout.EndScrollView();
EditorGUILayout.HelpBox("Gray = floor, brown = wall, green = environment. Drag with the selected brush to paint the chunk visually.", MessageType.None);
EditorGUILayout.EndVertical();
}
private void DrawProfileEditor()
{
EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true));
EditorGUILayout.LabelField("Tile Profile", EditorStyles.boldLabel);
if (profile == null) if (profile == null)
{ {
EditorGUILayout.HelpBox("Assign a WorldAutotileProfile to map real tiles onto ground, walls, and environment.", MessageType.Info); EditorGUILayout.HelpBox("Assign or create a WorldAutotileProfile. The workflow is: create the authoring layout prefab, edit tiles inside that grid, then build the final profile back from the layout.", MessageType.Info);
EditorGUILayout.EndVertical();
return; return;
} }
@@ -137,163 +50,216 @@ namespace InfiniteWorld.Editor
} }
serializedProfile.Update(); serializedProfile.Update();
profileScroll = EditorGUILayout.BeginScrollView(profileScroll); scroll = EditorGUILayout.BeginScrollView(scroll);
DrawPaletteTools();
EditorGUILayout.Space(8f);
DrawValidation();
EditorGUILayout.Space(8f);
DrawProfileFields();
EditorGUILayout.EndScrollView();
serializedProfile.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(profile);
WorldAutotileProfilePipeline.QueueProfileRefresh(profile);
}
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
profile = (WorldAutotileProfile)EditorGUILayout.ObjectField(profile, typeof(WorldAutotileProfile), false, GUILayout.Width(position.width * 0.55f));
if (GUILayout.Button("New Profile", EditorStyles.toolbarButton, GUILayout.Width(90f)))
{
CreateProfileAsset();
}
using (new EditorGUI.DisabledScope(profile == null))
{
if (GUILayout.Button("Create Authoring Layout", EditorStyles.toolbarButton, GUILayout.Width(155f)))
{
WorldAutotileProfilePipeline.GenerateAuthoringLayout(profile);
}
}
EditorGUILayout.EndHorizontal();
}
private void DrawPaletteTools()
{
EditorGUILayout.LabelField("Palette Tools", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
EditorGUILayout.HelpBox("Create Authoring Layout builds a temporary editor-only prefab with marked grid sections. Edit tiles there, then use Build Profile From Layout to write the final WorldAutotileProfile.", MessageType.None);
EditorGUILayout.PropertyField(serializedProfile.FindProperty("autoUpdatePaletteLayout"), new GUIContent("Auto Update Authoring Layout"));
EditorGUILayout.PropertyField(serializedProfile.FindProperty("autoRefreshGeneratedWorld"), new GUIContent("Auto Refresh Generated World"));
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Create / Update Layout"))
{
WorldAutotileProfilePipeline.GenerateAuthoringLayout(profile);
}
using (new EditorGUI.DisabledScope(WorldAutotileProfilePipeline.LoadAuthoringLayoutAsset(profile) == null))
{
if (GUILayout.Button("Build Profile From Layout"))
{
WorldAutotileProfilePipeline.BuildProfileFromAuthoringLayout(profile);
}
if (GUILayout.Button("Open Layout Prefab"))
{
GameObject layout = WorldAutotileProfilePipeline.LoadAuthoringLayoutAsset(profile);
if (layout != null)
{
AssetDatabase.OpenAsset(layout);
}
}
}
EditorGUILayout.EndHorizontal();
using (new EditorGUI.DisabledScope(WorldAutotileProfilePipeline.LoadAuthoringLayoutAsset(profile) == null))
{
if (GUILayout.Button("Ping Layout Prefab"))
{
GameObject layout = WorldAutotileProfilePipeline.LoadAuthoringLayoutAsset(profile);
if (layout != null)
{
EditorGUIUtility.PingObject(layout);
Selection.activeObject = layout;
}
}
}
EditorGUILayout.LabelField("Layout Path", WorldAutotileProfilePipeline.GetAuthoringLayoutPath(profile));
EditorGUILayout.EndVertical();
}
private void DrawValidation()
{
EditorGUILayout.LabelField("Profile Check", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical("box");
List<string> missingShapes = GetMissingWallShapes();
if (missingShapes.Count == 0)
{
EditorGUILayout.HelpBox("All wall variants are assigned. The generated palette layout will include every corner, side, and center tile.", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("Missing wall variants: " + string.Join(", ", missingShapes), MessageType.Warning);
}
int environmentCount = CountAssignedEnvironmentTiles();
int prefabCount = CountAssignedRandomPrefabs();
EditorGUILayout.LabelField("Background", profile.baseGroundTile != null ? profile.baseGroundTile.name : "Not assigned");
EditorGUILayout.LabelField("Environment Tiles", environmentCount.ToString());
EditorGUILayout.LabelField("Random Prefabs", prefabCount.ToString());
EditorGUILayout.EndVertical();
}
private void DrawProfileFields()
{
EditorGUILayout.LabelField("Profile Assets", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedProfile.FindProperty("baseGroundTile"), new GUIContent("Background Tile"));
EditorGUILayout.PropertyField(serializedProfile.FindProperty("baseGroundTile"));
EditorGUILayout.Space(6f); EditorGUILayout.Space(6f);
EditorGUILayout.LabelField("Wall Autotile", EditorStyles.miniBoldLabel); EditorGUILayout.LabelField("Wall Variants", EditorStyles.miniBoldLabel);
SerializedProperty walls = serializedProfile.FindProperty("wallTiles"); SerializedProperty walls = serializedProfile.FindProperty("wallTiles");
DrawWallGrid(walls); DrawWallGrid(walls);
EditorGUILayout.Space(6f); EditorGUILayout.Space(6f);
EditorGUILayout.PropertyField(serializedProfile.FindProperty("environmentTiles"), true); EditorGUILayout.PropertyField(serializedProfile.FindProperty("environmentTiles"), true);
EditorGUILayout.EndScrollView(); EditorGUILayout.Space(6f);
serializedProfile.ApplyModifiedProperties(); EditorGUILayout.PropertyField(serializedProfile.FindProperty("randomPrefabs"), true);
if (GUI.changed)
{
EditorUtility.SetDirty(profile);
}
EditorGUILayout.EndVertical();
} }
private void DrawWallGrid(SerializedProperty walls) private void DrawWallGrid(SerializedProperty walls)
{ {
DrawTriple(walls, "outerTopLeft", "top", "outerTopRight"); DrawLabeledRow(walls, AutoTileShape.OuterTopLeft, AutoTileShape.Top, AutoTileShape.OuterTopRight);
DrawTriple(walls, "left", "center", "right"); DrawLabeledRow(walls, AutoTileShape.Left, AutoTileShape.Center, AutoTileShape.Right);
DrawTriple(walls, "outerBottomLeft", "bottom", "outerBottomRight"); DrawLabeledRow(walls, AutoTileShape.OuterBottomLeft, AutoTileShape.Bottom, AutoTileShape.OuterBottomRight);
DrawDouble(walls, "innerTopLeft", "innerTopRight"); DrawLabeledRow(walls, AutoTileShape.InnerTopLeft, AutoTileShape.InnerTopRight);
DrawDouble(walls, "innerBottomLeft", "innerBottomRight"); DrawLabeledRow(walls, AutoTileShape.InnerBottomLeft, AutoTileShape.InnerBottomRight);
} }
private void DrawTriple(SerializedProperty root, string a, string b, string c) private static void DrawLabeledRow(SerializedProperty root, params AutoTileShape[] shapes)
{ {
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(root.FindPropertyRelative(a), GUIContent.none); for (int i = 0; i < shapes.Length; i++)
EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); {
EditorGUILayout.PropertyField(root.FindPropertyRelative(c), GUIContent.none); DrawLabeledCell(root, shapes[i]);
}
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
} }
private void DrawDouble(SerializedProperty root, string a, string b) private static void DrawLabeledCell(SerializedProperty root, AutoTileShape shape)
{ {
EditorGUILayout.BeginHorizontal(); SerializedProperty property = root.FindPropertyRelative(WorldAutotileEditorLabels.GetPropertyName(shape));
EditorGUILayout.PropertyField(root.FindPropertyRelative(a), GUIContent.none); EditorGUILayout.BeginVertical(GUILayout.MaxWidth(150f));
EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); EditorGUILayout.LabelField(WorldAutotileEditorLabels.GetShapeLabel(shape), EditorStyles.miniLabel);
EditorGUILayout.EndHorizontal(); EditorGUILayout.PropertyField(property, GUIContent.none);
EditorGUILayout.EndVertical();
} }
private void DrawExitToggle(string label, ChunkExit exit) private List<string> GetMissingWallShapes()
{ {
bool current = template.GetExit(exit); List<string> missing = new List<string>();
bool next = GUILayout.Toggle(current, label, "Button"); for (int i = 0; i < WallShapeOrder.Length; i++)
if (next == current)
{ {
return; AutoTileShape shape = WallShapeOrder[i];
} if (profile.wallTiles == null || profile.wallTiles.GetAssignedTile(shape) == null)
Undo.RecordObject(template, "Toggle Chunk Exit");
switch (exit)
{
case ChunkExit.Top:
template.exitTop = next;
break;
case ChunkExit.Right:
template.exitRight = next;
break;
case ChunkExit.Bottom:
template.exitBottom = next;
break;
case ChunkExit.Left:
template.exitLeft = next;
break;
}
EditorUtility.SetDirty(template);
}
private void DrawGrid(Rect rect)
{
const float cellSize = 24f;
Event evt = Event.current;
for (int y = 0; y < template.height; y++)
{
for (int x = 0; x < template.width; x++)
{ {
Rect cellRect = new Rect(rect.x + x * cellSize, rect.y + (template.height - 1 - y) * cellSize, cellSize - 1f, cellSize - 1f); missing.Add(shape.ToString());
EditorGUI.DrawRect(cellRect, GetCellColor(x, y));
if ((evt.type == EventType.MouseDown || evt.type == EventType.MouseDrag) && cellRect.Contains(evt.mousePosition) && evt.button == 0)
{
isPainting = true;
PaintCell(x, y);
evt.Use();
}
} }
} }
if (evt.type == EventType.MouseUp) return missing;
{
isPainting = false;
}
if (isPainting)
{
Repaint();
}
} }
private Color GetCellColor(int x, int y) private int CountAssignedEnvironmentTiles()
{ {
if (template.GetWall(x, y)) if (profile.environmentTiles == null)
{ {
return new Color(0.52f, 0.36f, 0.22f, 1f); return 0;
} }
if (template.GetEnvironment(x, y)) int count = 0;
for (int i = 0; i < profile.environmentTiles.Count; i++)
{ {
return new Color(0.2f, 0.54f, 0.25f, 1f); if (profile.environmentTiles[i] != null && profile.environmentTiles[i].tile != null)
{
count++;
}
} }
return new Color(0.4f, 0.4f, 0.4f, 1f); return count;
} }
private void PaintCell(int x, int y) private int CountAssignedRandomPrefabs()
{ {
Undo.RecordObject(template, "Paint Chunk Template"); if (profile.randomPrefabs == null)
switch (brushMode)
{ {
case BrushMode.Erase: return 0;
template.SetWall(x, y, false);
template.SetEnvironment(x, y, false);
break;
case BrushMode.Wall:
template.SetWall(x, y, true);
break;
case BrushMode.Environment:
template.SetEnvironment(x, y, true);
break;
}
EditorUtility.SetDirty(template);
}
private void CreateChunkAsset()
{
string path = EditorUtility.SaveFilePanelInProject("Create Chunk Template", "ChunkTemplate", "asset", "Choose where to save the chunk template.");
if (string.IsNullOrEmpty(path))
{
return;
} }
ChunkTemplate asset = CreateInstance<ChunkTemplate>(); int count = 0;
asset.Resize(16, 16); for (int i = 0; i < profile.randomPrefabs.Count; i++)
asset.ApplyBorderWallsFromExits(3); {
AssetDatabase.CreateAsset(asset, path); if (profile.randomPrefabs[i] != null && profile.randomPrefabs[i].prefab != null)
AssetDatabase.SaveAssets(); {
template = asset; count++;
pendingWidth = asset.width; }
pendingHeight = asset.height; }
Selection.activeObject = asset;
return count;
} }
private void CreateProfileAsset() private void CreateProfileAsset()
@@ -0,0 +1,10 @@
using UnityEngine;
namespace InfiniteWorld
{
[DisallowMultipleComponent]
public sealed class WorldAutotileAuthoringRoot : MonoBehaviour
{
public WorldAutotileProfile profile;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bbd0bbbd74f81084bb8661f761798856
@@ -0,0 +1,18 @@
using UnityEngine;
namespace InfiniteWorld
{
public enum WorldAutotileAuthoringSectionType
{
WallShapes,
BackgroundSample,
EnvironmentPalette
}
[DisallowMultipleComponent]
public sealed class WorldAutotileAuthoringSection : MonoBehaviour
{
public WorldAutotileAuthoringSectionType sectionType;
public Vector2Int size = Vector2Int.one;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fbfe986c83f0c96419c804643d142e37