This document is about: PUN 2
SWITCH TO

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

Instantiation

Most multiplayer games need to create and synchronize some GameObjects.
Maybe it's a character, some units or monsters that should be present on all clients inside a room.
PUN provides a convenient way to do just that.

As usual in Unity, Instantiate and Destroy are used to manage the lifetime of GameObjects.
PUN 2 can use a pool to create (and return) them.
Each networked GameObject must have a PhotonView component (and a ViewID) as identifier via the network.

This page and the following ones explain how to create, sync. and use networked GameObjects.

PhotonNetwork.Instantiate

To create networked GameObjects, use PhotonNetwork.Instantiate instead of Unity's Object.Instantiate.
Any client in a room can call this to create objects which it will control.

C#

PhotonNetwork.Instantiate("MyPrefabName", new Vector3(0, 0, 0), Quaternion.identity, 0);

The first parameter of PhotonNetwork.Instantiate is a string which defines the "prefab" to instantiate.
Internally, PUN will fetch the GameObject from the PhotonNetwork.PrefabPool, set it up for the network and enable it.
The position and rotation where to create the object must be set.
Players who join later, will initially instantiate the object at this place, even if it moved already.

Any prefab must have a PhotonView component.
This contains a ViewID (the identifier for network messages), who owns the object, which scripts will write and read network updates (the "observed" list) and how those updates are sent (the "Observe option").
Check the inspector to setup a PhotonView via the Editor.
A networked object may contain more than one PhotonView but for performance reasons we suggest to use only one.

By default, PUN instantiate uses the DefaultPool, which loads prefabs from "Resources" folders and Destroys the GameObject later on.
A more sophisticated IPunPrefabPool implementation can return objects to a pool in Destroy and re-use them in Instantiate.
In that case, the GameObjects are not truly created in Instantiate, which means that Start() is not being called by Unity in such a case.
Due to this, scripts on networked GameObjects should just implement OnEnable() and OnDisable().

To setup GameObjects when they got instantiated, you could also implement IPunInstantiateMagicCallback in a script.
PUN will check if the interface is implemented on a component and calls OnPhotonInstantiate(PhotonMessageInfo info) when the instance comes into use.
The PhotonMessageInfo parameter contains who instantiated the GameObject and when.

Note: Looking up IPunInstantiateMagicCallback implementations is a costly action, so PUN caches which prefabs are not making use of the interface and skips this lookup when this prefab is used again.

For example, you could setup the instantiated GameObject as a player's Tag object:

C#

void OnPhotonInstantiate(PhotonMessageInfo info)
{
    // e.g. store this gameobject as this player's charater in Player.TagObject
    info.sender.TagObject = this.GameObject;
}

Behind the scenes, PhotonNetwork.Instantiate stores an event on the server for players who join later on.

Lifetime of Networked Objects

By default, GameObjects created with PhotonNetwork.Instantiate exist as long as the creator is in the room.
When you swap rooms, objects don't carry over, just like when you switch a scene in Unity.

When a client leaves a room, the remaining players will destroy the GameObjects created by the leaving player.
If this doesn't fit your game logic, you can disable this: Set the RoomOptions.CleanupCacheOnLeave to false, when you create a room.

The Master Client can create GameObjects that have the lifetime of the room by using PhotonNetwork.InstantiateRoomObject().
Note: The object is not associated with the Master Client but the room.
By default, the Master Client controls these objects but you can pass on control with photonView.TransferOwnership().

You can also manually and explicitly destroy networked objects, see "PhotonNetwork.Destroy".

Networked Scene Objects

It is perfectly fine to place PhotonViews on objects in a scene.
They will be controlled by the Master Client by default and can be useful to have a "neutral" object to send room-related RPCs.

Important: When you load a scene with networked objects before being in a room, some PhotonView values are not useful yet.
For example: You can't check IsMine in Awake() when you're not in a room.

Switching Scenes

When you load a scene, Unity usually destroys all GameObjects currently in the hierarchy.
This includes networked objects, which can be confusing at times.

Example: In a menu scene, you join a room and load another.
You might actually arrive in the room a bit too early and get the initial messages of the room.
PUN begins to instantiate networked objects but your logic loads another scene and they are gone.

