Full Match Replays

Learn how to configure, save, and play full match replays

SnapNet replays are generated server-side and emitted in small chunks. This chunked approach enables quick seeking when playing back replay, reduces server memory usage, and facilitates match streaming.

Configuration

The duration of chunks can be configured by changing the Replay Chunk Seconds property in the SnapNet Project Settings under the advanced section of the Common category. Higher values will yield better compression but will require more server memory and increase the CPU cost when seeking within replays. The default is 10 seconds.

Saving Replays

Save to file

By default, when a SnapNet server is running on desktop platforms, the current match replay is automatically streamed to disk under Saved\Replays\replay.tmp. At any time during gameplay, you can call USnapNetServer::SaveReplayFile() or use the SaveSnapNetReplay console command to copy it to a permanent file. The console command will automatically append a .rpl extension and place it in the Saved/Replays folder. When calling USnapNetServer::SaveReplayFile() you must specify the entire path.

void UExampleGameInstance::SaveSnapNetReplay( const FString& name )
{
    if ( USnapNetServer* server = USnapNetServer::Get( this ) )
    {
        const FString filename = FPaths::ConvertRelativePathToFull( FPaths::ProjectSavedDir() / TEXT( "Replays" ) / FString::Printf( TEXT( "%s.rpl" ), *name ) );
        server->SaveReplayFile( filename );
    }
}

Stream

To receive the replay chunks directly, you can bind a function to the server’s OnReplayChunkGenerated delegate. The bound function will be passed an FSnapNetReplayChunk struct containing the chunk index, its start and end times, and the binary replay data.

Return true from the OnReplayChunkGenerated delegate to prevent the server from streaming chunks into a temporary file as described above.

void UExampleGameInstance::RegisterReplayChunkHandler()
{
    if ( USnapNetServer* Server = USnapNetServer::Get( this ) )
    {
        Server->OnReplayChunkGenerated.BindUObject( this, &UExampleGameInstance::ReplayChunkGenerated );
    }
}

bool UExampleGameInstance::ReplayChunkGenerated( FSnapNetReplayChunk ReplayChunk )
{
    // Stream replay chunk to backend or CDN here
    // Game is responsible for serializing the FSnapNetReplayChunk which it will need to reconstruct later for playback

    // Continue to also save replay file
    return false;
}

Playing Replays

Play from file

To play back a replay from a file, call USnapNetClient::PlayReplayFile() or use the PlaySnapNetReplay console command. Calling USnapNetClient::PlayReplayFile() returns the start and end time, in milliseconds, of the replay which can be helpful when displaying UI for playback and seeking.

void UExampleGameInstance::PlayReplayFile( const FString& filename )
{
    if ( USnapNetClient* Client = USnapNetClient::Get( this ) )
    {
        int32 StartTime, EndTime;
        if ( Client->PlayReplayFile( filename, StartTime, EndTime ) )
        {
            UE_LOG( LogTemp, Log, TEXT( "Playing replay with a duration of %dms" ), EndTime - StartTime );
        }
        else
        {
            UE_LOG( LogTemp, Log, TEXT( "Failed to play replay file: %s" ), *filename );
        }
    }
}

Play from stream

To play back a replay stream, first bind a function to the USnapNetClient::OnReplayChunkRequest delegate and then call USnapNetClient::PlayReplay() with a FSnapNetReplayChunk struct. The replay will begin playing back from the beginning of the provided chunk and will trigger the bound function to request new chunks as needed when playing past the end of the current chunk or after seeking.

If the chunk requested is not yet available—due to network delays, for example—the bound function can return without filling out the provided FSnapNetReplayChunk structure. Replay playback would then pause and the request delegate would be executed every frame until the chunk becomes available, playback seeks to a different chunk, or playback is stopped.

void UExampleGameInstance::DownloadAndCacheChunk( int32 ChunkIndex )
{
    // Fetch the chunk from your game backend or storage
    // Deserialize it into an FSnapNetReplayChunk
    // Save it in an in-memory cache
}

FSnapNetReplayChunk UExampleGameInstance::GetReplayChunkFromCache()
{
    // Return the chunk from the in-memory cache populated by DownloadAndCacheChunk()
}

bool UExampleGameInstance::IsReplayChunkCached( int32 ChunkIndex )
{
    // Return true if the chunk is present in the in-memory cache populated by DownloadAndCacheChunk()
}

void UExampleGameInstance::OnReplayChunkRequest( FSnapNetReplayChunk* ReplayChunk )
{
    // ReplayChunk->chunk_index will be populated to indicate what chunk is being requested but otherwise needs to be filled out by the game and returned
    const int32 RequestedChunkIndex = ReplayChunk->chunk_index;
    if ( IsReplayChunkCached( RequestedChunkIndex ) )
    {
        // The chunk is downloaded so provide it to SnapNet
        *ReplayChunk = GetReplayChunkFromCache( RequestedChunkIndex );

        // Download and cache the next chunk so that it's ready when SnapNet gets to it
        DownloadAndCacheChunk( RequestedChunkIndex + 1 );
    }
    else
    {
        // Chunk is not yet downloaded. Download it now.
        // Do not populate ReplayChunk because we don't have it yet. 
        // SnapNet will execute this delegate again next frame to re-request the chunk and see if it is ready i.e., IsReplayChunkCached() returns true.
        DownloadAndCacheChunk( RequestedChunkIndex );
    }
}

void UExampleGameInstance::PlayReplayStream( FSnapNetReplayChunk FirstChunk )
{
    if ( USnapNetClient* Client = USnapNetClient::Get( this ) )
    {
        Client->OnReplayChunkRequest.BindUObject( this, &UExampleGameInstance::OnReplayChunkRequest );
        Client->PlayReplay( FirstChunk );
    }
}

Seeking

To seek within a replay, call USnapNetClient::SeekReplay() or use the SeekSnapNetReplay console command.

void UExampleGameInstance::SeekReplay( int32 TimestampMs )
{
    if ( USnapNetClient* Client = USnapNetClient::Get( this ) )
    {
        Client->SeekReplay( TimestampMs );
    }
}

Spectating

To spectate a particular player’s perspective, call USnapNetClient::SetSpectatedPlayerIndexForReplay() or use the SpectateSnapNetReplay console command and provide the player index you wish to spectate. If the player index is valid, then SnapNet will automatically apply lag compensation so that the world is shown just as the specified player saw it during live gameplay. In genres like shooters, this ensures that if the player had an enemy in their crosshairs that it still lines up as they saw it when later spectating their view in the replay.

Compatibility

The compatibility of a replay from one build to the next is the same as client and server compatibility between builds i.e., it depends entirely on whether the protocol ID matches. See the question about client and server compatibility in the FAQ for more information. Typically, once network properties are added/removed or new entities/events/messages are added, the build will no longer be compatible. Consider providing older versions of game clients for playing back historical replays and/or allowing players to render clips/replays to video files.