A Deep Dive Into Seamless Travel in Unreal Engine




A Deep Dive Into Seamless Travel in Unreal Engine


What actually happens under the hood — based on real testing.


When I first set out to implement Seamless Travel in Unreal Engine, I expected it to be straightforward. After all, the official documentation describes it in just a few sentences: Seamless Travel allows clients to remain connected while the server moves from one map to another.

What the documentation doesn’t explain is how that transition actually works, which engine functions fire, which objects are replaced, and most importantly where developers are expected to store the data that needs to survive the map transition.

So I began testing. I traced every callback, logged every function call, and tracked the flow of PlayerControllers, PlayerStates, Pawns, and GameMode functions during multiple seamless transitions. What I found is that Seamless Travel is incredibly useful, but many of its most important behaviors are not obvious unless you observe it directly.

This post breaks down the real sequence of events I discovered during testing: what persists, what doesn’t, which functions actually run, and how Unreal expects developers to transfer data between levels. If you're planning to use Seamless Travel in your project—or if you’ve tried before and found it confusing—this should give you a much clearer picture.

In this article, we’ll walk through:

  • What actually survives travel

  • What gets replaced

  • Which events really fire (and in what order)

  • How PlayerState handles data transfer

  • A practical architecture for using Seamless Travel correctly

Enabling Seamless Travel in PIE


By default, Seamless Travel does not work inside Play-In-Editor. If you try to server-travel between maps without enabling it, PIE will not simulate the real seamless behavior.

To enable it, enter the following command in the console:
net.AllowPIESeamlessTravel true

This must be run each time you restart the editor (unless you add it to DefaultEngine.ini). Once enabled, PIE will follow the same seamless travel flow as standalone: clients stay connected, new PlayerControllers and PlayerStates are created in the destination map, and PlayerState CopyProperties runs as expected.

What Actually Happens During Server Travel


When you initiate Seamless Travel, typically with a call like:

ServerTravel("/Game/Maps/MyGameMap?listen?seamless");

Unreal enters a very specific sequence of function calls on the server and on each client. A lot of the official documentation summarizes this as “controllers are swapped and PlayerStates persist,” but the actual order of events is critical if you want to reliably move data between levels.
The key things to understand are:


  • A new PlayerController and PlayerState are created in the destination level.

  • PlayerState::CopyProperties() is called automatically on the server to copy data from the old PlayerState into the new one.

  • OnSwapPlayerControllers() is called on the GameMode, but it runs only on the server and is not a good place to transfer gameplay data.

  • New pawns are spawned, possessed, and then BeginPlay is called on the server and on each client.

High-Level Phases of Seamless Travel


To make the call stack easier to reason about, it's helpful to think of Seamless Travel in three phases:

  • 1. Server global phase: the server transitions to the new map and begins setting up players.

  • 2. Server-per-player phase: for each player (including the listen-server player and any remote clients), the server creates new controller/state/pawn objects and wires them together.

  • 3. Client-local phase: each client receives its replicated objects and runs its own BeginPlay calls.

The detailed order below is from a test setup with a listen server and one remote client, but the same pattern repeats for every player.

Server Global Phase (After ServerTravel Is Called)


Immediately after calling ServerTravel with seamless travel enabled, the server begins transitioning players into the new map. The first part of the sequence (for the listen-server player) looked like this in my logs:

  • Server – PlayerState CopyProperties() is called (old → new PlayerState)

  • ServerOnSwapPlayerControllers

  • ServerSpawnDefaultPawnFor

  • Server – Pawn PossessedBy

  • ServerOnRestartPlayer

  • ServerHandleStartingNewPlayer

  • Server – PlayerController BeginPlay

  • Server – PlayerState BeginPlay

  • Server – Pawn BeginPlay

A few important details from this:

  • CopyProperties on PlayerState always runs before OnSwapPlayerControllers. This is where you should transfer any data that needs to survive the map change.

  • OnSwapPlayerControllers gives you access to the old and new PlayerControllers, but it only runs on the server. Anything you set here would have to be replicated manually, which is why it’s a poor choice for gameplay data.

  • SpawnDefaultPawnFor, PossessedBy, OnRestartPlayer, and HandleStartingNewPlayer form the core of the respawn logic for the new map.

