Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Run edit-mode tests
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@
# OS
.DS_Store
Thumbs.db

# Session worktrees from Claude Code agent runs.
.claude/
2 changes: 2 additions & 0 deletions Editor/AssemblyInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 8 additions & 12 deletions Editor/Importers/JsonEnvironmentImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@

namespace OpenApparatus.Unity.Editor.Importers
{
[ScriptedImporter(version: 1, ext: "json")]
[ScriptedImporter(version: 1, ext: "oae")]
public sealed class JsonEnvironmentImporter : ScriptedImporter
{
// Spawn-time settings persisted on the importer so they survive reimport.
// Editable via JsonEnvironmentImporterEditor (top "Import Settings" section).
public ColliderMode ColliderMode = ColliderMode.None;
public PrefabSubstitutionTable Substitution;

public override void OnImportAsset(AssetImportContext ctx)
{
string text;
Expand Down Expand Up @@ -47,20 +52,11 @@ public override void OnImportAsset(AssetImportContext ctx)
asset.ObjectSlots = MapSlots(doc.objectSlots);
asset.Rooms = MapRooms(doc.rooms);
asset.OutsideObjects = MapOutside(doc.outside);
asset.ColliderMode = ColliderMode;
asset.Substitution = Substitution;

ctx.AddObjectToAsset("environment", asset);
ctx.SetMainObject(asset);

if (asset.Rooms != null)
{
foreach (var room in asset.Rooms)
{
var mesh = JsonGeometryBuilder.BuildRoomMesh(room, asset.Parameters,
$"OpenApparatus_Room_{room.Id}");
ctx.AddObjectToAsset($"mesh_room_{room.Id}", mesh);
asset.SetRoomMesh(room.Id, mesh);
}
}
}

static EnvironmentParameters MapParameters(JsonParameters p)
Expand Down
98 changes: 98 additions & 0 deletions Editor/Importers/JsonEnvironmentImporterEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using UnityEditor;
using UnityEditor.AssetImporters;
using UnityEngine;
using OpenApparatus.Unity.Editor.Internal;

namespace OpenApparatus.Unity.Editor.Importers
{
[CustomEditor(typeof(JsonEnvironmentImporter))]
public sealed class JsonEnvironmentImporterEditor : ScriptedImporterEditor
{
SerializedProperty _colliderMode;
SerializedProperty _substitution;

public override void OnEnable()
{
base.OnEnable();
_colliderMode = serializedObject.FindProperty(nameof(JsonEnvironmentImporter.ColliderMode));
_substitution = serializedObject.FindProperty(nameof(JsonEnvironmentImporter.Substitution));
}

public override void OnInspectorGUI()
{
var importer = (JsonEnvironmentImporter)target;
var asset = AssetDatabase.LoadAssetAtPath<MultiRoomEnvironmentAsset>(importer.assetPath);

DrawSummary(asset);

EditorGUILayout.Space();
EditorGUILayout.LabelField("Spawn options", EditorStyles.boldLabel);

serializedObject.Update();
EditorGUILayout.PropertyField(_colliderMode);
EditorGUILayout.PropertyField(_substitution);
serializedObject.ApplyModifiedProperties();

EditorGUILayout.HelpBox(
"Changing collider mode or substitution applies on reimport (Apply below) " +
"and on the next Spawn into scene.",
MessageType.Info);

EditorGUILayout.Space();
ApplyRevertGUI();

EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(asset == null))
{
if (GUILayout.Button("Spawn into scene", GUILayout.Height(28)))
{
var root = EnvironmentSpawner.Spawn(asset);
if (root != null) Selection.activeGameObject = root;
}
}
}

static void DrawSummary(MultiRoomEnvironmentAsset asset)
{
EditorGUILayout.LabelField("Summary", EditorStyles.boldLabel);
if (asset == null)
{
EditorGUILayout.HelpBox("No imported asset (file may have failed to parse).",
MessageType.Warning);
return;
}
EditorGUILayout.LabelField("Schema version", asset.SchemaVersion.ToString());
EditorGUILayout.LabelField("Rooms", (asset.Rooms?.Length ?? 0).ToString());
int objectCount = 0;
if (asset.Rooms != null)
foreach (var r in asset.Rooms) objectCount += r.Objects?.Length ?? 0;
EditorGUILayout.LabelField("Objects", objectCount.ToString());
EditorGUILayout.LabelField("Object slots", (asset.ObjectSlots?.Length ?? 0).ToString());
if (asset.Parameters != null)
{
EditorGUILayout.LabelField($"Tile size: {asset.Parameters.TileSize:0.##}");
EditorGUILayout.LabelField(
$"Wall: {asset.Parameters.WallThickness:0.##} x {asset.Parameters.WallHeight:0.##}");
}

if (asset.ObjectSlots != null && asset.ObjectSlots.Length > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Object types in this environment",
EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"Use the bold type name on the left as the 'Object Type' " +
"value in a PrefabSubstitutionTable entry.",
MessageType.None);
foreach (var slot in asset.ObjectSlots)
{
var label = !string.IsNullOrEmpty(slot.ObjectType)
? slot.ObjectType
: (slot.DisplayName ?? $"slot {slot.Id}");
var meta = $"slot {slot.Id} · {slot.Shape ?? "?"} · size {slot.Size:0.##}";
EditorGUILayout.LabelField(label, meta);
}
}
}
}
}
2 changes: 2 additions & 0 deletions Editor/Importers/JsonEnvironmentImporterEditor.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 10 additions & 30 deletions Editor/Inspectors/MultiRoomEnvironmentAssetEditor.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,36 @@
using UnityEditor;
using UnityEngine;
using OpenApparatus.Unity.Editor.Internal;

