added scrath stage editor

This commit is contained in:
Nilton Constantino 2026-06-15 12:32:42 +01:00
parent c4e3723e07
commit bfab987ae7
No known key found for this signature in database
36 changed files with 2031 additions and 11 deletions

View File

@ -7,7 +7,7 @@ Material:
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_Name: ContactCollider_v2 m_Name: ObstacleCollider_v2
m_Shader: {fileID: -6465566751694194690, guid: 0ca531f654dad4a6ca8be40f93612d9e, type: 3} m_Shader: {fileID: -6465566751694194690, guid: 0ca531f654dad4a6ca8be40f93612d9e, type: 3}
m_Parent: {fileID: 0} m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0 m_ModifiedSerializedProperties: 0
@ -47,7 +47,7 @@ Material:
- _QueueOffset: 0 - _QueueOffset: 0
- _SideDarkness: 0.6 - _SideDarkness: 0.6
m_Colors: m_Colors:
- _BaseColor: {r: 1, g: 0.5529412, b: 0.5647059, a: 1} - _BaseColor: {r: 0.3990299, g: 0.5432971, b: 0.5754717, a: 1}
m_BuildTextureStacks: [] m_BuildTextureStacks: []
m_AllowLocking: 1 m_AllowLocking: 1
--- !u!114 &1069900037742449899 --- !u!114 &1069900037742449899

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 78ca007dda30545739e1fce83a853093
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,22 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 778f9d79eb2c410482efdc25b248dd46, type: 3}
m_Name: StageDefinition
m_EditorClassIdentifier: Assembly-CSharp::Intrepid.Gameplay.Prototyping.StageDefinition
stageId: stagedefinition
cellSize: 1
floorHeight: 6
activeFloorIndex: 0
floors:
- floorId: F0
cells: []
objects: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bb896366809bc439b8be4d0f1158dc6f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -298,7 +298,7 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 7939123536182165016} - component: {fileID: 7939123536182165016}
m_Layer: 30 m_Layer: 30
m_Name: '# Contact_Prototype_BASE' m_Name: '# Obstacle_Prototype_BASE'
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0

View File

@ -74,7 +74,7 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 8449749668273782584, guid: 64620cfd283d648b5b4d9c9a4fac7751, type: 3} - target: {fileID: 8449749668273782584, guid: 64620cfd283d648b5b4d9c9a4fac7751, type: 3}
propertyPath: m_Name propertyPath: m_Name
value: Contact_Cube_Prototype value: Obstacle_Cube_Prototype
objectReference: {fileID: 0} objectReference: {fileID: 0}
m_RemovedComponents: [] m_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []

View File

@ -12,10 +12,9 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8f4d583fd594418e8ecea0731be75f23, type: 3} m_Script: {fileID: 11500000, guid: 8f4d583fd594418e8ecea0731be75f23, type: 3}
m_Name: Dash - Cadence Profile m_Name: Dash - Cadence Profile
m_EditorClassIdentifier: Assembly-CSharp::Intrepid.Gameplay.Entities.Player.Sentinel.Cadence.CadenceProfile m_EditorClassIdentifier: Assembly-CSharp::Intrepid.Gameplay.Entities.Player.Sentinel.Cadence.CadenceProfile
totalTimeMS: 400 strained: 0.25
strainedEndP: 0.25 aligned: 0.65
alignedEndP: 0.65 resonant: 1
resonantEndP: 1
strainedSettlement: 1 strainedSettlement: 1
alignedSettlement: 0.5 alignedSettlement: 0.5
resonantSettlement: 0 resonantSettlement: 0

View File

@ -16,4 +16,4 @@ MonoBehaviour:
unstableThresholdP: 0.7 unstableThresholdP: 0.7
criticalThresholdP: 0.35 criticalThresholdP: 0.35
collapsedRecoveryStability: 25 collapsedRecoveryStability: 25
naturalRecoveryPerSecond: 10 naturalRecoveryPerSecond: 2

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 93fdd19587ea435285157d73a25958f0
timeCreated: 1781500522

View File

@ -0,0 +1,109 @@
# Gardens Stage Editor Starter Kit
Starter kit para um editor lógico de stages do **Gardens of Nil** dentro da Unity.
Este pacote contém somente:
- modelo de dados (`StageDefinition`, floors, células estruturais e objetos lógicos);
- janela de editor com UI Toolkit;
- grid 1x1 com pan/zoom;
- paleta por layer;
- criação/seleção/remoção de elementos lógicos;
- inspector simples;
- validação básica.
Não contém bake/rebake de scene, geração de colliders ou integração com Tiled.
## Instalação
Descompacte a pasta `GardensStageEditor` dentro de:
```txt
Assets/Editor/
```
O resultado deve ficar parecido com:
```txt
Assets/Editor/GardensStageEditor/
Data/
Editor/
README.md
```
Depois espere a Unity recompilar.
## Abrir o editor
Na Unity:
```txt
Gardens > Stage Editor
```
Na janela:
1. Clique em `New` para criar um `StageDefinition.asset`.
2. Escolha a layer na paleta inferior.
3. Use `Paint`, `Select`, `Erase` ou `Pan`.
4. Pinte o mapa lógico no canvas central.
5. Edite o elemento selecionado no inspector à direita.
6. Use `Validate` para checagens básicas.
## Controles
```txt
LMB: pintar / selecionar
RMB ou MMB: pan
Mouse wheel: zoom
```
## Modelo mental
O editor trabalha em um plano lógico 2D:
```txt
Editor X/Y
```
Mais tarde, o baker pode mapear isso para o mundo Unity:
```txt
Editor X -> Unity X
Editor Y -> Unity Z
Floor index -> Unity Y offset
```
## Layers iniciais
```txt
Structural: Ground, Wall, Hole, Erase Structural
Design: Falling Floor, Door, Interactable, Portal
Camera: Camera Zone
Markers: Player Spawn, Elevator, Marker
```
## Filosofia
A scene não é fonte da verdade.
```txt
StageDefinition.asset = source of truth
Generated scene/prefab = output futuro do baker
```
Este pacote só cuida do primeiro lado: autoria visual e dados lógicos.
## Próximos passos sugeridos
1. Transformar `StagePaletteEntry` em assets configuráveis, se necessário.
2. Adicionar hotkeys e melhor seleção/move/resize visual.
3. Adicionar overlays de erro no canvas.
4. Implementar o baker separado consumindo `StageDefinition`.
5. Depois, se fizer sentido, criar import/export Tiled apenas para o mapa lógico.
## Requisitos recomendados
Unity 2022.3 LTS ou superior. Unity 6 também deve funcionar.
O canvas usa UI Toolkit custom drawing (`Painter2D`). Em versões muito antigas da Unity, pode ser necessário trocar o canvas por IMGUI ou SceneView Handles.

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 494fb84451f94e218111409283a7f795
timeCreated: 1781500522

