Our indie game, The Drifting Crown is now on Unreal 5.8.2. Most of the upgrade was routine. Three engine changes caused nearly all of the work, and one of them broke three things that looked completely unrelated to each other.

What stopped the build
FJsonObject changed its key type. It used to be TMap<FString, TSharedPtr<FJsonValue>> and is now keyed on UE::FSharedString, an interned shared string type. Anything that copies FJsonObject::Values into an FString-keyed map stops compiling.
For us that was Inkpot, the Ink narrative plugin, which does this in several places while deserialising story state. If you use a plugin that touches JSON directly, expect it to need a pass. The fix is a conversion at the boundary rather than changing how the plugin works internally, since the key type only matters where the engine’s map meets your own.
The other one worth knowing about is that UE_LOG argument counts are now validated against the format string at compile time:
error C2338: static_assert failed: 'missing format specifier'Code language: JavaScript (javascript)
This found four calls in our plugins passing more arguments than they had specifiers. All were years old and had been silently dropping the extra values. One of them was passing something genuinely useful that had never been printed.
If you are on an installed engine build rather than one compiled from source, you will also need to move your targets to BuildSettingsVersion.V7. The 5.8 editor ships built with V7 defaults, your game target has to match it exactly, and UBT refuses to compile anything until it does. That does not move your include order version, which stays where it was.
There is also a batch of deprecations carrying “update your code before upgrading to the next release, otherwise your project will no longer compile.” Cheaper to clear now than to rediscover in 5.9.
5.8 deletes navmeshes saved in sub-levels
This is the one worth reading about.
After the build went green, the editor crashed on tick, teleports between tiles stopped working, and navmesh visualisation in PIE drew nothing at all. Three separate symptoms, one cause.
Part of The Drifting Crown is a board game. Players get cards representing hex tiles and place them to build the map, so every tile is a Level Instance streamed in at runtime, and each one carries its own saved navmesh. In 5.8, all of them were being destroyed the moment they loaded:
LogNavigation: Warning: NavData RegistrationFailed_DataPendingKillLogCrowdFollowing: Warning: Unable to find RecastNavMesh instanceCode language: HTTP (http)
DataPendingKill means the navmesh was already dead by the time it tried to register.
The culprit is ARecastNavMesh::CheckToDiscardSubLevelNavData, which throws away any navmesh whose level is not the persistent level. The engine source explains why it started happening now:
// This used to be in ARecastNavMesh::PostInitProperties() but the OwningWorld// is not always available, it might be too soon to query it. Moved here so// sublevel data can be discarded when requested.Code language: JSON / JSON with Comments (json)
The check was there in 5.7. It ran at a point where GetWorld() was usually null, so it quietly never did anything. 5.8 moved it to PostLoad, where it works as intended. It is gated behind bShouldDiscardSubLevelNavData, which defaults to true, and one line in DefaultEngine.ini turns it off:
[/Script/NavigationSystem.NavigationSystemV1]bShouldDiscardSubLevelNavData=FalseCode language: JavaScript (javascript)
If you build levels out of streamed sub-levels with baked navigation, this will hit you.

Side effect one: the editor crashes
Destroying that many navmeshes at load exposed a bug in the navigation system itself.
Nav data waiting to register sits in NavDataRegistrationQueue, a UPROPERTY array of TObjectPtr. When a queued navmesh is destroyed, garbage collection nulls the entry but leaves the element in the array. The engine’s cleanup pass cannot remove it, because it only removes entries whose owning level is visible, and a null entry has no level. So it stays there. Then SpawnMissingNavigationDataInLevel walks the queue and dereferences it without a null check:
EXCEPTION_ACCESS_VIOLATION reading address 0x00000000000004b8UNavigationSystemV1::SpawnMissingNavigationDataInLevel() [NavigationSystem.cpp:4649]Code language: CSS (css)
Address 0x4b8 is a null pointer plus a member offset. Someone else has reported the same crash and confirmed it does not happen in 5.7.
Our first attempt at a workaround was a UNavigationSystemV1 subclass overriding the functions that reach the crash site. It had no effect, because the navigation system class is serialised into each map’s World Settings and that beats the config default. 378 of our 391 maps had it baked in.
What works is a world subsystem that purges nulls from the queue after every garbage collection. Entries only ever get added from a live reference, so GC is the only thing that can put a null in there, and post-GC delegates run before anything ticks. Turning the discard off removes the cause, but the workaround stays in until Epic fixes the null check.
Side effect two: teleports sent the player to the world origin
Tiles can sit at different heights, so we place trigger volumes along their edges. Walk into one and you get moved onto the neighbouring tile.
Picking the landing spot is the interesting part, because you do not want to arrive inside a handrail or on top of a wall. The code traces down to find ground, asks the navmesh for a reachable point near it, checks the surface it landed on is tagged as valid, and then confirms a walkable path exists from there to a known-good point on the tile. Two of those four steps need a navmesh.
With no navmesh, the queries failed, and the player ended up at (0, 0, 150) instead of on the tile.
That last part was our bug, not Epic’s. All the validation ran against one variable, and the teleport used another:
const FVector TargetLocation = NavLoc.Location + FVector::UpVector * 150.0f;Code language: PHP (php)
NavLoc only gets assigned when the navmesh query succeeds, and otherwise default-constructs to a zero vector. The code was validating a perfectly good location and then teleporting somewhere else entirely. git blame dated the line to May.
It had never fired before because two navigation calls that look interchangeable are not. GetRandomReachablePointInRadius reads MainNavData directly and returns false when it is null. ProjectPointToNavigation goes through GetDefaultNavDataInstance, which repairs a null MainNavData by rescanning the registered set. With a working navmesh the first call always succeeded, so the bad line never ran with a zero vector in it.

Game instance subsystems are not ready in Event Init
Small, easy to miss, and it will affect anyone using subsystems from Blueprint.
UGameInstance::Init() fires ReceiveInit(), which is your Blueprint’s Event Init, on line 100. It initialises the subsystem collection on line 129. In 5.7 those happened the other way round. Every Get Game Instance Subsystem node in Event Init now returns None:
LogScript: Warning: Accessed None trying to read (real) propertyCallFunc_GetGameInstanceSubsystem_ReturnValue_1Code language: HTTP (http)
We added a BlueprintImplementableEvent that fires straight after Super::Init() and moved the affected logic onto it.
Check every game instance Blueprint you have, not just the one that warned. The warning only appears if something dereferences the result. Ours had two subsystem calls in the same function and only one of them said anything, because the other was calling a static function that never needed a valid target.

Notes for anyone doing the same upgrade
Three of these were not new bugs. The navmesh discard had been written to run and never ran. The format string mismatches had always been wrong. The teleport bug had been sitting in the code since May. 5.8 changed the conditions that were hiding them, which is an argument for upgrading more often, while there are fewer of them stacked up to untangle at once.
Reading the engine source beat reading release notes. The navmesh answer was a code comment describing exactly what had moved and why, and the crash was clear as soon as we looked at the line the callstack named.
Do not assume a config default reaches your content. We lost half a day to a fix that was correct and never ran, because hundreds of maps had the old value serialised into their World Settings. If you are changing engine behaviour through config, check what your maps have already saved over it.
You can try The Drifting Crown, the demo is available on Steam. Also, join our Discord and let us know what you think.


