“But our main issue comes from the dedicated server loading in PIE.”
Aha, that clarifies my confusion. I was confused about why LoadMap is happening after OnWorldBeginPlay, but in PIE with server and client under a single process, I think the following is happening:
- The server duplicates the world for PIE (no LoadMap needed).
- The server calls OnWorldBeginPlay. You request some async loads.
- The client’s PIE context is created. LoadMap is called for the client, which triggers FlushAsyncLoading. When running under a single process, this also flushes your server’s async load requests.
- The client calls OnWorldBeginPlay.
The problem happens when the server and client run under a single process, because the client’s world life cycle is interfering with the server’s.
If you want to work around this, you can consider letting the UWorldSubsystems defer the async load requests until all PIE clients have executed their LoadMap (and the FlushAsyncLoading() calls from those). It would look a bit like this:
#include "UObject/UObjectGlobals.h"
void UMyWorldSubsystem::OnWorldBeginPlay(UWorld& World)
{
Super::OnWorldBeginPlay(World);
#if !WITH_EDITOR
// Not an editor build, load immediately
AsyncLoadAssets();
#else
if (World.WorldType == EWorldType::PIE)
{
// Setup PostLoadMapWithWorld triggering of async loads
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMyWorldSubsystem::PostLoadMapWithWorld);
}
else
{
// Not a PIE world, load immediately
AsyncLoadAssets();
}
#endif
}
#if WITH_EDITOR
void UMyWorldSubsystem::OnWorldEndPlay(UWorld& World)
{
Super::OnWorldEndPlay(World);
// Clean PostLoadMapWithWorld triggering of async loads
if (World.WorldType == EWorldType::PIE)
{
FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this);
}
}
void UMyWorldSubsystem::PostLoadMapWithWorld(UWorld* LoadedWorld)
{
if (/*Check if LoadedWorld is the last PIE client*/)
{
AsyncLoadAssets();
}
}
#endif
This is just a barebones code sample, needs to be thoroughly tested for all configurations (PIE settings, and build types), but hopefully you get the idea and find this a useful direction to investigate more in. Two changes that are still needed:
1) You need to handle the single client standalone starting PIE case since that skips PostLoadMapWithWorld. Note that checking world NetMode == NM_Standalone isn’t enough, since you can start PIE with multiple clients standalone.
2) You have to check whether LoadedWorld is the latest PIE client, since when starting PIE with 2 clients, PostLoadMapWithWorld will be called for each client, in this order:
- Server calls OnWorldBeginPlay
- Client1 calls LoadMap -> FlushAsyncLoading, OnWorldBeginPlay, PostLoadMapWithWorld
- Client2 calls LoadMap -> FlushAsyncLoading, OnWorldBeginPlay, PostLoadMapWithWorld
You only want everyone to react on the last PostLoadMapWithWorld.
In both cases, what can help is getting GetDefault<ULevelEditorPlaySettings>() to get your PIE settings. It gets a bit messy in runtime modules, because those play settings are in an editor module. Be sure to:
- Encapsulate code in #if WITH_EDITOR
- Add to your Build.cs:
if (Target.bBuildEditor)
{
PublicDependencyModuleNames.Add("UnrealEd");
}
I’m thinking of using something like World->GetOutermost()->GetPIEInstanceID(), which is, for example, 3 if your PIE settings are server + 3 clients under a single process. (server = 0, player 3 = 3).
It’s a big write-up for an incomplete solution, but hopefully it makes sense. This is surprisingly difficult, too. If “hackier” solutions are fine, then a one-frame delay on PIE might be a much simpler solution. 