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 Start()
Message which will indicated we've loaded the Arena, which means by our design that we are in a room.
Open
GameManager
ScriptIn the Public Fields region add the following variable
C#
[Tooltip("The prefab to use for representing the player")] public GameObject playerPrefab;
In the
Start()
method, add the followingC#
if (playerPrefab == null) { Debug.LogError("<Color=Red><a>Missing</a></Color> playerPrefab Reference. Please set it up in GameObject 'Game Manager'",this); } else { Debug.LogFormat("We are Instantiating LocalPlayer from {0}", 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); }
Save
GameManager
Script
This exposes a public field for you to reference the "Player" prefab.
It's convenient, because in this particular case we can drag and drop directly in the "GameManager" prefab, instead of in each scene, because the "Player" prefab is an asset, and so the reference will be kept intact (as opposed to referencing a GameObject in the hierarchy, which a prefab can only do when instantiated in the same scene).
Then, in Start()
, we instantiate it (after having checked that we have a "Player" prefab referenced properly).
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 and 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
Open
PlayerManager
scriptIn the "Public Fields" 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;
In the
Awake()
method, add the followingC#
// #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);
Save
PlayerManager
script
With these modifications, we can then implement the check to only instantiate if necessary inside the GameManager
script.
Open
GameManager
ScriptSurround the instantiation call with an
if
conditionC#
if (PlayerManager.LocalPlayerInstance == null) { Debug.LogFormat("We are Instantiating LocalPlayer from {0}", SceneManagerHelper.ActiveSceneName); // 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.LogFormat("Ignoring scene load for {0}", SceneManagerHelper.ActiveSceneName); }
Save
GameManager
Script
With this, we now only instantiate if the PlayerManager
doesn't have a reference to an existing instance of 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 leaves and the other players are near the border of the current arena size, they will simply find themselves outside the smaller arena when it will load, we need to take this into account, 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 5.3.7 to the latest). So we'll need different code based on the Unity version.
It's unrelated to Photon Unity Networking, but important to master for your projects to survive updates.
Open
PlayerManager
scriptAdd 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
At the end of the
Start()
method, add the following codeC#
#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
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); } }
Override
OnDisable
method as followsC#
#if UNITY_5_4_OR_NEWER public override void OnDisable() { // Always call the base to remove callbacks base.OnDisable (); UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; } #endif
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 a CalledOnLevelWasLoaded
method that will be called either from OnLevelWasLoaded
or from the SceneManager.sceneLoaded
callback.