一、前言
Photon Unity Networking (PUN)是一种用于多人游戏的Unity软件包。 灵活的匹配可以让玩家进入房间,可以通过网络同步对象。 快速和可靠的通信是通过专用的Photon 服务器完成的,因此客户端连接不需要1对1。
二、参考文章
1、【PUN】Photon Unity Networking(PUN)的简单使用
2、【Unity3D】 Photon多人游戏开发教程3、PUN介绍(干货)
4、Photon Unity Networking 案例(一)
5、Unity3D利用Photon实现实时联网对战(二)PUN SDK介绍
6、Photon Unity Networking基础教程 7 修改Player的联网版本
7、使用Photon Unity Networking开发多人网络游戏的基本思路(一):大厅与等待房间
三、正文
快速搭建
1.下载PUN插件,下载地址:doc.photonengine.com/en-us/pun/c…
会跳转到AssetStore商店:
或者直接在Unity里面Alt+9访问商店,然后搜索PUN插件
2.然后需要打开Photon的官网注册一个账号,dashboard.photonengine.com/Account/Sig…
复制App ID,到Unity项目中的Photon/PhotonUnityNetworking/Resources/PhotonServerSettings的 App Id Realtim
3.新建场景,新建一个Plane,和Cube,将Cube设成预制体,放到Resouces文件夹:
4.给Cube加上Photon View组件,如果要同步的话,这个组件是必须的
将Cube的Transform拖入Observed Components 5.新建脚本ClickFloor,将脚本付给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); } } } } 复制代码
6.新建脚本PhotonConnect.cs
using UnityEngine; using Photon.Pun;//导入Photon命名空间 using Photon.Realtime; public class PhotonConnect : MonoBehaviour { void Start() { //初始化版本号 PhotonNetwork.ConnectUsingSettings(); PhotonNetwork.GameVersion = "1"; } //按钮事件 创建房间 public void Btn_CreateRoom(string _roomName) { //设置房间属性 RoomOptions m_Room = new RoomOptions { IsOpen = true, IsVisible = true, MaxPlayers = 4 }; PhotonNetwork.CreateRoom(_roomName, m_Room); } //根据房间名加入房间 public void Btn_JoinRoom(string _roomName) { PhotonNetwork.JoinRoom(_roomName); } //随机加入已经创建的房间 public void Btn_JoinRandomRoom() { PhotonNetwork.JoinRandomRoom(); } void OnGUI() { //显示连接信息 GUILayout.Label(PhotonNetwork.NetworkClientState.ToString(),GUILayout.Width(300),GUILayout.Height(100)); } } 复制代码
7.将脚本付给Main Camera(任意一个场景中的对象就行),然后新建3个按钮,绑定事件:
8.Cube预制体Apply一下,然后从场景中删除,运行:
API解析
连接和回调
ConnectUsingSettings 建立连接
PhotonNetwork.ConnectUsingSettings(); 复制代码
PUN 使用回调让你知道客户什么时候建立了连接,加入了一个房间等等。
例如:IConnectionCallbacks.OnConnectedToMaster.
为了方便起见,可以继承MonoBehaviourPunCallbacks接口,它实现了重要的回调接口并自动注册自己,只需覆盖特定的回调方法
public class YourClass : MonoBehaviourPunCallbacks { public override void OnConnectedToMaster() { Debug.Log("Launcher: 连接到主客户端"); } } 复制代码
加入和创建房间
加入房间
PhotonNetwork.JoinRoom("someRoom"); 复制代码
加入存在的随机房间
PhotonNetwork.JoinRandomRoom(); 复制代码
创建房间
PhotonNetwork.CreateRoom("MyMatch"); 复制代码
如果想跟朋友一起玩,可以编一个房间名称,并使用JoinOrCreateRoom创建房间,将IsVisible 设为false,那么就只能使用房间名来加入(而不是随机加入创建的房间了)
RoomOptions roomOptions = new RoomOptions(); roomOptions.IsVisible = false; roomOptions.MaxPlayers = 4; PhotonNetwork.JoinOrCreateRoom(nameEveryFriendKnows, roomOptions, TypedLobby.Default); 复制代码
游戏逻辑
可以使用PhotonView组件将游戏对象实例化为“联网游戏对象”,它标识对象和所有者(或控制器)更新状态给其他人
需要添加一个PhotonView组件选择Observed组件并使用PhotonNetwork.Instantiate若要创建实例,请执行以下操作。
PhotonStream 负责写入(和读取)网络对象的状态,每秒钟几次,脚本需要继承接口IPunObservable,它定义了OnPhotonSerializeView。看起来是这样的:
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); } } 复制代码
远程过程调用
Remote Procedure Calls (RPC)使你可以调用”networked GameObjects”上的方法,对由用户输入等触发的不常用动作很有用。
一个RPC会被在同房间里的每个玩家在相同的游戏对象上被执行,所以你可以容易地触发整个场景效果就像你可以修改某些GameObject。
作为RPC被调用的方法必须在一个带PhotonView组件的游戏对象上。该方法自身必须要被[PunRPC]属性标记。
[PunRPC] void ChatMessage(string a, string b) { Debug.Log("ChatMessage " + a + " " + b); } 复制代码
要调用该方法,先访问到目标对象的PhotonView组件。而不是直接调用目标方法,调用PhotonView.RPC()并提供想要调用的方法名称:
PhotonView photonView = PhotonView.Get(this); photonView.RPC("ChatMessage", PhotonTargets.All, "jup", "and jup!"); 复制代码
回调函数
接口 | 解释 |
IConnectionCallbacks | 与连接相关的回调。 |
IInRoomCallbacks | 房间内发生的回调 |
ILobbyCallbacks | 与游戏大厅有关的回调。 |
IMatchmakingCallbacks | 与配对有关的回调 |
IOnEventCallback | 对接收到的事件进行一次回拨。这相当于C#事件OnEventReceived. |
IWebRpcCallback | 一个用于接收WebRPC操作响应的回调。 |
IPunInstantiateMagicCallback | 实例化双关预制板的单个回调。 |
IPunObservable | PhotonView序列化回调。 |
IPunOwnershipCallbacks | 双关所有权转让回调。 |
更多API参考:doc-api.photonengine.com/en/pun/v2/n…
四、案例
1.简单的多人游戏
1.新建Launcher.cs脚本
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 } } 复制代码
打开PhotonServerSettings:
2.扩展MonoBehaviourPunCallback 修改MonoBehaviour为MonoBehaviourPunCallbacks 加using Photon.Realtime;命名空间 添加以下两个方法:
public class Launcher : MonoBehaviourPunCallbacks { 复制代码
#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); } #endregion 复制代码
当我们有效地加入一个房间时,它将通知您的脚本:
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."); } 复制代码
新建字段:
/// <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; 复制代码
然后修改PhototonNetwork.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 { MaxPlayers = maxPlayersPerRoom }); 复制代码
3.UI界面搭建 开始按钮 新建一个Button,命名为Play Button,绑定事件Launcher.Connect() 打卡脚本Launcher.cs,移除Start()函数
4.玩家名字 创建PlayerNameInputField.cs脚本:
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 } } 复制代码
5.为玩家的名字创建UI 在场景中新建UI---InputField,添加事件On Value Change (String),拖动PlayerNameInputField附加到对象上,选择SetPlayerName方法
6.连接信息显示 使用“GameObject/UI/Panel”菜单创建UI面板,命名为Control Panel, 拖放Play Button和Name InputField在Control Panel 新建一个text用作信息显示,命名为Progress Label
[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; 复制代码
添加到Start()方法:
progressLabel.SetActive(false); controlPanel.SetActive(true); 复制代码
添加到Connect()方法:
progressLabel.SetActive(true); controlPanel.SetActive(false); 复制代码
添加到OnDisconnected()方法:
progressLabel.SetActive(false); controlPanel.SetActive(true); 复制代码
7.创建不同的场景 创建一个新场景,保存它并命名Room for 1 新建一个Plane,缩放到20,1,20 新建4个Cube:
Cube1
Cube2
Cube3
Cube4
8.新建c#脚本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 } } 复制代码
9.退出房间按钮 新建一个面板Top Panel,设置锚点
10.创造其他场景
2人场景:
Cube1:
Cube2:
Cube3:
Cube4:
3人场景: Cube1:
Cube2:
Cube3:
Cube4:
4人场景: Floor 比例尺:60,1,60 Cube1:
Cube2:
Cube3:
Cube4:
11.生成设置场景列表 File/Build Settings拖放所有场景
12.加载场景 打开GameManager.cs 添加新方法:
#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); } #endregion 复制代码
13.检测其他玩家的加入:
#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(); } } #endregion 复制代码
14.加入游戏大厅 将下列内容附加到OnJoinedRoom()方法
// #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"); } 复制代码
打开场景Launcher运行它。点击“Play”但如果你离开房间,你会注意到当你回到大厅时,它会自动重新加入要解决这个问题,我们可以修改Launcher.cs脚本
添加新属性:
/// <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; 复制代码
Connect()方法添加:
// 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(); 复制代码
结果:
public void Connect() { progressLabel.SetActive(true); controlPanel.SetActive(false); if (PhotonNetwork.IsConnected) { PhotonNetwork.JoinRandomRoom(); } else { isConnecting = PhotonNetwork.ConnectUsingSettings(); PhotonNetwork.GameVersion = gameVersion; } } 复制代码
OnConnectedToMaster()方法加入:
// 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; } 复制代码
15.玩家设置 模型在Assets\Photon\PhotonUnityNetworking\Demos\Shared Assets\Models Kyle Robot.fbx 新建一个空场景,拖入Kyle Robot.fbx进入场景,将模型拖入Resources文件夹,做成一个预制体:
双击My Kyle Robot修改碰撞器:
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 } } 复制代码
创建变量:
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); } 复制代码
动画管理员脚本:方向控制 编辑脚本PlayerAnimatorManager
#region Private Fields [SerializeField] private float directionDampTime = 0.25f; #endregion 复制代码
Update里面添加:
animator.SetFloat("Direction", h, directionDampTime, Time.deltaTime); 复制代码
动画管理员脚本:跳跃 编辑脚本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"); } } 复制代码
结果:
using UnityEngine; using System.Collections; namespace Com.MyCompany.MyGame { public class PlayerAnimatorManager : MonoBehaviour { #region Private Fields [SerializeField] private float directionDampTime = .25f; 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 } } 复制代码
摄像机设置 添加组件CameraWork到My Kyle Robot预制件
PhotonView组件 给模型添加一个PhotonView组件: 设置Observe Option到Unreliable On Change
增加武器射线 点击模型,打开层级列表,找到头部:
设置两个Cube为射线,然后父对象为Head:
控制射线: 创建一个新的脚本: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 } } 复制代码
生命值 打开PlayerManager剧本 增加一个公众Health属性
[Tooltip("The current Health of our player")] public float Health = 1f; 复制代码
以下两个方法添加到MonoBehaviour Callbacks区域。
/// <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; } 复制代码
在公共字段区域中添加此变量
public static GameManager Instance; 复制代码
Start()方法添加:
void Start() { Instance = this; } 复制代码
Update函数添加:
if (Health <= 0f) { GameManager.Instance.LeaveRoom(); } 复制代码
16.联网 Transform 同步
添加组件PhotonTransformView
动画同步 添加组件PhotonAnimatorView
if (photonView.IsMine == false && PhotonNetwork.IsConnected == true) { return; } 复制代码
17.摄像机控制 打开PlayerManager剧本
/// <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); } } 复制代码
禁用Follow on Start
18.开火控制
打开脚本PlayerManager
if (photonView.IsMine) { ProcessInputs (); } 复制代码
添加接口IPunObservable
public class PlayerManager : MonoBehaviourPunCallbacks, IPunObservable { #region IPunObservable implementation public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { } #endregion 复制代码
IPunObservable.OnPhotonSerializeView添加以下代码
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(); } 复制代码
将PlayerManager组件拖入PhotonView组件
19.生命值同步 打开脚本PlayerManager
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(); } 复制代码
20.实例化玩家
打开GameManager脚本 在公共字段区域中添加以下变量
[Tooltip("The prefab to use for representing the player")] public GameObject playerPrefab; 复制代码
在Start()方法,添加以下内容
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); } 复制代码
21.跟随玩家
打开PlayerManager脚本 在“公共字段”区域中,添加以下内容
[Tooltip("The local player instance. Use this to know if the local player is represented in the Scene")] public static GameObject LocalPlayerInstance; 复制代码
在Awake()方法,添加以下内容
// #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); 复制代码
将实例化调用包围在if条件
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); } 复制代码
22.管理场景外的玩家 打开PlayerManager脚本
#if UNITY_5_4_OR_NEWER void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode loadingMode) { this.CalledOnLevelWasLoaded(scene.buildIndex); } #endif 复制代码
在Start()方法,添加以下代码
#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 复制代码
在“MonoBehaviour回调”区域中添加以下两个方法
#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); } } 复制代码
覆盖OnDisable方法如下
#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 复制代码
23.玩家UI 血条和名字预设体 在场景中新建UI,Slider,锚点,中间位置,rect宽度80高度15,背景设置成红色,加一个CanvasGroup组件,设置Interactable和Blocks Raycast为false,拖入到Prefab文件夹,删除场景中的实例,我们不再需要它了
创建一个新的C#脚本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 } } 复制代码
添加属性:
private PlayerManager target; 复制代码
添加此公共方法
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; } } 复制代码
添加此方法
void Update() { // Reflect the Player Health if (playerHealthSlider != null) { playerHealthSlider.value = target.Health; } } 复制代码
24.实例化 打开脚本PlayerManager 添加一个公共字段以保存对Player UI预置的引用,如下所示:
[Tooltip("The Player's UI GameObject Prefab")] [SerializeField] public GameObject PlayerUiPrefab; 复制代码
将此代码添加到Start()方法
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); } 复制代码
将此添加到Update()功能
// 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; } 复制代码
将此代码添加到CalledOnLevelWasLoaded()方法
GameObject _uiGo = Instantiate(this.PlayerUiPrefab); _uiGo.SendMessage("SetTarget", this, SendMessageOptions.RequireReceiver); 复制代码
在“MonoBehaviour回调”区域中添加此方法
void Awake() { this.transform.SetParent(GameObject.Find("Canvas").GetComponent<Transform>(), false); } 复制代码
在“公共字段”区域中添加此公共属性
[Tooltip("Pixel offset from the player target")] [SerializeField] private Vector3 screenOffset = new Vector3(0f,30f,0f); 复制代码
将这四个字段添加到“私有字段”区域
float characterControllerHeight = 0f; Transform targetTransform; Renderer targetRenderer; CanvasGroup _canvasGroup; Vector3 targetPosition; 复制代码
将此添加到Awake方法域
_canvasGroup = this.GetComponent<CanvasGroup>(); 复制代码
将下列代码追加到SetTarget()后法_target已经设定好了。
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; } 复制代码
在“MonoBehaviour回调”区域中添加此公共方法
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; } } 复制代码
2.游戏大厅与等待房间
1.游戏大厅
// 用于连接Cloud并进入游戏大厅 PhotonNetwork.ConnectUsingSettings(string version) //进入游戏大厅的默认回调函数 void OnJoinedLobby() //显示连接日志 GUILayout.Label(PhotonNetwork.connectionStateDetailed.ToString()) 复制代码
2.创建等待房间
//设置房间属性并创建房间 RoomOptions ro = new RoomOptions(); ro.IsOpen = true;ro.IsVisible = true; //设置最大玩家数 为了简单就从2个人开始做起吧 可以随意设置 ro.MaxPlayers = 2; PhotonNetwork.CreateRoom(srting roomName, ro, TypedLobby.Default); //创建房间失败的回调函数 void OnPhotonCreateRoomFailed() 复制代码
3.加入等待房间
//随机加入房间 PhotonNetwork.JoinRandomRoom(); //随机进入房间失败(可能是因为没有空房间)的回调函数 //默认的回调函数一定不能眼花写错!!! void OnPhotonRandomJoinFailed() {可以调用PhotonNetwork.CreateRoom创建一个} //进入房间的回调函数 void OnJoinedRoom() { StartCoroutine(this.ChangeToWaitScene()); //写一个协程 当成功进入房间后就加载等待房间的场景 } IEnumerator ChangeToWaitScene() { //切换场景期间中断与photon服务器的网络信息传输 //(加载场景尚未完成的情况下 服务器传递的网络信息可能会引发不必要的错误) PhotonNetwork.isMessageQueueRunning = false; //加载场景 AsyncOperation ao = SceneManager.LoadSceneAsync("RoomForWait"); yield return ao; } 复制代码
加入房间的同时最好将玩家姓名PhotonNetwork.player.NickName读取或者设置,可以与PlayerPrefs连用实现数据的持久化。
4.房间列表的显示 UGUI里面的Grid Layout Group和 Horizontal Layout Group就是针对于这种情况设计的。我们可以将一个房间列表存储成一个预设,每次有新房间生成就生成一个预设。上面这俩组件可以帮助你把这些房间列表预设排列得整齐划一。
需要用到的prefab都要存在根目录下的Resources文件夹。硬性规定。
//只有在大厅里的房间有玩家进入的时候才会执行 接收房间列表
void OnReceivedRoomListUpdate() { //给单个房间列表的预设增加标签 GameObject[] a = GameObject.FindGameObjectsWithTag("OneRoom"); for (int i = 0; i < a.Length; i++) Destroy(a[i].gameObject); //每次接收房间列表前把旧的预设销毁 这样就能更新在线人数和房间总人数 //利用接收房间目录信息的函数生成单个列表预设 //PhotonNetwork.GetRoomList()可以获取房间列表里的房间数组 foreach (RoomInfo _room in PhotonNetwork.GetRoomList()) { //接收房间列表 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();//把数据都获取并设置好了就显示在面板上 } } 复制代码
至于roomData脚本里面存储的就是房间名、房间人数、最大容纳人数等基本信息,同时最好根据房间人数是否满来设置加入房间的按钮interactable= ture还是false。
这时候如果点击房间列表上的Join 就能进入房间了。
大致效果如下(那个NO.是我给房间用随机数字命名的房间名。场景中其实还有个输入玩家姓名的输入框,如果玩家没有输入姓名就自动随机给个数字当名称。)