we found out some Actors gets BeginPlay called but never receive EndPlay in certain circumstances. I managed to get a very high repro rate with gc.CollectGarbageEveryFrame 1 and ended up in UWorld::CleanupActors()
This function can call Level->Actors.RemoveAt with an index or range smaller than Level->RouteActorEndPlayForRemoveFromWorldIndex and some actors will then be skipped in next FLevelRemoveFromWorldAccessor::RouteActorEndPlayForRemoveFromWorld.
Here is a simple fix that feels very legit according to existing Engine comment :
void UWorld::CleanupActors()
{
// Remove NULL entries from actor list. Only does so for dynamic actors to avoid resorting; in theory static
// actors shouldn't be deleted during gameplay.
for (ULevel* Level : Levels)
{
// Don't compact actors array for levels that are currently in the process of being made visible as the
// code that spreads this work across several frames relies on the actor count not changing as it keeps
// an index into the array.
//@CYA EDIT add IsLevelMakingInvisible as it uses index too
if (ensure(Level != nullptr) && !IsLevelMakingVisible(Level) && !IsLevelMakingInvisible(Level))
//@CYA END
{
Steps to Reproduce
in UWorld::RemoveFromWorld add this check
if (!Timeout.IsExpired() && FLevelRemoveFromWorldAccessor::IsFinishedRouteActorEndPlayForRemoveFromWorld(Level))
{
//@CYA EDIT missing endplay tracking
for (AActor* A : Level->Actors)
{
ensureAlways(!A || !A->HasActorBegunPlay());
}
//@CYA END
// The level can be removed from the pending visibility state
SetLevelPendingVisibilityState(Level, ELevelPendingVisibilityState::None);
Use a small value for s.LevelStreamingRouteActorEndPlayForRemoveFromWorldGranularity so ensure endplay will be pretty long (we’re running with 16)
Set gc.CollectGarbageEveryFrame 1
In a packaged build unload levels with many EditorActors (those will end up as nullptr in Level->Actors)
Fantastic! Thanks for providing that. I did manage to repro on my end. I’m going to ping the dev that added this and get his feedback. Your proposed fix seems fine to me, but I’m not sure if there were any other gotchas that we to be aware of.
I confirm that this is a regression that only happens when the newly added s.LevelStreamingRouteActorEndPlayForRemoveFromWorldGranularity is greater than 0.
Your proposed fix is good.
I will make sure to submit a fix and I’ll post here the CL number in our main branch.
no problem for the delay my fix is working fine for shipping our game, we dropped down from ~2 crash per days to 0 in two weeks. Sharing our game is not possible right now as it’s not yet shipped and it’s pretty fat.
The key point is to have some nullptr early in Level->Actors array, i suspect those comes from EditorOnly actor being nullified at Cook time (we’re using a bunch of EditorOnly actors for Level design visualization of internal game system)
We’re close to certification but i may have a few hours to try building a small repro project in the next weeks.
i managed to build a small sample project on 5.7.1.
my steps were not accurate i struggled to find the same conditions as our game but i did !
Some actors needs to be destroyed by gameplay, the level must then start unloading and gc must pass in the meantime (the nullptr were not EditorOnly actors).
I added a .zip of a very simple project with the right CVars in DefaultEngine.ini and a simple load/unload script in the level.
I packaged it using these commandline to reproduce 100% in development
RunUAT.bat BuildCookRun -project=EndPlayIssue -Build -Cook -Pak -Manifest -Stage -Package -Platform=Win64 -ClientConfig=Development -Prereqs -iostore -zenloader -VersionCookedContent -BuildMachine -NoP4 -NoSign -NoCodeSign -Compressi hope you’ll be able to reproduce issue on your side with this