This document is about: PUN 1
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.

8 - Player Instantiation

This section will cover "Player" prefab instantiation over the network and implement the various features needed to accommodate automatic scenes switches while playing.

Instantiating the Player

It's actually very easy to instantiate our "Player" prefab. We need to instantiate it when we've just entered the room and we can rely on the GameManager script's Start() method indicating that we've loaded the Arena, which means by our design that we are in a room.

  1. Open GameManager Script

  2. In the Public Variables region add the following variable

    C#

    [Tooltip("The prefab to use for representing the player")]
    public GameObject playerPrefab;
    
  3. In the Start() method, add the following and save GameManager Script

    C#

    if (playerPrefab == null) 
    {
        Debug.LogError("<Color=Red><a>Missing</a></Color> playerPrefab Reference. Please set it up in GameObject 'Game Manager'",this);
    } 
    else 
    {
        Debug.Log("We are Instantiating LocalPlayer from "+Application.loadedLevelName);
    // we're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(this.playerPrefab.name, new Vector3(0f,5f,0f), Quaternion.identity, 0);
    }
    
  4. Save GameManager Script

This exposes a public field for you to reference the "Player" prefab. It's convenient, because in this particular we can drag and drop directly in the "GameManager" prefab, instead of in each scenes, because the "Player" prefab is an asset and so the reference will be kept intact (as opposed to referencing a GameObject in a hierarchy, which a Prefab can only do when instantiated in the same scene).

WARNING: Always make sure Prefabs that are supposed to be instantiated over the network are within a Resources folder, this is a Photon requirement.
gamemanager inspector
GameManager Inspector

Then, on Start(), we do instantiate it (after having properly check we have a "Player" prefab referenced)

Notice that we instantiate well above the floor (5 units above while the player is only 2 units high). This is one way amongst many other to prevent collisions when new players join the room, players could be already moving around the center of the arena and so it avoids abrupt collisions. A "falling" player is also a nice and clean indication of an introduction of a new entity in the game.

However, this is not enough for our case, we have a twist :) When other players will join in, different scenes will be loaded and we want to keep consistency and not destroy existing players just because one of them left. So we need to tell Unity to not destroy the instance we created, which in turn implies we need to now check if instantiation is required when a scene is loaded.

Keeping Track Of The Player Instance

  1. Open PlayerManager Script

  2. In the "Public Variables" Region, add the following

    C#

    [Tooltip("The local player instance. Use this to know if the local player is represented in the Scene")]
    public static GameObject LocalPlayerInstance;
    
  3. In the Awake() method, add the following

    C#

    // #Important
    // used in GameManager.cs: we keep track of the localPlayer instance to prevent instantiation when levels are synchronized
    if ( photonView.isMine)
    {
        PlayerManager.LocalPlayerInstance = this.gameObject;
    }
    // #Critical
    // we flag as don't destroy on load so that instance survives level synchronization, thus giving a seamless experience when levels load.
    DontDestroyOnLoad(this.gameObject);
    
  4. Save PlayerManager script

With these modifications, we can then implement inside the GameManager Script the check to only instantiate if necessary.

  1. Open GameManager script

  2. Surround the instantiation call with an if statement

    C#

    if (PlayerManager.LocalPlayerInstance==null)
    {
        Debug.Log("We are Instantiating LocalPlayer from "+Application.loadedLevelName);
        // we're in a room. spawn a character for the local player. it gets synced by using PhotonNetwork.Instantiate
        PhotonNetwork.Instantiate(this.playerPrefab.name, new Vector3(0f,5f,0f), Quaternion.identity, 0);
    }
    else
    {
        Debug.Log("Ignoring scene load for "+Application.loadedLevelName);
    }
    
  3. Save GameManager script

With this, we now only instantiate if the PlayerManager doesn't have a reference to an existing instance of the localPlayer.

Manage Player Position When Outside The Arena

We have one more thing to watch out for. The size of the Arena is changing based on the number of players, which means that there is a case where if one player leave and the other players are near the limits of the current arena size, they will simply find themselves outside the smaller arena when it will load, we need to account for this and simply reposition the player back to the center of the arena in this case. It's something that is an issue in your gameplay and level design specifically.

There is currently an added complexity because Unity has revamped "Scene Management" and Unity 5.4 has deprecated some callbacks, which makes it slightly more complex to create a code that works across all Unity versions (from Unity 4.7 to the latest). So we'll need different code based on the Unity version. It's unrelated to Photon Networking but however important to master for your projects to survive updates.

  1. Open PlayerManager script

  2. Add a new method inside "Private Methods" region:

    C#

    #if UNITY_5_4_OR_NEWER
    void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode loadingMode)
    {
        this.CalledOnLevelWasLoaded(scene.buildIndex);
    }
    #endif
    
  3. At the end of the Start() method, add the following code

    C#

    #if UNITY_5_4_OR_NEWER
    // Unity 5.4 has a new scene management. register a method to call CalledOnLevelWasLoaded.
    UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
    #endif
    
  4. Add the following two methods inside the "MonoBehaviour Callbacks" region

    C#

    #if !UNITY_5_4_OR_NEWER
    /// <summary>See CalledOnLevelWasLoaded. Outdated in Unity 5.4.</summary>
    void OnLevelWasLoaded(int level)
    {
        this.CalledOnLevelWasLoaded(level);
    }
    #endif
    
    void CalledOnLevelWasLoaded(int level)
    {
        // check if we are outside the Arena and if it's the case, spawn around the center of the arena in a safe zone
        if (!Physics.Raycast(transform.position, -Vector3.up, 5f))
        {
            transform.position = new Vector3(0f, 5f, 0f);
        }
    }
    
  5. Add OnDisable method as follows

    C#

    #if UNITY_5_4_OR_NEWER
    private void OnDisable()
    {
        UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
    }
    #endif
    
  6. Save PlayerManager script

What this new code does is watching for a level being loaded and raycast downwards the current player's position to see if we hit anything. If we don't, this is means we are not above the arena's ground and we need to be repositioned back to the center, exactly like when we are entering the room for the first time.

If you are on a Unity version lower than Unity 5.4, we'll use Unity's callback OnLevelWasLoaded. If you are on Unity 5.4 or up, OnLevelWasLoaded is not available anymore, instead you have to use the new SceneManagement system. Finally, to avoid duplicating code, we simply have CalledOnLevelWasLoaded method that will be called either from OnLevelWasLoaded or from the SceneManager.sceneLoaded callback.

Next Part.
Previous Part.

Back to top