View File

@ -0,0 +1,71 @@
# Stage Model Notes
## StageDefinition
Fonte da verdade do stage lógico.
Contém:
- `stageId`
- `cellSize`
- `floorHeight`
- `floors`
- `objects`
## StageFloorDefinition
Um andar lógico do stage.
Contém:
- `floorId`
- `width`
- `height`
- células estruturais preenchidas
Células vazias não são serializadas explicitamente. Apenas células com `Ground`, `Wall` ou `Hole` são guardadas.
## Structural cells
A layer estrutural é cell-based.
```txt
Empty
Ground
Wall
Hole
```
## StageObjectDefinition
Objetos lógicos são object-based.
Exemplos:
- traps
- doors
- interactables
- portals
- camera zones
- spawn points
- elevators
- markers
Cada objeto tem:
- `id`
- `layer`
- `kind`
- `typeId`
- `prefabId`
- `floorId`
- `x`, `y`, `width`, `height`
- flags como `cutsGround` e `blocksMovement`
## Regra sugerida para bake futuro
```txt
GroundCollision = Structural.Ground - Structural.Hole - Objects(cutsGround)
```
Depois o baker pode compactar células em BoxColliders maiores.

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ffb9c2df7a0f4b3ca08544979287cf20
timeCreated: 1781500522

View File

