From e2dab2208a039e0ef685fcd26953beb4b2a13030 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sun, 29 Mar 2026 09:58:59 +0700 Subject: [PATCH] [Add] WorldAutotile authoring pipeline --- Assets/Editor/WorldAutotileEditorLabels.cs | 75 ++ .../Editor/WorldAutotileEditorLabels.cs.meta | 2 + Assets/Editor/WorldAutotileProfilePipeline.cs | 642 ++++++++++++++++++ .../WorldAutotileProfilePipeline.cs.meta | 2 + Assets/Editor/WorldGeneratorEditorWindow.cs | 404 +++++------ .../WorldGen/WorldAutotileAuthoringRoot.cs | 10 + .../WorldAutotileAuthoringRoot.cs.meta | 2 + .../WorldGen/WorldAutotileAuthoringSection.cs | 18 + .../WorldAutotileAuthoringSection.cs.meta | 2 + 9 files changed, 938 insertions(+), 219 deletions(-) create mode 100644 Assets/Editor/WorldAutotileEditorLabels.cs create mode 100644 Assets/Editor/WorldAutotileEditorLabels.cs.meta create mode 100644 Assets/Editor/WorldAutotileProfilePipeline.cs create mode 100644 Assets/Editor/WorldAutotileProfilePipeline.cs.meta create mode 100644 Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs create mode 100644 Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs.meta create mode 100644 Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs create mode 100644 Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs.meta diff --git a/Assets/Editor/WorldAutotileEditorLabels.cs b/Assets/Editor/WorldAutotileEditorLabels.cs new file mode 100644 index 00000000..3814ba32 --- /dev/null +++ b/Assets/Editor/WorldAutotileEditorLabels.cs @@ -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; + } + } + } +} diff --git a/Assets/Editor/WorldAutotileEditorLabels.cs.meta b/Assets/Editor/WorldAutotileEditorLabels.cs.meta new file mode 100644 index 00000000..781b676d --- /dev/null +++ b/Assets/Editor/WorldAutotileEditorLabels.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7f9b9a3347cfc82428971840cb651f99 \ No newline at end of file diff --git a/Assets/Editor/WorldAutotileProfilePipeline.cs b/Assets/Editor/WorldAutotileProfilePipeline.cs new file mode 100644 index 00000000..7ca9158f --- /dev/null +++ b/Assets/Editor/WorldAutotileProfilePipeline.cs @@ -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 PendingProfilePaths = new HashSet(); + 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(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(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(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(); + 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(); + 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(); + renderer.sortingOrder = sortingOrder; + return child.GetComponent(); + } + + 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(); + 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(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() : 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 ExtractEnvironmentTiles(Tilemap tilemap, Vector2Int size, List previousEntries) + { + List entries = new List(); + 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 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 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(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(path); + if (profile != null) + { + QueueProfileRefresh(profile); + } + } + } + } +} diff --git a/Assets/Editor/WorldAutotileProfilePipeline.cs.meta b/Assets/Editor/WorldAutotileProfilePipeline.cs.meta new file mode 100644 index 00000000..f7700fd0 --- /dev/null +++ b/Assets/Editor/WorldAutotileProfilePipeline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 24d3fe24380362140ab8b0a89ecb61e4 \ No newline at end of file diff --git a/Assets/Editor/WorldGeneratorEditorWindow.cs b/Assets/Editor/WorldGeneratorEditorWindow.cs index a2b2bdda..dc641e10 100644 --- a/Assets/Editor/WorldGeneratorEditorWindow.cs +++ b/Assets/Editor/WorldGeneratorEditorWindow.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEditor; using UnityEngine; @@ -5,22 +6,26 @@ namespace InfiniteWorld.Editor { public class WorldGeneratorEditorWindow : EditorWindow { - private enum BrushMode + private static readonly AutoTileShape[] WallShapeOrder = { - Erase, - Wall, - Environment - } + 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 WorldAutotileProfile profile; - private ChunkTemplate template; private SerializedObject serializedProfile; - private Vector2 profileScroll; - private Vector2 chunkScroll; - private BrushMode brushMode = BrushMode.Wall; - private bool isPainting; - private int pendingWidth = 16; - private int pendingHeight = 16; + private Vector2 scroll; [MenuItem("Tools/Infinite World/World Builder")] public static void Open() @@ -33,101 +38,9 @@ namespace InfiniteWorld.Editor DrawToolbar(); 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) { - EditorGUILayout.HelpBox("Assign a WorldAutotileProfile to map real tiles onto ground, walls, and environment.", MessageType.Info); - EditorGUILayout.EndVertical(); + 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); return; } @@ -137,163 +50,216 @@ namespace InfiniteWorld.Editor } 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 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.LabelField("Wall Autotile", EditorStyles.miniBoldLabel); + EditorGUILayout.LabelField("Wall Variants", EditorStyles.miniBoldLabel); SerializedProperty walls = serializedProfile.FindProperty("wallTiles"); DrawWallGrid(walls); EditorGUILayout.Space(6f); EditorGUILayout.PropertyField(serializedProfile.FindProperty("environmentTiles"), true); - EditorGUILayout.EndScrollView(); - serializedProfile.ApplyModifiedProperties(); - if (GUI.changed) - { - EditorUtility.SetDirty(profile); - } - EditorGUILayout.EndVertical(); + EditorGUILayout.Space(6f); + EditorGUILayout.PropertyField(serializedProfile.FindProperty("randomPrefabs"), true); } private void DrawWallGrid(SerializedProperty walls) { - DrawTriple(walls, "outerTopLeft", "top", "outerTopRight"); - DrawTriple(walls, "left", "center", "right"); - DrawTriple(walls, "outerBottomLeft", "bottom", "outerBottomRight"); - DrawDouble(walls, "innerTopLeft", "innerTopRight"); - DrawDouble(walls, "innerBottomLeft", "innerBottomRight"); + DrawLabeledRow(walls, AutoTileShape.OuterTopLeft, AutoTileShape.Top, AutoTileShape.OuterTopRight); + DrawLabeledRow(walls, AutoTileShape.Left, AutoTileShape.Center, AutoTileShape.Right); + DrawLabeledRow(walls, AutoTileShape.OuterBottomLeft, AutoTileShape.Bottom, AutoTileShape.OuterBottomRight); + DrawLabeledRow(walls, AutoTileShape.InnerTopLeft, AutoTileShape.InnerTopRight); + 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.PropertyField(root.FindPropertyRelative(a), GUIContent.none); - EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); - EditorGUILayout.PropertyField(root.FindPropertyRelative(c), GUIContent.none); + for (int i = 0; i < shapes.Length; i++) + { + DrawLabeledCell(root, shapes[i]); + } + EditorGUILayout.EndHorizontal(); } - private void DrawDouble(SerializedProperty root, string a, string b) + private static void DrawLabeledCell(SerializedProperty root, AutoTileShape shape) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.PropertyField(root.FindPropertyRelative(a), GUIContent.none); - EditorGUILayout.PropertyField(root.FindPropertyRelative(b), GUIContent.none); - EditorGUILayout.EndHorizontal(); + SerializedProperty property = root.FindPropertyRelative(WorldAutotileEditorLabels.GetPropertyName(shape)); + EditorGUILayout.BeginVertical(GUILayout.MaxWidth(150f)); + EditorGUILayout.LabelField(WorldAutotileEditorLabels.GetShapeLabel(shape), EditorStyles.miniLabel); + EditorGUILayout.PropertyField(property, GUIContent.none); + EditorGUILayout.EndVertical(); } - private void DrawExitToggle(string label, ChunkExit exit) + private List GetMissingWallShapes() { - bool current = template.GetExit(exit); - bool next = GUILayout.Toggle(current, label, "Button"); - if (next == current) + List missing = new List(); + for (int i = 0; i < WallShapeOrder.Length; i++) { - return; - } - - 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++) + AutoTileShape shape = WallShapeOrder[i]; + if (profile.wallTiles == null || profile.wallTiles.GetAssignedTile(shape) == null) { - Rect cellRect = new Rect(rect.x + x * cellSize, rect.y + (template.height - 1 - y) * cellSize, cellSize - 1f, cellSize - 1f); - 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(); - } + missing.Add(shape.ToString()); } } - if (evt.type == EventType.MouseUp) - { - isPainting = false; - } - - if (isPainting) - { - Repaint(); - } + return missing; } - 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"); - switch (brushMode) + if (profile.randomPrefabs == null) { - case BrushMode.Erase: - 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; + return 0; } - ChunkTemplate asset = CreateInstance(); - asset.Resize(16, 16); - asset.ApplyBorderWallsFromExits(3); - AssetDatabase.CreateAsset(asset, path); - AssetDatabase.SaveAssets(); - template = asset; - pendingWidth = asset.width; - pendingHeight = asset.height; - Selection.activeObject = asset; + int count = 0; + for (int i = 0; i < profile.randomPrefabs.Count; i++) + { + if (profile.randomPrefabs[i] != null && profile.randomPrefabs[i].prefab != null) + { + count++; + } + } + + return count; } private void CreateProfileAsset() diff --git a/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs b/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs new file mode 100644 index 00000000..7ca2c9dc --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace InfiniteWorld +{ + [DisallowMultipleComponent] + public sealed class WorldAutotileAuthoringRoot : MonoBehaviour + { + public WorldAutotileProfile profile; + } +} diff --git a/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs.meta b/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs.meta new file mode 100644 index 00000000..e3d414cd --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileAuthoringRoot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bbd0bbbd74f81084bb8661f761798856 \ No newline at end of file diff --git a/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs b/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs new file mode 100644 index 00000000..632023e2 --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs @@ -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; + } +} diff --git a/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs.meta b/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs.meta new file mode 100644 index 00000000..3e1c3d17 --- /dev/null +++ b/Assets/Scripts/WorldGen/WorldAutotileAuthoringSection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fbfe986c83f0c96419c804643d142e37 \ No newline at end of file