推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客
  • QQ群:1040082875

一、前言

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插件,下载地址:https://doc.photonengine.com/en-us/pun/current/getting-started/pun-intro

会跳转到AssetStore商店:

需要注意的是版本要Unity2017.4.7以上,如果是以前的版本,可以安装PUN1.0版本

或者直接在Unity里面Alt+9访问商店,然后搜索PUN插件

2.然后需要打开Photon的官网注册一个账号,https://dashboard.photonengine.com/Account/SignIn?ReturnUrl=%2fzh-CN%2fpubliccloud

登录以后,点击新建一个APP:

类型的话,如果是聊天室可以选择Photon Chat,普通的选择Photon PUN就可以了

复制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参考:
https://doc-api.photonengine.com/en/pun/v2/namespace_photon_1_1_pun.html

四、案例

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 automaticallyPhotonNetwork.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 Callbackspublic 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 typosconst 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){// #Importantif (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

7.打开Launcher.cs脚本
添加以下两个属性

[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 Methodspublic void LeaveRoom(){PhotonNetwork.LeaveRoom();}#endregion}
}

9.退出房间按钮
新建一个面板Top Panel,设置锚点

添加一个退出按钮,命名为Leave Button,绑定事件Game Manager的LeaveRoom()

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 Methodsvoid 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 Callbackspublic override void OnPlayerEnteredRoom(Player other)
{Debug.LogFormat("OnPlayerEnteredRoom() {0}", other.NickName); // not seen if you're the player connectingif (PhotonNetwork.IsMasterClient){Debug.LogFormat("OnPlayerEnteredRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoomLoadArena();}
}public override void OnPlayerLeftRoom(Player other)
{Debug.LogFormat("OnPlayerLeftRoom() {0}", other.NickName); // seen when other disconnectsif (PhotonNetwork.IsMasterClient){Debug.LogFormat("OnPlayerLeftRoom IsMasterClient {0}", PhotonNetwork.IsMasterClient); // called before OnPlayerLeftRoomLoadArena();}
}#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修改碰撞器:

动画设置

配这个Kyle Robot我们的控制器预制件,只需设置属性Controller要指向的动画组件的Kyle Robot

使用控制器参数
新建PlayerAnimatorManager.cs脚本:

using UnityEngine;
using System.Collections;namespace Com.MyCompany.MyGame
{public class PlayerAnimatorManager : MonoBehaviour{#region MonoBehaviour Callbacks// Use this for initializationvoid Start(){}// Update is called once per framevoid 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 parameterif (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 initializationvoid Start(){animator = GetComponent<Animator>();if (!animator){Debug.LogError("PlayerAnimatorManager is Missing Animator Component", this);}}// Update is called once per framevoid Update(){if (!animator){return;}// deal with JumpingAnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);// only allow jumping if we are running.if (stateInfo.IsName("Base Layer.Run")){// When using trigger parameterif (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 firingbool 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 stateif (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


16.用户输入管理
打开PlayerAnimatorManager.cs
Update添加

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 implementationpublic void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info){}#endregion

IPunObservable.OnPhotonSerializeView添加以下代码

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

将PlayerManager组件拖入PhotonView组件


19.生命值同步
打开脚本PlayerManager

if (stream.IsWriting)
{// We own this player: send the others our datastream.SendNext(IsFiring);stream.SendNext(Health);
}
else
{// Network player, receive datathis.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.InstantiatePhotonNetwork.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.InstantiatePhotonNetwork.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);
}
#endifvoid 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 zoneif (!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 callbacksbase.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 efficiencytarget = _target;if (playerNameText != null){playerNameText.text = target.photonView.Owner.NickName;}
}

添加此方法

void Update()
{// Reflect the Player Healthif (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.是我给房间用随机数字命名的房间名。场景中其实还有个输入玩家姓名的输入框,如果玩家没有输入姓名就自动随机给个数字当名称。)

【Unity3D插件】Photon Unity Networking(PUN)插件分享《多人联机服务器》相关推荐

  1. Photon Unity Networking(PUN)的简单使用 1(创建加入房间)

    public GameObject player; string _gameVersion = "1.0"; void Start () { PhotonNetwork.Conne ...

  2. 基于XR Interaction ToolKit与PUN实现VR多人协同功能

    最近在整VR多人协同相关的开发工作,因为没怎么接触过多人开发这一块,碰到了不少坑,在这里总结一下个人的一些经验. 一.开发环境配置 Unity版本:2019.3.4 相关插件版本: Universal ...

  3. 基于Unity的弹幕游戏多人联机尝试

    给一个已经定型的游戏添加哪怕是一个小特性,都是棘手的事情,很容易引入新的bug,或者破坏已有的功能.复杂的联网更是如此,它涉及到的改动几乎遍及系统的方方面面.玩家的一举一动都需要在其他人的屏幕上展现出 ...

  4. Unity多人联机的实现

    实现多人联机的方法有几种,具体取决于你的具体需求.以下是一些选项: Unity Multiplayer:Unity有自己的内置网络解决方案,称为Unity Multiplayer(以前称为UNET). ...

  5. [Unity3D]Unity资料大全免费分享

    都是网上找的连七八糟的资料了,整理好分享的,有学习资料,视频,源码,插件--等等 东西比较多,不是所有的都是你需要的,可以按  ctrl+F 来搜索你要的东西,如果有广告,不用理会,关掉就可以了,如果 ...

  6. Unity 通过Unity Admob Plugin插件集成admob教程

    原创:officemaster.cn 下载Unity Admob Demo,插件里面包含 Admob_Unity_Demo.unitypackage 插件文件 AdmobPluginRes  是Adm ...

  7. 【Unity3D插件】Build Report Tool插件,Build报告,优化包体,查看资源占用

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦. 一.前言 ...

  8. unity3d 气泡效果_Unity粒子特效插件打造冰与水物语

    原标题:Unity粒子特效插件打造冰与水物语 粒子系统用于在三维空间渲染一些二维图像,在游戏中模拟一些逼真的烟雾.火焰.雨.雪.冰.沙尘及爆炸等效果,而一般来说,水特效是其中最具挑战性的.今天我们就为 ...

  9. 【原创】Unity3D WebPlayer 网页版播放器插件离线配置方案

    如何能隐秘地在没有连接外网的机器上安装Unity3D的WebPlayer播放器插件? Unity发布成网页版后,需要安装WebPlayer播放器才能加载显示文件内容. 一般情况下,只需要从官网下载完整 ...

最新文章

  1. django 快速实现文件上传
  2. 祝贺自己itpub和csdn双双荣获专家博客标题
  3. (四)java B2B2C Springboot仿淘宝电子商城系统技术解决方案
  4. 为什么只需要一个eden而需要两个survivor?
  5. 14种冷热源及空调系统特点介绍
  6. 瞎说系列之Object.assign入门
  7. CodeForces - 801C Voltage Keepsake(二分)
  8. python发送qq邮件列表_Python SMTP发送邮件
  9. golang生成随机定长的验证吗
  10. 前端基础:html基础(css基础和JavaScript基础)/01/B/S网络结构,html文件结构,html标签格式,lt;bodygt;中的一些常用标记
  11. python画二维图_使用python绘制二维图形示例
  12. 高端轻奢国风美妆海报模板,东方美到极致
  13. 《C语言及程序设计》实践参考——体重监测器
  14. python常用程序代码(持续更新收集)
  15. acp cp linux命令,系统运维
  16. linux iptable 使用指南
  17. 在线购物系统 问题描述、词汇表、领域类图
  18. OJ每日一练——细菌个数
  19. 基于华为云的在线拍卖数据分析
  20. error: resource android:style/TextAppearance.Material.Widget.Button.Colored not found. 解决办法

热门文章

  1. MySQL的索引(普通索引、唯一索引,主键索引、组合索引、全文索引、空间索引)相关操作
  2. 电脑不识别移动硬盘--解决了
  3. Entering emergency mode
  4. openwrt ec20 4g网卡使用
  5. Java基础(一) —— JDK安装、第一个Java程序、Java数据类型、修饰符、运算符、循环、条件
  6. 程序员用什么样的电脑?
  7. 超好用的4个短视频素材下载网站,快收藏下来
  8. 2022数维杯国际赛C题思路(思路已更完)
  9. micro和macro Fscore的区别
  10. Java的来历和它的创始人