To avoid issues with loading scenes, you can set PhotonNetwork.AutomaticallySyncScene to true and use PhotonNetwork.LoadLevel() to switch scenes.

Read Timing for RPCs and Loading Levels

Custom Instantiation Data

You can send some initial custom data when the instantiation call.
Just make use of the last parameter in the PhotonNetwork.Instantiate* method.

This has two main advantages:

  • save traffic by avoiding extra messages: we don't have to use a separate RPC or RaiseEvent call for synchronizing this kind of information
  • timing: the data exchanged is available on the time of the prefab instantiation which could be useful to do some initialization

The instantiation data is an object array (object[]) of anything Photon can serialize.

Example:

Instantiate with custom data:

C#

object[] myCustomInitData = GetInitData();
PhotonNetwork.Instantiate("MyPrefabName", new Vector3(0, 0, 0), Quaternion.identity, 0, myCustomInitData);

Receive custom data:

C#

public void OnPhotonInstantiate(PhotonMessageInfo info)
{
    object[] instantiationData = info.photonView.InstantiationData;
    // ...
}

PhotonNetwork.Destroy

Usually, when you leave a room, the GameObjects get destroyed automatically.
See "Lifetime of Networked Objects".
However if you are connected and joined to a room and you want to "network-destroy" a GameObject created with a PhotonNetwork.Instantiate call, use PhotonNetwork.Destroy.
This includes:

  • Removal of the cached Instantiate event from the room on the server.
  • Removing RPCs buffered for PhotonViews that are in the hierarchy of the GameObject to be destroyed.
  • Sending a message to other clients to remove the GameObject also (affected by network lag).

In order for this to succeed, the GameObject to be destroyed must have the following conditions:

  • The GameObject is instantiated at runtime using PhotonNetwork.Instantiate* method call.
  • If the client is joined to an online room, the GameObject's PhotonView must be owned or controlled by the same client.
    GameObjects created with PhotonNetwork.InstantiateRoomObject can be destroyed only by the Master Client.
  • GameObjects can be destroyed locally if the client is not joined to a room or joined to an offline one.

Using the PrefabPool

By default, PUN uses a simple DefaultPool to instantiate and destroy GameObjects.
This uses the Resources folder to load prefabs and it will not pool objects that get destroyed (to simplify usage).
If either affects your game's performance negatively, it is time to set a custom PrefabPool.

A custom pool class must implement the IPunPrefabPool interface with just two methods:

GameObject Instantiate(string prefabId, Vector3 position, Quaternion rotation) gets an instance of a prefab.
It must return a valid, disabled GameObject with a PhotonView.

Destroy(GameObject gameObject) gets called to destroy (or just return) the instance of a prefab.
The GameObject is already disabled and the pool may reset and cache it for later use in Instantiate.

Note: When a custom IPunPrefabPool is used, PhotonNetwork.Instantiate is probably not creating the GameObject and (e.g.) Start() is not called.
Use OnEnable() and OnDisable() accordingly and disable any physical or other components that may still run otherwise.

Manual Instantiation

If you don't want to use PUN's built-in instantiation and pools, you can reimplement the behaviour with RPCs or RaiseEvent as shown in the following example.

You need to tell the remote clients which object to instantiate (prefab name) and how to identify it (ViewID).

The PhotonView.ViewID is the key to routing network messages to the correct GameObject/scripts.
If you instantiate manually, you have to allocate a new ViewID with PhotonNetwork.AllocateViewID() and send it along.
Everyone in the room has to set the same ID on the new object.

Keep in mind that a Manual Instantiation event needs to be buffered:
Clients that connect later have to receive the spawn instructions as well.

C#

