Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
f50cd68
Add DotRecast-based navigation subsystem
Acissathar Aug 5, 2026
a5556c9
Remove dead code - Apply General
Acissathar Aug 7, 2026
1e6099a
Keep UI dirty while assets stream in
michaelsakharov Aug 4, 2026
d6ef0d2
Made transform gizmo 25 pixels bigger
michaelsakharov Aug 4, 2026
dcd5d39
Added some tests to ensure unreadable or unwritable files should neve…
michaelsakharov Aug 4, 2026
0e087d6
Made EngineObject.OnDispose Protected
michaelsakharov Aug 4, 2026
8a05c20
Add end-of-frame EngineObject destruction
michaelsakharov Aug 4, 2026
078f11b
EngineObject.Destroy() Defer dispose to end of frame, Migrated Undo/Redo
michaelsakharov Aug 4, 2026
74a9c50
Added Cursor shapes for Scene Editors and Gizmo's
michaelsakharov Aug 4, 2026
3d18d35
Refactor Scene API
michaelsakharov Aug 4, 2026
89d1c47
Guard hierarchy state for invalid GameObjects
michaelsakharov Aug 4, 2026
0df574a
Always dispose removed components (remove requirement that OnEnabled …
michaelsakharov Aug 4, 2026
f17a23e
Guard RemoveComponent against null input
michaelsakharov Aug 4, 2026
b033f45
Skip re-adding components to same GameObject
michaelsakharov Aug 4, 2026
5d7ece7
Detach component before reassigning owner
michaelsakharov Aug 4, 2026
dee6c94
Snapshot GameObject component queries
michaelsakharov Aug 4, 2026
cb99fce
Guard against reloading current scene
michaelsakharov Aug 4, 2026
7ddc2a6
Make Scene lifecycle ops idempotent
michaelsakharov Aug 4, 2026
1703f83
Dont need to make a copy of GetComponents<MonoBehaviour> anymore
michaelsakharov Aug 4, 2026
8a7af6e
Remove MonoBehaviour array copies in Scene
michaelsakharov Aug 4, 2026
83be85b
Some additional scene/component lifecycle tests
michaelsakharov Aug 4, 2026
4c526cd
Added Scene.DontDestroyOnLoad(go)
michaelsakharov Aug 5, 2026
d37252f
Fixed typo in Character Controller Template
michaelsakharov Aug 6, 2026
d0f2899
Fixed DontDestroyOnLoad preserving objects after play mode as well, m…
xZekro51 Aug 5, 2026
4d70b7a
Modified script recompile to be on project load, so that any outstand…
xZekro51 Aug 5, 2026
7207cac
Trigger script recompile on asset moves
michaelsakharov Aug 6, 2026
497265b
Fix UI anchor parent rect resolution
michaelsakharov Aug 6, 2026
d12f161
Fixes to MeshCollider
michaelsakharov Aug 6, 2026
645b288
Treat indexless UI meshes as empty
michaelsakharov Aug 6, 2026
66ef2ea
Add more utilities to RectTransform
michaelsakharov Aug 6, 2026
fed394a
Fix dropdown closing when clicking items
michaelsakharov Aug 6, 2026
394da16
Centralize default scene creation logic
michaelsakharov Aug 6, 2026
c162ea0
Add launch options for samples in VS Code
PaperPrototype Aug 6, 2026
8d256c0
UI Graphic Base Type
michaelsakharov Aug 6, 2026
99e35a4
Tons of refactors and improvements to GameObject UI
michaelsakharov Aug 6, 2026
0954e15
Remove sealed from Graphic.GetMaterial()
michaelsakharov Aug 6, 2026
36c79da
Avoid lazy-init of font system on atlas version check
michaelsakharov Aug 6, 2026
7181601
Clear layout cache after canvas rebuild
michaelsakharov Aug 6, 2026
ccd2d91
Lazy-capture authored sprite on first use
michaelsakharov Aug 6, 2026
b55d0b3
Fix ParentSize to walk up ancestor chain
michaelsakharov Aug 6, 2026
848e8bd
Fix UI navigation across canvases
michaelsakharov Aug 6, 2026
7e7166a
Fixed Tooltip attribute not working
michaelsakharov Aug 6, 2026
8349b21
Fixed double click and right click -> open, behaving much differently
michaelsakharov Aug 6, 2026
dda53a8
Removed EditorSceneManager.HandleAssetDoubleClick - Obsolete
michaelsakharov Aug 6, 2026
272cf8f
Fixed created assets landing in wrong directories at times
michaelsakharov Aug 6, 2026
b611d0b
Fixed double drawing button attributes in inspector
michaelsakharov Aug 6, 2026
6c5005c
Show empty folder icon for empty folders
michaelsakharov Aug 6, 2026
ae5475c
Bumped version to v1.0-preview-3
michaelsakharov Aug 7, 2026
acd0f7e
Move Enum rename to its own drawer, then cleanup previous references
Acissathar Aug 7, 2026
042d413
Fix range attribute drawing to stay in the property grid
Acissathar Aug 7, 2026
c3b4210
Add DotRecast-based navigation subsystem
Acissathar Aug 5, 2026
81293f0
Swap DotRecast namespaces to Prowl recast
Acissathar Aug 7, 2026
136f369
Adjustments to account for NavMesh workarounds modified upstream in P…
Acissathar Aug 7, 2026
8dee297
Fix agent API contracts
Acissathar Aug 7, 2026
fc489ca
Fix build and data correctness issues from review
Acissathar Aug 7, 2026
85a0010
NavMesh performance tweaks from review
Acissathar Aug 7, 2026
66aed3a
Adjust navmesh thread safety and lock lifetime
Acissathar Aug 8, 2026
0d58393
Make navigation work in the editor and draw its links
Acissathar Aug 8, 2026
69d5cd5
Fix navmesh asset handling and expose the path limits
Acissathar Aug 8, 2026
97745b0
More tweaks to make navigation work in the editor and draw its links
Acissathar Aug 8, 2026
e559063
Coalesce link rebuilds into one pass per frame
Acissathar Aug 8, 2026
35e6569
Copy navmesh data only when the asset database owns it
Acissathar Aug 8, 2026
7684205
Harden the link rebuild drain and tidy the agent API
Acissathar Aug 8, 2026
152426a
Merge branch 'main' into NavMesh-Single-Representation
Acissathar Aug 8, 2026
158f714
Merge branch 'main' into NavMesh-Single-Representation
Acissathar Aug 9, 2026
402bafd
Fix navmesh terrain support, tile seams, and the navmesh debug overlay
Acissathar Aug 11, 2026
8917183
Revert Gizmo Shader changes - not meant to be committed
Acissathar Aug 11, 2026
92e9bd2
Trim down some comments
Acissathar Aug 12, 2026
6afd29d
Merge branch 'main' into NavMesh-Single-Representation
Acissathar Aug 26, 2026
056e1dd
Update Unit Test to account for new Terrain ResizeHeightmap method
Acissathar Aug 26, 2026
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
31 changes: 31 additions & 0 deletions Prowl.Editor.Test/AssetDatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -789,5 +789,36 @@ public void UnknownExtension_IsTrackedButNotResolvable()
Assert.Null(Assets.Get(g));
}

