So I had some questions about the persistence across map change of PlayerState and talked to JMarkiewicz about it on IRC.
I’m posting this for posterity in case someone else is looking for the same answers.
Hopefully this quote can help the guys that, like me, were looking for some answers on how to preserve a player’s state across a map change.
[20:40] anyone available to answer a few questions about the PlayerState class?
[20:46] I’m curious as to it’s actual purpose… The way I understand it, it’s meant to hold stuff such as score, health, money, lives, etc
[20:46] But my question is, does it persist across map changes?
[20:46] or are we supposed to serialize it to disk and load it again after changing level?
[20:47] <+Epic|jmarkiewicz> good question… promise to post this to answerhub
[20:47] <+Epic|jmarkiewicz> so the playerstate is the lightweight sibling of the playercontroller. Servers have all the player controllers and all the playerstates… clients have only their player controller but all playerstates
[20:47] <+Epic|jmarkiewicz> helps replication
[20:47] <+Epic|jmarkiewicz> now
[20:47] <+Epic|jmarkiewicz> servertravel comes in two flavors… hard / seamless (as I refer to them)
[20:48] <+Epic|jmarkiewicz> seamless travel will preserve the playerstate but does copy it to a new one on the other side for you.
[20:48] <+Epic|jmarkiewicz> hard travel loses everything.
[20:48] <+Epic|jmarkiewicz> look at HandleSeamlessTravelPlayer
[20:48] <+Epic|jmarkiewicz> so you don’t have to worry about, but you do need to leverage/override CopyProperties
[20:48] is this the process you’d do in a strictly singleplayer game, too?
[20:49] <+Epic|jmarkiewicz> ::CopyProperties(APlayerState* PlayerState)
[20:49] <+Epic|jmarkiewicz> blinks single player? hadn’t thought about that in a long time
[20:49] multiplayer is overrated
[20:49] <+Epic|jmarkiewicz> I think traditionally we change maps by LoadMap (which is internal to both hard/seamless travel processes)
[20:50] <+Epic|jmarkiewicz> in that case you would have to serialize your data
[20:50] <+Epic|jmarkiewicz> but…
[20:50] <+Epic|jmarkiewicz> you can always make a UObject (not an AActor) and AddToRoot before travel, then TObjectIterator find the object on the other side and RemoveFromRoot after restoring what you want from it
[20:50] <+Epic|jmarkiewicz> we are looking at adding a new class that persists across all map travel to make this easier
[20:51] so AddToRoot on character?
[20:51] <+Epic|jmarkiewicz> AddToRoot is on any UObject, but don’t do this for AActors
[20:52] okay, thank you
[20:52] <+Epic|jmarkiewicz> there is “streaming levels” which you could theoretically never leave/travel/load in a way that would preserve the PlayerState for the duration
[20:53] <+Epic|jmarkiewicz> I’m not as familiar with that, there is editor setup and map creation stuff I’m not familiar with
[20:53] yeah but that’d be no good for, let’s say a shoot’em up or a platformer
[20:53] <+Epic|jmarkiewicz> you basically would be loading/unloading pieces within one “map”
[20:53] <+Epic|jmarkiewicz> sure, I guess it depends on your gameplay, level scope, etc
[20:53] <+Epic|jmarkiewicz> if you want load screens, etc
[20:53] yeah definitely
[20:54] <+Epic|jmarkiewicz> but playerstate was a “network optimization” basically
[20:54] I think serializing is the way to go
[20:54] <+Epic|jmarkiewicz> and an “organizational” thing
[20:54] there is no SP only Coop
[20:55] sp in the case of a “simple” platformer or shoot’em up or the like, you’d recommend putting the player attribues (score, health, etc) in the player controller?
[20:55] <+Epic|jmarkiewicz> Mons is right in a way, you could be “networked, but not advertised” and then travel around as you would networked but never accepting connections
[20:55] <+Epic|jmarkiewicz> you could also change a few things to ensure you reject anyone even trying to connect to your ip :
[20:56] <+Epic|jmarkiewicz> I would always put all of that in the PlayerState regardless… its just part of the framework and I don’t think going against it here would buy you anything
[20:56] that was my reasoning as well
[20:58] thank you for the reply
Also, as I understand, if you want to preserve information for players that reconnect after losing the connection (which is handled by inactive player states) you should also make sure that your properties are copied.
Right now, none of these things is mentioned in the docs.
To followup, I think that page is too high level to put your suggestions. I’ll be looking to add it a deeper conversation about travel and object persistence. Either way, this is good feedback and I’ll make sure it’s incorporated.
You might want to check out the new GameInstance class. It is designed to persist across level transitions and might be a good place for you to hang data that doesn’t necessarily need replication but does need to last longer than a single map.
Adding to root is always kind of a last resort and a little heavy handed.
It looks like there isn’t much documentation specifically on APlayerState, but you would do well to read up on networking and multiplayer.
The player name is already stored on the APlayerState class. It is the representation of each player given to every client in the game (along with their APawn/ACharacter). You would store things like health/score/name/id on this object so everyone can have read only access to it. Only the server can make changes to the object that transfer to all clients. If a client (including the owning client) makes changes to the variables on this class it will only exist locally and probably be overwritten the next time it actually changes on the server.
**Important to note you cannot use it in the editor because seamless travel does not work in the editor. Your variable cannot be “BlueprintReadWrite”. And you also need to enable seamless travel and that only works in standalone or packaged build.
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Preplan")
Test = "Original Value";
void AShooterPlayerState::CopyProperties(class APlayerState* PlayerState)
this->Test = "New Value";
AShooterPlayerState* ShooterPlayerState = Cast<AShooterPlayerState>(PlayerState);
ShooterPlayerState->Test = this->Test;