Server-Per-Player Phase (Server Handling a Remote Client)


After the server has handled its own listen-server player, it runs a similar sequence for each connected client. In my test with a single remote client, the server-side logs for “Client 1” looked like this:

  • Server (Client 1) – PlayerState BeginPlay

  • Server (Client 1) – PlayerController BeginPlay

  • Server (Client 1) – PlayerState CopyProperties()

  • Server (Client 1)OnSwapPlayerControllers

  • Server (Client 1) – Pawn BeginPlay

  • Server (Client 1)SpawnDefaultPawnFor

  • Server (Client 1) – Pawn PossessedBy

  • Server (Client 1)OnRestartPlayer

  • Server (Client 1)HandleStartingNewPlayer

A few key points from this part of the flow:

  • The “Server (Client 1)” prefix just means “the server is executing logic for Client 1’s connection” — this is the server’s local representation of that remote player.

  • PlayerState::CopyProperties() again runs on the server only. PlayerStates are replicated by default, so the server copies data into the new PlayerState and then that replicated state is pushed down to clients.

  • Just like with the listen-server player, the server handles pawn spawning, possession, and restart logic for each remote player.

For additional players, this exact pattern repeats: the server runs the same sequence per client, creating new controller/state/pawn objects for each one in the destination map.

Client-Local Phase (Each Client’s Own BeginPlay)


Once the server has created and initialized the new objects for each player, the replicated actors arrive on the clients. Each client then runs its own local BeginPlay calls. For Client 1 in my test, the client-side logs looked like this:

  • Client 1 (Client 1) – PlayerController BeginPlay

  • Client 1 (Client 1) – Pawn BeginPlay

  • Client 1 (Client 1) – PlayerState BeginPlay

You will also see some server-side BeginPlay calls mirrored again as the server finalizes replication for that client:

  • Client 1 (Server) – Pawn BeginPlay

  • Client 1 (Server) – PlayerState BeginPlay

From the client’s perspective, they:

  • Never disconnect from the server.

  • Load into the new map.

  • Receive a new PlayerController, PlayerState, and Pawn.

  • See their local BeginPlay fire once those replicated actors are ready.

All of this happens without the client having to reconnect, which is the core benefit of Seamless Travel.

Why PostLogin Does Not Fire During Seamless Travel


One of the most common mistakes when first working with Seamless Travel is assuming that PostLogin will run again when players arrive in the destination map. It doesn’t and this is by design.

PostLogin only fires when a client initially connects to the server. Because Seamless Travel keeps all existing clients connected throughout the transition, none of them reconnect, and therefore PostLogin is never called during the map change.

The only time PostLogin would run in the game map is if your game allows new players to join mid-session. In my setup, players can only join while in the lobby, so PostLogin never fires after traveling into the gameplay map.

This means you should only use PostLogin for lobby-side initialization. Any data that needs to survive seamless travel must be handled through PlayerState and the automatic CopyProperties call during the transition.

PlayerState: The Right Place for Cross-Map Data


During Seamless Travel, Unreal does not keep the same PlayerState object alive between maps. Instead, a new PlayerState is created in the destination level, and Unreal automatically calls CopyProperties on the server to transfer data from the old PlayerState into the new one.

This makes PlayerState the correct and officially supported place to store any per-player information that needs to move between maps. In my testing, CopyProperties fired exactly once per player (including the listen-server player) during each seamless transition, and it always ran on the server.

PlayerState is uniquely suited for this because it:

  • Exists on both the server and all clients

  • Is recreated automatically in the destination map

  • Receives data through CopyProperties during travel

  • Replicates its variables to clients cleanly after the transition

This makes it ideal for storing anything that the next map needs to rebuild the player's state, such as:

  • Character class, role, or archetype

  • Skill or talent selections

  • Cosmetics, appearance data, or player identity

  • Equipment or loadout selections

  • Any persistent gameplay variables needed for pawn initialization

Below is a simple example of overriding CopyProperties in C++.

// Example PlayerState override for seamless travel

