This document is about: PUN 2
SWITCH TO

PUN Classic (v1), PUN 2 and Bolt are in maintenance mode. PUN 2 will support Unity 2019 to 2022, but no new features will be added. Of course all your PUN & Bolt projects will continue to work and run with the known performance in the future. For any upcoming or new projects: please switch to Photon Fusion or Quantum.

Optimization Tips

Performance Optimization

Pool Objects for Instantiation

PUN has a built-in option to instantiate objects from a pool, rather than from a loaded resource (which is kept in-memory to speed up things). Read Using the PrefabPool in the Instantiation doc page.

Cache RPC Targets

In some cases, you might use a lot of RPCs in your game. Any Component on the target GameObject may implement RPCs, so PUN will use reflection to find the fitting methods. Of course, this is expensive and even wasteful, if the components don't change.

By default, PUN caches a MethodInfo list per Type of script. It does not cache which MonoBehaviours are on the GameObject as potential target.

You can set PhotonNetwork.UseRpcMonoBehaviourCache = true, to cache the MonoBehaviours per PhotonView for RPCs. This speeds up finding the components to call. Should the scripts on a GameObject change, call photonView.RefreshRpcMonoBehaviourCache() to update as needed.

Network Traffic Optimization

Compact Serialization

You can optimize traffic quite a bit by having a closer look at what you are sending and how. This can be done later in the development, without a lot of extra hassle. As usual with optimization, start with the most frequently sent messages first.

Often, values and objects are sent with more values than needed to share the state. If your character can't scale, don't sync scale! If your character is never leaning to a side, rotation could be a single float. Actually, a byte may be enough for the rotation without much precision loss. This is fine for non-physical objects.

In general, have a look at what you do in OnPhotonSerializeView and RPCs. You can send a variable amount of values per OnPhotonSerializeView. Sending compact byte arrays is often leaner than registering and sending a lot of Custom Types.

There are some tools that may make your life easier without much hassle.

An example of a library that can help with serialization is NetStack. It comes with various other useful features, too.

Network Culling and Interest Groups

Another technique to save quite some bandwidth is "Network Culling". See Interest Groups

Reduce Allocations With Pooled ByteArraySlice

By default, Photon clients in C# SDKs serializes byte[] and ArraySegment<byte> as byte[]. On the receiving side, this allocates a new byte[] of the same length, which is passed to the OnEvent callbacks. ByteArraySlice is a non-alloc / non-boxing alternative to these options.

ByteArraySlice is a wrapper class for a byte[] very similar to ArraySegment<byte>, except that it is a recyclable class. As a class it can be cast to object (which all Photon messages are cast to) without creating an allocation from boxing.

The fields/properties of ByteArraySlice are:

  • Buffer: The wrapped byte[] array.
  • Offset: The starting byte the transport will read from.
  • Count: The number of bytes that were written past the Offset.

ByteArraySlice and the non-alloc conduit availability can be tested with the custom define #if PUN_2_19_OR_NEWER.

Serialization Usage

This can be done in of two ways:

Acquire from ByteArraySlicePool

C#

void Serialization()
{
    // Get a pooled Slice.
    var pool = PhotonNetwork.NetworkingClient.LoadBalancingPeer.ByteArraySlicePool;
    var slice = pool.Acquire(256);

    // Write your serialization to the byte[] Buffer.
    // Set Count to the number of bytes written.
    slice.Count = MySerialization(slice.Buffer);

    // Send (works for RPCs as well)
    PhotonNetwork.RaiseEvent(MSG_ID, slice, opts, sendOpts);

    // The ByteArraySlice that was Acquired is automatically returned to the pool
    // inside of the RaiseEvent
}

Maintain your own ByteArraySlice

C#

private ByteArraySlice slice = new ByteArraySlice(new byte[1024]);

void Serialization()
{
    // Write your serialization to the byte[] Buffer.
    // Set Count to the number of bytes written.
    slice.Count = MySerialization(slice.Buffer);

    // Send (works for RPCs as well)
    PhotonNetwork.RaiseEvent(MSG_ID, slice, opts, sendOpts);
}

Deserialization Usage

By default byte[] data is deserialized to new byte[x]. We must set LoadBalancingPeer.UseByteArraySlicePoolForEvents = true to enable the non-alloc conduit. Once enabled, we cast incoming objects to ByteArraySlice rather than byte[].

C#

// By default byte arrays arrive as byte[]
// UseByateArraySlicePoolForEvents must be enabled to use this feature
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void EnableByteArraySlicePooling()
{
    PhotonNetwork.NetworkingClient.LoadBalancingPeer.UseByteArraySlicePoolForEvents = true;
}

private void OnEvent(EventData photonEvent)
{
    // Rather than casting to byte[], we now cast to ByteArraySlice
    ByteArraySlice slice = photonEvent.CustomData as ByteArraySlice;

    // Read in the contents of the byte[] Buffer
    // Your custom deserialization code for byte[] will go here.
    Deserialize(slice.Buffer, slice.Count);

    // Be sure to release the slice back to the pool
    slice.Release();
}

Reusing EventData

The C# clients receive events via OnEvent(EventData ev). By default, each EventData is a new instance, which causes some extra work for the garbage collector.

In many cases, it is easily possible to reuse the EventData and avoid the overhead. This can be enabled via the PhotonPeer.ReuseEventInstance setting.

In PUN 2, set PhotonNetwork.NetworkingClient.LoadBalancingPeer.ReuseEventInstance to true.

Other Tips

Send Right Away

RPCs and events raised are not sent at the same moment of the method calls photonView.RPC or PhotonNetwork.RaiseEvent. The same applies to other operation requests like PhotonNetwork.LeaveRoom or PhotonNetwork.SetMasterClient or SetProperties. Instead, all these messages are queued until a periodic routine is called by the PhotonHandler (frequency is set using PhotonNetwork.SendRate). This aggregates messages into fewer packages to avoid traffic overhead but introduces some variable lag. To avoid this lag and you want to send the RPC or event right away call PhotonNetwork.SendAllOutgoingCommands in the next line.

This makes sense when your game relies on the timing of those messages, examples:

  • timed competitive trivia or quiz games, the quicker the better
  • the opponent is waiting for your turn

However, there are other uses cases for this, like sending a message:

  • before disconnecting
  • before leaving the room
  • before quitting the app (inside OnApplicationQuit)
  • before the app is moved to the background or loses focus (inside OnApplicationPause or inside OnApplicationFocus)

For these cases, you should know that:

  • OnApplicationQuit will not be called on all platforms, on Android for example, this will not be called when the application is terminated by the system. You could use OnApplicationPause instead.
  • Average package loss is typically 1.2%. Even if sent reliable there is no guarantee that what you sent will arrive to destination since the client will be unresponsive or disconnected or no longer joined to the room in case a retry attempt is needed.
  • A message sent in these cases should be relatively small to fit into one package as the client may not have enough time to send multiple fragments.

Do not "pause" PUN while Time.timeScale == 0

To allow dispatching received messages in PUN even if Time.timeScale is zero or low, set PhotonNetwork.MinimalTimeScaleToDispatchInFixedUpdate to a value equal or higher than Time.timeScale. By default, it's set to -1 which means received events (including RPCs) or operations responses will not be processed (no callbacks and no operation can finish execution) if Time.timeScale is equal to 0.

Back to top