Starting From Snapshot
Quantum supports promoting a local simulation to an online simulation (SDK 3.0.5+).
Requirements
Local games, that are later promoted to online games, have to be starting with the final target SessionRunner.Arguments.PlayerCount
, because the ECS layout needs to match the player count of the online game later created from the snapshot.
The new online simulation will always be started from beginning and without any players. They have to be re-added after the start. For simplicity we suggest to remove the local player from the local simulation before taking the snapshot. Otherwise player related entities like avatars have to be reclaimed when adding the player again on the new online simulation.
When starting the online game from a snapshot SessionRunner.Arguments.FrameData
has to be set to the serialized frame and InitialTick
has to be set to RollbackWindow - 1
. This will configure the server to accept the snapshot as initial state for a new simulation that starts from the tick RollbackWindow
.
Because the simulation restarts from the beginning, timers that are based on ticks will be compromised.
How to Save a Snapshot
The following snippet uses the default runner QuantumRunner.Default
to access the running local game.
The process is async
for reading simplicity. It waits for the player removed confirmation before saving the snapshot.
Stop and destroy the runner before continuing with LoadAndStart()
.
C#
using Photon.Deterministic;
using Quantum;
using System;
using System.IO;
using UnityEngine;
public class Foo : QuantumMonoBehaviour
{
[EditorButton("Save")]
public async void Save() {
// Remove the player and wait for the remove confirmation callback.
var completionSource = new System.Threading.Tasks.TaskCompletionSource<bool>();
using (QuantumCallback.SubscribeManual<CallbackLocalPlayerRemoveConfirmed>(c => completionSource.TrySetResult(true)))
using (QuantumCallback.SubscribeManual<CallbackLocalPlayerRemoveFailed>(c => completionSource.TrySetException(new Exception(c.Message)))) {
QuantumRunner.Default.Game.RemovePlayer();
await completionSource.Task;
}
// Save the snapshot to a file.
var snapshot = QuantumRunner.Default.Game.Frames.Verified.Serialize(DeterministicFrameSerializeMode.Serialize);
File.WriteAllBytes(Path.Combine(Application.dataPath, "savegame.quantum"), snapshot);
// Save the runtime config to a file.
File.WriteAllText(Path.Combine(Application.dataPath, "config.quantum"), JsonUtility.ToJson(QuantumRunner.Default.Game.Configurations.Runtime, true));
// Shutdown the runner, the simulation has to be restarted.
await QuantumRunner.Default.ShutdownAsync();
}
}
How to Start From a Snapshot
The player that created the snapshot now connects to a new Photon online room using a secret and unique room name. IsRoomVisible
is false to prevent random matchmaking to add players to this room. The room name is later send to the friendly player that is supposed to join.
Load the snapshot from a file or from a byte[]
member.
Start Quantum using the snapshot as FrameData
and RollbackWindow - 1
as InitialTick
.
Wait for the simulation to start, then add the player, wait for the confirmation and finally invite the friend to join the online game.
C#
using Photon.Deterministic;
using Photon.Realtime;
using Quantum;
using System;
using System.IO;
using UnityEngine;
public class Foo : QuantumMonoBehaviour
{
[EditorButton("LoadAndStart")]
public async void LoadAndStart() {
// Delete the debug runner if it exists, it interferes with adding players.
var debugRunner = FindAnyObjectByType<QuantumRunnerLocalDebug>();
if (debugRunner != null) {
Destroy(debugRunner);
}
// Connect to a room with a secret room name.
var arguments = new MatchmakingArguments {
PhotonSettings = PhotonServerSettings.Global.AppSettings,
MaxPlayers = Quantum.Input.MAX_COUNT,
// This name has be unique and must be shared with the other players that are expected to join this room
RoomName = "my secret room name",
PluginName = "QuantumPlugin",
AuthValues = new AuthenticationValues(),
// Don't use for random matchmaking
IsRoomVisible = false
};
var client = await MatchmakingExtensions.ConnectToRoomAsync(arguments);
// Load the snapshot from file, or just from a member variable.
byte[] snapshot = default;
if (File.Exists(Path.Combine(Application.dataPath, "savegame.quantum"))) {
snapshot = File.ReadAllBytes(Path.Combine(Application.dataPath, "savegame.quantum"));
}
// Use global session config
var sessionConfig = QuantumDeterministicSessionConfigAsset.DefaultConfig;
// Set and use custom runtime config instead
var runtimeConfig = JsonUtility.FromJson<RuntimeConfig>(File.ReadAllText(Path.Combine(Application.dataPath, "config.quantum")));
// Start and wait for the game, if snapshot is set, it will start from that snapshot.
var sessionRunnerArguments = new SessionRunner.Arguments {
FrameData = snapshot,
InitialTick = snapshot == null ? 0 : sessionConfig.RollbackWindow - 1,
RunnerFactory = QuantumRunnerUnityFactory.DefaultFactory,
GameParameters = QuantumRunnerUnityFactory.CreateGameParameters,
ClientId = "client secret",
PlayerCount = Quantum.Input.MaxCount,
GameMode = DeterministicGameMode.Multiplayer,
RuntimeConfig = runtimeConfig,
SessionConfig = sessionConfig,
Communicator = new QuantumNetworkCommunicator(client),
};
var runner = (QuantumRunner)await SessionRunner.StartAsync(sessionRunnerArguments);
// Add player back to the game and wait for the confirmation.
var completionSource = new System.Threading.Tasks.TaskCompletionSource<bool>();
using (QuantumCallback.SubscribeManual<CallbackLocalPlayerAddConfirmed>(c => completionSource.TrySetResult(true)))
using (QuantumCallback.SubscribeManual<CallbackLocalPlayerAddFailed>(c => completionSource.TrySetException(new Exception(c.Message)))) {
runner.Game.AddPlayer(0, new RuntimePlayer());
await completionSource.Task;
}
// Invite other player to the room
Debug.Log($"Invite to room {client.CurrentRoom.Name} on region {client.CurrentRegion}");
}
}
Back to top