This document is about: PUN 1
SWITCH TO

PUN Classic (v1)、PUN 2 和 Bolt 處於維護模式。 PUN 2 將支援 Unity 2019 至 2022,但不會添加新功能。 當然,您所有的 PUN & Bolt 專案可以用已知性能繼續運行使用。 對於任何即將開始或新的專案:請切換到 Photon Fusion 或 Quantum。

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.

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.

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", PhotonTargets.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 PhotonPlayer.SetCustomProperties(Hashtable propsToSet) and include the key-values to add or update.
A shortcut to the local player object is: PhotonNetwork.player.
Similarly, use PhotonNetwork.room.SetCustomProperties(Hashtable propsToSet) to update the room you are in.

All updates take a moment to distribute but all clients will update room.customProperties and player.customProperties accordingly.
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 RoomOptions.customRoomPropertiesForLobby accordingly.

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.
With expectedValues, the server will only update the properties, if its current key-values match the ones in expectedValues.
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.

Using 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.

Note: As 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.

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!

RPCs are not sent in the moment when you call photonView.RPC("rpcName", ...).
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 PhotonNetwork.SendOutgoingCommands().
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 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. See class InRoomRoundTimer in the PUN packages.

Back to top