Simplify online games! What about the PUN plug-in? (Unity3D)

Simplify online games! What about the PUN plug-in? (Unity3D)

Recommended reading

I. Introduction

Photon Unity Networking (PUN) is a Unity software package for multiplayer games. Flexible matching allows players to enter the room, and objects can be synchronized via the network. Fast and reliable communication is done through a dedicated Photon server, so client connections do not need to be one-to-one.

2. Reference articles

1. [PUN] Simple use of Photon Unity Networking (PUN)

2. [Unity3D] Photon multiplayer game development tutorial 3. PUN introduction (dry goods)

4. Photon Unity Networking case (1)

5. Unity3D uses Photon to realize real-time online battle (2) PUN SDK introduction

6. Photon Unity Networking basic tutorial 7 Modify the networked version of Player

7. The basic idea of using Photon Unity Networking to develop multiplayer online games (1): lobby and waiting room

3. the text

Quickly build

1. Download the PUN plug-in, the download address: doc.photonengine.com/en-us/pun/c... It will jump to the AssetStore store: Note that the version must be Unity2017.4.7 or higher. If it is the previous version, you can install PUN1 .0 version

Or directly visit the store with Alt+9 in Unity, and then search for the PUN plugin

2. Then you need to open Photon's official website to register an account, dashboard.photonengine.com/Account/Sig... After logging in, click to create a new APP: Type, if it is a chat room, you can choose Photon Chat, or you can choose Photon PUN. Copy the App ID to the App Id Realtim of Photon/PhotonUnityNetworking/Resources/PhotonServerSettings in the Unity project 3. Create a new scene, create a new Plane, and Cube, set the Cube as a prefab, and put it in the Resouces folder: 4. Add to the Cube Upload the Photon View component. If you want to synchronize, this component is necessary. Drag the Cube's Transform into Observed Components 5. Create a new script ClickFloor, and pay the script to Plane

using Photon.Pun; using UnityEngine; public class ClickFloor : MonoBehaviour { public GameObject m_Prefab; void Update () { if (Input.GetMouseButtonDown( 0 )) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { PhotonNetwork.Instantiate(m_Prefab.name, hit.point + new Vector3( 0 , 3 , 0 ), Quaternion.identity, 0 ); } } } } Copy code

6. Create a new script PhotonConnect.cs

using UnityEngine; using Photon.Pun; //Import the Photon namespace using Photon.Realtime; public class PhotonConnect : MonoBehaviour { void Start () { //Initialize the version number PhotonNetwork.ConnectUsingSettings(); PhotonNetwork.GameVersion = "1" ; } //Button event to create a room public void Btn_CreateRoom ( string _roomName ) { //Set room properties RoomOptions m_Room = new RoomOptions {IsOpen = true , IsVisible = true , MaxPlayers = 4 }; PhotonNetwork.CreateRoom(_roomName, m_Room); } //Join the room according to the room name public void Btn_JoinRoom ( string _roomName ) { PhotonNetwork.JoinRoom(_roomName); } //Randomly join an already created room public void Btn_JoinRandomRoom () { PhotonNetwork.JoinRandomRoom(); } void OnGUI () { //Display connection information GUILayout.Label(PhotonNetwork.NetworkClientState.ToString(),GUILayout.Width( 300 ),GUILayout.Height( 100 )); } } Copy code

7. Pay the script to the Main Camera (any object in the scene will do), and then create 3 buttons to bind events: 8. Cube prefabricated body Apply, then delete from the scene, run:

API analysis

Connection and callback

ConnectUsingSettings to establish a connection

PhotonNetwork.ConnectUsingSettings(); Copy code

PUN uses callbacks to let you know when a client has established a connection, joined a room, and so on.

For example: IConnectionCallbacks.OnConnectedToMaster.

For convenience, you can inherit the MonoBehaviourPunCallbacks interface, which implements the important callback interface and automatically registers itself, just overwrite the specific callback method

public class YourClass : MonoBehaviourPunCallbacks { public override void OnConnectedToMaster () { Debug.Log( "Launcher: Connect to the main client" ); } } Copy code

Join and create rooms

Join room

PhotonNetwork.JoinRoom( "someRoom" ); Copy code

Join a random room that exists

PhotonNetwork.JoinRandomRoom(); Copy code

Create a room

