sinbad / SPUD

Steve's Persistent Unreal Data library
MIT License
300 stars 44 forks source link

How to know already in BeginPlay if SpudPostRestore will be called? #53

Closed andra961 closed 1 year ago

andra961 commented 1 year ago

Hi, awesome plugin!I'm using World Partition and i noticed that when a cell loads, SpudPostRestore WON'T be called always (if it's a new game for instance it doesn't it seems). I have some initialization logic that is executed in my actors and i'm faced with a dilemma. Since this init logic depends on the loaded actor data, i can't just execute it in BeginPlay(data would be incorrect), but i can't execute it on SpudPostRestore neither because it's not granted it will fire and i risk skipping initialization altogether. Is there a way, inside BeginPlay, to know for sure that SpudPostRestore will fire, so that i can skip the init logic and execute it (only once) in SpudPostRestore?

sinbad commented 1 year ago

Yeah SpudPostRestore only happens when data has actually been restored, so for new cells it won't won't occur. While I could add a function that gets called for every actor in the level whether it's a load case or not, that's an extra overhead that I'm not sure a lot of people would want.

What I usually do is let init happen in BeginPlay anyway, and then if then subsequently new data is restored after then I run the init again with the new data (so both would call the same function) which overrides whatever the previous init did and you can't tell that both happened. It requires that it's safe to call this function more than once, which sometimes means dealing with things that should never happen twice (like subbing to an event - either have a flag which prevents it happening again, or use the 'unique' variants of subscribe so it's safe to do).

In general it's a good idea to code defensively like this anyway rather than relying on an init only ever getting called once. BeginPlay is notoriously unreliable in terms of timing e.g. what gets called first, when exactly it's called.

andra961 commented 1 year ago

i see, thanks!one last doubt though. I have an actor init that depends on the loaded data of an other actor (a GameState object for instance, that gathers informations and saves them) first. Where should i place the init actor of this actor in a way that when it's executed i'm sure the actor it depends on has already been loaded correctly first?

sinbad commented 1 year ago

It's the same as with BeginPlay: you can't know the ordering that actors will be initialised by BeginPlay, and you can't know the order that actors are restored by SPUD either. So the only reliable answer is not to rely on ordering between actors, either by not having cross-dependencies, or resolving them at the end of all actors having been loaded. What you can do instead is use the event that occurs after a level is fully loaded, which is the PostLevelRestore event on the subsystem. When that is fired, all actors in that level have had their state restored so you can resolve any cross-dependencies then.

Rcurreli commented 1 year ago

Hi @sinbad , Thank you! (I'm in team with @andra961 ) I tried using PostLevelRestore, but to check if the restored level is the actor specific level(a world partition cell,data layer or just the persistent level) I must know the actor specific level name. I managed to get it this way, but it looks a bit messy, do you know if there is a better way to get the actor specific level name?

FString UMyBlueprintFunctionLibrary::GetActorLevelName(const UObject* WorldContextObject, const AActor* actor)
{
    const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);

    //Discover if the actor is currently in an active DataLayer
    bool bActorInDataLayer = false;
    if (actor->GetDataLayerInstances().Num() > 0)
    {
        TArray<const UDataLayerInstance*> LayerInstances = actor->GetDataLayerInstances();
        for (const UDataLayerInstance* LayerI : LayerInstances)
        {
            if (LayerI->IsRuntime() && LayerI->IsVisible())
            {
                bActorInDataLayer = true;
            }
        }
    }
    FString ActorLevelName;

    //if the actor is currently in a cell of world partition or in an active data layer
    if (actor->GetIsSpatiallyLoaded() || bActorInDataLayer)
    {
        //get the specific name of cell or data layer
        ActorLevelName = actor->GetLevel()->GetOuter()->GetOuter()->GetName();
        int Index = INDEX_NONE;
        ActorLevelName.FindLastChar('/', Index);
        if (Index != INDEX_NONE)
        {
            ActorLevelName = ActorLevelName.RightChop(Index + 1);
        }

        ActorLevelName = ActorLevelName.Mid(World->StreamingLevelsPrefix.Len());
    }
    //the actor is currently just in the persistent level
    else
    {
        //get the name of the persistent level
        ActorLevelName = actor->GetLevel()->GetOuter()->GetName();
    }

    return ActorLevelName;
}
sinbad commented 1 year ago

Have you considered only subscribing to the PostLevelRestore event inside the SpudPostRestore for the actor that depends on other actors? Then unsubscribing after PostLevelRestore is fired. Because the PostLevelRestore gets fired just after doing all the actors for that level, it should be the same level that the event gets fired for. Then you wouldn't need to check the level, you're just asking for a final mop-up callback at the end of this specific restored level.

Rcurreli commented 1 year ago

Thank you! I wasn't sure if PostLevelRestore of the levels was sequential so I hadn't thought about using it with SpudPostRestore like this! I'll go with this approach instead of brutally comparing level names every time its possible!

Unfortunately it is not so simple when it comes to dependencies between actors of different levels, In our case between a persistent manager and observers in the specific levels: It would be very convenient if observers could know if the manager is ready and get the data, or wait for him to be ready before asking him for the data. The manager would trigger his PostLevelRestore to notify observers that he's ready with the method you mentioned earlier.

However, to avoid observers waiting indefinitely (for example, in the case of a new game the manager is not loading so it will never be ready since its PostLevelRestore will never be called) it would be very convenient check IsLoadingGame() in the manager BeginPlay and if not, set that the manager is ready and notify the observers immediately.

Basically I would like to know: When we load a game, does a persistent level actor that calls IsLoadingGame() in BeginPlay always returns true? Is this a good practice? Do you think there is a better solution for this specific case?

sinbad commented 1 year ago

When we load a game, does a persistent level actor that calls IsLoadingGame() in BeginPlay always returns true?

Yes. The reason is the way that UE loads its maps, which calls BeginPlay and then calls PostLoadMapWithWorld callback, the latter of which is what triggers SUDS to know that the persistent level has finished loading, and the state of the system changes to return false from IsLoading(). Of course, streaming / world partition levels can load after this.

Also, you can rely on global objects and actors in the persistent level will always load before streaming levels.

andra961 commented 1 year ago

Life saving information right there, thank you again for your exhaustive answers!