namespace OpenApparatus.Unity.Editor.Inspectors
{
// Bottom "Imported Object" section of the .oae inspector.
// Unity locks this entire section read-only to prevent edits that would
// be lost on reimport. Spawn options + Spawn button therefore live on
// JsonEnvironmentImporterEditor (the top "Import Settings" section), not
// here. This editor stays as a passive summary.
[CustomEditor(typeof(MultiRoomEnvironmentAsset))]
public sealed class MultiRoomEnvironmentAssetEditor : UnityEditor.Editor
{
SerializedProperty _colliderMode;
SerializedProperty _substitution;

void OnEnable()
{
_colliderMode = serializedObject.FindProperty(nameof(MultiRoomEnvironmentAsset.ColliderMode));
_substitution = serializedObject.FindProperty(nameof(MultiRoomEnvironmentAsset.Substitution));
}

public override void OnInspectorGUI()
{
var asset = (MultiRoomEnvironmentAsset)target;

EditorGUILayout.LabelField("Summary", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Schema version", asset.SchemaVersion.ToString());
EditorGUILayout.LabelField("Rooms", (asset.Rooms?.Length ?? 0).ToString());
int objectCount = 0;
if (asset.Rooms != null)
foreach (var r in asset.Rooms) objectCount += r.Objects?.Length ?? 0;
EditorGUILayout.LabelField("Objects", objectCount.ToString());
EditorGUILayout.LabelField("Object slots", (asset.ObjectSlots?.Length ?? 0).ToString());

if (asset.Parameters != null)
{
EditorGUILayout.LabelField($"Tile size: {asset.Parameters.TileSize:0.##}");
EditorGUILayout.LabelField($"Wall: {asset.Parameters.WallThickness:0.##} x {asset.Parameters.WallHeight:0.##}");
}
EditorGUILayout.LabelField("Collider mode", asset.ColliderMode.ToString());
EditorGUILayout.LabelField("Substitution table",
asset.Substitution != null ? asset.Substitution.name : "(none)");

EditorGUILayout.Space();
EditorGUILayout.LabelField("Spawn options", EditorStyles.boldLabel);
serializedObject.Update();
EditorGUILayout.PropertyField(_colliderMode);
EditorGUILayout.PropertyField(_substitution);
EditorGUILayout.HelpBox(
"Collider mode and substitution table take effect on next spawn.",
"Edit spawn options and trigger spawn via the Import Settings " +
"section above (top of inspector).",
MessageType.Info);
serializedObject.ApplyModifiedProperties();

EditorGUILayout.Space();
if (GUILayout.Button("Spawn into scene", GUILayout.Height(28)))
{
var root = EnvironmentSpawner.Spawn(asset);
if (root != null) Selection.activeGameObject = root;
}
}
}
}
58 changes: 42 additions & 16 deletions Editor/Internal/ColliderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,36 @@ namespace OpenApparatus.Unity.Editor.Internal
{
public static class ColliderBuilder
{
const string WallColliderChildName = "WallCollider";
const string FloorCollidersChildName = "FloorColliders";
const float FloorColliderThickness = 0.1f;
const string WallColliderChildName = "WallCollider";
const string FloorColliderChildName = "FloorCollider";
const string CeilingColliderChildName = "CeilingCollider";
const float SurfaceColliderThickness = 0.1f;

public static void Apply(GameObject environmentRoot,
ColliderMode mode,
EnvironmentParameters parameters)
{
if (mode == ColliderMode.None || environmentRoot == null || parameters == null) return;

bool doWalls = mode == ColliderMode.WallsOnly || mode == ColliderMode.All;
bool doFloors = mode == ColliderMode.FloorsOnly || mode == ColliderMode.All;

if (doWalls)
if ((mode & ColliderMode.Walls) != 0)
foreach (var wall in environmentRoot.GetComponentsInChildren<Wall>(includeInactive: true))
AddWallCollider(wall, parameters);

if (doFloors)
if ((mode & (ColliderMode.Floors | ColliderMode.Ceilings)) != 0)
foreach (var room in environmentRoot.GetComponentsInChildren<Room>(includeInactive: true))
AddFloorColliders(room, parameters);
{
if ((mode & ColliderMode.Floors) != 0)
AddSurfaceCollider(room, "Floor", FloorColliderChildName,
parameters, atCeiling: false);
if ((mode & ColliderMode.Ceilings) != 0)
AddSurfaceCollider(room, "Ceiling", CeilingColliderChildName,
parameters, atCeiling: true);
}

// Placeholder colliders (ColliderMode.Objects) are owned by the
// spawner, not by this pass. Spawner removes the primitive's
// default collider when the flag is off; substituted prefabs are
// never touched (prefab author decides).
}

static void AddWallCollider(Wall wall, EnvironmentParameters parameters)
Expand All @@ -48,23 +58,39 @@ static void AddWallCollider(Wall wall, EnvironmentParameters parameters)
col.size = new Vector3(length, parameters.WallHeight, parameters.WallThickness);
}

static void AddFloorColliders(Room room, EnvironmentParameters parameters)
// One BoxCollider per tile, all attached to a single child node under
// the room's Floor or Ceiling surface object. Floor sits a hair below
// y=0; ceiling sits a hair above wallHeight — both flush with the
// visible mesh but invisible to the agent.
static void AddSurfaceCollider(Room room,
string surfaceChildName,
string colliderChildName,
EnvironmentParameters parameters,
bool atCeiling)
{
if (room.TileIndices == null || room.TileIndices.Length == 0) return;

ClearExistingChild(room.transform, FloorCollidersChildName);
var surface = room.transform.Find(surfaceChildName);
if (surface == null) return;

var holder = new GameObject(FloorCollidersChildName);
holder.transform.SetParent(room.transform, worldPositionStays: false);
ClearExistingChild(surface, colliderChildName);
var holder = new GameObject(colliderChildName);
holder.transform.SetParent(surface, worldPositionStays: false);

float t = parameters.TileSize;
float colliderCenterY = atCeiling
? parameters.WallHeight + SurfaceColliderThickness * 0.5f
: -SurfaceColliderThickness * 0.5f;

// Tile indices are Studio coords; mirror X to share the room's
// Unity-space frame (same as the floor/ceiling mesh builder).
foreach (var idx in room.TileIndices)
{
var col = holder.AddComponent<BoxCollider>();
col.size = new Vector3(t, FloorColliderThickness, t);
col.size = new Vector3(t, SurfaceColliderThickness, t);
col.center = new Vector3(
(idx.x + 0.5f) * t,
-FloorColliderThickness * 0.5f,
-(idx.x + 0.5f) * t,
colliderCenterY,
(idx.y + 0.5f) * t);
}
}
Expand Down
Loading
Loading