PhotonNetwork.CreateRoom( "MyMatch" ); Copy code

If you want to play with friends, you can edit a room name, and use JoinOrCreateRoom to create a room, set IsVisible to false, then you can only use the room name to join (instead of randomly joining the created room)

RoomOptions roomOptions = new RoomOptions(); roomOptions.IsVisible = false ; roomOptions.MaxPlayers = 4 ; PhotonNetwork.JoinOrCreateRoom(nameEveryFriendKnows, roomOptions, TypedLobby.Default); Copy code

Game logic

You can use the PhotonView component to instantiate the game object as a "networked game object", which identifies the object and the owner (or controller) to update the state to others

Need to add a PhotonView component, select Observed component and use PhotonNetwork.Instantiate To create an instance, please do the following.

PhotonStream is responsible for writing (and reading) the state of network objects. Several times per second, the script needs to inherit the interface IPunObservable, which defines OnPhotonSerializeView. It looks like this:

public void OnPhotonSerializeView ( PhotonStream stream, PhotonMessageInfo info ) { if (stream.IsWriting) { Vector3 pos = transform.localPosition; stream.Serialize( ref pos); } else { Vector3 pos = Vector3.zero; stream.Serialize( ref pos); } } Copy code

Remote procedure call

Remote Procedure Calls (RPC) allows you to call methods on "networked GameObjects", which is useful for infrequent actions triggered by user input, etc.

An RPC will be executed by each player in the same room on the same game object, so you can easily trigger the entire scene effect just like you can modify some GameObject.

The method called as an RPC must be on a game object with a PhotonView component. The method itself must be marked with the [PunRPC] attribute.

[ PunRPC ] void ChatMessage ( string a, string b ) { Debug.Log( "ChatMessage " + a + " " + b); } Copy code

To call this method, first visit the PhotonView component of the target object. Instead of calling the target method directly, call PhotonView.RPC() and provide the name of the method you want to call:

PhotonView photonView = PhotonView.Get( this ); photonView.RPC( "ChatMessage" , PhotonTargets.All, "jup" , "and jup!" ); Copy code

Callback

interfaceExplanation
IConnectionCallbacksCallbacks related to the connection.
IInRoomCallbacksCallbacks that occurred in the room
ILobbyCallbacksCallbacks related to the game lobby.
IMatchmakingCallbacksCallbacks related to pairing
IOnEventCallbackMake a callback to the received event. This is equivalent to the C# event OnEventReceived.
IWebRpcCallbackA callback for receiving WebRPC operation response.
IPunInstantiateMagicCallbackInstantiate a single callback of the pun prefab board.
IPunObservablePhotonView serialization callback.
IPunOwnershipCallbacksPun ownership transfer callback.

More API reference: doc-api.photonengine.com/en/pun/v2/n...

4. case

1. Simple multiplayer game

1. Create a new Launcher.cs script

