[Add] WorldAutotile authoring pipeline
This commit is contained in:
@@ -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<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.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<string> GetMissingWallShapes()
|
||||
{
|
||||
bool current = template.GetExit(exit);
|
||||
bool next = GUILayout.Toggle(current, label, "Button");
|
||||
if (next == current)
|
||||
List<string> missing = new List<string>();
|
||||
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<ChunkTemplate>();
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user