public void SpawnPlayer()
{
    GameObject player = Instantiate(PlayerPrefab);
    PhotonView photonView = player.GetComponent<PhotonView>();

    if (PhotonNetwork.AllocateViewID(photonView))
    {
        object[] data = new object[]
        {
            player.transform.position, player.transform.rotation, photonView.ViewID
        };

        RaiseEventOptions raiseEventOptions = new RaiseEventOptions
        {
            Receivers = ReceiverGroup.Others,
            CachingOption = EventCaching.AddToRoomCache
        };

        SendOptions sendOptions = new SendOptions
        {
            Reliability = true
        };

        PhotonNetwork.RaiseEvent(CustomManualInstantiationEventCode, data, raiseEventOptions, sendOptions);
    }
    else
    {
        Debug.LogError("Failed to allocate a ViewId.");

        Destroy(player);
    }
}

We are instantiating the player prefab locally first.
This is necessary because we need the reference to the object's PhotonView component.
If we have successfully allocated an ID for the PhotonView, we are collecting all the data we want to send to the other clients and store them in an array of objects.
In this example we are sending the position and the rotation of the instantiated object and - most important - the allocated ViewID.
Afterwards we are creating the RaiseEventOptions and the SendOptions.
With the RaiseEventOptions we make sure, that this event is added to the room's cache and only send to the other clients, because we already have instantiated our object locally.
With the SendOptions we just define, that this event is sent reliable.
Finally we are using PhotonNetwork.RaiseEvent(...) to send our custom event to the server.
In this case we are using CustomManualInstantiationEventCode, which is simply a byte value representing this certain event.
If allocating an ID for the PhotonView fails, we log an error message and destroy the previously instantiated object.

Since we are using PhotonNetwork.RaiseEvent, we have to use an OnEvent callback handler.
Don't forget, to register it properly.
To see how this works, you can take a look at the RPCs and RaiseEvent documentation page.
In this example the OnEvent handler looks like this:

C#

public void OnEvent(EventData photonEvent)
{
    if (photonEvent.Code == CustomManualInstantiationEventCode)
    {
        object[] data = (object[]) photonEvent.CustomData;

        GameObject player = (GameObject) Instantiate(PlayerPrefab, (Vector3) data[0], (Quaternion) data[1]);
        PhotonView photonView = player.GetComponent<PhotonView>();
        photonView.ViewID = (int) data[2];
    }
}

Here we simply check, if the received event is our custom manual instantiation event.
If so, we instantiate the player prefab with the position and rotation information we have received.
Afterwards we get a reference to the object's PhotonView component and assign the ViewID we have received, too.

If you want to use asset bundles to load your network objects, all you have to do is to add your own asset bundle loading code and replace the PlayerPrefab from the example with the prefab from your asset bundle.

ViewID Limits

Most games will never need more than a few PhotonViews per player; one or two for the character and that's usually it.
If you need many more you might be doing something wrong.
For example, it is extremely inefficient to network instantiate and assign a PhotonView for every bullet that your weapon fires, instead keep track of your fired bullets via the player or weapon's PhotonView via RPCs.

By default, PUN supports up to 999 (PhotonNetwork.MAX_VIEW_IDS - 1) PhotonViews per player and a maximum theoritical number of players equal to 2,147,483.
You can easily allow for more PhotonViews per player.

This works as follows:

PhotonViews send out a viewID for every network message.
This viewID is an integer, composed of the actor number and the player's viewID.
The maximum size of an int is 2,147,483,647 (int.MaxValue), divided by our PhotonNetwork.MAX_VIEW_IDS (1000) that allows for over 2 million players, each having 1000 view IDs.

So for the default case of MAX_VIEW_IDS equal to 1000:

  • ViewIDs multiple of MAX_VIEW_IDS are not used: 0, 1000, 2000, etc. until 2,147,483,000.
  • ViewID 0 is reserved. It means that a viewID is not allocated yet.
  • Scene objects have viewIDs between 1 and 999.
  • Actor number 1's objects have viewIDs between 1001 and 1999.
  • Actor number x's objects have viewIDs between x * MAX_VIEW_IDS + 1 and (x + 1) * MAX_VIEW_IDS - 1.
  • Actor number 2,147,483's objects have viewIDs between 2,147,483,001 and 2,147,483,647. Only 647 possible values since we hit the int.MaxValue limit.

ViewIDs are reserved once allocated and are recycled once freed.
Meaning a viewID can be reused after network destroying its corresponding networked GameObject.

Back to top