/// <summary>
/// A .navmesh is written and read as binary Echo. Its payload is compressed voxelization
/// blobs, which as text become base64 — bigger, slower to parse, and no more readable. A
/// text one does not parse as binary, so it fails the import outright rather than loading
/// as something wrong; rebaking is the migration.
/// </summary>
[Fact]
public void NavMesh_RoundTripsAsBinary_AndRejectsText()
{
NavMeshData? baked = NavMeshBuilder.Build(new NavMeshBuildSettings(), [FlatQuad(20f)]);
Assert.NotNull(baked);
EchoObject echo = Serializer.Serialize(typeof(object), baked!);

echo.WriteToBinary(new FileInfo(AssetAbsolutePath("Baked.navmesh")));
Guid guid = Assets.ImportFile("Baked.navmesh");
Assert.NotEqual(Guid.Empty, guid);

var loaded = Assets.Get(guid) as NavMeshData;
Assert.NotNull(loaded);
Assert.Equal(baked!.CacheLayers.Count, loaded!.CacheLayers.Count);

File.WriteAllText(AssetAbsolutePath("Legacy.navmesh"), echo.WriteToString());
Assert.Null(Assets.Get(Assets.ImportFile("Legacy.navmesh")));
}

private static NavMeshGeometrySource FlatQuad(float size)
{
Prowl.Vector.Float3[] verts = [new(0, 0, 0), new(0, 0, size), new(size, 0, size), new(size, 0, 0)];
return new NavMeshGeometrySource(verts, [0, 1, 2, 0, 2, 3], Prowl.Vector.Float4x4.Identity);
}

