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.
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.
Most of our demos make use of Object Synchronization.
Some script implements
OnPhotonSerializeView() and becomes the observed component of a PhotonView.
OnPhotonSerializeView(), the position and other values are written to a stream and read from it.
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:
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
See Manual Instantiation.
A drawback is that the buffer will grow and grow, if you are not careful.
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
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
PhotonPlayer.SetCustomProperties(Hashtable propsToSet) and include the key-values to add or update.
A shortcut to the local player object is:
PhotonNetwork.room.SetCustomProperties(Hashtable propsToSet) to update the room you are in.
All updates take a moment to distribute but all clients will update
As callback when properties changed, PUN calls
OnPhotonCustomRoomPropertiesChanged(Hashtable propertiesThatChanged) or
OnPhotonPlayerPropertiesChanged(object playerAndUpdatedProps) respectively.
You can also set a 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
The documentation for matchmaking explains how to use Custom 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
expectedValues parameter, which can be used as condition.
expectedValues, the server will only update the properties, if its current key-values match the ones in
Updates with outdated
expectedValues will be ignored (the clients get an error as a result, others won't notice the failed update).
In our example, the
expectedValues 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
expectedValues 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.
SetCustomPropertiesmight 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.
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
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!
RPCs are not sent in the moment when you call
Instead, they are buffered until the Object Synchronization frequency sends an update anyways.
This aggregates RPCs into less packages (avoiding overhead) but introduces some variable lag.
To avoid this local lag, you could finish an update loop with RPCs by calling
This makes sense when your game relies a lot on RPCs to send turns, etc.
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 upgradeded 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.
Rare Updates and State (Open/Close Doors, Map, Character Equipment)
Very infrequent changes are usually best stored in
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. See class
InRoomRoundTimer in the PUN packages.