This document is about: PUN 2
SWITCH TO

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

RPCs and RaiseEvent

Remote Procedure Calls

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

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 [PunRPC] attribute.

C#

[PunRPC]
void ChatMessage(string a, string b)
{
    Debug.Log(string.Format("ChatMessage {0} {1}", a, b));
}

To call the methods marked as PunRPC, you need a PhotonView component. Example call:

C#

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

Pro tip: If your script is a MonoBehaviourPun you can use: this.photonView.RPC().

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

Method Signature

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 (Read about "Serialization in Photon"). 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.LogFormat("Info: {0} {1} {2}", info.Sender, info.photonView, info.SentServerTime);
}

Using PhotonMessageInfo, you can implement an RPC for 'shooting' without extra parameters. You know who shot and what was hit and when.

Considerations

  • 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 RPC method cannot be static.

  • Generic methods are not supported as PUN RPCs.

  • An RPC method that has a return value other than void can be called but the return value will not be used. Unless you need the return value in other explicit direct calls to the same method, always use void as a return value.

  • If the RPC method is overridden, do not forget to add the PunRPC attribute to it. Otherwise, inherited methods not overridden that have PunRPC attribute on the base class can be used.

  • Do not attach more than one component of the same type and that have RPC methods on the same GameObject as a PhotonView. If you have a class called MyClass that implements a method MyRPC marked as PunRPC, you should not attach more than one instance of it to the same GameObject. You can even make use of [DisallowMultipleComponent] class attribute. Read more about it here.

  • Use a unique name per RPC method.

  • Do not create overload RPC methods: one with the special PhotonMessageInfo last parameter and one without it. Choose either option.

  • It is not recommended to use optional parameters in RPC methods. If necessary, pass all parameters including optional ones during the RPC call. Otherwise, the receiving clients will not be able to find and process the incoming RPC.

  • If you want to send an object array as a parameter of an RPC method, you need to cast it to object type first.

    Example:

    C#

    object[] objectArray = GetDataToSend();
    photonView.RPC("RpcWithObjectArray", target, objectArray as object);
    
    // ...
    
    [PunRPC]
    void RpcWithObjectArray(object[] objectArray)
    {
        // ...
    }
    

Targets, Buffering and Order

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

RpcTarget 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 a long buffer list causes longer join times.

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

Alternatively, you can call a RPC for a specific player in the room. Use the overload with the target Player as second parameter.

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]. The event code 0 should be avoided if you are going to use advanced events cache manipulation (it's a wild card in the filter when removing cached events based on event code). In the following examples we have an event, which tells a bunch of units to move to a certain position.

C#

using ExitGames.Client.Photon;
using Photon.Realtime;
using Photon.Pun;

public class SendEventExample
{
    // If you have multiple custom events, it is recommended to define them in the used class
    public const byte MoveUnitsToTargetPositionEventCode = 1;

    private void SendMoveUnitsToTargetPositionEvent()
    {
        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
        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(MoveUnitsToTargetPositionEventCode, content, raiseEventOptions, SendOptions.SendReliable);
    }
}

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 third parameter describes the RaiseEventOptions. By using these 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 these 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.

The last parameter describes the SendOptions. By using this options, you can for example choose if this event is sent reliable or unreliable or choose if the message should be encrypted. In our example we just want to make sure, that our event is sent reliable.

IOnEventCallback Callback

To receive custom events, we have two different possibilities. The first one is to implement the IOnEventCallback interface. When implementing this, you have to add the OnEvent callback handler. The method for this handler looks like this:

C#

public void OnEvent(EventData photonEvent)
{
    // Do something
}

To register this handler properly, we can make use of Unity's OnEnable and OnDisable methods. 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#

private void OnEnable()
{
    PhotonNetwork.AddCallbackTarget(this);
}

private void OnDisable()
{
    PhotonNetwork.RemoveCallbackTarget(this);
}

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

C#

public void OnEvent(EventData photonEvent)
{
    byte eventCode = photonEvent.Code;

    if (eventCode == MoveUnitsToTargetPositionEvent)
    {
        object[] data = (object[])photonEvent.CustomData;

        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, e.g. a List or Dictionary) and apply the new target position.

Final code should look like:

C#

using ExitGames.Client.Photon;
using Photon.Realtime;
using Photon.Pun;

public class ReceiveEventExample : MonoBehaviour, IOnEventCallback
{
    private void OnEnable()
    {
        PhotonNetwork.AddCallbackTarget(this);
    }

    private void OnDisable()
    {
        PhotonNetwork.RemoveCallbackTarget(this);
    }

    public void OnEvent(EventData photonEvent)
    {
        byte eventCode = photonEvent.Code;
        if (eventCode == MoveUnitsToTargetPositionEvent)
        {
            object[] data = (object[])photonEvent.CustomData;
            Vector3 targetPosition = (Vector3)data[0];
            for (int index = 1; index < data.Length; ++index)
            {
                int unitId = (int)data[index];
                UnitList[unitId].TargetPosition = targetPosition;
            }
        }
    }
}

LoadBalancingClient.EventReceived

The second way to receive custom events is to register a method which is called whenever an event is received. To do this properly, we can use Unity's OnEnable and OnDisable method like we did before.

C#

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

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

Final code should look like:

C#

using ExitGames.Client.Photon;
using Photon.Realtime;
using Photon.Pun;

public class ReceiveEventExample : MonoBehaviour
{
    private void OnEnable()
    {
        PhotonNetwork.NetworkingClient.EventReceived += OnEvent;
    }

    private void OnDisable()
    {
        PhotonNetwork.NetworkingClient.EventReceived -= OnEvent;
    }

    private void OnEvent(EventData photonEvent)
    {
        byte eventCode = photonEvent.Code;
        if (eventCode == MoveUnitsToTargetPositionEvent)
        {
            object[] data = (object[])photonEvent.CustomData;
            Vector3 targetPosition = (Vector3)data[0];
            for (int index = 1; index < data.Length; ++index)
            {
                int unitId = (int)data[index];
                UnitList[unitId].TargetPosition = targetPosition;
            }
        }
    }
}

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

Interest Groups

"Interest Groups" are an alternative way to define who receives an event. You can use the global group 0, to raise an event for all clients. You can also use a specific group that is not 0 to only raise an event for a certain group. To receive an event which is sent to a certain group, the client has to subscribe to this group before.

Read more about Interest Groups here.

Target Actors

"Target Actors" are the third way to define who receives an event. Using this way you can raise an event to one or more specific clients which you can select by using their unique actor numbers.

Caching Options

The most interesting option is probably the event caching option. PUN uses events caching in two places:

  • all PhotonNetwork.Instantiate* calls are cached.
  • all RPCs sent with RpcTarget.*Buffered are cached. PUN uses 'buffered' wording in this case for the same meaning.

Read more about events cache here.

Send Options

With the SendOptions parameter you define if the event is sent reliable or encrypted.

Reliability

The Reliability option describes, if the event is sent reliable or unreliable.

Encryption

With the Encrypt option you can define, if the event gets encrypted before it is sent. By default, events are not encrypted.

Back to top