@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Editor.GardensStageEditor
{
public enum StageLogicalLayer
{
Structural = 0,
Design = 1,
Camera = 2,
Markers = 3
}
public enum StructuralCellKind
{
Empty = 0,
Ground = 1,
Wall = 2,
Hole = 3
}
public enum StageObjectKind
{
Trap = 0,
Door = 1,
Interactable = 2,
Portal = 3,
CameraZone = 4,
Spawn = 5,
Elevator = 6,
Marker = 7,
Custom = 100
}
[CreateAssetMenu(menuName = "GON/Editor/Stage Definition", fileName = "StageDefinition")]
public sealed class StageDefinition : ScriptableObject
{
public string stageId = "stage_new";
public float cellSize = 1f;
public float floorHeight = 6f;
public int activeFloorIndex;
public List<StageFloorDefinition> floors = new();
public List<StageObjectDefinition> objects = new();
public StageFloorDefinition ActiveFloor
{
get
{
EnsureDefaults();
if (activeFloorIndex < 0) activeFloorIndex = 0;
if (activeFloorIndex >= floors.Count) activeFloorIndex = floors.Count - 1;
return floors[activeFloorIndex];
}
}
public void EnsureDefaults()
{
floors ??= new List<StageFloorDefinition>();
objects ??= new List<StageObjectDefinition>();
if (floors.Count == 0)
{
floors.Add(new StageFloorDefinition
{
floorId = "F0"
});
}
if (cellSize <= 0f)
{
cellSize = 1f;
}
if (floorHeight <= 0f)
{
floorHeight = 6f;
}
}
public StageFloorDefinition GetFloor(string floorId)
{
EnsureDefaults();
foreach (var stageFloorDefinition in floors)
{
if (stageFloorDefinition != null && stageFloorDefinition.floorId == floorId)
{
return stageFloorDefinition;
}
}
return null;
}
public bool ContainsFloor(string floorId)
{
return GetFloor(floorId) != null;
}
public string CreateUniqueObjectId(StageObjectKind kind)
{
objects ??= new List<StageObjectDefinition>();
var prefix = kind.ToString().ToLowerInvariant();
var index = 1;
while (true)
{
var candidate = prefix + "_" + index.ToString("000");
var exists = false;
foreach (var stageObjectDefinition in objects)
{
if (stageObjectDefinition != null && stageObjectDefinition.id == candidate)
{
exists = true;
break;
}
}
if (!exists)
{
return candidate;
}
index++;
}
}
}
[Serializable]
public sealed class StageFloorDefinition
{
public string floorId = "F0";
[SerializeField]
private List<LogicalCell> cells = new();
public List<LogicalCell> Cells
{
get
{
cells ??= new List<LogicalCell>();
return cells;
}
}
// Infinite/sparse floor: every integer coordinate is valid.
public bool IsInside(int x, int y)
{
return true;
}
public StructuralCellKind GetCell(int x, int y)
{
int index = FindCellIndex(x, y);
if (index < 0)
{
return StructuralCellKind.Empty;
}
return Cells[index].structural;
}
public void SetCell(int x, int y, StructuralCellKind kind)
{
var index = FindCellIndex(x, y);
if (kind == StructuralCellKind.Empty)
{
if (index >= 0)
{
Cells.RemoveAt(index);
}
return;
}
LogicalCell cell = new LogicalCell
{
x = x,
y = y,
structural = kind
};
if (index >= 0)
{
Cells[index] = cell;
}
else
{
Cells.Add(cell);
}
}
public void FillRect(RectInt rect, StructuralCellKind kind)
{
for (var y = rect.yMin; y < rect.yMax; y++)
{
for (var x = rect.xMin; x < rect.xMax; x++)
{
SetCell(x, y, kind);
}
}
}
public bool TryGetUsedBounds(out RectInt bounds)
{
if (Cells.Count == 0)
{
bounds = default;
return false;
}
var minX = Cells[0].x;
var maxX = Cells[0].x;
var minY = Cells[0].y;
var maxY = Cells[0].y;
for (var i = 1; i < Cells.Count; i++)
{
var cell = Cells[i];
minX = Mathf.Min(minX, cell.x);
maxX = Mathf.Max(maxX, cell.x);
minY = Mathf.Min(minY, cell.y);
maxY = Mathf.Max(maxY, cell.y);
}
bounds = new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
return true;
}
private int FindCellIndex(int x, int y)
{
for (var i = 0; i < Cells.Count; i++)
{
var cell = Cells[i];
if (cell.x == x && cell.y == y)
{
return i;
}
}
return -1;
}
}
[Serializable]
public struct LogicalCell
{
public int x;
public int y;
public StructuralCellKind structural;
}
[Serializable]
public sealed class StageObjectDefinition
{
public string id;
public StageLogicalLayer layer;
public StageObjectKind kind;
public string typeId;
public string prefabId;
public string floorId;
public int x;
public int y;
public int width = 1;
public int height = 1;
public bool cutsGround;
public bool blocksMovement;
public string notes;
public RectInt Bounds => new(x, y, Mathf.Max(1, width), Mathf.Max(1, height));
public bool ContainsCell(int cellX, int cellY)
{
var bounds = Bounds;
return cellX >= bounds.xMin && cellX < bounds.xMax && cellY >= bounds.yMin && cellY < bounds.yMax;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 778f9d79eb2c410482efdc25b248dd46

View File

@ -0,0 +1,46 @@
using UnityEngine;
namespace Editor.GardensStageEditor
{
internal static class StageEditorColors
{
public static readonly Color Background = new(0.13f, 0.13f, 0.13f, 1f);
public static readonly Color GridMinor = new(1f, 1f, 1f, 0.08f);
public static readonly Color GridMajor = new(1f, 1f, 1f, 0.18f);
public static readonly Color GridBorder = new(1f, 1f, 1f, 0.28f);
public static readonly Color PreviewFill = new(1f, 1f, 1f, 0.18f);
public static readonly Color PreviewStroke = new(1f, 1f, 1f, 0.9f);
public static readonly Color SelectionStroke = new(1f, 0.85f, 0.25f, 1f);
public static Color Structural(StructuralCellKind kind)
{
switch (kind)
{
case StructuralCellKind.Ground:
return new Color(0.28f, 0.52f, 0.35f, 0.8f);
case StructuralCellKind.Wall:
return new Color(0.38f, 0.38f, 0.42f, 0.9f);
case StructuralCellKind.Hole:
return new Color(0.05f, 0.05f, 0.05f, 0.9f);
default:
return new Color(0f, 0f, 0f, 0f);
}
}
public static Color Layer(StageLogicalLayer layer)
{
switch (layer)
{
case StageLogicalLayer.Design:
return new Color(0.85f, 0.35f, 0.25f, 0.75f);
case StageLogicalLayer.Camera:
return new Color(0.25f, 0.48f, 0.95f, 0.35f);
case StageLogicalLayer.Markers:
return new Color(0.85f, 0.75f, 0.25f, 0.75f);
case StageLogicalLayer.Structural:
default:
return new Color(0.35f, 0.7f, 0.45f, 0.75f);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa76e08c40c94bf3a8e3aa57f849200d
timeCreated: 1781500522

View File

@ -0,0 +1,46 @@
using System;
using Intrepid.Gameplay.Prototyping;
namespace Editor.GardensStageEditor
{
public sealed class StageEditorState
{
public StageLogicalLayer activeLayer = StageLogicalLayer.Structural;
public StagePaletteEntry activePaletteEntry;
public StageObjectDefinition selectedObject;
public event Action Changed;
public void ClearListeners()
{
Changed = null;
}
public void SetActiveLayer(StageLogicalLayer layer)
{
activeLayer = layer;
NotifyChanged();
}
public void SetPaletteEntry(StagePaletteEntry entry)
{
activePaletteEntry = entry;
if (entry != null)
{
activeLayer = entry.layer;
}
NotifyChanged();
}
public void SelectObject(StageObjectDefinition obj)
{
selectedObject = obj;
NotifyChanged();
}
public void NotifyChanged()
{
Changed?.Invoke();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b999d27637ff4b45bb5fbdf1ef4de17b

View File

@ -0,0 +1,295 @@
using System.Collections.Generic;
using Intrepid.Gameplay.Prototyping;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Editor.GardensStageEditor
{
public sealed class StageEditorWindow : EditorWindow
{
private StageDefinition stage;
private StageEditorState state;
private List<StagePaletteEntry> palette;
private VisualElement topBar;
private StageMapCanvas canvas;
private StageInspectorPanel inspector;
private StagePalettePanel palettePanel;
[MenuItem("Tools/Gardens Of Nil/Stage Editor")]
public static void Open()
{
var window = GetWindow<StageEditorWindow>();
window.titleContent = new GUIContent("Gardens Stage Editor");
window.minSize = new Vector2(900, 600);
window.Show();
}
public void CreateGUI()
{
state ??= new StageEditorState();
state.ClearListeners();
if (palette is null)
{
palette = StagePaletteEntry.CreateBuiltInPalette();
if (state.activePaletteEntry is null && palette.Count > 0)
{
state.activePaletteEntry = palette[0];
state.activeLayer = palette[0].layer;
}
}
rootVisualElement.Clear();
rootVisualElement.style.flexDirection = FlexDirection.Column;
topBar = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row,
alignItems = Align.Center,
paddingLeft = 8,
paddingRight = 8,
paddingTop = 6,
paddingBottom = 6,
borderBottomWidth = 1,
borderBottomColor = new Color(1f, 1f, 1f, 0.12f)
}
};
rootVisualElement.Add(topBar);
var main = new VisualElement
{
style =
{
flexGrow = 1f,
flexDirection = FlexDirection.Row
}
};
rootVisualElement.Add(main);
canvas = new StageMapCanvas(this, state);
canvas.SetStage(stage);
main.Add(canvas);
inspector = new StageInspectorPanel(this, state);
inspector.SetStage(stage);
main.Add(inspector);
palettePanel = new StagePalettePanel(state, palette);
rootVisualElement.Add(palettePanel);
state.Changed += delegate
{
canvas?.RequestRepaint();
inspector?.Rebuild();
};
RebuildTopBar();
}
public void MutateStage(string undoName, System.Action mutation)
{
if (stage is null || mutation is null)
{
return;
}
Undo.RecordObject(stage, undoName);
mutation();
stage.EnsureDefaults();
EditorUtility.SetDirty(stage);
canvas?.RequestRepaint();
inspector?.Rebuild();
RebuildTopBar();
Repaint();
}
private void RebuildTopBar()
{
if (topBar is null)
{
return;
}
topBar.Clear();
var stageField = new ObjectField("Stage")
{
objectType = typeof(StageDefinition),
value = stage,
style =
{
minWidth = 300
}
};
stageField.RegisterValueChangedCallback(evt =>
{
SetStage(evt.newValue as StageDefinition);
});
topBar.Add(stageField);
var newButton = new Button(CreateNewStageAsset)
{
text = "New",
style =
{
marginLeft = 6
}
};
topBar.Add(newButton);
if (stage is not null)
{
stage.EnsureDefaults();
var floorNames = new List<string>();
foreach (var stageFloorDefinition in stage.floors)
{
floorNames.Add(stageFloorDefinition.floorId);
}
var floorPopup = new PopupField<string>("Floor", floorNames, Mathf.Clamp(stage.activeFloorIndex, 0, floorNames.Count - 1))
{
style =
{
marginLeft = 10,
minWidth = 170
}
};
floorPopup.RegisterValueChangedCallback(evt =>
{
var index = floorNames.IndexOf(evt.newValue);
if (index >= 0)
{
MutateStage("Change active floor", delegate { stage.activeFloorIndex = index; });
}
});
topBar.Add(floorPopup);
var addFloor = new Button(AddFloor)
{
text = "+ Floor",
style =
{
marginLeft = 4
}
};
topBar.Add(addFloor);
var validate = new Button(ValidateStage)
{
text = "Validate",
style =
{
marginLeft = 12
}
};
topBar.Add(validate);
var save = new Button(delegate
{
EditorUtility.SetDirty(stage);
AssetDatabase.SaveAssets();
Debug.Log("Saved StageDefinition: " + AssetDatabase.GetAssetPath(stage));
})
{
text = "Save Asset",
style =
{
marginLeft = 4
}
};
topBar.Add(save);
}
var spacer = new VisualElement
{
style =
{
flexGrow = 1f
}
};
topBar.Add(spacer);
var hint = new Label("Infinite grid · LMB select/place/paint · RMB/MMB pan · Wheel zoom")
{
style =
{
opacity = 0.75f
}
};
topBar.Add(hint);
}
private void SetStage(StageDefinition newStage)
{
stage = newStage;
stage?.EnsureDefaults();
state.SelectObject(null);
canvas?.SetStage(stage);
inspector?.SetStage(stage);
RebuildTopBar();
}
private void CreateNewStageAsset()
{
var path = EditorUtility.SaveFilePanelInProject(
"Create Stage Definition",
"StageDefinition",
"asset",
"Choose where to create the new StageDefinition asset."
);
if (string.IsNullOrEmpty(path))
{
return;
}
var asset = CreateInstance<StageDefinition>();
asset.stageId = System.IO.Path.GetFileNameWithoutExtension(path).ToLowerInvariant();
asset.EnsureDefaults();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
SetStage(asset);
Selection.activeObject = asset;
}
private void AddFloor()
{
if (stage is null)
{
return;
}
MutateStage("Add floor", delegate
{
var index = stage.floors.Count;
var floor = new StageFloorDefinition
{
floorId = "F" + index
};
stage.floors.Add(floor);
stage.activeFloorIndex = stage.floors.Count - 1;
});
}
private void ValidateStage()
{
var messages = StageValidator.Validate(stage);
var report = StageValidator.Format(messages);
Debug.Log(report);
EditorUtility.DisplayDialog("Stage Validation", report, "OK");
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f7065bab1fc14f53909281036dbb6368

View File

@ -0,0 +1,258 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace Editor.GardensStageEditor
{
public sealed class StageInspectorPanel : VisualElement
{
private readonly StageEditorWindow owner;
private readonly StageEditorState state;
private StageDefinition stage;
public StageInspectorPanel(StageEditorWindow owner, StageEditorState state)
{
this.owner = owner;
this.state = state;
style.width = 300;
style.minWidth = 260;
style.maxWidth = 380;
style.paddingLeft = 8;
style.paddingRight = 8;
style.paddingTop = 8;
style.paddingBottom = 8;
style.borderLeftWidth = 1;
style.borderLeftColor = new Color(1f, 1f, 1f, 0.12f);
style.flexShrink = 0;
state.Changed += Rebuild;
}
public void SetStage(StageDefinition newStage)
{
stage = newStage;
Rebuild();
}
public void Rebuild()
{
Clear();
var title = new Label("Inspector")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
marginBottom = 8
}
};
Add(title);
if (stage is null)
{
Add(new HelpBox("Select or create a StageDefinition asset.", HelpBoxMessageType.Info));
return;
}
stage.EnsureDefaults();
if (state.selectedObject is not null && stage.objects.Contains(state.selectedObject))
{
BuildObjectInspector(state.selectedObject);
}
else
{
state.selectedObject = null;
BuildStageInspector();
}
}
private void BuildStageInspector()
{
var section = new Label("Stage")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold
}
};
Add(section);
var stageId = new TextField("Stage Id")
{
value = stage.stageId
};
stageId.RegisterValueChangedCallback(evt => owner.MutateStage("Change stage id", delegate { stage.stageId = evt.newValue; }));
Add(stageId);
var cellSize = new FloatField("Cell Size")
{
value = stage.cellSize
};
cellSize.RegisterValueChangedCallback(evt => owner.MutateStage("Change cell size", delegate { stage.cellSize = Mathf.Max(0.01f, evt.newValue); }));
Add(cellSize);
var floorHeight = new FloatField("Floor Height")
{
value = stage.floorHeight
};
floorHeight.RegisterValueChangedCallback(evt => owner.MutateStage("Change floor height", delegate { stage.floorHeight = Mathf.Max(0.01f, evt.newValue); }));
Add(floorHeight);
var spacer = new VisualElement
{
style =
{
height = 10
}
};
Add(spacer);
var floor = stage.ActiveFloor;
var floorSection = new Label("Active Floor")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold
}
};
Add(floorSection);
var floorId = new TextField("Floor Id")
{
value = floor.floorId
};
floorId.RegisterValueChangedCallback(evt => owner.MutateStage("Change floor id", delegate
{
var oldId = floor.floorId;
floor.floorId = string.IsNullOrEmpty(evt.newValue) ? oldId : evt.newValue;
foreach (var stageObjectDefinition in stage.objects)
{
if (stageObjectDefinition is not null && stageObjectDefinition.floorId == oldId)
{
stageObjectDefinition.floorId = floor.floorId;
}
}
}));
Add(floorId);
Add(new Label("Structural Cells: " + floor.Cells.Count));
Add(floor.TryGetUsedBounds(out var bounds)
? new Label("Used Bounds: x " + bounds.xMin + ".." + bounds.xMax + ", y " + bounds.yMin + ".." +
bounds.yMax)
: new Label("Used Bounds: empty"));
Add(new HelpBox("The grid is infinite/sparse. The stage size emerges from painted cells and logical objects.", HelpBoxMessageType.Info));
Add(new HelpBox("Editor plane uses X/Y. Later, the stage baker can map editor Y to Unity Z and floor index to Unity Y.", HelpBoxMessageType.Info));
}
private void BuildObjectInspector(StageObjectDefinition obj)
{
var section = new Label("Selected Object")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold
}
};
Add(section);
var id = new TextField("Id")
{
value = obj.id
};
id.RegisterValueChangedCallback(evt => owner.MutateStage("Change object id", delegate { obj.id = evt.newValue; }));
Add(id);
var layer = new EnumField("Layer", obj.layer);
layer.RegisterValueChangedCallback(evt => owner.MutateStage("Change object layer", delegate { obj.layer = (StageLogicalLayer)evt.newValue; }));
Add(layer);
var kind = new EnumField("Kind", obj.kind);
kind.RegisterValueChangedCallback(evt => owner.MutateStage("Change object kind", delegate { obj.kind = (StageObjectKind)evt.newValue; }));
Add(kind);
var typeId = new TextField("Type Id")
{
value = obj.typeId
};
typeId.RegisterValueChangedCallback(evt => owner.MutateStage("Change type id", delegate { obj.typeId = evt.newValue; }));
Add(typeId);
var prefabId = new TextField("Prefab Id")
{
value = obj.prefabId
};
prefabId.RegisterValueChangedCallback(evt => owner.MutateStage("Change prefab id", delegate { obj.prefabId = evt.newValue; }));
Add(prefabId);
var x = new IntegerField("X")
{
value = obj.x
};
x.RegisterValueChangedCallback(evt => owner.MutateStage("Move object x", delegate { obj.x = evt.newValue; }));
Add(x);
var y = new IntegerField("Y")
{
value = obj.y
};
y.RegisterValueChangedCallback(evt => owner.MutateStage("Move object y", delegate { obj.y = evt.newValue; }));
Add(y);
var width = new IntegerField("Width")
{
value = obj.width
};
width.RegisterValueChangedCallback(evt => owner.MutateStage("Resize object width", delegate { obj.width = Mathf.Max(1, evt.newValue); }));
Add(width);
var height = new IntegerField("Height")
{
value = obj.height
};
height.RegisterValueChangedCallback(evt => owner.MutateStage("Resize object height", delegate { obj.height = Mathf.Max(1, evt.newValue); }));
Add(height);
var cutsGround = new Toggle("Cuts Ground")
{
value = obj.cutsGround
};
cutsGround.RegisterValueChangedCallback(evt => owner.MutateStage("Toggle cuts ground", delegate { obj.cutsGround = evt.newValue; }));
Add(cutsGround);
var blocksMovement = new Toggle("Blocks Movement")
{
value = obj.blocksMovement
};
blocksMovement.RegisterValueChangedCallback(evt => owner.MutateStage("Toggle blocks movement", delegate { obj.blocksMovement = evt.newValue; }));
Add(blocksMovement);
var notes = new TextField("Notes")
{
multiline = true,
value = obj.notes
};
notes.RegisterValueChangedCallback(evt => owner.MutateStage("Change object notes", delegate { obj.notes = evt.newValue; }));
Add(notes);
var delete = new Button(delegate
{
owner.MutateStage("Delete object", delegate
{
stage.objects.Remove(obj);
state.SelectObject(null);
});
})
{
text = "Delete Object",
style =
{
marginTop = 8
}
};
Add(delete);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a1b3226b742b4e5e94cab859de25494a

View File

@ -0,0 +1,466 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace Editor.GardensStageEditor
{
public sealed class StageMapCanvas : VisualElement
{
private const float MinCellPixels = 12f;
private const float MaxCellPixels = 96f;
private readonly StageEditorWindow owner;
private readonly StageEditorState state;
private StageDefinition stage;
private Vector2 pan = new(40f, 40f);
private float cellPixels = 32f;
private bool isPanning;
private bool isPainting;
private Vector2 panStartMouse;
private Vector2 panStartValue;
private Vector2Int dragStartCell;
private Vector2Int dragCurrentCell;
public StageMapCanvas(StageEditorWindow owner, StageEditorState state)
{
this.owner = owner;
this.state = state;
focusable = true;
style.flexGrow = 1f;
style.backgroundColor = StageEditorColors.Background;
generateVisualContent += OnGenerateVisualContent;
RegisterCallback<MouseDownEvent>(OnMouseDown);
RegisterCallback<MouseMoveEvent>(OnMouseMove);
RegisterCallback<MouseUpEvent>(OnMouseUp);
RegisterCallback<WheelEvent>(OnWheel);
}
public void SetStage(StageDefinition newStage)
{
stage = newStage;
stage?.EnsureDefaults();
MarkDirtyRepaint();
}
public void RequestRepaint()
{
MarkDirtyRepaint();
}
private void OnWheel(WheelEvent evt)
{
if (stage is null)
{
return;
}
var mouse = evt.localMousePosition;
var before = ScreenToGridFloat(mouse);
var delta = -evt.delta.y * 0.08f;
cellPixels = Mathf.Clamp(cellPixels * (1f + delta), MinCellPixels, MaxCellPixels);
var after = ScreenToGridFloat(mouse);
pan += (after - before) * cellPixels;
evt.StopPropagation();
MarkDirtyRepaint();
}
private void OnMouseDown(MouseDownEvent evt)
{
Focus();
if (stage is null)
{
return;
}
// Pan is no longer a palette tool. It is always available through RMB/MMB.
if (evt.button == 2 || evt.button == 1)
{
isPanning = true;
panStartMouse = evt.localMousePosition;
panStartValue = pan;
this.CaptureMouse();
evt.StopPropagation();
return;
}
if (evt.button != 0)
{
return;
}
if (!TryScreenToCell(evt.localMousePosition, out Vector2Int cell))
{
return;
}
// LMB always tries to select an existing logical object first.
// This removes the need for a Select tool.
var hit = FindObjectAt(cell, false);
if (hit != null)
{
state.SelectObject(hit);
MarkDirtyRepaint();
evt.StopPropagation();
return;
}
state.SelectObject(null);
// Empty space uses the active palette item to paint/place.
dragStartCell = cell;
dragCurrentCell = cell;
isPainting = true;
this.CaptureMouse();
evt.StopPropagation();
MarkDirtyRepaint();
}
private void OnMouseMove(MouseMoveEvent evt)
{
if (isPanning)
{
pan = panStartValue + evt.localMousePosition - panStartMouse;
evt.StopPropagation();
MarkDirtyRepaint();
return;
}
if (isPainting)
{
if (TryScreenToCell(evt.localMousePosition, out Vector2Int cell))
{
dragCurrentCell = cell;
MarkDirtyRepaint();
}
evt.StopPropagation();
}
}
private void OnMouseUp(MouseUpEvent evt)
{
if (isPanning)
{
isPanning = false;
if (this.HasMouseCapture())
{
this.ReleaseMouse();
}
evt.StopPropagation();
return;
}
if (!isPainting || evt.button != 0)
{
return;
}
isPainting = false;
if (this.HasMouseCapture())
{
this.ReleaseMouse();
}
var rect = GetDragRect();
ApplyPalette(rect);
evt.StopPropagation();
MarkDirtyRepaint();
}
private void ApplyPalette(RectInt rect)
{
StageFloorDefinition floor = stage?.ActiveFloor;
if (floor is null)
{
return;
}
var entry = state.activePaletteEntry;
if (entry is null)
{
return;
}
if (entry.paintsStructural)
{
owner.MutateStage("Paint " + entry.displayName, delegate
{
floor.FillRect(rect, entry.structuralKind);
});
return;
}
var objectRect = rect;
if (rect is { width: 1, height: 1 })
{
objectRect = new RectInt(rect.x, rect.y, Mathf.Max(1, entry.defaultSize.x), Mathf.Max(1, entry.defaultSize.y));
}
objectRect.width = Mathf.Clamp(objectRect.width, Mathf.Max(1, entry.minSize.x), Mathf.Max(1, entry.maxSize.x));
objectRect.height = Mathf.Clamp(objectRect.height, Mathf.Max(1, entry.minSize.y), Mathf.Max(1, entry.maxSize.y));
owner.MutateStage("Place " + entry.displayName, delegate
{
var obj = new StageObjectDefinition
{
id = stage.CreateUniqueObjectId(entry.objectKind),
layer = entry.layer,
kind = entry.objectKind,
typeId = entry.typeId,
prefabId = entry.prefabId,
floorId = floor.floorId,
x = objectRect.x,
y = objectRect.y,
width = Mathf.Max(1, objectRect.width),
height = Mathf.Max(1, objectRect.height),
cutsGround = entry.cutsGround,
blocksMovement = entry.blocksMovement
};
stage.objects.Add(obj);
state.SelectObject(obj);
});
}
private StageObjectDefinition FindObjectAt(Vector2Int cell, bool activeLayerOnly)
{
if (stage is null)
{
return null;
}
var floorId = stage.ActiveFloor.floorId;
for (var i = stage.objects.Count - 1; i >= 0; i--)
{
var obj = stage.objects[i];
if (obj is null || obj.floorId != floorId)
{
continue;
}
if (activeLayerOnly && obj.layer != state.activeLayer)
{
continue;
}
if (obj.ContainsCell(cell.x, cell.y))
{
return obj;
}
}
return null;
}
private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
var p = mgc.painter2D;
FillRect(p, contentRect, StageEditorColors.Background);
if (stage is null)
{
return;
}
stage.EnsureDefaults();
var floor = stage.ActiveFloor;
if (floor is null)
{
return;
}
DrawInfiniteGrid(p);
DrawCells(p, floor);
DrawObjects(p, floor.floorId);
DrawOriginAxes(p);
DrawPreview(p);
}
private void DrawCells(Painter2D p, StageFloorDefinition floor)
{
foreach (var cell in floor.Cells)
{
var color = StageEditorColors.Structural(cell.structural);
if (color.a <= 0f)
{
continue;
}
var rect = CellRect(cell.x, cell.y);
FillRect(p, rect, color);
}
}
private void DrawObjects(Painter2D p, string floorId)
{
foreach (var obj in stage.objects)
{
if (obj is null || obj.floorId != floorId)
{
continue;
}
var rect = GridRect(obj.Bounds);
var color = StageEditorColors.Layer(obj.layer);
FillRect(p, rect, color);
StrokeRect(p, rect, color.WithAlpha(Mathf.Min(1f, color.a + 0.25f)), 2f);
if (state.selectedObject == obj)
{
StrokeRect(p, rect.Expanded(2f), StageEditorColors.SelectionStroke, 3f);
}
}
}
private void DrawInfiniteGrid(Painter2D p)
{
var r = contentRect;
var minX = Mathf.FloorToInt((r.xMin - pan.x) / cellPixels) - 1;
var maxX = Mathf.CeilToInt((r.xMax - pan.x) / cellPixels) + 1;
var minY = Mathf.FloorToInt((r.yMin - pan.y) / cellPixels) - 1;
var maxY = Mathf.CeilToInt((r.yMax - pan.y) / cellPixels) + 1;
for (var x = minX; x <= maxX; x++)
{
var sx = pan.x + x * cellPixels;
var c = x % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor;
DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), c, 1f);
}
for (var y = minY; y <= maxY; y++)
{
var sy = pan.y + y * cellPixels;
var c = y % 4 == 0 ? StageEditorColors.GridMajor : StageEditorColors.GridMinor;
DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), c, 1f);
}
}
private void DrawOriginAxes(Painter2D p)
{
var r = contentRect;
var sx = pan.x;
var sy = pan.y;
if (sx >= r.xMin && sx <= r.xMax)
{
DrawLine(p, new Vector2(sx, r.yMin), new Vector2(sx, r.yMax), StageEditorColors.GridBorder, 2f);
}
if (sy >= r.yMin && sy <= r.yMax)
{
DrawLine(p, new Vector2(r.xMin, sy), new Vector2(r.xMax, sy), StageEditorColors.GridBorder, 2f);
}
}
private void DrawPreview(Painter2D p)
{
if (!isPainting || state.activePaletteEntry is null)
{
return;
}
var rect = GetDragRect();
var entry = state.activePaletteEntry;
if (!entry.paintsStructural && rect is { width: 1, height: 1 })
{
rect = new RectInt(rect.x, rect.y, Mathf.Max(1, entry.defaultSize.x), Mathf.Max(1, entry.defaultSize.y));
}
var screenRect = GridRect(rect);
FillRect(p, screenRect, StageEditorColors.PreviewFill);
StrokeRect(p, screenRect, StageEditorColors.PreviewStroke, 2f);
}
private RectInt GetDragRect()
{
var minX = Mathf.Min(dragStartCell.x, dragCurrentCell.x);
var maxX = Mathf.Max(dragStartCell.x, dragCurrentCell.x);
var minY = Mathf.Min(dragStartCell.y, dragCurrentCell.y);
var maxY = Mathf.Max(dragStartCell.y, dragCurrentCell.y);
return new RectInt(minX, minY, maxX - minX + 1, maxY - minY + 1);
}
private bool TryScreenToCell(Vector2 screen, out Vector2Int cell)
{
var grid = ScreenToGridFloat(screen);
cell = new Vector2Int(Mathf.FloorToInt(grid.x), Mathf.FloorToInt(grid.y));
return stage?.ActiveFloor is not null;
}
private Vector2 ScreenToGridFloat(Vector2 screen)
{
return new Vector2((screen.x - pan.x) / cellPixels, (screen.y - pan.y) / cellPixels);
}
private Rect CellRect(int x, int y)
{
return new Rect(pan.x + x * cellPixels, pan.y + y * cellPixels, cellPixels, cellPixels);
}
private Rect GridRect(RectInt rect)
{
return new Rect(pan.x + rect.x * cellPixels, pan.y + rect.y * cellPixels, rect.width * cellPixels, rect.height * cellPixels);
}
private void FillRect(Painter2D p, Rect rect, Color color)
{
p.fillColor = color;
p.BeginPath();
p.MoveTo(new Vector2(rect.xMin, rect.yMin));
p.LineTo(new Vector2(rect.xMax, rect.yMin));
p.LineTo(new Vector2(rect.xMax, rect.yMax));
p.LineTo(new Vector2(rect.xMin, rect.yMax));
p.ClosePath();
p.Fill();
}
private void StrokeRect(Painter2D p, Rect rect, Color color, float width)
{
p.strokeColor = color;
p.lineWidth = width;
p.BeginPath();
p.MoveTo(new Vector2(rect.xMin, rect.yMin));
p.LineTo(new Vector2(rect.xMax, rect.yMin));
p.LineTo(new Vector2(rect.xMax, rect.yMax));
p.LineTo(new Vector2(rect.xMin, rect.yMax));
p.ClosePath();
p.Stroke();
}
private void DrawLine(Painter2D p, Vector2 a, Vector2 b, Color color, float width)
{
p.strokeColor = color;
p.lineWidth = width;
p.BeginPath();
p.MoveTo(a);
p.LineTo(b);
p.Stroke();
}
}
internal static class StageEditorRectExtensions
{
public static Rect Expanded(this Rect rect, float amount)
{
return new Rect(rect.x - amount, rect.y - amount, rect.width + amount * 2f, rect.height + amount * 2f);
}
public static Color WithAlpha(this Color color, float alpha)
{
color.a = alpha;
return color;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8c584058841343debb04c00199a79393

View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
using UnityEngine;
namespace Editor.GardensStageEditor
{
public sealed class StagePaletteEntry
{
public string id;
public string displayName;
public StageLogicalLayer layer;
public bool paintsStructural;
public StructuralCellKind structuralKind;
public StageObjectKind objectKind;
public string typeId;
public string prefabId;
public Vector2Int defaultSize = Vector2Int.one;
public Vector2Int minSize = Vector2Int.one;
public Vector2Int maxSize = new(32, 32);
public bool resizable = true;
public bool cutsGround;
public bool blocksMovement;
public static StagePaletteEntry Structural(string id, string name, StructuralCellKind kind)
{
return new StagePaletteEntry
{
id = id,
displayName = name,
layer = StageLogicalLayer.Structural,
paintsStructural = true,
structuralKind = kind,
defaultSize = Vector2Int.one,
minSize = Vector2Int.one,
maxSize = new Vector2Int(64, 64),
resizable = true
};
}
public static StagePaletteEntry Object(string id, string name, StageLogicalLayer layer, StageObjectKind kind, string typeId, Vector2Int defaultSize)
{
return new StagePaletteEntry
{
id = id,
displayName = name,
layer = layer,
paintsStructural = false,
objectKind = kind,
typeId = typeId,
prefabId = string.Empty,
defaultSize = defaultSize,
minSize = Vector2Int.one,
maxSize = new Vector2Int(64, 64),
resizable = true
};
}
public static List<StagePaletteEntry> CreateBuiltInPalette()
{
var entries = new List<StagePaletteEntry>
{
Structural("struct_ground", "Ground", StructuralCellKind.Ground),
Structural("struct_wall", "Wall", StructuralCellKind.Wall),
Structural("struct_hole", "Hole", StructuralCellKind.Hole),
Structural("struct_empty", "Erase Structural", StructuralCellKind.Empty)
};
var trap = Object("trap_falling_floor", "Falling Floor", StageLogicalLayer.Design, StageObjectKind.Trap, "falling_floor", Vector2Int.one);
trap.cutsGround = true;
trap.blocksMovement = true;
entries.Add(trap);
var door = Object("door_standard", "Door", StageLogicalLayer.Design, StageObjectKind.Door, "door_standard", new Vector2Int(1, 2));
door.blocksMovement = true;
entries.Add(door);
entries.Add(Object("interactable", "Interactable", StageLogicalLayer.Design, StageObjectKind.Interactable, "interactable", Vector2Int.one));
entries.Add(Object("portal", "Portal", StageLogicalLayer.Design, StageObjectKind.Portal, "portal", new Vector2Int(2, 2)));
entries.Add(Object("camera_zone", "Camera Zone", StageLogicalLayer.Camera, StageObjectKind.CameraZone, "camera_zone", new Vector2Int(8, 5)));
entries.Add(Object("spawn_player", "Player Spawn", StageLogicalLayer.Markers, StageObjectKind.Spawn, "player_spawn", Vector2Int.one));
entries.Add(Object("elevator", "Elevator", StageLogicalLayer.Markers, StageObjectKind.Elevator, "elevator", new Vector2Int(2, 2)));
entries.Add(Object("marker", "Marker", StageLogicalLayer.Markers, StageObjectKind.Marker, "marker", Vector2Int.one));
return entries;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0e7ed87816264200a12fcc4bf3584153
timeCreated: 1781500522

View File

@ -0,0 +1,163 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace Editor.GardensStageEditor
{
public sealed class StagePalettePanel : VisualElement
{
private readonly StageEditorState state;
private readonly List<StagePaletteEntry> entries;
public StagePalettePanel(StageEditorState state, List<StagePaletteEntry> entries)
{
this.state = state;
this.entries = entries;
style.flexDirection = FlexDirection.Column;
style.minHeight = 110;
style.maxHeight = 170;
style.paddingLeft = 8;
style.paddingRight = 8;
style.paddingTop = 6;
style.paddingBottom = 6;
style.borderTopWidth = 1;
style.borderTopColor = new Color(1f, 1f, 1f, 0.12f);
state.Changed += Rebuild;
Rebuild();
}
private void Rebuild()
{
Clear();
var row = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row,
alignItems = Align.Center
}
};
Add(row);
var title = new Label("Palette")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
marginRight = 12
}
};
row.Add(title);
AddLayerButton(row, StageLogicalLayer.Structural);
AddLayerButton(row, StageLogicalLayer.Design);
AddLayerButton(row, StageLogicalLayer.Camera);
AddLayerButton(row, StageLogicalLayer.Markers);
var hint = new Label("LMB selects objects. Empty space paints/places selected palette item. RMB/MMB pans.")
{
style =
{
opacity = 0.65f,
marginLeft = 12
}
};
row.Add(hint);
var scroll = new ScrollView(ScrollViewMode.Horizontal)
{
style =
{
flexGrow = 1f,
marginTop = 8
}
};
Add(scroll);
var itemRow = new VisualElement
{
style =
{
flexDirection = FlexDirection.Row
}
};
scroll.Add(itemRow);
foreach (var entry in entries)
{
if (entry.layer != state.activeLayer)
{
continue;
}
var button = new Button(delegate { state.SetPaletteEntry(entry); })
{
text = entry.displayName,
style =
{
minWidth = 120,
height = 48,
marginRight = 6,
unityTextAlign = TextAnchor.MiddleCenter
}
};
if (state.activePaletteEntry == entry)
{
button.style.borderBottomWidth = 3;
button.style.borderBottomColor = StageEditorColors.SelectionStroke;
}
itemRow.Add(button);
}
}
private void AddLayerButton(VisualElement row, StageLogicalLayer layer)
{
var button = new Button(delegate
{
state.SetActiveLayer(layer);
if (state.activePaletteEntry == null || state.activePaletteEntry.layer != layer)
{
var first = FindFirstEntryForLayer(layer);
if (first != null)
{
state.SetPaletteEntry(first);
}
}
})
{
text = layer.ToString(),
style =
{
marginRight = 4
}
};
if (state.activeLayer == layer)
{
button.style.borderBottomWidth = 3;
button.style.borderBottomColor = StageEditorColors.SelectionStroke;
}
row.Add(button);
}
private StagePaletteEntry FindFirstEntryForLayer(StageLogicalLayer layer)
{
foreach (var stagePaletteEntry in entries)
{
if (stagePaletteEntry.layer == layer)
{
return stagePaletteEntry;
}
}
return null;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9ffd5aa3cf34c84be81a3c01679fc3e

View File

@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.Text;
namespace Editor.GardensStageEditor
{
public enum StageValidationSeverity
{
Info,
Warning,
Error
}
public readonly struct StageValidationMessage
{
public readonly StageValidationSeverity severity;
public readonly string message;
public StageValidationMessage(StageValidationSeverity severity, string message)
{
this.severity = severity;
this.message = message;
}
public override string ToString()
{
return severity + ": " + message;
}
}
public static class StageValidator
{
public static List<StageValidationMessage> Validate(StageDefinition stage)
{
var messages = new List<StageValidationMessage>();
if (stage is null)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "No StageDefinition selected."));
return messages;
}
stage.EnsureDefaults();
if (string.IsNullOrEmpty(stage.stageId))
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Stage id is empty."));
}
if (stage.floors.Count == 0)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Stage has no floors."));
}
for (int i = 0; i < stage.floors.Count; i++)
{
StageFloorDefinition floor = stage.floors[i];
if (floor is null)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Floor at index " + i + " is null."));
continue;
}
if (string.IsNullOrEmpty(floor.floorId))
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Floor at index " + i + " has no floor id."));
}
}
var hasSpawn = false;
var ids = new HashSet<string>();
for (var i = 0; i < stage.objects.Count; i++)
{
var obj = stage.objects[i];
if (obj is null)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Object at index " + i + " is null."));
continue;
}
if (string.IsNullOrEmpty(obj.id))
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Object at index " + i + " has no id."));
}
else if (!ids.Add(obj.id))
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Duplicate object id: " + obj.id));
}
var floor = stage.GetFloor(obj.floorId);
if (floor is null)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Object " + obj.id + " references missing floor: " + obj.floorId));
continue;
}
if (obj.width <= 0 || obj.height <= 0)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Error, "Object " + obj.id + " has invalid size."));
}
if (obj.kind == StageObjectKind.Spawn)
{
hasSpawn = true;
}
}
if (!hasSpawn)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Warning, "Stage has no player spawn marker."));
}
if (messages.Count == 0)
{
messages.Add(new StageValidationMessage(StageValidationSeverity.Info, "No validation issues found."));
}
return messages;
}
public static string Format(List<StageValidationMessage> messages)
{
var sb = new StringBuilder();
foreach (var stageValidationMessage in messages)
{
sb.AppendLine(stageValidationMessage.ToString());
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a32d1b16f27f4bfd90425ffa3fde2c89

View File

@ -9,7 +9,7 @@
"com.unity.2d.tilemap": "1.0.0", "com.unity.2d.tilemap": "1.0.0",
"com.unity.2d.tilemap.extras": "6.0.2", "com.unity.2d.tilemap.extras": "6.0.2",
"com.unity.2d.tooling": "1.0.3", "com.unity.2d.tooling": "1.0.3",
"com.unity.cinemachine": "3.1.6", "com.unity.cinemachine": "3.1.7",
"com.unity.collab-proxy": "2.12.4", "com.unity.collab-proxy": "2.12.4",
"com.unity.ide.rider": "3.0.40", "com.unity.ide.rider": "3.0.40",
"com.unity.ide.visualstudio": "2.0.27", "com.unity.ide.visualstudio": "2.0.27",

View File

@ -117,7 +117,7 @@
"url": "https://packages.unity.com" "url": "https://packages.unity.com"
}, },
"com.unity.cinemachine": { "com.unity.cinemachine": {
"version": "3.1.6", "version": "3.1.7",
"depth": 0, "depth": 0,
"source": "registry", "source": "registry",
"dependencies": { "dependencies": {