This document is about: PUN 1
SWITCH TO

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

RPCs and RaiseEvent

One feature that sets PUN aside from other Photon packages is the support for Remote Procedure Calls (RPCs).

Remote Procedure Calls

Remote Procedure Calls are exactly what the name implies: Method-calls on remote clients (in the same room).

To enable remote calling for some method, you must apply the attribute: [PunRPC].

C#

[PunRPC]
void ChatMessage(string a, string b)
{
    Debug.Log(string.Format("ChatMessage {0} {1}", a, b));
}
Unity 5.1 made the RPC attribute obsolete and it will be removed later on. Due to that, PUN uses another attribute: PunRPC, beginning in v1.56. All other usages of RPC and "Remote Procedure Call" are unaffected.
To call the functions marked as RPC, you need a `PhotonView`. Example call:

C#

PhotonView photonView = PhotonView.Get(this);
photonView.RPC("ChatMessage", PhotonTargets.All, "jup", "and jup!");

Pro tip: If your script is a Photon.MonoBehaviour or Photon.PunBehaviour you can use: this.photonView.RPC().

So, instead of directly calling the target method, call RPC() on a PhotonView and provide the name of the method to call.

Targets and Parameters

By design, the script that has the RPC methods needs to be attached to the exact same GameObject as the PhotonView not its parent nor its child.

The PhotonView is like a "target" for the RPC:
All clients execute the method only on the networked GameObject with that specific PhotonView.
If you hit a specific object and call "ApplyDamage" RPC, then the receiving clients will apply the damage to the same object!

You can add multiple parameters (provided PUN can serialize them).
When you do, the method and the call must have the same parameters.
If the receiving client can’t find a matching method, it will log an error.

There is one exception to this rule:
The last parameter of a RPC method can be of type PhotonMessageInfo,
which will provide the context for each call. You don't set the PhotonMessageInfo in the call.

C#

[PunRPC]
void ChatMessage(string a, string b, PhotonMessageInfo info)
{
    // the photonView.RPC() call is the same as without the info parameter.
    // the info.sender is the player who called the RPC.
    Debug.Log(string.Format("Info: {0} {1} {2}", info.sender, info.photonView, info.timestamp));
}

With the sender being in the PhotonMessageInfo and the "targetting" via PhotonView, you can implement shooting someone without extra parameters.
You know who shot and what was hit.

Targets, Buffering and Order

You can define which clients execute an RPC. To do that, use the values of PhotonTargets.
Most commonly, you want All clients to call the RPC.
Sometimes only Others.

PhotonTargets has some values ending on Buffered.
The server will remember those RPCs and when a new player joins, it gets the RPC, even though it happened earlier.
Use this with care, as the a long buffer causes longer join times.

PhotonTargets has values ending on ViaServer.
Usually, when the sending client has to execute an RPC, it does so immediately - without sending the RPC through the server.
This, however, affects the order of events, because there is no lag when you call a method locally.

ViaServer disables the "All" shortcut.
This is especially interesting when RPCs should be done in order:
RPCs sent through the server are executed in the same order by all receiving clients.
It is the order of arrival on the server.

Example: In a racing game you could send the RPC "finished" as AllViaServer.
The first "finished" RPC call will tell you who won.
The following "finished" calls will tell you who's on the ranks.

Shortcuts for RPC Names

Because strings are not the most effective piece to send via network, PUN uses a trick to cut them short.
PUN detects RPCs in the Editor and compiles a list.
Each method name gets an ID via that list and when you call an RPC by name, PUN will silently just send the ID.

Due to this shortcut, different builds of games maybe don't use the same IDs for RPCs.
If this is a problem, you can disable the shortcut.
However, if clients of the same build are matched, this is not a problem.

The list of RPCs is stored and managed via the PhotonServerSettings.

If RPC calls go wrong between different builds of a project, check this list.
The Get HashCode button calculates a hashcode, which is easy to compare between project folders.

If required, you can clear the list (Clear RPCs button) and refresh the list manually by clicking Refresh RPC List button.

Timing for RPCs and Loading Levels

RPCs are called on specific PhotonViews and always target the matching one on receiving clients.
If a remote client did not load or create the matching PhotonView yet, the RPC gets lost!

Due to that, a typical cause for lost RPCs is when clients load new scenes.
It just needs one client which already loaded a scene with new GameObjects and the other clients can't understand this one (until they also loaded the same scene).

PUN can take care of that.
Just set PhotonNetwork.automaticallySyncScene = true before you connect and use PhotonNetwork.LoadLevel() on the Master Client of a room.
This way, one client defines which level all clients have to load in the room / game.

To prevent losing RPCs a client can stop executing incoming messages (this is what LoadLevel does for you).
When you get an RPC to load some scene, immediately set isMessageQueueRunning = false until the content is initialized.
Disabling the message queue will delay incoming and outgoing messages until the queue is unlocked.
Obviously, it's very important to unlock the queue when you're ready to go on.

Example:

C#

private IEnumerator MoveToGameScene()
{
    // Temporary disable processing of futher network messages
    PhotonNetwork.isMessageQueueRunning = false;
    LoadNewScene(newSceneName); // custom method to load the new scene by name
    while(newSceneDidNotFinishLoading)
    {
        yield return null;
    }
    PhotonNetwork.isMessageQueueRunning = true;
}

RaiseEvent

In some cases, RPCs are not exactly what you need.
They need a PhotonView and some method to be called.

