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 usevoid
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 havePunRPC
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 calledMyClass
that implements a methodMyRPC
marked asPunRPC
, 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.