using UnityEngine; using Photon.Pun; namespace Com.MyCompany.MyGame { public class Launcher : MonoBehaviour { # region Private Serializable Fields # endregion # region Private Fields /// <summary> /// This client's version number. Users are separated from each other by gameVersion (which allows you to make breaking changes). /// </summary> string gameVersion = "1" ; # endregion # region MonoBehaviour CallBacks /// <summary> /// MonoBehaviour method called on GameObject by Unity during early initialization phase. /// </summary> void Awake () { //#Critical //this makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically PhotonNetwork.AutomaticallySyncScene = true ; } /// <summary> /// MonoBehaviour method called on GameObject by Unity during initialization phase. /// </summary> void Start () { Connect(); } # endregion # region Public Methods /// <summary> /// Start the connection process. /// -If already connected, we attempt joining a random room /// -if not yet connected, Connect this application instance to Photon Cloud Network /// </summary> public void Connect () { //we check if we are connected or not, we join if we are, else we initiate the connection to the server. if (PhotonNetwork.IsConnected) { //#Critical we need at this point to attempt joining a Random Room. If it fails, we'll get notified in OnJoinRandomFailed() and we'll create one. PhotonNetwork.JoinRandomRoom(); } else { //#Critical, we must first and foremost connect to Photon Online Server. PhotonNetwork.ConnectUsingSettings(); PhotonNetwork.GameVersion = gameVersion; } } # endregion } } Copy code

Open PhotonServerSettings:

2. Extend MonoBehaviourPunCallback, modify MonoBehaviour to MonoBehaviourPunCallbacks plus using Photon.Realtime; add the following two methods to the namespace:

public class Launcher : MonoBehaviourPunCallbacks { Copy code
# region MonoBehaviourPunCallbacks Callbacks public override void OnConnectedToMaster () { Debug.Log( "PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN" ); PhotonNetwork.JoinRandomRoom(); } public override void OnDisconnected ( DisconnectCause cause ) { Debug.LogWarningFormat( "PUN Basics Tutorial/Launcher: OnDisconnected() was called by PUN with reason {0}" , cause); } # endregionCopy code

When we effectively join a room, it will notify your script:

public override void OnJoinRandomFailed ( short returnCode, string message ) { Debug.Log( "PUN Basics Tutorial/Launcher:OnJoinRandomFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom" ); //#Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room. PhotonNetwork.CreateRoom( null , new RoomOptions()); } public override void OnJoinedRoom () { Debug.Log( "PUN Basics Tutorial/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room." ); } Copy code

New field:

/// <summary> /// The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created. /// </summary> [ Tooltip ( "The maximum number of players per room. When a room is full, it can't be joined by new players, and so new room will be created" ) ] [ SerializeField ] private byte maxPlayersPerRoom = 4 ; copy the code

Then modify the PhototonNetwork.CreateRoom() call and use this new field

#Critical//: WE failed to the Join Random Room A, or Maybe none EXISTS They are All Full No worries, WE A new new Create Room.. PhotonNetwork.CreateRoom ( null , new new RoomOptions maxPlayersPerRoom} = {maxPlayers); duplicated code

3. Build a start button on the UI interface Create a new Button, name it Play Button, bind the event Launcher.Connect() to the script Launcher.cs, remove the Start() function

4. Create the PlayerNameInputField.cs script for the player name:

using UnityEngine; using UnityEngine.UI; using Photon.Pun; using Photon.Realtime; using System.Collections; namespace Com.MyCompany.MyGame { /// <summary> /// Player name input field. Let the user input his name, will appear above the player in the game. /// </summary> [ RequireComponent(typeof(InputField)) ] public class PlayerNameInputField : MonoBehaviour { # region Private Constants //Store the PlayerPref Key to avoid typos const string playerNamePrefKey = "PlayerName" ; # endregion # region MonoBehaviour CallBacks /// <summary> /// MonoBehaviour method called on GameObject by Unity during initialization phase. /// </summary> void Start () { string defaultName = string .Empty; InputField _inputField = this .GetComponent<InputField>(); if (_inputField!= null ) { if (PlayerPrefs.HasKey(playerNamePrefKey)) { defaultName = PlayerPrefs.GetString(playerNamePrefKey); _inputField.text = defaultName; } } PhotonNetwork.NickName = defaultName; } # endregion # region Public Methods /// <summary> /// Sets the name of the player, and save it in the PlayerPrefs for future sessions. /// </summary> /// <param name="value"> The name of the Player </param> public void SetPlayerName ( string value ) { //#Important if ( string .IsNullOrEmpty( value )) { Debug.LogError( "Player Name is null or empty" ); return ; } PhotonNetwork.NickName = value ; PlayerPrefs.SetString(playerNamePrefKey, value ); } # endregion } } Copy code

5. Create a UI for the player s name. Create a new UI---InputField in the scene, add the event On Value Change (String), drag the PlayerNameInputField to attach to the object, and select the SetPlayerName method. 6. Use "GameObject/UI/Panel for connection information "Menu to create a UI panel, named Control Panel, drag and drop Play Button and Name InputField in the Control Panel to create a new text for information display, named Progress Label 7. Open the Launcher.cs script and add the following two properties

[ Tooltip( "The Ui Panel to let the user enter name, connect and play" ) ] [ SerializeField ] private GameObject controlPanel; [ Tooltip( "The UI Label to inform the user that the connection is in progress" ) ] [ SerializeField ] private GameObject progressLabel; copy code

Added to the Start() method:

progressLabel.SetActive( false ); controlPanel.SetActive( true ); Copy code

Added to the Connect() method:

progressLabel.SetActive( true ); controlPanel.SetActive( false ); Copy code

Added to the OnDisconnected() method:

progressLabel.SetActive( false ); controlPanel.SetActive( true ); Copy code

7. Create a different scene Create a new scene, save it and name it Room for 1. Create a new Plane, zoom to 20, 1, 20, create 4 cubes:

Cube1

Cube2

Cube3

Cube4

8. New c# script GameManager.cs

using System; using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; using Photon.Pun; using Photon.Realtime; namespace Com.MyCompany.MyGame { public class GameManager : MonoBehaviourPunCallbacks { # region Photon Callbacks /// <summary> /// Called when the local player left the room. We need to load the launcher scene. /// </summary> public override void OnLeftRoom () { SceneManager.LoadScene( 0 ); } # endregion # region Public Methods public void LeaveRoom () { PhotonNetwork.LeaveRoom(); } # endregion } } Copy code

9. Exit the room button Create a new panel Top Panel, set the anchor point to add an exit button, named Leave Button, and bind the event Game Manager's LeaveRoom()

10. Create other scenes

2-person scene:

Cube1:

Cube2:

Cube3:

Cube4:

3-player scene: Cube1:

Cube2:

Cube3:

Cube4:

4-person scene: Floor Scale: 60, 1, 60 Cube1:

Cube2:

Cube3:

Cube4:

11. Generate a list of settings scene File/Build Settings Drag and drop all scenes

12. Load the scene and open GameManager.cs to add a new method:

# region Private Methods void LoadArena () { if (!PhotonNetwork.IsMasterClient) { Debug.LogError( "PhotonNetwork: Trying to Load a level but we are not the master Client" ); } Debug.LogFormat( "PhotonNetwork: Loading Level: {0}" , PhotonNetwork.CurrentRoom.PlayerCount); PhotonNetwork.LoadLevel( "Room for " + PhotonNetwork.CurrentRoom.PlayerCount); } # endregionCopy code

13. Detect the joining of other players:

# region Photon Callbacks public override void OnPlayerEnteredRoom ( Player other ) { Debug.LogFormat( "OnPlayerEnteredRoom() {0}" , other.NickName); //not seen if you're the player connecting if (PhotonNetwork.IsMasterClient) { Debug.LogFormat( "OnPlayerEnteredRoom IsMasterClient {0}" , PhotonNetwork.IsMasterClient); //called before OnPlayerLeftRoom LoadArena(); } } public override void OnPlayerLeftRoom ( Player other ) { Debug.LogFormat( "OnPlayerLeftRoom() {0}" , other.NickName); //seen when other disconnects if (PhotonNetwork.IsMasterClient) { Debug.LogFormat( "OnPlayerLeftRoom IsMasterClient {0}" , PhotonNetwork.IsMasterClient); //called before OnPlayerLeftRoom LoadArena(); } } # endregionCopy code

14. Join the game lobby and append the following to the OnJoinedRoom() method

//#Critical: We only load if we are the first player, else we rely on `PhotonNetwork.AutomaticallySyncScene` to sync our instance scene. if (PhotonNetwork.CurrentRoom.PlayerCount == 1 ) { Debug.Log( "We load the'Room for 1'" ); //#Critical //Load the Room Level. PhotonNetwork.LoadLevel( "Room for 1" ); } Copy code

Open the scene Launcher and run it. Click "Play" but if you leave the room, you will notice that when you return to the lobby, it will automatically rejoin. To solve this problem, we can modify the Launcher.cs script

Add new attributes:

/// <summary> /// Keep track of the current process. Since connection is asynchronous and is based on several callbacks from Photon, /// we need to keep track of this to properly adjust the behavior when we receive call back by Photon. /// Typically this is used for the OnConnectedToMaster() callback. /// </summary> bool isConnecting; copy code

The Connect() method is added:

//keep track of the will to join a room, because when we come back from the game we will get a callback that we are connected, so we need to know what to do then isConnecting = PhotonNetwork.ConnectUsingSettings(); Copy code

result:

public void Connect () { progressLabel.SetActive( true ); controlPanel.SetActive( false ); if (PhotonNetwork.IsConnected) { PhotonNetwork.JoinRandomRoom(); } else { isConnecting = PhotonNetwork.ConnectUsingSettings(); PhotonNetwork.GameVersion = gameVersion; } } Copy code

The OnConnectedToMaster() method was added:

//we don't want to do anything if we are not attempting to join a room. //this case where isConnecting is false is typically when you lost or quit the game, when this level is loaded, OnConnectedToMaster will be called, in that case //we don't want to do anything. if (isConnecting) { //#Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed() PhotonNetwork.JoinRandomRoom(); isConnecting = false ; } Copy code

15. The player sets the model in Assets\Photon\PhotonUnityNetworking\Demos\Shared Assets\Models Kyle Robot.fbx to create a new empty scene, drag into Kyle Robot.fbx into the scene, drag the model into the Resources folder to make a prefab:

Double-click My Kyle Robot to modify the collider: To set up the animation with this Kyle Robot our controller prefab, just set the Kyle Robot of the animation component that the Controller will point to. Use the controller parameters to create a new PlayerAnimatorManager.cs script:

using UnityEngine; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerAnimatorManager : MonoBehaviour { # region MonoBehaviour Callbacks //Use this for initialization void Start () { } //Update is called once per frame void Update () { } # endregion } } Copy code

Create variables:

private Animator animator; //Use this for initialization void Start () { animator = GetComponent<Animator>(); if (!animator) { Debug.LogError( "PlayerAnimatorManager is Missing Animator Component" , this ); } } //Update is called once per frame void Update () { if (!animator) { return ; } float h = Input.GetAxis( "Horizontal" ); float v = Input.GetAxis( "Vertical" ); if (v < 0 ) { v = 0 ; } animator.SetFloat( "Speed" , h * h + v * v); } Copy code

Animation manager script: direction control editing script PlayerAnimatorManager

# region Private Fields [ SerializeField ] private float directionDampTime = 0.25f ; # endregionCopy code

Added in Update:

animator.SetFloat( "Direction" , h, directionDampTime, Time.deltaTime); Copy code

Animation manager script: Jump editing script PlayerAnimatorManager

//deal with Jumping AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo( 0 ); //only allow jumping if we are running. if (stateInfo.IsName( "Base Layer.Run" )) { //When using trigger parameter if (Input.GetButtonDown( "Fire2" )) { animator.SetTrigger( "Jump" ); } } Copy code

result:

using UnityEngine; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerAnimatorManager : MonoBehaviour { # region Private Fields [ SerializeField ] private float directionDampTime = .25 f; private Animator animator; # endregion # region MonoBehaviour CallBacks //Use this for initialization void Start () { animator = GetComponent<Animator>(); if (!animator) { Debug.LogError( "PlayerAnimatorManager is Missing Animator Component" , this ); } } //Update is called once per frame void Update () { if (!animator) { return ; } //deal with Jumping AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo( 0 ); //only allow jumping if we are running. if (stateInfo.IsName( "Base Layer.Run" )) { //When using trigger parameter if (Input.GetButtonDown( "Fire2" )) { animator.SetTrigger( "Jump" ); } } float h = Input.GetAxis( "Horizontal" ); float v = Input.GetAxis( "Vertical" ); if (v < 0 ) { v = 0 ; } animator.SetFloat( "Speed" , h * h + v * v); animator.SetFloat( "Direction" , h, directionDampTime, Time.deltaTime); } # endregion } } Copy code

Camera settings add component CameraWork to My Kyle Robot prefab

PhotonView component Add a PhotonView component to the model: Set Observe Option to Unreliable On Change

Add weapon ray Click the model, open the hierarchical list, and find the head: Set two Cubes as rays, and then the parent object is Head:

Control ray: Create a new script: PlayerManager.cs

using UnityEngine; using UnityEngine.EventSystems; using Photon.Pun; using System.Collections; namespace Com.MyCompany.MyGame { /// <summary> /// Player manager. /// Handles fire Input and Beams. /// </summary> public class PlayerManager : MonoBehaviourPunCallbacks { # region Private Fields [ Tooltip( "The Beams GameObject to control" ) ] [ SerializeField ] private GameObject beams; //True, when the user is firing bool IsFiring; # endregion # region MonoBehaviour CallBacks /// <summary> /// MonoBehaviour method called on GameObject by Unity during early initialization phase. /// </summary> void Awake () { if (beams == null ) { Debug.LogError( "<Color=Red><a>Missing</a></Color> Beams Reference." , this ); } else { beams.SetActive( false ); } } /// <summary> /// MonoBehaviour method called on GameObject by Unity on every frame. /// </summary> void Update () { ProcessInputs(); //trigger Beams active state if (beams != null && IsFiring != beams.activeInHierarchy) { beams.SetActive(IsFiring); } } # endregion # region Custom /// <summary> /// Processes the inputs. Maintain a flag representing when the user is pressing Fire. /// </summary> void ProcessInputs () { if (Input.GetButtonDown( "Fire1" )) { if (!IsFiring) { IsFiring = true ; } } if (Input.GetButtonUp( "Fire1" )) { if (IsFiring) { IsFiring = false ; } } } # endregion } } Copy code

Health value opens the PlayerManager script to add a public Health attribute

[ Tooltip( "The current Health of our player" ) ] public float Health = 1f ; Copy code

The following two methods are added to the MonoBehaviour Callbacks area.

/// <summary> /// MonoBehaviour method called when the Collider'other' enters the trigger. /// Affect Health of the Player if the collider is a beam /// Note: when jumping and firing at the same, you 'll find that the player's own beam intersects with itself /// One could move the collider further away to prevent this or check if the beam belongs to the player. /// </summary> void OnTriggerEnter ( Collider other ) { if (!photonView.IsMine) { return ; } //We are only interested in Beamers //we should be using tags but for the sake of distribution, let's simply check by name. if (!other.name.Contains( "Beam" )) { return ; } Health -= 0.1f ; } /// <summary> /// MonoBehaviour method called once per frame for every Collider'other' that is touching the trigger. /// We're going to affect health while the beams are touching the player /// </summary > /// <param name="other"> Other. </param> void OnTriggerStay ( Collider other ) { //we dont' do anything if we are not the local player. if (! photonView.IsMine) { return ; } //We are only interested in Beamers //we should be using tags but for the sake of distribution, let's simply check by name. if (!other.name.Contains( "Beam" )) { return ; } //we slowly affect health when beam is constantly hitting us, so player has to move to prevent death. Health -= 0.1f *Time.deltaTime; } Copy code

Add this variable in the public field area

public static GameManager Instance; copy code

Start() method is added:

void Start () { Instance = this ; } Copy code

Update function added:

if (Health <= 0f ) { GameManager.Instance.LeaveRoom(); } Copy code

16. Networking Transform synchronization

Add the component PhotonTransformView

Animation synchronization add component PhotonAnimatorView

16. User input management opens PlayerAnimatorManager.cs Update to add

if (photonView.IsMine == false && PhotonNetwork.IsConnected == true ) { return ; } Copy code

17. Camera control opens the PlayerManager script

/// <summary> /// MonoBehaviour method called on GameObject by Unity during initialization phase. /// </summary> void Start () { CameraWork _cameraWork = this .gameObject.GetComponent<CameraWork>(); if (_cameraWork != null ) { if (photonView.IsMine) { _cameraWork.OnStartFollowing(); } } else { Debug.LogError( "<Color=Red><a>Missing</a></Color> CameraWork Component on playerPrefab." , this ); } } Copy code

Disable Follow on Start

18. Fire control

Open the script PlayerManager

if (photonView.IsMine) { ProcessInputs (); } Copy code

Add interface IPunObservable

public class PlayerManager : MonoBehaviourPunCallbacks , IPunObservable { # region IPunObservable implementation public void OnPhotonSerializeView ( PhotonStream stream, PhotonMessageInfo info ) { } # endregionCopy code

IPunObservable.OnPhotonSerializeView add the following code

if (stream.IsWriting) { //We own this player: send the others our data stream.SendNext(IsFiring); } else { //Network player, receive data this .IsFiring = ( bool )stream.ReceiveNext(); } Copy code

Drag the PlayerManager component into the PhotonView component

19. Open the PlayerManager script synchronously with the value of life

if (stream.IsWriting) { //We own this player: send the others our data stream.SendNext(IsFiring); stream.SendNext(Health); } else { //Network player, receive data this .IsFiring = ( bool )stream.ReceiveNext(); this .Health = ( float )stream.ReceiveNext(); } Copy code

20. Instantiate the player

Open the GameManager script and add the following variables in the public field area

[ Tooltip( "The prefab to use for representing the player" ) ] public GameObject playerPrefab; Copy code

In the Start() method, add the following

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 ); } Copy code

21. Follow the player

Open the PlayerManager script in the "Public Fields" area and add the following

[ Tooltip( "The local player instance. Use this to know if the local player is represented in the Scene" ) ] public static GameObject LocalPlayerInstance; Copy code

In the Awake() method, add the following

//#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// //In Flag AS WE do the destroy ON Not Load Level Synchronization SO that survives instance, THUS A Seamless Giving preference Experience Levels When Load. DontDestroyOnLoad ( the this .gameObject); duplicated code

Surround the instantiation call in the if condition

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); } Copy code

22. Manage players outside the scene to open the PlayerManager script

# if UNITY_5_4_OR_NEWER void OnSceneLoaded ( UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode loadingMode ) { this .CalledOnLevelWasLoaded(scene.buildIndex); } # endifCopy code

In the Start() method, add the following code

# 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; # endifCopy code

Add the following two methods in the "MonoBehaviour Callback" area

# 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 ); } } Copy code

Override the OnDisable method as follows

# if UNITY_5_4_OR_NEWER public override void OnDisable () { //Always call the base to remove callbacks base .OnDisable (); UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; } # endifCopy code

23. Player UI Blood Bar and Name Preset Create a new UI in the scene, Slider, anchor point, middle position, rect width 80 height 15, background set to red, add a CanvasGroup component, set Interactable and Blocks Raycast to false, drag Enter the Prefab folder, delete the instance in the scene, we don t need it anymore

Create a new C# script PlayerUI.cs

using UnityEngine; using UnityEngine.UI; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerUI : MonoBehaviour { # region Private Fields [ Tooltip( "UI Text to display Player's Name" ) ] [ SerializeField ] private Text playerNameText; [ Tooltip( "UI Slider to display Player's Health" ) ] [ SerializeField ] private Slider playerHealthSlider; # endregion # region MonoBehaviour Callbacks # endregion # region Public Methods # endregion } } Copy code

Add attributes:

private PlayerManager target; copy code

Add this public method

public void SetTarget ( PlayerManager _target ) { if (_target == null ) { Debug.LogError( "<Color=Red><a>Missing</a></Color> PlayMakerManager target for PlayerUI.SetTarget." , this ); return ; } //Cache references for efficiency target = _target; if (playerNameText != null ) { playerNameText.text = target.photonView.Owner.NickName; } } Copy code

Add this method

void Update () { //Reflect the Player Health if (playerHealthSlider != null ) { playerHealthSlider. value = target.Health; } } Copy code

24. Instantiate and open the script PlayerManager to add a public field to save a reference to the Player UI preset, as shown below:

[ Tooltip( "The Player's UI GameObject Prefab" ) ] [ SerializeField ] public GameObject PlayerUiPrefab; Copy code

Add this code to the Start() method

if (PlayerUiPrefab != null ) { GameObject _uiGo = Instantiate(PlayerUiPrefab); _uiGo.SendMessage ( "SetTarget" , this , SendMessageOptions.RequireReceiver); } else { Debug.LogWarning( "<Color=Red><a>Missing</a></Color> PlayerUiPrefab reference on player Prefab." , this ); } Copy code

Add this to the Update() function

//Destroy itself if the target is null, It's a fail safe when Photon is destroying Instances of a Player over the network if (target == null ) { Destroy( this .gameObject); return ; } Copy code

Add this code to the CalledOnLevelWasLoaded() method

GameObject _uiGo = Instantiate( this .PlayerUiPrefab); _uiGo.SendMessage( "SetTarget" , this , SendMessageOptions.RequireReceiver); Copy code

Add this method in the "MonoBehaviour Callback" area

void Awake () { this .transform.SetParent(GameObject.Find( "Canvas" ).GetComponent<Transform>(), false ); } Copy code

Add this public attribute in the "Public Fields" area

[ Tooltip( "Pixel offset from the player target" ) ] [ SerializeField ] Private Vector3 screenOffset = new new Vector3 ( 0F , 30f , 0F ); duplicated code

Add these four fields to the "Private Fields" area

float characterControllerHeight = 0f ; Transform targetTransform; Renderer targetRenderer; CanvasGroup _canvasGroup; Vector3 targetPosition; Copy code

Add this to the Awake method domain

_canvasGroup = this .GetComponent<CanvasGroup>(); Copy code

After appending the following code to SetTarget(), the method _target has been set.

targetTransform = this .target.GetComponent<Transform>(); targetRenderer = this .target.GetComponent<Renderer>(); CharacterController characterController = _target.GetComponent<CharacterController> (); //Get data from the Player that won't change during the lifetime of this Component if (characterController != null ) { characterControllerHeight = characterController.height; } Copy code

Add this public method in the "MonoBehaviour Callback" area

void LateUpdate () { //Do not show the UI if we are not visible to the camera, thus avoid potential bugs with seeing the UI, but not the player itself. if (targetRenderer!= null ) { this ._canvasGroup.alpha = targetRenderer.isVisible? 1f : 0f ; } //#Critical //Follow the Target GameObject on screen. if (targetTransform != null ) { targetPosition = targetTransform.position; targetPosition.y += characterControllerHeight; this .transform.position = Camera.main.WorldToScreenPoint (targetPosition) + screenOffset; } } Copy code

2. Game lobby and waiting room

1. Game lobby

//Used to connect to Cloud and enter the game lobby PhotonNetwork.ConnectUsingSettings( string version) //The default callback function for entering the game lobby void OnJoinedLobby () //Display connection log GUILayout. Label ( PhotonNetwork.connectionStateDetailed.ToString( )) Copy code

2. Create a waiting room

//Set room properties and create a room RoomOptions ro = new RoomOptions(); ro.IsOpen = true ; ro.IsVisible = true ; //Set the maximum number of players for simplicity, start with 2 people, you can set it at will ro.MaxPlayers = 2 ; PhotonNetwork.CreateRoom(srting roomName, ro, TypedLobby.Default); //Callback function for failed room creation void OnPhotonCreateRoomFailed () Copy code

3. Join the waiting room

//Randomly join the room PhotonNetwork.JoinRandomRoom(); //Randomly failed to enter the room (maybe because there is no empty room) callback function //The default callback function must not be dazzled! ! ! void OnPhotonRandomJoinFailed () {You can call PhotonNetwork.CreateRoom to create one} //Callback function to enter the room void OnJoinedRoom () { StartCoroutine( this .ChangeToWaitScene()); //Write a coroutine and load the waiting room scene after successfully entering the room } IEnumerator ChangeToWaitScene () { //Interrupt the network information transmission with the photon server during the scene switching //( The network information transmitted by the server may cause unnecessary errors when the loading scene has not been completed) PhotonNetwork.isMessageQueueRunning = false ; //Loading the scene AsyncOperation ao = SceneManager .LoadSceneAsync( "RoomForWait" ); yield return ao; } Copy code

It is best to read or set the player name PhotonNetwork.player.NickName when joining the room, which can be used in conjunction with PlayerPrefs to achieve data persistence.

4. The display of the room list The Grid Layout Group and Horizontal Layout Group in UGUI are designed for this situation. We can store a room list as a preset, and generate a preset every time a new room is generated. The above two components can help you arrange these room lists neatly and uniformly by default.

All prefabs that need to be used must be stored in the Resources folder under the root directory. Hard and fast rules.

//The receiving room list will only be executed when a player enters the room in the lobby

void OnReceivedRoomListUpdate () { //Add a tag GameObject[] a = GameObject.FindGameObjectsWithTag( " OneRoom " ); for ( int i = 0 ; i <a.Length; i++) to Destroy(a[i].gameObject); //Destroy the old preset before receiving the room list every time so that the number of people online and the total number of people in the room can be updated.//Use the function of receiving room directory information to generate a single list preset //PhotonNetwork.GetRoomList() can get the room list The room array foreach (RoomInfo _room in PhotonNetwork.GetRoomList()) { //Receive room list GameObject room = (GameObject)Instantiate(OneRoom); room.transform.SetParent(RoomList.transform, false ); roomData rd = room.GetComponent<roomData>(); rd.roomName = _room.Name; rd.connectPlayer = _room.PlayerCount; rd.maxPlayers = _room.MaxPlayers; rd.DisplayRoomData(); //Get all the data and set it up and display it on the panel } } Copy code

As for the basic information stored in the roomData script, such as the room name, the number of people in the room, and the maximum occupancy, it is best to set the button to join the room interactable = true or false according to whether the room is full.

At this time, if you click Join on the room list, you can enter the room.

The general effect is as follows (the NO. is the room name I named the room with a random number. There is actually an input box for entering the player's name in the scene. If the player does not enter the name, it will automatically give a random number as the name.)