Overriding default spawn when loading a save

I am still new to Unreal Engine (5.3) and it’s C++ API, but I am comfortable with C++ in general. I have started on game saving and loading fairly early in things in order to understand a lot of the object relationships sooner instead of retrofitting to them later. I found inspiration for how I wanted to save and load games from this post:

It is saving off actors and replacing the ones in the level with the saved ones on reload.

I am having a problem with loading the player. I don’t get the old player, but instead get a default one at the default player start as if starting a new game. Some important things that are involved:

  1. A custom game mode I am using for trying different overrides. This is just for testing and wasn’t even there until I saw this problem.
  2. A BP_ThirdPersonGameMode I have set to be use my custom game mode as a parent. I do see my overrides run. So both my project and world settings properly use this game mode chain-of-command.
  3. The save-load logic itself.
  4. A level loading call. Specifically: UGameplayStatics::OpenLevel(GetWorld(), TEXT("/Game/IntroLevel"), TRAVEL_Absolute);. I bring it up because I see something like LogLoad: Took 0.041540 seconds to LoadMap(/Game/UEDPIE_0_IntroLevel) as the last message I get so I wouldn’t be surprised if there’s some hidden sauce that OpenLevel does to make things interesting.

The current situation when I opt to load a game:

  1. I note the current player’s actor GUID in the editor. Let’s say it is AAAA.
  2. I activate my code to load from a save. This starts with the OpenLevel call UGameplayStatics::OpenLevel(GetWorld(), TEXT("/Game/IntroLevel"), TRAVEL_Absolute);
  3. I load the game state I want and create a new player actor reconstructed from the save data. I should note in particular that I have a player transform here. I have seen this code run.
  4. I then possess the new actor after casting it to a pawn. I should note here that when I scan for all instances of APawn in this code, I get two. First, there is the original one with guid AAAA that has been marked to be destroyed. I guess it isn’t destroyed yet and still comes up. I then get a second one. Let us give it guid BBBB. This is the one I possess.
  5. I note in the editor that the player pawn has been moved to the Player Start, not the location where I wanted it to be. Furthermore, my player has a third, new guid of CCCC that I didn’t see when loading. So it looks like I’m generating a completely new one. I also don’t have control over it.

This is where I start to inject an overridden AGameModeBase to try to have some agency over the pawn logic. I tried to override RestartPlayer() to call the base call just once using a bool. So after calling it once, it’ll set a boolean to true and then suppress calling it again. This didn’t change the behavior. I then decided to set DefaultPawnClass to nullptr in the same block that sets the boolean. That didn’t change anything. Now, if I completely suppress RestartPlayer, then I wind up with some 1st-person floating viewport, so it’s clear this function does do something overall. I just can’t tell what exactly.

I searched the forums for other things I could override in AGameModeBase, but a few of them are no longer virtual functions. For example: SpawnDefaultPawnFor, SpawnDefaultPawnAtTransform, PlayerCanRestart. I don’t get why because they are native events so I figure in Blueprint you could try to mess with them (and I might!). But I figured I’ve done my due diligence at this point and I can just ask.

I figured out the first problem. I thought that the game mode persisted between level loads, but it did not. So my chicken bit was not actually preserving. I stuffed it into a game instance subsystem and that stopped spawning the third player. I still lose the loaded player, which I assume has to do with when I’m trying to create it in relation to level loading.

The second thing is I discovered I can override PlayerCanRestart under the alias of PlayerCanRestart_Implementation.

What I figured out is that OpenLevel is not a blocking call, but just a signal to start the process. After trying all kinds of delegates, I settled on attaching to FCoreUObjectDelegates::PostLoadMapWithWorld. When I run my load logic there, I get much better results. A pawn is available and its GUID matches one I find and possess.

I still have problems with actually controlling that pawn (I am guessing that possessing with the controller doesn’t mean my scripts actually doing control have the updated actor), but I’m over the hump.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.