void AMyPlayerState::CopyProperties(APlayerState* NewPlayerState)
{
Super::CopyProperties(NewPlayerState);

AMyPlayerState* NewPS = Cast(NewPlayerState);
if (!NewPS)
{
return;
}

// Copy any variables you want to persist across seamless travel.
NewPS->SelectedClass = SelectedClass;
NewPS->CosmeticID = CosmeticID;
NewPS->LoadoutData = LoadoutData;
}

Because CopyProperties is the only automatic transfer mechanism during seamless travel, any data that needs to survive the map transition should live inside PlayerState. The GameMode will then use this new PlayerState to initialize the player’s pawn and gameplay state in the destination map.

Pawn Spawning and Setup During Seamless Travel


During seamless travel, the pawn from the previous map is not kept alive. In the destination map, a new pawn is created for each player, and the server wires everything back together using the usual spawn and possession flow. Conceptually, you can treat seamless travel as a controlled respawn into a new level.

On the server, the core sequence for each player looks like this:

  • SpawnDefaultPawnFor is called to create a new pawn in the new map.

  • The PlayerController calls PossessedBy on the new pawn.

  • OnRestartPlayer and HandleStartingNewPlayer run to finalize the spawn.

  • BeginPlay is called on the pawn (and then on the client once it replicates).

For the local player on each client, that client also sees its own PlayerController, Pawn, and PlayerState run BeginPlay once the replicated objects arrive. For other players, each client only sees the remote pawns and PlayerStates, not their controllers.

Because the pawn is always recreated in the new map, any pawn-specific state that needs to carry over should either:

  • Live in PlayerState and be reapplied to the new pawn, or

  • Be recomputed from persistent data after the pawn is spawned.

In practice, this makes seamless travel a natural point to initialize all of your in-game systems that depend on the pawn: abilities, equipment, animation state, movement configuration, and so on. Once server travel is complete and the new pawn exists, you have everything you need to rebuild the player’s in-game state cleanly in the destination map.

Putting It All Together: A Practical Architecture


Now that we’ve covered the detailed flow of seamless travel, it’s useful to step back and look at how this process fits into a typical multiplayer game architecture. At a high level, seamless travel can be broken into three phases: the lobby, the travel itself, and the setup in the destination map.

Lobby Phase (Before Travel)


  • PostLogin fires as players join the session.

  • Each client sends any necessary setup data to the server (class selection, cosmetics, loadouts, etc.).

  • The server stores this data in the PlayerState, where it will later be carried into the next map.

  • Once all players are ready, the server initiates seamless travel to the gameplay map.

During Travel


During seamless travel, the engine handles the transition to the new map and recreates the PlayerController, PlayerState, and Pawn for each player. The important thing to understand here is not the full call order (covered earlier), but the conceptual responsibilities:

  • The old PlayerState is copied into a newly created PlayerState using CopyProperties.

  • The engine performs the controller swap internally and calls OnSwapPlayerControllers on the server.

  • A new pawn is created in the destination map for each player.

  • No new client data should be requested during this phase all persistent information must already be in PlayerState.

Game Map Phase (After Travel)


Once the new map has loaded, the server finishes setting up each player:

  • A new GameMode instance takes over for the new level.

  • A new PlayerController is created and linked to the new PlayerState.

  • New pawns are spawned via SpawnDefaultPawnFor() and possessed by their controllers.

  • Each actor that needs player-specific data (the pawn, ability system, loadout system, etc.) reads it from the PlayerState.

  • After all players have been initialized, normal gameplay begins.

In this architecture, PlayerState serves as the authoritative source of persistent player information. Every actor that needs to initialize player-specific state after seamless travel should read from the PlayerState, ensuring that all data is synchronized and carried forward cleanly.

Key Takeaways


After testing and tracing the full call flow, Seamless Travel becomes much easier to reason about if you keep a few simple rules in mind:

  • PostLogin only runs when a player first connects to the server.

  • PlayerState is recreated during seamless travel, but you can persist data through the CopyProperties call.

  • A new PlayerController is also created in the destination map during seamless travel.

  • OnSwapPlayerControllers is a server-only callback and should not be used for transferring gameplay data.

  • Store all persistent cross-map data in PlayerState, and use it to initialize your pawns and gameplay systems after travel.

Once you align your architecture around these behaviors, seamless travel becomes a clean and predictable way to transition players between maps without forcing any reconnections.


Share This Post