Async Extensions
The Async Extensions for the Realtime API are built as an optional model on top of the well-known callback approach and have been added to the .Net Realtime SDKs v5 and up.
These async methods are based on C# async/await and Tasks (TAP) which are supported on Unity for a while now (for more information, read the TAP documentation from Microsoft).
The implementation of these async APIs is not optimized for performance but for ease of use. As they are not involved in the gameplay loop (e.g. Quantum simulation), this should be fine.
Establishing a connection and sending operations is, by itself, an asynchronous process and Task-based asynchronous patterns produce much more readable and maintainable code.
C#
var appSettings = new new AppSettings();
var client = new RealtimeClient();
await client.ConnectUsingSettingsAsync(appSettings);
var joinRandomRoomParams = new JoinRandomRoomArgs();
var enterRoomArgs = new EnterRoomArgs();
var result = await client.JoinRandomOrCreateRoomAsync(joinRandomRoomParams, enterRoomArgs);
An Async-version of most Realtime API is already implemented. When something is missing it should be easy to add locally or talk to the Photon team with a request. The code can be found in the AsyncExtensions.cs
file.
None of the async methods will start processing or sending before being awaited.
The RealtimeClient
does not need to be updated (RealtimeClient.Service()
) while waiting for an async operation method to complete.
Error Handling
All Async-methods will throw exceptions when encountering errors. Different types of exceptions are thrown to improve further error handling (see the related method summary).
It's a bit unfortunate to pollute the code with try/catch blocks but it makes the API much simpler.
C#
try {
await client.ConnectUsingSettingsAsync(appSettings);
} catch (Exception e) {
Debug.LogException(e);
}
C#
try {
// Disconnecting can also fail
await client.DisconnectAsync();
} catch (Exception e) {
Debug.LogException(e);
}
Unity And Async
When working with async
/await
in Unity there are a few specialties to consider:
Contrary to .Net, using await
from the Unity thread will always resume execution on the Unity thread.
- This makes using await for our purpose quite harmless in Unity. But it will cause multi-threading issues when used outside of Unity.
New Tasks
are ran on the thread pool (which in most cases is very undesirable in Unity) if not created with a custom TaskFactory based on the Unity SynchronizationContext.
- The global default
AsyncConfig
creates aTaskFactory
that is internally used to create and continue the task. - Follow the trail of
AsyncConfig.InitForUnity()
.
C#
var taskFactory = new TaskFactory(
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.FromCurrentSynchronizationContext());
Unity does not stop running Tasks
when switching play mode.
- This is a headache. We solve this by using a global
CancellationTokenSource
inside theAsyncSetup
class that gets triggered on play mode change callbacks (seeAsyncSetup.Startup()
). - All task and continuations created internally use either an explicit
AsyncConfig
passed as an argument orAsyncConfig.Global
.
Exceptions in Tasks
can be suppressed by Unity under some circumstances.
- In short, use this pattern:
public async void Update() {}
C#
// Does NOT log exception.
// Why? Because Unity does not handle exception inside tasks by design.
public Task Update1() {
return Task.Run(() => throw new Exception("peng"));
}
// Does NOT log exception.
// Why? Because we return at await and continue as a task object and Unity swallows the exception.
public async Task Update3() {
await Task.Delay(100);
throw new Exception("peng");
}
// Logs exception.
// Why? because we unwrap the task and run it synchronously with .Wait().
public void Update2() {
Task.Run(() => throw new Exception("peng")).Wait();
}
// Logs exception.
// Why? Because we resume the execution in this method and not return a task.
public async void Update4() {
await Task.Delay(100);
throw new Exception("peng");
}
// Logs exception.
// Why? We add a continuation task that logs (in any thread) when the task faulted.
public Task Update5() {
var task = Task.Run(() => throw new Exception("peng")).ContinueWith(t => {
if (t.IsFaulted) {
Debug.LogException(t.Exception.Flatten().InnerException);
};
});
return task;
}
WebGL Requirements
The Realtime Async extensions are supported for WebGL. Because of threading restrictions in the browsers, multi-threading code can not be used:
e.g. Task.Delay()
Matchmaking Async Extensions
The Realtime Matchmaking extensions are combining the most common connection and reconnection logic into two comfortable extension methods for the RealtimeClient
class.
ConnectToRoomAsync
C#
public Task<RealtimeClient> ConnectToRoomAsync(MatchmakingArguments arguments)
ConnectToRoomAsync
will perform a couple things:
- Connect to the Photon Cloud using provided the
PhotonSettings
andAuthValues
- Perform simple matchmaking based on the configuration
- Random matchmaking:
RoomName:null
,CanOnlyJoin:false
- Join an existing room:
RoomName:"room-name"
,CanOnlyJoin:false
- Join or create a room:
RoomName:"room-name"
,CanOnlyJoin:true
- Use a typed lobby:
Lobby:MyLobby
- Use lobby properties:
CustomLobbyProperties:MyLobbyProperties
- Random matchmaking:
These values have to be set: PhotonSettings
, MaxPlayer
, PluginName
, AuthValues
/ UserId
MatchmakingArguments
Property | Type | Description |
---|---|---|
PhotonSettings |
AppSettings |
The Photon AppSetting class containing information about the AppId and Photon server addresses. |
PlayerTtlInSeconds |
int |
Player TTL, in seconds. |
EmptyRoomTtlInSeconds |
int |
Empty room TTL, in seconds. |
RoomName |
string |
Set a desired room name to create or join. If the RoomName is null, random matchmaking is used instead. |
MaxPlayers |
int |
Max clients for the Photon room. 0 = unlimited. |
CanOnlyJoin |
bool |
Configure if the connect request can also create rooms or if it only tries to join. |
CustomProperties |
Hashtable |
Custom room properties that are configured as EnterRoomArgs.RoomOptions.CustomRoomProperties . |
CustomLobbyProperties |
string[] |
List of room properties that are used for lobby matchmaking. Will be configured as EnterRoomArgs.RoomOptions.CustomRoomPropertiesForLobby . |
AsyncConfig |
AsyncConfig |
Async configuration that include TaskFactory and global cancellation support. If null, then AsyncConfig.Global is used. |
NetworkClient |
RealtimeClient |
Optionally provide a client object. If null, a new client object is created during the matchmaking process. |
AuthValues |
AuthenticationValues |
Provide authentication values for the Photon server connection. Use this in conjunction with custom authentication. This field is created when UserId is set. |
PluginName |
string |
Photon server plugin to connect to. |
ReconnectInformation |
MatchmakingReconnectInformation |
Optional object to save and load reconnect information. |
Lobby |
TypedLobby |
Optional Realtime lobby to use for matchmaking. |
ReconnectToRoomAsync
C#
public Task<RealtimeClient> ReconnectToRoomAsync(MatchmakingArguments arguments)
ReconnectToRoomAsync
will try to return to the previous room.
It will attempt to fast reconnect to a room (skipping master server) when the client object is in a reusable state (e.g. after a timeout). Otherwise, a complete connection sequence runs and attempts to rejoin the room.
If a previous connection for the user is not yet discarded by the server (e.g. during the 10 second timeout), a re-join fails. ReconnectToRoomAsync
will automatically cover this specific case and make several attempts to rejoin a room, before finally failing.
ReconnectToRoomAsync
can also be used after restarting the application or with a new client object. In those cases, the arguments.ReconnectInformation
of type MatchmakingReconnectInformation
must be set to provide the information to re-join a room.
In Quantum, the QuantumReconnectInformation
can be used, which automatically saves the rejoin-information on the PlayerPrefs
.
Caveat: Saving the UserId
to PlayerPrefs
is likely a security risk and should always be replaced with Custom Authentication before going into the reconnection logic after restarting the app.
The Quantum demo menu can be configured to check saved QuantumReconnectInformation
on start up. Use QuantumMenuUIMain.IsReconnectionCheckEnabled
to enable this.
Any MatchmakingReconnectInformation
instance has a lifetime to prevent rejoin attempts for outdated sessions. Call Set(client)
repeatedly during the online game to refresh this timeout. It is automatically called from the matchmaking extension methods after a successful connection or re-connection.
The virtual method MatchmakingReconnectInformation.Set(client)
can be overwritten if needed.
C#
virtual void Set(RealtimeClient client)
MatchmakingReconnectInformation
Property | Type | Description |
---|---|---|
Room |
string |
The room name that the client was connected to. |
Region |
string |
The region the client was connected to. |
AppVersion |
string |
The app version used in the former connection. |
UserId |
string |
The user id the client used to connect to the server. |
TimeoutInTicks |
long |
The timeout after this information is considered to be unusable. Use the Timeout property to set and get this value. |