Synchronization and State
Games are all about updating the other players and keeping the same state.
You want to know who the other players are, what they do, where they are and how their game world looks.
PUN (and Photon in general) offers several tools for updates and keeping a state.
This page will explain the options and when to use each.
Object Synchronization
With PUN, you can easily make certain game objects "network aware".
Assign a PhotonView component and an object can sync the position, rotation and other values with its remote duplicates.
A PhotonView must be setup to "observe" a component like a Transform or (more commonly) one of its scripts.
There are four different options to synchronize data:
- Off:
Synchronization does not occur, nothing is sent or received - Reliable Delta Compressed:
Data is guaranteed to be received with an internal optimization mechanism which sends null if data doesn't change.
For this to work, make sure you populate the stream with distinctSendNext
calls. - Unreliable:
Data is received in order but some updates may be lost.
This means no delay in cases of loss. - Unreliable OnChange:
Data is received in order but some updates may be lost.
If updates repeat the last information, the PhotonView will pause sending updates until the next change.
Most of our demos make use of Object Synchronization.
Some script implements OnPhotonSerializeView()
and becomes the observed component of a PhotonView.
In OnPhotonSerializeView()
, the position and other values are written to a stream and read from it.
To make use of this function, the script has to implement the IPunObservable
interface.
Remote Procedure Call (RPC)
You can mark your methods to be callable by any client in a room.
If you implement 'ChangeColorToRed()' with the attribute [PunRPC]
, remote players can: change the game object's color to red by calling:
photonView.RPC("ChangeColorToRed", RpcTarget.All);
.
A call always targets a specific PhotonView on a GameObject.
So, when 'ChangeColorToRed()' gets called, it only executes on the GameObject with that PhotonView.
This is useful when you want to affect specific objects.
Of course, an empty GameObject can be put into a scene as "dummy" for methods that don't have a target really.
For example, you could implement a chat with RPCs but that is not related to a specific GameObject.
RPCs can be "buffered".
The server will remember the call and send it to anyone who's joining after the RPC got called.
This enables you to store some actions and to implement an alternative to PhotonNetwork.Instantiate()
.
See Manual Instantiation.
A drawback is that the buffer will grow and grow, if you are not careful.
Custom Properties
Photon's Custom Properties consist of a key-values Hashtable which you can fill on demand.
The values are synced and cached on the clients, so you don't have to fetch them before use.
Changes are pushed to the others by SetCustomProperties()
.
How is this useful? Typically, rooms and players have some attributes that are not related to a GameObject:
The current map or the color of a player's character (think: 2d jump and run).
Those can be sent via Object Synchronization or RPC, but it is often more convenient to use Custom Properties.
To set Custom Properties for a Player, use Player.SetCustomProperties(Hashtable propsToSet)
and include the key-values to add or update.
A shortcut to the local player object is: PhotonNetwork.LocalPlayer
.
Similarly, use PhotonNetwork.CurrentRoom.SetCustomProperties(Hashtable propsToSet)
to update the room you are in.
All updates take a moment to distribute but all clients will update CurrentRoom.CustomProperties
and Player.CustomProperties
accordingly.
As callback when properties changed, PUN calls OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
or OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
respectively.
You can also set properties when you create a new room.
This is especially useful because room properties can be used for matchmaking.
There is a JoinRandomRoom()
overload which uses a properties-hashtable to filter acceptable rooms for joining.
When you create a room, make sure to define which room properties are available for filtering in the lobby by setting RoomOptions.CustomRoomPropertiesForLobby
accordingly.
The documentation for matchmaking explains how to use Custom Room Properties for matchmaking.
Check And Swap for Properties (CAS)
When you use SetCustomProperties
, the server usually accepts new values from any client, which can be tricky in some situations.
For example, a property could be used to store who picked up a unique item in a room.
So the key for the property would be the item and the value defines who picked it up.
Any client can set the property to his actorNumber anytime.
If all do it at about the same time, the last SetCustomProperties
call will win the item (set the final value).
That's counter-intuitive and probably not what you want.
SetCustomProperties
has an optional expectedProperties
parameter, which can be used as condition.
With expectedProperties
, the server will only update the properties, if its current key-values match the ones in expectedProperties
.
Updates with outdated expectedProperties
will be ignored (the clients get an error as a result, others won't notice the failed update).
In our example, the expectedProperties
could contain the current owner from which you take the unique item.
Even if everyone tries to take the item, only the first will succeed, because every other update request will contain an outdated owner in the expectedProperties
.
Using the expectedProperties
as a condition in SetCustomProperties
, is called Check and Swap (CAS).
It is useful to avoid concurrency issues but can also be used in other creative ways.
SetCustomProperties
might fail with CAS, all clients update their custom properties by server-sent events only.
This includes the client which attempts to set new values.This is a different timing, compared to setting values without CAS.
You should know that initializing (i.e. first time creating a new property) using CAS is not supported.
Also, currently, there is no callback for SetProperties failures with CAS.
If you want to get notified about CAS failures, here is an example code to add to your MonoBehaviour:
IInRoomCallbacks.OnPlayerPropertiesUpdate
and IInRoomCallbacks.OnRoomPropertiesUpdate
) which should be triggered in case of success only, either with CAS or without it.
C#
private void OnEnable()
{
PhotonNetwork.NetworkingClient.OpResponseReceived += NetworkingClientOnOpResponseReceived;
}
private void OnDisable()
{
PhotonNetwork.NetworkingClient.OpResponseReceived -= NetworkingClientOnOpResponseReceived;
}
private void NetworkingClientOnOpResponseReceived(OperationResponse opResponse)
{
if (opResponse.OperationCode == OperationCode.SetProperties &&
opResponse.ReturnCode == ErrorCode.InvalidOperation)
{
// CAS failure
}
}
Properties Synchronization
This is done 'via server' by default, meaning:
By default, setting actor properties for actor or room properties will not take effect on the sender/setter client (actor that sets the properties) immediately when joined to an online room.
Instead, the sender/setter client (actor that sets the properties) will wait for the server event PropertiesChanged
to apply/set changes locally.
So you need to wait until OnPlayerPropertiesUpdate
or OnRoomPropertiesUpdate
callbacks are triggered for the local client in order to access them.
The reason behind this is that properties can easily go out of synchronization if we set them locally first and then send the request to do so on the server and for other actors in the room.
The latter might fail and we may end up with properties of the sender/setter client (actor that sets the properties) different locally from what's on the server or on other clients.
If you want to have the old behaviour (set properties locally before sending the request to the server to synchronize them) set roomOptions.BroadcastPropsChangeToAll
to false
before creating rooms.
But we highly recommend against doing this.
The client can still cache properties of the local player outside of rooms.
Those properties will be sent when entering rooms.
Also, setting properties in offline mode happens right away.
Besides, by default, local actor's properties are not cleared between rooms, you should do that yourself.
Making the Most of Synchronization, RPCs and Properties
To decide which synchronization method is best for a value, it's usually a good idea to check how often it needs an update and if it needs a "history" of values or not.
Frequent Updates (Positions, Character State)
For frequent updates, use Object Synchronization
.
In doubt, your own script can skip updates by not writing anything into the stream for any number of updates.
Positions for characters change frequently.Each update is useful but is likely to be replaced by a newer one quickly.
A PhotonView can be setup to send "Unreliable" or "Unreliable On Change".
The first will send updates in a fixed frequency - even if the character did not move.
The latter will stop sending updates when the GameObject (character, unit) rests.
Infrequent Updates (Actions of Players)
Changing equipment on a character, using a tool or ending a turn of a game are all infrequent actions.
They are based on user input and probably best sent as RPC.
The line to using using Object Synchronization is not a very clear one.
If you do Object Synchronization anyways, it can make a lot of sense to "in-line" some actions with the more frequent updates.
As example: If you send a character's position anyways, you can easily add a value to send a "Jumping" state along.
This does not have to be a separate RPC then!
Unlike Object Synchronization, RPCs might be buffered.
Any buffered RPC will be sent to players who join later, which can be useful if actions have to be replayed one after another.
For example a joining client can replay how someone placed a tool in the scene and how someone else upgraded it.
The latter depends on the first action.
Sending buffered RPCs to new players takes some traffic and it means your clients have to play back and apply each action before they get into the "live" gameplay.
This can be annoying and excess buffering might break weak clients, so use buffering with care.
Note: RPCs are not sent right away.
Read more here.
Rare Updates and State (Open/Close Doors, Map, Character Equipment)
Very infrequent changes are usually best stored in Custom Properties
.
Unlike buffered RPCs, the property Hashtable contains only the current key-values.
This is great for a door's state being "open" (or not).The players don't care how a door opened and closed earlier on.
In the RPC example above, someone placed a tool in the scene and it gets upgraded.
Using RPCs for a few actions is fine.For a lot of modifications, it is probably easier to aggregate the current state in a single value of a property.
Multiple "+10 defense" upgrades could easily be stored in a single value instead of a lot of RPCs.
Again, the line between using Custom Properties and using RPCs is not exact.
Another good use case for Custom Properties is to store a room's "start time".
When the game begins, store PhotonNetwork.Time
as property.
That value is (approximately) the same for all clients in the room and with the start time, any client can calculate how long the game is running already and which turn it is.
Of course, you could also store each turn's start time.
This works better if the game can be paused.