With PhotonNetwork.RaiseEvent, you can make up your own events and send them without relation to some networked object.

Events are described by using an unique identifier, the Event Code. In Photon this Event Code is described as a byte value, which allows up to 256 different events. However some of them are already used by Photon itself, so you can't use all of them for custom events. After excluding all of the built-in events, you still have the possibility to use up to 200 custom Event Codes [0..199]. In the following examples we have an event, which tells a bunch of units to move to certain position.

C#

byte evCode = 0; // Custom Event 0: Used as "MoveUnitsToTargetPosition" event
object[] content = new object[] { new Vector3(10.0f, 2.0f, 5.0f), 1, 2, 5, 10 }; // Array contains the target position and the IDs of the selected units
bool reliable = true;
RaiseEventOptions raiseEventOptions = new RaiseEventOptions { Receivers = ReceiverGroup.All }; // You would have to set the Receivers to All in order to receive this event on the local client as well
PhotonNetwork.RaiseEvent(evCode, content, reliable, raiseEventOptions);

Let's take a closer look on what we are doing here. In this case we are defining the Event Code directly before raising the event. If you have multiple custom events, it is recommended to define them in the used class, like this:

C#

private readonly byte MoveUnitsToTargetPositionEvent = 0;

We can also use this when calling the RaiseEvent function:

C#

PhotonNetwork.RaiseEvent(MoveUnitsToTargetPositionEvent, content, reliable, raiseEventOptions);

The content can be anything that PUN can serialize. In this case we are using an array of objects, because we have different types in this example.

The last parameter describes the RaiseEventOptions. By using this options, you can for example choose if this event should be cached on the server, select which clients should receive this event or choose to which Interest Group this event should be forwarded. Instead of defining those options yourself, you can also use null which would apply the default RaiseEventOptions. Since we want the sender to receive this event as well, we are setting the Receivers to ReceiverGroup.All. There will be some more information about the RaiseEventOptions at the bottom of this page.

To receive custom events, we have to implement and register an OnEvent callback handler. The function for this handler have to look like this:

C#

public void OnEvent(byte eventCode, object content, int senderId)
{
    // Do something
}

To register this handler properly, we can make use of Unity's OnEnable and OnDisable functions. This way we also make sure, that we are adding and removing the callback handler properly and don't run into any issues related to this certain handler.

C#

public void OnEnable()
{
    PhotonNetwork.OnEventCall += OnEvent;
}

public void OnDisable()
{
    PhotonNetwork.OnEventCall -= OnEvent;
}

To do something with the received information, our OnEvent function might look similar to this one:

C#

public void OnEvent(byte eventCode, object content, int senderId)
{
    if (eventCode == MoveUnitsToTargetPositionEvent)
    {
        object[] data = (object[])content;

        Vector3 targetPosition = (Vector3)data[0];

        for (int index = 1; index < data.Length; ++index)
        {
            int unitId = (int)data[index];

            UnitList[unitId].TargetPosition = targetPosition;
        }
    }
}

Firstly we are checking if the received Event Code matches the code we set up earlier. If so, we are casting the content of the event to the format we have sent before, which is an array of objects in our example. Afterwards we are getting the target position from that array, which is the first object we have added to the content earlier. Since we know, that we just have byte values left in that array, we can iterate through the rest of the data by using a for-loop. For each byte value in this array, we are getting the unique identifier and using this to get the certain Unit from our UnitList (basically a structure which contains our Units, i.e. a Dictionary) and apply the new target position.

Under the hood, PUN also uses RaiseEvent for pretty much all communication.

Raise Event Options

With the RaiseEventOptions parameter, you define which clients get the event, if it's buffered and more.

Receiver Groups

"Receiver Groups" are an alternative way to define who receives an event.
This option is available via RaiseEventOptions.

There are three defined groups:

  • "Others": all other active actors joined to the same room.
  • "All": all active actors joined to the same room including sender.
  • "MasterClient": currently designated Master Client inside the room.

Caching Options

The most interesting option is probably the event cache / buffering.
PUN uses that for Instantiate and it can be useful when new (joining) players should get events that happened even before they were in the room.

RaiseEventOptions.EventCaching has three important options:
AddToRoomCache, AddToRoomCacheGlobal and RemoveFromRoomCache.
These work best when you send a Hashtable in an event.

Calling RaiseEvent with EventCaching.AddToRoomCache, the event will be put into the server's cache.
That means, any player who joins later on, will also get the event.
New players get cached events in the order they arrived on the server.

When a player leaves, the cached events get automatically removed from the cache.
To avoid this for specific events, call RaiseEvent with EventCaching.AddToRoomCacheGlobal.
That puts the event into the "room's event cache".

If you put a lot of events into the cache, new players will get a lot of messages when they get into the room.
This can take a moment for larger numbers of events, so you should clean up stuff that's no longer relevant, which is done with EventCaching.RemoveFromRoomCache.

When you use RemoveFromRoomCache, the EventCode of RaiseEvent will be used as filter.
So instead of setting some event, you can remove all instances of it.

To get more fine control, the content of an event can be used for filtering.
For that, you have to use a Hashtable as content type.
You can set a key/value pair to identify a specific event and when you RaiseEvent with RemoveFromRoomCache, you only have that key/value pair in the content filter.

You can identify individual events this way, or events belonging to some object or turn or whatever.

Read more about events cache here.

Back to top