#endregion
}
1 change: 1 addition & 0 deletions Prowl.Editor.Test/BuildSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,7 @@ public void EveryDesktopTarget_MapsToTheRightRuntimePrefix(string targetId, stri
[InlineData(PlayerSettingsFiles.Time)]
[InlineData(PlayerSettingsFiles.Assets)]
[InlineData(PlayerSettingsFiles.TagsAndLayers)]
[InlineData(PlayerSettingsFiles.Navigation)]
public void EveryFileThePlayerReads_IsExportedByATypeOfThatName(string expected)
{
var entry = Assert.Single(EditorRegistries.SettingsEntries.Where(e => e.Type.Name == expected));
Expand Down
19 changes: 19 additions & 0 deletions Prowl.Editor.Test/ProjectSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ public ProjectSettingsTests()
EditorRegistries.OnProjectOpened();
}

// ResetToDefaults is not a button — it runs when a project is opened, before that project's
// own settings load. Deriving the "defaults" from the live NavMeshAreas table would therefore
// carry the PREVIOUS project's area names and costs into the new one.
[Fact]
public void NavigationSettings_ResetToDefaults_IgnoresTheLiveAreaTable()
{
const int Custom = 3; // built-in areas are immutable, so only custom ones can drift
Prowl.Runtime.NavMeshAreas.SetAreaName(Custom, "Swamp");
Prowl.Runtime.NavMeshAreas.SetAreaCost(Custom, 5f);

var settings = EditorRegistries.GetSettings<NavigationSettings>();
settings.ResetToDefaults();

Assert.Equal(string.Empty, settings.AreaNames[Custom]);
Assert.Equal(1f, settings.AreaCosts[Custom]);
Assert.Equal("Walkable", settings.AreaNames[Prowl.Runtime.NavMeshAreas.Walkable]);
Assert.Equal("Jump", settings.AreaNames[Prowl.Runtime.NavMeshAreas.Jump]);
}

// Settings persist as Echo YAML: a saved value must survive a save/load round-trip.
[Fact]
public void SettingsSaveLoad_RoundTripsYaml()
Expand Down
14 changes: 12 additions & 2 deletions Prowl.Editor/AssetsDatabase/Importers/ImportHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ public static bool ImportEchoObject(ImportContext ctx, string errorLabel)
=> ImportEcho<EngineObject>(ctx, errorLabel);

public static bool ImportEcho<T>(ImportContext ctx, string errorLabel) where T : EngineObject
=> ImportEcho<T>(ctx, errorLabel, static path => EchoObject.ReadFromString(File.ReadAllText(path)));

/// <summary>
/// As <see cref="ImportEcho{T}(ImportContext, string)"/>, for assets written with Echo's
/// binary format — the one to use when the payload is bulk bytes rather than something a
/// human reads or diffs.
/// </summary>
public static bool ImportEchoBinary<T>(ImportContext ctx, string errorLabel) where T : EngineObject
=> ImportEcho<T>(ctx, errorLabel, static path => EchoObject.ReadFromBinary(new FileInfo(path)));

private static bool ImportEcho<T>(ImportContext ctx, string errorLabel, Func<string, EchoObject> read) where T : EngineObject
{
try
{
string text = File.ReadAllText(ctx.AbsolutePath);
var echo = EchoObject.ReadFromString(text);
var echo = read(ctx.AbsolutePath);
var serCtx = CreateTrackingContext(out var dependencies);
var asset = Serializer.Deserialize<T>(echo, serCtx);
if (asset != null)
Expand Down
18 changes: 18 additions & 0 deletions Prowl.Editor/AssetsDatabase/Importers/NavMeshDataImporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Prowl.Runtime;

namespace Prowl.Editor.Importers;

/// <summary>
/// Imports .navmesh files - Echo-serialized NavMeshData objects (baked navigation meshes).
/// Binary, because a navmesh is almost entirely compressed voxelization blobs: as text they are
/// base64, which is larger, slower to parse, and undiffable anyway.
/// </summary>
[ImporterFor(".navmesh")]
public class NavMeshDataImporter : AssetImporter
{
/// <summary>Bumping this reimports every .navmesh. A text-written one does not parse as
/// binary, so it fails the import rather than loading wrong — rebake it.</summary>
public override int Version => 2;

public override bool Import(ImportContext ctx) => ImportHelper.ImportEchoBinary<NavMeshData>(ctx, "nav mesh data");
}
3 changes: 3 additions & 0 deletions Prowl.Editor/GUI/AttributeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,8 @@ public static void Register(OrigamiUI.AttributeHandlerRegistry registry)
registry.Register<RangeAttribute>(new RangeAttributeHandler());
registry.Register<TextAreaAttribute>(new TextAreaAttributeHandler());
registry.Register<TooltipAttribute>(new TooltipAttributeHandler());
registry.Register<NavMeshAreaAttribute>(new NavMeshAreaAttributeHandler());
registry.Register<NavMeshAreaMaskAttribute>(new NavMeshAreaMaskAttributeHandler());
registry.Register<NavMeshAgentTypeAttribute>(new NavMeshAgentTypeAttributeHandler());
}
}
147 changes: 147 additions & 0 deletions Prowl.Editor/GUI/CustomEditors/NavMeshSurfaceEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// This file is part of the Prowl Game Engine
// Licensed under the MIT License. See the LICENSE file in the project root for details.

using System;
using System.IO;

using Prowl.Echo;
using Prowl.Editor.Core;
using Prowl.Editor.GUI;
using Prowl.Editor.GUI.SceneView;
using Prowl.Editor.Projects;
using Prowl.Editor.Theming;
using Prowl.OrigamiUI;
using Prowl.PaperUI;
using Prowl.Runtime;

namespace Prowl.Editor.Inspector;

/// <summary>
/// Inspector for <see cref="NavMeshSurface"/>: the default property grid plus an editor bake
/// that saves the result as a <c>.navmesh</c> asset in a SceneName_navmesh folder (mirroring
/// the lightmapper's SceneName_lightmaps convention) and assigns it to the surface, so the
/// baked navmesh survives scene reloads and ships with the project.
/// </summary>
[CustomEditor(typeof(NavMeshSurface))]
public class NavMeshSurfaceEditor : CustomEditor
{
public override void OnGUI(Paper paper, string id, object target)
{
var surface = (NavMeshSurface)target;

// Pre-snapshot: captures entire component state before any widget mutates it, so
// default-grid edits, the Advanced fields, and the Reset button all undo uniformly.
Undo.Snapshot(surface);

// Basic fields (agent type, collection, layers, geometry, data ref) via the default
// grid; DefaultArea and the build overrides are [HideInInspector] and drawn below —
// the same basic/advanced split Unity's NavMeshSurface uses.
DrawDefaultInspector(paper, $"{id}_def", target);

paper.Box($"{id}_adv_sp").Height(4);
Origami.Foldout(paper, $"{id}_adv", "Advanced").Body(() =>
{
NavMeshAreaAttributeHandler.DrawAreaField(paper, $"{id}_adv_area", "Default Area", surface.DefaultArea, v =>
{
surface.DefaultArea = v;
EditorSceneManager.MarkDirty();
});

PropertyGridUtils.Draw(paper, $"{id}_adv_ovr", surface.BuildOverrides,
_ => EditorSceneManager.MarkDirty());

paper.Box($"{id}_adv_reset_sp").Height(4);
Origami.Button(paper, $"{id}_adv_reset", "Reset Advanced To Defaults", () =>
{
surface.BuildOverrides = new NavMeshBuildOverrides();
surface.DefaultArea = NavMeshAreas.Walkable;
EditorSceneManager.MarkDirty();
}).Show();
});

paper.Box($"{id}_sp").Height(6);
Origami.Header(paper, $"{id}_bake_hdr", $"{EditorIcons.Map} Baking").Underline().Show();

// One Bake button (Unity-style). In edit mode it bakes to a .navmesh asset so the
// result persists; during play it does an in-memory bake (baking to disk mid-play is
// rarely intended). The code-only runtime API remains NavMeshSurface.BuildNavMesh().
Origami.Button(paper, $"{id}_bake", "Bake NavMesh", () =>
{
if (Application.IsPlaying) surface.BuildNavMesh();
else BakeToAsset(surface);
}).Show();

var data = surface.NavMeshData.Res;
if (data.IsValid() && data!.HasTiles)
{
Origami.Button(paper, $"{id}_clear", $"{EditorIcons.Trash} Clear", () => Clear(surface)).Show();

paper.Box($"{id}_sp2").Height(4);
Origami.Label(paper, $"{id}_stats",
$"{data.CacheLayers.Count} cache layers · agent r={data.Settings.AgentRadius:0.##} h={data.Settings.AgentHeight:0.##} · voxel {data.Settings.EffectiveVoxelSize:0.###} · tile {data.Settings.EffectiveTileSize}")
.Show();
}
}

/// <summary>Unregister and drop the surface's baked data reference. The .navmesh file (if
/// any) is left on disk; delete it from the Assets panel to remove it fully.</summary>
private static void Clear(NavMeshSurface surface)
{
surface.NavMeshData = default;
surface.RefreshRegistration();
EditorSceneManager.MarkDirty();
}

private static void BakeToAsset(NavMeshSurface surface)
{
try
{
// Bake in memory first (also registers with the scene while playing/valid).
if (!surface.BuildNavMesh())
return;

Runtime.NavMeshData? data = surface.NavMeshData.Res;
if (data.IsNotValid()) return;

var db = EditorAssetBackend.Instance;
var scene = surface.GameObject.Scene;

// A subfolder named after the scene, next to the scene asset, holding
// "<SceneName> NavMesh.navmesh" (e.g. Assets/Scenes/Test Scene/Test Scene NavMesh.navmesh).
string sceneRel = scene.IsValid() && !string.IsNullOrEmpty(scene!.AssetPath) ? scene.AssetPath : "";
string sceneDir = string.IsNullOrEmpty(sceneRel) ? "" : (Path.GetDirectoryName(sceneRel) ?? "").Replace('\\', '/');
string sceneName = string.IsNullOrEmpty(sceneRel) ? "Scene" : Path.GetFileNameWithoutExtension(sceneRel);
string folderRel = (string.IsNullOrEmpty(sceneDir) ? "" : sceneDir + "/") + sceneName;
string folderAbs = Path.Combine(Project.Current!.AssetsPath, folderRel);
Directory.CreateDirectory(folderAbs);

string fileRel = folderRel + "/" + Sanitize(sceneName + " NavMesh") + ".navmesh";
string fileAbs = Path.Combine(Project.Current.AssetsPath, fileRel);
data!.Name = Path.GetFileNameWithoutExtension(fileRel);
Serializer.Serialize(typeof(object), data).WriteToBinary(new FileInfo(fileAbs));

Guid guid = db.ImportFile(fileRel);
if (guid == Guid.Empty)
{
Runtime.Debug.LogError($"[Navigation] Failed to import baked navmesh at {fileRel}.");
return;
}

surface.NavMeshData = new AssetRef<Runtime.NavMeshData>(guid);
surface.RefreshRegistration();
EditorSceneManager.MarkDirty();
Runtime.Debug.Log($"[Navigation] Baked navmesh saved to {fileRel} ({data.CacheLayers.Count} cache layers).");
}
catch (Exception e)
{
Runtime.Debug.LogError($"[Navigation] Bake failed: {e.Message}\n{e.StackTrace}");
}
}

private static string Sanitize(string name)
{
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return string.IsNullOrEmpty(name) ? "NavMesh" : name;
}
}
Loading