赞
踩
Photon Unity Networking(首字母缩写 PUN)是一个 Unity 多人游戏插件包。它提供了身份验证选项、匹配,以及快速、可靠的通过我们的 Photon 后端实现的游戏内通信。
PUN 输出几乎所有 Unity 支持的平台,且有两种选项:
注意:对于 Unity 5,两个 PUN 插件包都含相同的文件。你可以买 PUN+ 来获得 60 个月的 100 CCU 1 ,但客户端上仍使用 PUN Free。
CCU,即 concurrent user 的缩写,意为同时在线用户,100CCU 就是最多容纳 100 个同时在线用户。
PhotonNetwork.ConnectUsingSettings("v4.2");
上面的代码是你需要连接并开始使用 Photon 功能的所有代码。
ConnectUsingSettings 设置你的客户端的游戏版本并使用一个由 PUN 设置向导写入的配置文件,该配置文件保存在 PhotonServerSettings 里面。
//加入名为"someRoom"的房间
PhotonNetwork.JoinRoom("someRoom");
//如果没有开放的游戏就会失败。错误回调: OnPhotonJoinRoomFailed
//尝试加入任何随机游戏:
PhotonNetwork.JoinRandomRoom();
//如果没有开放的游戏就会失败。错误回调: OnPhotonRandomJoinFailed
//创建名为"MyMatch"的房间。
PhotonNetwork.CreateRoom("MyMatch");
//如果名为"MyMatch"的房间已存在就会失败并调用:OnPhotonCreateRoomFailed
好朋友常常想要一起玩游戏。如果他们可以交流(例如 使用 Photon Chat,Facebook), 他们可以瞎编一个房间名并使用 JoinOrCreateRoom 方法。因为他们知道房间的名字,他们可以创建为他人不可见,像这样:
RoomOptions roomOptions = new RoomOptions() { isVisible = false,maxPlayers = 4 };
PhotonNetwork.JoinOrCreateRoom(nameEveryFriendKnows, roomOptions, 5TypedLobby.Default);
使用 JoinOrCreateRoom 方法,如果房间不存在就会创建该房间。如果房间满了,OnPhotonJoinRoomFailed 会被调用 (如果你在某个地方实现了这个回调函数)。
GameObjects 可以被实例化为"networked GameObjects" 。它们会有一个可以被识别的 PhotonView 组件和一个所有者(或控制者)。所有者会更新其他人。持续更新可以通过拖拽一个脚本到一个 PhotonView 的 Observed 字段被发送。需要更新的脚本必须实现 OnPhotonSerializeView 像这样:
// 在一个"observed" 脚本里:
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); // pos 被填充。必须在某个地方使用
}
"networked GameObjects",网络游戏对象,会在网络上进行同步。
"observed",被观察的。
客户端可以为不见用的操作执行 Remote Procedure Calls :
// 定义一个可以被其他客户端调用的方法:
[PunRPC]
public void OnAwakeRPC(byte myParameter)
{
//Debug.Log("RPC: 'OnAwakeRPC' Parameter: " + myParameter + " PhotonView: " + this.photonView);
}
// [...]
// 在别的某个地方调用该 RPC
photonView.RPC("OnAwakeRPC", PhotonTargets.All, (byte)1);
独立于 GameObjects, 你也可以发送你自己的事件:
PhotonNetwork.RaiseEvent((byte)eventCode, (object)eventContent, (bool)sendReliable, (RaiseEventOptions)options)
Remote Procedure Calls,首字母缩写为 RPCs,意为远程过程调用。
Photon Unity Networking (PUN)真的很容易设置。把 PUN 导入到一个新的项目中,然后 PUN 设置向导就会弹出来,如图 0-1 所示。通过输入一个邮箱地址来注册一个新的(免费) Photon Cloud 帐号,或者复制粘贴一个已有的AppId 到该字段里。打完收工。如果你想要自己托管一个 Photon 服务器,点击"skip",然后像如下描述的
那样编辑 PhotonServerSettings 。
Photon Cloud 在后面的章节会有详细解释,你可以理解为云服务。
要连接,你只需在你的代码中调用 PhotonNetwork.ConnectUsingSettings() 。如果你需要更多的控制,详见下面的 Connect Manually 。
设置向导会添加一个 PhotonServerSettings 文件到你的项目,用来保存配置。如图 0-2 所示,这也是去编辑服务器设置的地方。
你可以设置 AppId、Photon Cloud Region 和更多的。你的客户端的 GameVersion 是在代码里被设置的。要选择的最重要的选项是托管类型。
托管类型:通过 Hosting Type 你选择处理你游戏的服务器和其他配置。Photon Cloud 和 Best Region 都涉及到我们管理的云服务。您可以选择特定区域,
也可以让客户选择最佳 ping 区域。如果你想在别的地方运行 Photon 服务器,选择 Self Hosted 。安装程序如下。或者,你的客户可以在脱机模式。
最佳托管区域:最佳区域模式将在应用首次启动的时候 ping 所有已知区域。由于这需要一点时间,结果被存储在 PlayerPrefs。这会加快连接时间。你可以设置哪些区域可以忽略。在更少的区域分发客户端会导致剩余区域的玩家更多。这在游戏流行之前是有益的。使用 PhotonNetwork.OverrideBestCloudServer() 来定义要使用的另一个区域。
自托管:如果你要自己托管一个 Photon 服务器,你应在 PhotonServerSettings 里面设置好它的地址和端口。当这些都被正确设置了,你可以在你的代码里调用
PhotonNetwork.ConnectUsingSettings() 。确保您的客户端可以到达输入的地址。它可以是一个公共的、静态的 IP 地址、主机名或在你的客户端也使用的网络中的任何地址。端口取决于所选协议,所以请确保这两个字段匹配。清除该字段会将其重置为默认端口。
如果你为 iOS 开发游戏可以考虑阅读 PUN and IPv6 和 how to setup Photon Server for IPv6。
协议:这里默认是(可靠的)UDP,但 Photon 还支持使用 TCP 以及将允许一个可靠的 HTTP 协议。我们建议你坚持 UDP。PUN+不支持 TCP。WebGL 导出只能使用WebSockets。
客户端设置:客户端设置部分包含了每个项目应设置的几个选项。当你勾选 Auto-Join Lobby 时,PUN 将在连接(或离开房间)时自动加入默认大厅。Photon 的大厅提供当前房间的列表,这样玩家可以选择一个加入。这个默认是关闭的,因为更好的选择是使用随机匹配,就像所有的演示案例中使用的那样。启用 Enable Lobby Stats 来从服务器获取大厅统计信息。如果游戏使用多个大厅,并且你想要向玩家展示每一个活动,则这个统计信息会很有用。每个大厅,你都可以获取这些属性: name、type、room 和 playercount。详见PhotonNetworking.LobbyStatistics !这些设置在 PUN v1.60 版本引入。
远程过程调用列表:Remote Procedure Calls 使你可以在一个房间里调用所有客户端上的方法。PUN将这些方法的列表保存在 PhotonServerSettings。对于最初的设置,这是不相关的。详见 Remote Procedure Calls。
作为替代自动连接的 PhotonNetwork.ConnectUsingSettings() 方法你可以通过PhotonNetwork.ConnectToMaster() 方法来手动连接你自己的 Photon 服务器。当你托
管付费 Photon 服务器时这是有用的。对于 ConnectToMaster() ,你需要提供一个 masterServerAddress 和一个 port 参数。地址可以是你的 On-Premises DNS 名称或一个 IP。它可以包括冒号后的端口(然后传递 0 作为端口)或您可以单独通过端口。ConnectToMaster() 方法有更多的另外两个参数 : "appID"和"gameVersion"。
两者都只与 Photon Cloud 有关,并且当你自己托管 Photon 服务器时,可以设置为任何值。对于 Photon Cloud, 使用 ConnectUsingSettings() 方法。它涉及到我们的 Name Server 自动找到一个区域的主服务器。
DNS,即 Domain Name Server 的首字母缩写,意为域名服务器,这里指你自己架构的服务器。
PUN 由相当多的文件组成, 然而只有一个是真正重要的: PhotonNetwork 。这个类包含所有需要的函数和变量.。如果您有自定义要求,可以随时修改源文件。
要从UnityScript中使用PUN,你需要把 "PhotonNetwork"和"UtilityScripts" 文件夹移动到 Assets\Plugins\文件夹。为了告诉你这个 API 如何工作,这里有几个例子。
PhotonNetwork 始终使用主服务器和一个或多个游戏服务器。主服务器管理当前可用的游戏并进行匹配。一旦房间被发现或创建,实际的游戏是在游戏服务器上完成的。所有的服务器都运行在专用的机器上,没有所谓的玩家托管的服务器。你不必费心记住该服务器组织,PUN 会为你处理它。
PhotonNetwork.ConnectUsingSettings("v1.0");
上面的代码是你需要连接并开始使用 Photon 功能的所有代码。ConnectUsingSettings 设置你的客户端的游戏版本并使用一个由 PUN 设置向导写入的配置文件,该配置文件保存在 PhotonServerSettings 里面。你也可以修改文件PhotonServerSettings 属性来连接到你自己的服务器。或者,使用 Connect() 方法来忽略该 PhotonServerSettings 文件。
Photon 的负载均衡逻辑使用你的 AppId 来区分你的和他人的游戏。玩家也会被游戏版本分开, ConnectUsingSettings 的参数(见上文)。通过这种方式,您可以发布新功能的客户端,而不破坏旧版本的游戏。由于我们不能保证不同 PUN 的版本之间相互兼容,PUN 把它自己的版本号添加到你的游戏里。更新 PUN 可能会从旧的版本中分离出新的客户端,但不会打破老客户端。
接下来,你想加入或创建一个房间。下面的代码展示了一些必要的函数:
//加入一个房间
PhotonNetwork.JoinRoom(roomName);
//创建这个房间。
PhotonNetwork.CreateRoom(roomName);
// 如果该房间已存在则会失败并调用: OnPhotonCreateGameFailed
//尝试加入任何随机游戏:
PhotonNetwork.JoinRandomRoom();//如果没有匹配的游戏则会失败并调用: OnPhotonRandomJoinFailed
在最好的情况下,您的游戏使用随机配对。 JoinRandomRoom() 将尝试加入任何房间。如果该方法失败了(没有房间接受另一个玩家),只需创建一个新的房间,并等到其他玩家随机加入它为止。或者,您的客户端可以获得当前可用的房间列表。这是通过加入一个大厅来获得的。大厅自动发送他们的房间列表到客户端,并在时间间隔内更新(从而减少流量)。玩家不会看到对方,且无法沟通(以防止当您的游戏繁忙时出问题)。PhotonNetwork 插件可以在其连接时自动加入默认大厅。把 PhotonServerSettings 文件里的"Auto-Join Lobby"属性开启即可。当你的客户端在一个大厅里时,房间列表会得到更新, 这些更新会缓存。如果需要的话,你可以通过 GetRoomList 方法来每一帧访问房间列表。
foreach (RoomInfo room in PhotonNetwork.GetRoomList())
{
GUILayout.Label(room.name + " " + room.playerCount + "/" + room.maxPlayers);}
PhotonNetwork 使用多个回调函数来让你的游戏知道状态的变化,如“已连接”或“已加入一个游戏”。像往常对 Unity 一样,回调可在任何脚本里实现。如果你的脚本扩展 Photon.PunBehaviour , 你可以单独重写每个回调。在这种情况下,您不必调用基类实现。
public override void OnJoinedRoom()
{
Debug.Log("OnJoinedRoom() called by PUN: " + PhotonNetwork.room.name);
}
你不需要扩展 PunBehaviour 。如果你在其本身身上实现它所有的回调函数也会起作用。它们也在枚举 PhotonNetworkingMessage 中被列出和描述。这包括建立游戏房间的基础知识。接下来是游戏中的实际交流。
在一个房间里,你可以发送网络信息给其他连接的玩家。此外,您还可以发送缓冲消息,也将被发送到未来连接的玩家(以玩家生成为例)。
发送消息可以使用两种方法。无论是 RPCs,还是通过在一个由 PhotonView 观察的脚本里实现 OnSerializePhotonView 。
然而有更多的网络互动。你可以监听一些网络事件的回调函数,如 OnPhotonInstantiate或 OnPhotonPlayerConnected ,并且你可以触发其中一些事件,如 PhotonNetwork.Instantiate 。如果你被最后一段弄糊涂了,不要担心,下一步我们会为这些主题逐个做解释。
PhotonView 是一个用于发送消息(RPCs 和 OnSerializePhotonView )的脚本组件。你需要将 PhotonView 依附到游戏对象或预设上。请注意,PhotonView 和 Unity 的
NetworkView 非常相似。整个过程,你的游戏中需要至少一个 PhotonView,才能发送消息和可选的实例化/分配其他的 PhotonViews。如图下图所示,添加一个 PhotonView 到一个游戏对象,只需选择一个游戏对象并使用: "Components/Miscellaneous/Photon View"。
如果你将一个 Transform 绑定到 PhotonView 的观察属性上,你可以选择同步位置、旋转和尺度或玩家的这些属性组合。这可以极大的帮助制作原型或小游戏。注意:任何观察到的值变化将发送所有观察到的值-而不只是发生变化的那个单一值。此外,更新的值是不平滑的或插值。
PhotonView 可以被设置来观察 MonoBehaviour。在这种情况下,脚本的OnPhotonSerializeView 方法会被调用。此方法被调用来写入对象的状态并读取它,这取决于
脚本是否由本地玩家控制。下面简单的代码展示了如何用几行代码来增加角色状态同步:
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.isWriting)
{
//我们拥有这个玩家:把我们的数据发送给别的玩家stream.SendNext((int)controllerScript._characterState);
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
else
{
//网络玩家,接收数据
controllerScript._characterState = (CharacterState)(int)stream.ReceiveNext();
correctPlayerPos = (Vector3)stream.ReceiveNext();
correctPlayerRot = (Quaternion)stream.ReceiveNext();
}
}
Observe Option 字段让你选择更新如何发送以及何时被发送。该字段还会影响到OnPhotonSerializeView 被调用的频率。
Off 顾名思义,关掉。如果该 PhotonView 被保留为 RPCs 限定时可以很有用。
Unreliable 更新如是被发送,但可能会丢失。这个想法是,下一次更新很快到来,并提供所需的正确的/绝对的值。这对于位置和其他绝对数据来说是有利的,但对于像切换武器这样触发器来说是不好的。当用于同步的游戏对象的位置,它会总是发送更新,即使该游戏对象停止运动(这是不好的)。
Unreliable on Change 将检查每一个更新的更改。如果所有值与之前发送的一样,该更新将作为可靠的被发送,然后所有者停止发送更新直到事情再次发生变化。这对于那些可能会停止运动的以及暂时不会创建进一步更新的游戏对象来说是有利的。例如那些在找到自己的位置后就不再移动的箱子。
Reliable Delta Compressed 将更新的每个值与它之前的值进行比较。未更改的值将跳过以保持低流量。接收端只需填入先前更新的值。任何你通过 OnPhotonSerializeView 写入的都会自动进行检查并以这种方式被压缩。如果没有改变, OnPhotonSerializeView 不会再接收客户端调用。该“可靠的”部分需要一些开销,所以对于小的更新,应该考虑这些开销。
现在开始,以另一种方式交流:RPCs。
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!");
你可以发送一系列的参数,但它必须匹配该 RPC 方法的定义。
这些是最基本的。详情请阅读 Remote Procedure Calls.
RPCs 在指定的 PhotonViews 上被调用,并总是以接收客户端上的匹配者为目标。如果一个远程客户端还没有加载或创建匹配的 PhotonView,这个 RPC 就会丢失!
因此,丢失 RPCs 一个典型的原因就是当客户端加载新场景的时候。它只需要一个已经加载有新游戏对象的场景的客户端,并且其他客户端不能理解这个 RPC(直到这些客户端也加载了相同的场景)。
PUN 可以帮你解决此问题。只需在你连接之前设置PhotonNetwork.automaticallySyncScene = true 并在房间的主客户端上使用PhotonNetwork.LoadLevel() 。这样,一个客户端定义了所有客户端必须在房间/游戏中加载的关卡。
客户端可以停止执行接收到的消息来防止 RPCs 丢失(这正是 LoadLevel 方法帮你做的)。当你得到一个 RPC 来加载一些场景,立即设置 isMessageQueueRunning = false 直到该内容被初始化。
private IEnumerator MoveToGameScene()
{
// 加载关卡前临时禁用进一步的网络信息处理
PhotonNetwork.isMessageQueueRunning = false;
Application.LoadLevel(levelName);
}
禁用消息队列将延迟传入和传出消息,直到队列被解锁。显然,当你准备好要继续的时候,打开队列是非常重要的。
这个 PUN 基本教程是一个基于 Unity3D 的教程。它将教会你如何开发一个你自己的多人在线应用,当然,这是由 Photon Cloud 提供技术支持的。以及
怎样使用 Animator 来为角色做动画。同时我们将一路学习许多重要的功能、提示和技巧,以获得一个很好的以网络为基础的 PUN 开发路线总览。
本教程将从一个空的项目开始,在整个游戏创建过程中一步一步地引导您。一路上,概念将被解释,以及常见的陷阱和为网络游戏所做的设计考虑。为了不让玩家四处走动,什么也不做,我们将实现一个基本的射击系统,再加上玩家的健康管理,这将有助于我们支持变量通过网络同步的解释。
* Photon Cloud 直译为光子云,是类似于阿里云、腾讯云那样的云端服务。本文将保留英文名称,以方便读者追本溯源。后面的教程中还有更详细的解释。
我们也将使用一个根据在房间里的玩家数量来自定义大小的竞技场,竞技场的大小正在根据当前游戏的玩家数据被调整。这将展示几个关于自动同步场景功能、在加载不同场景时如何应对玩家、以及在这种情况下哪些容易出错的技巧 :)
当游戏启动时,用户将在 UI 中见证连接协议并了解连接进展情况。当玩家加入或创建一个房间后,玩家进入一个可同时容纳多达 4 个玩家的、可调整大小
的竞技场。玩家可以跑动、转弯以及射击。每一个玩家发射的光束会影响其他被击中玩家的健康值。当你的健康值是 0 时,那么你就游戏结束了并离开竞技场。
然后出现在屏幕上的,是再次让你开始或加入一个新的游戏,如果想要的话。
本教程假定你只有使用 Unity 编辑器和编程的基础知识。然而,为了集中介绍 Photon Networking 的新概念,读者最好有一个良好的知识储备以及创建
常规的、非网络游戏的项目经验。
示例代码是用 C#写的,但在 Unity 的脚本中同样起作用。
* Photon Networking 即 Photon 的网络架构,包括 Photon 服务器和客户端。
创建一个新的项目,一般情况这是在使用教程时推荐的操作。然后一旦你同化了概念和设计模式,就能将其应用到自己的项目中。如图 0-1 所示,该操作是比较简单的。
打开 Asset store 并找到 PUN 插件并下载/安装它。当你把所有 PUN 插件资源都导入完成时让 Unity 重新进行编译。如图 0-2 所示,按照步骤操作即可。
PUN 设置向导是帮助你完成网络设置,并提供了一个方便的方式来开始我们的多人游戏开发:The Photon Cloud!Cloud? Yes, Cloud. 这是一组我们可以用来为我们的游戏服务的 Photon服务器。我们将一点点做解释。使用云端的"Free Plan "是免费且没有任何义务的,所以现在我们只需输入我们的邮件地址,向导就会执行它的魔法。如图 0-3 所示,在 PUN 设置向导里面可以输入邮件地址,也可以输入"AppId",然后点击"Setup Project "即可。
* Free Plan 免费计划。
* AppId 应用的 Id,可以在 Photon 官网注册后在仪表盘处获得。
* Setup Project 设置项目。
新账户会立即获得一个"AppId"。如果你的邮箱地址已经被注册,你会被要求打开 Dashboard 。登录即可获得"AppId",将其复制粘贴到输入框里面。当"AppId"被保存,我们就完成了这一步的设置。
如图 0-4 所示,Photon Cloud 就像云团一样。那么,这个"Photon Cloud" 到底是做什么的呢?!
* Dashboard 仪表盘,即网站的管理后台,根据权限的不同会有不同的仪表盘界面。这里指用户仪表盘。
本质上,它是一组运行 Photon 服务器的 PC 机。这个服务器"cloud"是由Exit Games 维护并为您的多人游戏提供无忧的服务的。服务器会根据需求来进行添加,所以可以胜任处理任何数量的玩家。尽管 Photon Cloud 不是完全免费的,但是成本低,特别是和常规的托管主机相比。
Photon Unity Networking 将为你处理 Photon Cloud,但这就是内部的情况:首先每个玩家连接到一个"Name Server" 。服务器会检查你客户端使用的是哪个应用(使用 AppId 来识别),以及客户端想要使用哪个区域的服务器。然后"Name Server"将客户端转发到一个主服务器。主服务器是一组区域服务器的集线器。它知道所有现有的游戏。任何时候一个游戏(或房间)被创建或加入,客户端都会被转发到其他所谓"Game Server" 的机器。PUN 的设置是非常简单的,并且你无需担心托管费用、性能或维护。一次也不需要。
* Exit Games 是国际领先的多平台网络游戏引擎供应商,其所开发的高性能 photon 引擎,通过 SDK 的形式,为游戏开发者开发实时多人应用提供了最佳的解决方案。目前该引擎已支持 Unity,iOS,Android,Flash和 HTML5 等多个平台,它能帮助开发人员轻松实现浏览器、PC、Mac 或移动设备,包括 iPad、iPhone 和
Android 的游戏及应用中的多人实时功能。包括 OpenFeint,Bigpoint,Walt Disney,KONAMI 等游戏公司都在使用 Exit Games 的产品。
* Photon Unity Networking 即 PUN,是 Photon 为 Unity 定制的多人游戏网络解决方案插件。
* "Name Server",即“名称服务器”。
* "Game Server",即“游戏服务器”。
要记住 Photon Cloud 被构建来做"room-based games" ,意味着每次匹配有限数量的玩家(譬如说:少于 10),这些玩家是与其他房间的玩家分开的。在一个房间里面(通常情况下)的每一个玩家都会接收到其他玩家发送的任何信息。房间外的玩家则不能沟通,所以我们总是想要他们尽快加入房间。加入一个房间最好的方式就是使用随机匹配。我们只需请求服务器任何房间或带特定属性的房间即可。所有的房间都有一个名称作为标识符。除非房间已满或被关闭了,我们可以通过名称来加入房间。为了方便玩家,主服务器可以为我们的应用程序提供一个房间列表。
* "room-based games" ,即基于房间的游戏,类似于 CF、LOL 之类的。
您的应用的大厅存在于主服务器上,大厅中为您的游戏列出房间。在我们的例子中,我们将不使用大厅,如果有可用的房间就简单地加入一个随机房间,如果没有可用的房间可以加入(房间可以有一个最大容量,所以他们可能是全满了),则创建一个新的房间。
如果每个玩家都连接到相同的服务器,必须有一个方法来区分你和其他的玩家。每个游戏(同样适用于应用)在云端有它自己的"AppId"。玩家总是会遇到有同样"AppId"的其他玩家,仅在客户端里。也有一个"game version" ,您可以用来将不同客户端版本的玩家分开。
Photon Cloud 被组织在全球不同的区域,以防止玩家因为距离服务器太远而潜在的不良连接。重要的是要理解这个概念,尤其是当与分散在各地的远程团队工作时。与您的队友测试您的游戏也许不太可能,因为分散在不同地区。所以确保你强制所有想要和彼此互动测试员的区域是相同的。
每个部分涵盖了项目开发阶段的一个非常具体的部分,重要的是要按顺序进行工作。脚本和 Photon 知识的假设水平也逐渐增加。
所以,我们达成了一个工作系统,用户可以在互联网上互相对抗,具备基本的良好体验要求。我们会学到如何控制 PUN,如何监听 PUN 状态和现状,以及使最直观的组件来轻易地协同 Animator 工作。我们也了解到 Photon 的一些有趣的功能,如自动场景同步,来创建原创的且强大的游戏。
要创造一个完整的游戏,还有很多事情要做,为上线做准备,但这仅仅是建立在我们所涵盖的基础之上。
* API 参考文档,API,即 Application Programming Interface 的首字母缩写,意为应用程序编程接口。
让我们先解决这个教程的核心,能够连接到 Photon 云服务器,并加入一个房间或如有必要则创建一个。
1. 创建一个新场景,并将其保存为 Launcher.unity ;
2. 创建一个新的 C #脚本 Launcher ;
3. 在 Hierarchy 层次结构中创建一个空的 GameObject 游戏对象,命名为 Launcher;
4. 将 C #脚本 Launcher 添加到 GameObject Launcher 上;
5. 编辑 C #脚本 Launcher 成如下内容的样子。
- using UnityEngine;
- namespace Com.MyCompany.MyGame
- {
- public class Launcher : MonoBehaviour
- {
- #region Public Variables //公共变量区域
- #endregion
- #region Private Variables //私有变量区域
- /// <summary>
- /// 此客户端的版本号。用户通过 gameversion 彼此分离 (这让你可以做出突破性
- 的改变).
- /// </summary>
- string _gameVersion = "1";
- #endregion
- #region MonoBehaviour CallBacks //回调函数区域
- /// <summary>
- /// 在早期初始化阶段里被 Unity 在游戏对象上调用的 MonoBehaviour 方法。
- /// </summary>
- void Awake()
- {
- // #Critical | 极重要
- //我们不加入大厅。没有必要加入一个大厅来获得房间列表。
- PhotonNetwork.autoJoinLobby = false;
- // #Critical | 极重要
- //这样可以确保我们可以在主客户端上使用PhotonNetwork.LoadLevel()方法,
- 并且在相同房间里的所有客户端都会自动同步它们的关卡。
- PhotonNetwork.automaticallySyncScene = true;
- }
- /// <summary>
- /// 在初始化阶段里被 Unity 在游戏对象上调用的 MonoBehaviour 方法。
- /// </summary>
- void Start()
- {
- Connect();
- }
- #endregion
- #region Public Methods //公共方法
- /// <summary>
- /// 启动连接进程。
- /// - 如果已经连接,我们试图加入一个随机的房间
- /// - 如果尚未连接,请将此应用程序实例连接到 Photon 云网络
- /// </summary>
- public void Connect()
- {
- // 我们检查是否连接,如果我们已连接则加入,否则我们启动连接到服务器。
- if (PhotonNetwork.connected)
- {
- // #Critical | 极重要 -我们需要在这个点上企图加入一个随机房间。如果失
- 败,我们将在 OnPhotonRandomJoinFailed()里面得到通知,这样我们将创建一个房间。
- PhotonNetwork.JoinRandomRoom();
- }else{
- // #Critical | 极重要 -我们必须首先连接到 Photon 在线服务器。
- PhotonNetwork.ConnectUsingSettings(_gameVersion);
- }
- }
- #endregion
- }
- }
6. 保存该 C#脚本 Launcher
让我们回顾一下目前这个脚本中的内容,首先从一般的 Unity 角度来看,然后看看我们制作的 PUN 的具体调用。
命名空间: 虽然不是强制性的,给予你的脚本适当的 namespace 可以防止与其他资源和开发者发生冲突。万一另一个开发者也创建了一个 Launcher 类呢?Unity 将报错,并且您或该开发人员将不得不为 Unity 重命名该类,以允许执行该项目。如果冲突来自您从资源商店下载的资产,这可能是棘手的。现在, Launcher 类实际上是Com.MyCompany.MyGame.launcher,在引擎下不太可能有其他人会使用和我们一样的命名空间,因为你拥有这个域名,并且使用倒置域名惯例作为命名空间使你的工作安全而又组织良好。Com.MyCompany.MyGame 应该被你自己的反向域名和游戏名替换,这是一个值得遵循的良好约定。
MonoBehaviour Class: 请注意我们用 MonoBehaviour 派生出我们的类,这从本质上把我们的类转化成一个Unity Component,从而使我们可以把这些类添加到GameObject或Prefab上作为组件。一个继承 MonoBehaviour 的类可以访问许多非常重要的方法和属
性。在这个案例中我们使用了两个回调方法,Awake()和 Start()。
PhotonNetwork.autoJoinLobby: 在 Awake()方法中,我们设置 PhotonNetwork.autoJoinLobby 为 false,因为我们不需要 Lobby 功能,我们只需要获取当前房间的列表。强制设置通常是一个好主意,因为在同一个项目中你可以有另一个确实想要 autoJoin 该 Lobby 的场景,这样在转换不同的方法时就不会有问题了。
PhotonNetwork.ConnectUsingSettings(): 在 Start()方法中我们调用了我们的公共函数 connect() 来使用
PhotonNetwork.ConnectUsingSettings()连接 PUN云。请注意 _gameVersion 变量代表你的 gameversion。你应该保持该参数为 "1" 直到你在你的上线项目中产生突破性改变。这里要记住的重要信息是 PhotonNetwork.ConnectUsingSettings()是你的游戏网络化以及连接 connect 到 Photon 云的起点。
PhotonNetwork.automaticallySyncScene: 我们的游戏将有一个根据玩家的数量调整大小的竞技场,并确保每一个已连接的玩家所
加载的场景都是一样的,我们将使用 Photon 提供的很方便的功能:PhotonNetwork.automaticallySyncScene当该属性被设置为 true,MasterClient 可以调用 PhotonNetwork.LoadLevel(),这样所有已连接的玩家将自动加载相同的关卡。
在这一点上,你可以保存 Launch 场景,并打开 PhotonSettings (从 Unity 的菜单窗口 Window/Photon Unity Networking/Highlight Photon Server Settings 中选择它),如下图所示,我们需要像这样设置调试级别为 Full:
然后我们可以点击Play。你应该在Unity控制台看到好几十条日志。特别是“Connected to masterserver.”这条记录表明我们现在已连接并准备加入一个房间。
总是测试潜在的错误是写代码的一个好习惯。在这里我们假设计算机已连接到互联网,但如果计算机没有连接到互联网会发生什么?让我们探寻一下。关闭您计算机的互联网并运行该场景。你应该看到在 Unity 控制台出现一个错误"Connect() to 'ns.exitgames.com'
failed: System.Net.Sockets.SocketException: No such host is known"。理想情况下,我们的脚本应该意识到这个问题,并优雅地应对这些情况,并提出一个反应性的经验,无论出现什么情况或问题。让我们现在处理这两种情况,并在我们的 Launcher 脚本内通知我们确实已连接或没有连接到 PUN 服务器。这将是完美的 PUN 回调介绍。
PUN 的回调是非常灵活的,且提供了三种非常不同的实现。让我们覆盖所有三种方法的学习,我们会根据情况来选择一个最适合的。
"magic" methods | “魔术”方法
使用常规 MonoBehaviour 时,你可以简单地创建私有方法。
void OnConnectedToMaster()
{
Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN");}
这是神奇的因为任何 MonoBehaviour 可实现该方法或来自 PUN 的任何消息。它遵循和 Unity 正在发送到 MonoBehaviours 的诸如 Awake()或 Start()这样的主方法一样的原则。但是我们不会用这个,因为如果你拼错了这些“神奇”的方法,也不会告知你的错误,所以这是一个非常实用的快速实现,但只有在知道每个方法的确切名称,并且你很熟悉和擅长调试技术来快速找到这些问题时。
Using IPunCallbacks and IPunObservable Interfaces | 使用接口
PUN 提供了两种 C#接口 Interfaces ,你可以在你的类里面实现它们。 IPunObservable和 IPunCallbacks 。
这是一个非常安全的方法,以确保一个类符合接口的所有,但会强制开发人员实现接口的所有声明。大多数好的脚本编辑器会使这个任务很容易,如上图使用 MonoDevelop 所演示那样。但是,该脚本可能会有很多什么都不做的方法,但必须全部实现来让 Unity 编译器高兴。所以这是当你的脚本是要大量使用所有或大部分的 PUN 功能时适用。我们确实要在接下来的数据序列化的教程中使用 IPunObservable 接口。
Using Photon.PunBehaviour | 使用 Photon.PunBehaviour
最后的技术,这是一个我们将经常使用,且是最方便的。与其从 MonoBehaviour 中创建一个派生类,我们将从Photon.PunBehaviour 派生类,如下图所示,因为它暴露了特定的属性和虚函数 virtual methods 供我们使用以及方便我们重写 override。这是很实用的,因为我们可以肯定的是,我们没有任何的错别字,并且我们不需要实现所有的方法。
注:在重写时,大多数脚本编辑器将默认实现基本调用和自动为你填充,但在我们的案例中我们并不需要,所以就Photon.PunBehaviour 一般规则,不要调用基本方法。注意:重写的另一大好处是,只需悬停在方法的名称上你就可以得到上下文的帮助。所以让我们来实践一下 OnConnectedToMaster()和 OnDisconnectedFromPhoton()PUN 回调。
1. 编辑 C #脚本 Launcher
2. 把基类从 MonoBehaviour 修改成 Photon.PunBehaviour
public class Launcher : Photon.PunBehaviour {
3. 在类的结尾添加以下两种方法, 在域 region Photon.PunBehaviour CallBacks 内更加明了。
#region Photon.PunBehaviour CallBacks //域可以使代码结构更加清晰
public override void OnConnectedToMaster()
{
Debug.Log("DemoAnimator/Launcher: OnConnectedToMaster() was called by
PUN");
}public override void OnDisconnectedFromPhoton()
{
Debug.LogWarning("DemoAnimator/Launcher:
OnDisconnectedFromPhoton() was called by PUN");
}
#endregion
4. 保存 Launcher 脚本。
现在,如果我们玩这个场景,无论有或没有互联网,我们都可以采取适当的步骤来通知玩家,并进一步进入逻辑。我们将在下一节开始构建 UI 时处理这个问题。现在我们将处理成功的连接:
所以,我们在 OnConnectedToMaster()方法中追加如下的调用:
// #Critical | 极重要: 我们首先尝试要做的就是加入一个潜在现有房间。如果有,很好,
否则,我们将调用回调 OnPhotonRandomJoinFailed()
PhotonNetwork.JoinRandomRoom();
并且如注释所说那样,如果加入一个随机房间的企图失败了我们需要被通知,在该情况下我们需要创建一个房间,所以我们在脚本里实现 OnPhotonRandomJoinFailed()PUN 回调,并使用 PhotonNetwork.CreateRoom() 来创建一个房间,以及,你已经猜到了,相关
的 Pun 回调 OnJoinedRoom()将在我们实际加入一个房间时通知你的脚本:
public override void OnPhotonRandomJoinFailed (object[] codeAndMsg)
{
Debug.Log("DemoAnimator/Launcher:OnPhotonRandomJoinFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom(null, new RoomOptions() {maxPlayers = 4}, null);");
// #Critical | 极重要: 我们加入一个随机房间失败,也许没有房间存在或房间已满。别担心,我们创建一个新的房间即可。
PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers = 4 }, null);
}
public override void OnJoinedRoom()
{
Debug.Log("DemoAnimator/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room.");
}
现在如果你运行该场景,你应该遵循连接到 PUN 的逻辑序列,试图加入一个现有的房间,否则创建一个新的房间并加入那个新创建的房间。
在教程的这个点上,因为我们现在已经覆盖了连接和加入一个房间的关键部分,有几件事情不是很方便,并且这些问题需要尽早解决。这些问题都不是真正关系到学习 PUN 的,但重要的是从整体的角度来看。
Expose variables in Unity Inspector | 暴露变量
你可能已经知道了这一点,但假如你不知道,MonoBehaviours 自动暴露自己的公共属性到 Unity 的 Inspector 窗口。这在 Unity 里是一个非常重要的概念,在我们的情况下,我们要修改定义 LogLevel 的方式,并制作一个公共变量,这样我们不接触代码本身就可以
完成设置。
/// <summary>
/// PUN 的日志等级。
/// </summary>
public PhotonLogLevel Loglevel = PhotonLogLevel.Informational;
并且在 Awake()方法里面我们将进行接下来的修改:
// #NotImportant | 不重要
//强制为 LogLevel 赋值
PhotonNetwork.logLevel = Loglevel;
所以,现在我们不强制脚本是某种类型的 LogLevel,我们只需要在 Unity 的 Inspector里进行设置,然后运行,不需要打开脚本,编辑,保存,等待 Unity 重新编译,最后再运行。这是一个更富有成效和灵活的方式。
我们会为每个房间的最大玩家数量做同样的事情。在代码里硬编码并不是最佳实践,相反,让它作为一个公共变量,这样我们就可以决定和调试那个数值而无需重新编译。在类声明的开头,在公共变量 Public Variables 区域我们添加:
/// <summary>
/// 每个房间的最大玩家数。当一个房间满了,它不能加入新的玩家,所以新的房间将被创建。如图 11 所示,该公共变量将暴露在 Unity 的 Inspector 窗口面板中。
/// </summary>
[Tooltip("每个房间的最大玩家数。当一个房间满了,它不能加入新的玩家,所以新的房间将被创建。")]
public byte MaxPlayersPerRoom = 4;
然后我们修改 PhotonNetwork.CreateRoom()调用,并使用这个新的公共变量来取代我们之前使用的硬编码的数字。
// #Critical | 重要:我们没能进入一个随机的房间,也许没有这样的房间存在,或者都满了。不用担心,我们创造一个新的房间。
PhotonNetwork.CreateRoom(null, new RoomOptions() { maxPlayers =MaxPlayersPerRoom }, null);
这部分将着重于为大厅创建用户界面(UI)。它将保持非常基本的,因为它不是真正关系到网络。
目前我们的游戏大厅是自动把我们连接到一个房间,这是很好的早期测试,但我们真的希望让用户选择是否以及何时开始游戏。所以我们将简单地为此提供一个按钮即可。
1. 打开上一节创建的 Launcher 场景;
2. 使用 Unity 菜单'GameObject/UI/Button'创建一个 UI 按钮, 并命名为: Play Button注意该操作在创建按钮的同时在场景的Hierarchy 层级中创建了一个 Canvas 幕布和一个 EventSystem 事件系统游戏对象,所以我们不必手动创建;
3. 编辑 Play Button 按钮的子类 Text 的变量为"Play";
4. 选择 Play Button 并找到 Button 组件里的 On Click () 部分;
5. 点击'+'小按钮来添加一个条目;
6. 把 Launcher 游戏对象从 Hierarchy 层级中拖进该字段;
7. 在下拉菜单中选择 Launcher.connect() ,这样我们就把该按钮和 Launcher 脚本连接到了一起,当玩家按下该按钮的时候就会从 Launcher 脚本中调用"Connect()"方法;
8. 打开 Launcher 脚本;
9. 移除我们在 Start()方法里调用 Connect() 的那一行代码;
10. 保存 Launcher 脚本和场景;
如果你现在点击运行,你会发现不会再连接直到你点击该按钮。
典型游戏的另一个重要的最低要求就是让用户输入他们的名字,这样其他玩家就知道和他们一起玩游戏的是谁了。我们将为这个简单的任务添加一点曲折,通过使用 PLayerPrefs记住名字的值,这样当用户打开游戏,我们可以恢复该名字是什么。这是一个非常方便和非常重要的功能,实现在您的游戏的许多领域可以为用户带来非常好的体验。让我们首先创建该脚本,将用来管理和记住玩家的名称,然后创建相关的用户界面。
创建 PlayerNameInputField:
1. 创建一个新的 C #脚本,命名为 PlayerNameInputField;
2. 这里是它的全部内容。编辑并保存相应的 PlayerNameInputField 脚本;
- using UnityEngine;
- using UnityEngine.UI;
- using System.Collections;
- namespace Com.MyCompany.MyGame
- {
- /// <summary>
- /// 玩家姓名输入字段。让用户输入自己的名字,就会出现在游戏中的玩家头上。
- /// </summary>
- [RequireComponent(typeof(InputField))]
- public class PlayerNameInputField : MonoBehaviour
- {
- #region Private Variables //私有变量区域
- //保存 PlayerPref 键来避免错别字
- static string playerNamePrefKey = "PlayerName";
- #endregion
- #region MonoBehaviour CallBacks //回调函数区域
- /// <summary>
- /// 初始化阶段被 Unity 在游戏对象上调用的 MonoBehaviour 方法
- /// </summary>
- void Start () {
- string defaultName = "";
- InputField _inputField = this.GetComponent<InputField>();
- if (_inputField!=null)
- {
- if (PlayerPrefs.HasKey(playerNamePrefKey))
- {
- defaultName = PlayerPrefs.GetString(playerNamePrefKey);
- _inputField.text = defaultName;
- }
- }
- PhotonNetwork.playerName = defaultName;
- }
- #endregion
- #region Public Methods //公共方法区域
- /// <summary>
- ///设置该玩家的名字,并把它保存在 PlayerPrefs 待用。
- /// </summary>
- /// <param name="value">玩家姓名</param>
- public void SetPlayerName(string value)
- {
- // #Important | 重要
- PhotonNetwork.playerName = value + " "; //强制加一个拖尾空格字符串,
- 以免该值是一个空字符串,否则 playerName 不会被更新。
- PlayerPrefs.SetString(playerNamePrefKey,value);
- }
- #endregion
- }
- }
让我们来一起分析这个脚本:
RequireComponent(typeof(InputField)):我们首先要确保这个脚本强制执行 InputField,因为我们需要它,这是一个非常
方便快捷的方法,以保证该脚本的使用没有问题。
PlayerPrefs.HasKey(), PlayerPrefs.GetString()和 PlayerPrefs.SetString():PlayerPrefs 是一个简单的配对条目查找列表(就像两列的 Excel 表),一列是 key ,一列是 Value 。键 key 是一个字符串,并且是完全任意的,你决定如何命名且您将需要在整个开发中坚持该命名。正因为如此,总是把你的 PlayerPrefs 键只保存在一个地方是很有道理的,一个方便的方法是使用一个[Static|变量名,因为在游戏中它不会随着时间的推移而改变,而且每次都是一样的。所以,逻辑是非常直接的。如果 PlayerPrefs 有已给定的键,我们可以获取它并在启动该功能的时候直接为其注入值,在我们的案例中,在我们启动和编辑过程中用这个来为InputField 填入值,我们用 InputField 的当前值来设置 PlayerPref,然后我们确定它是在用户设备本地存储供以后检索(下一次用户将打开这个游戏)。
PhotonNetwork.playerName:这是这个脚本的要点,在网络上设置玩家的名称。该脚本在两个地方使用玩家名称,一次是在 Start()过程中,在检查完该名称是否被存储在 PlayerPrefs 里面之后,以及在公共方法 SetPlayerName() 里面。现在,并没有调用这个方法,我们需要绑定 InputField 的OnValueChange()来调用 SetPlayerName() ,这样每次用户编辑该 InputField 时我们都把它记录下来。我们只有在用户按下开始按钮后才能执行该操作,这取决于你,但是这是一个更明智的脚本,所以让我们为了清楚起见保持其简单。这也意味着,无论用户会做什么,输入将被记住,这往往是所需的行为。
1. 确保你仍然在 Launcher 场景里面,如果不在,打开该场景;
2. 使用 Unity 的菜单'GameObject/UI/InputField'创建一个 UI InputField , 将该游戏对象 GameObject 命名为 Name InputField;
3. 设置 RectTransform 里面的 PosY 值到 35 ,这样它会在 Play Button 按钮上面;
4. 找到 Name InputField 的子类 PlaceHolder ,并将它的 Text 值设置为"Enter your Name...";
5. 选择 Name InputField GameObject;
6. 把我们刚刚创建好的 PlayerNamerInputField 脚本添加到该对象上;
7. 找到 InputField 组件内的 On Value Change (String) 部分;
8. 添加那个'+'号来添加一条绑定事件;
9. 拖拽 Launch 的 PlayerNamerInputField 组件到该字段进行绑定;
10. 在 Dynamic String 部分的下拉菜单中选择 PlayerNameInputField.SetPlayerName();
11. 保存该场景(Ctrl+S);
现在你可以点击运行了,输入你的名字,停止运行,再次运行,你之前输入的名字就会出现。
我们越来越接近目标了,但是在用户体验 User Experience 方面我们缺少对连接进度的反馈,以及在连接和加入房间出错时的反馈。
我们将在这里保持简单,并隐藏名称字段和开始按钮,并用一个简单的文本“Connecting...”在连接期间代替,并在需要时切换回来。
要实现这样的效果,我们将开始按钮和名称字段归为一组,以便我们简单地激活和停用该组。后面更多的功能可以添加到该组,且它不会影响到我们的逻辑。
1. 老规矩,确保你仍然在 Launcher 场景;
2. 使 用 Unity 菜 单'GameObject/UI/Panel' 创 建一 个 UI Panel, 将 该游 戏对 象GameObject 命名为 Control Panel;
3. 从 Control Panel 中删除其 Image 和 Canvas Renderer 组件,我们不需要这个面板的任何视觉效果,我们只关心它的内容;
4. 把 Play Button 和 Name InputField 拖拽到 Control Panel 上;
5. 使用 Unity 菜单'GameObject/UI/Text'创建一个 UI Text, 将其命名为 Progress;Label 不要担心它会干扰视觉,我们将在运行时激活/关闭他们;
6. 选择 Progress Label 的 Text 组件;
7. 把 Alignment 设置成 center align 和 middle align;
8. 把 Text 的值设置成"Connecting...";
9. 把 Color 设置成白色或任何可以背景中凸显出来的颜色;
10. 保存该场景。
在这一点上,想要测试,您可以简单地启用/禁用控制面板 Control Panel 和进度标签Progress Label ,看看事情在不同的连接阶段会如何显示。现在让我们编辑脚本来控制这些两个游戏对象的激活。
1. 编辑脚本 Launcher
2. 在公共属性区域 Public Properties Region 中添加以下两个属性
[Tooltip("让用户输入名称、连接和开始游戏的 UI 面板")]
public GameObject controlPanel;
[Tooltip("通知用户连接正在进行中的 UI 标签")]
public GameObject progressLabel;
3. 把以下的代码添加到 Start()方法
progressLabel.SetActive(false);
controlPanel.SetActive(true);
4. 在 Connect()方法的开始处添加下面的代码
progressLabel.SetActive(true);
controlPanel.SetActive(false);
5.在 OnDisconnectedFromPhoton()方法开始处添加如下代码
progressLabel.SetActive(false);
controlPanel.SetActive(true);
6. 保存 Launcher 脚本并等待 Unity 完成编译
7. 确保你仍然在 Launcher 场景
8. 在 Hierarchy 层级中选择游戏对象 GameObject Launcher
9. 从 Hierarchy 层级中把 Control Panel 和 Progress Label 拖拽到 Launcher 组件里的对应字段。
10. 保存场景
现在,如果你运行该场景。你只会看到控制面板 Control Panel,只要你点击开始游戏,进程标签 Progres Label 就会出现。
现在,我们的游戏大厅部分暂时做好了。为了进一步添加功能到大厅,我们需要切换到游戏本身,并创建各种场景,以便我们终于可以在加入一个房间时加载正确的关卡。我们将在下一节和之后小节中完成,我们将最终完成大厅系统。
本节介绍了玩家将要进行游戏的各种场景的创建。每个场景将致力于满足特定数量的玩家,越来越大,以适应所有玩家,给他们足够的空间走动。在本教程中,我们将实现基于玩家数量加载正确关卡的逻辑,这将使用一个达到关卡的约定,关卡名称以这种格式“Room for X”命名,X 代表玩家的数量。
1. 创建一个新场景, 保存并命名为 "Room for 1" ;
2. 创建一个立方体 Cube 并命名为 " floor";
3. 将其放置在 0,0,0。这是重要的,因为我们的逻辑系统将生成的玩家放置在中心之上(0,x,0);
4. 把 floor 放大到 20,1,20。
这就足够一个可玩的水平了,但一些墙将使玩家在该范围内。只需创建更多的立方体,
调整其位置、旋转和缩放来使它们作为墙壁。以下是四墙壁与地板的游戏对象的位置和规模。
做到这里别忘了保存 Room For 1 场景。
在所有情况下,用户界面的最低要求是能够退出房间。为此,我们需要一个 UI Button按钮,但我们也需要一个调用 Photon 来使本地玩家离开房间的脚本,所以让我们开始创建我们将调用的 Game Manager 预设,和第一个任务将处理退出本地玩家当前所在的房间。
1. 创建一个新的 C #脚本 GameManager;
2. 在场景中创建一个空游戏对象,命名为 Game Manager;
3. 把 GameManager 脚本拖拽到 Game Manager 游戏对象上;
4. 通过把场景层级 Hierarchy 中的 Game Manager 拖拽到资源浏览器 Assets 中将其转化成预设,而层级中的原型就会变成蓝色的;
5. 编辑 GameManager 脚本;
6. 用以下的代码进行替换:
- using System;
- using System.Collections;
- using UnityEngine;
- using UnityEngine.SceneManagement;
- namespace Com.MyCompany.MyGame
- {
- public class GameManager : MonoBehaviour {
- #region Photon Messages //Photon 消息区域
- /// <summary>
- ///当本地玩家离开房间时被调用。我们需要加载 Launcher 场景。
- /// </summary>
- public void OnLeftRoom()
- {
- SceneManager.LoadScene(0);
- }
- #endregion
- #region Public Methods //公共方法区域
- public void LeaveRoom()
- {
- PhotonNetwork.LeaveRoom();
- }
- #endregion
- }
- }
7. 保存 GameManager 脚本。
因此,我们创建一个公共方法 LeaveRoom() 。它的作用是显式地让本地玩家离开 Photon网络房间,我们用我们自己的公共方法来将其包装使其抽象化。我们可能会想在稍后的阶段实现更多的功能,如保存数据,或插入一个用户将离开游戏等的确认步骤。基于我们的游戏要求,如果我们不在一个房间里我们需要显示 Launcher 场景,所以我们要监听 OnLeftRoom() Photon 回调并加载游戏大厅场景 Launcher ,该场景在 Build settings 的场景列表中的索引为 0,而我们将在这一节的 Build Settings Scene List 部分里进行设置。
但为什么要制作成预制体呢?因为我们的游戏需求意味着同一个游戏有几个不同的场景,所以我们需要重用这个游戏管理器 Game Manager 。在 Unity 中重用对象的最佳方式是把他们变成预设。
接下来,让我们创建 UI Button 按钮,用来调用我们的 GameManager 的 LeaveRoom()方法。
再次,就像 Game Manager 一样,长远来看我们会有许多不同的场景需要这个功能,所以睿智的做法是未雨绸缪并将该按钮做成一个预设,这样我们就可以重用它了,并且在将来需要改变它的时候在一个地方做修改即可。
1. 确保你在场景 Room for 1 里面;
2. 使用 Unity 菜单创建'GameObject/UI/Panel'一个 UI Panel,并命名为 Top Panel ;
3. 移除 Image 和 Canvas Renderer 组件清空这个面板。如果你觉得不删除好看点的话也可以保留,这只是为了审美而已;
4. 同时按住 Shift 和 Alt 设置垂直锚点预置到 top ,设置水平锚点预置到 stretch 。RectTransform 锚点需要一些经验去适应它,但它是值得的;
5. 把 RectTransform 的高度设置成 50;
6. 右键点击该面板的游戏对象 Top Panel 并添加一个 UI/Button,命名为 Leave button;
7. 选择 Leave button 的子类 Text, 并将它的文本设置为 Leave Game;
8. 如下图所示,把 OnClick 按钮事件连接到 Hierarchy 层级中的 Game Manager 实例来调用 LeaveRoom() 。
1. 通过把 Leave button 从场景 Hierarchy 层级中拖拽到 Assets 资源浏览器将其转变成一个预设,它将在 Hierarchy 层级中变成蓝色;
2. 保存该场景及项目。
现在,我们已经有一个房间做好了,让我们复制 3 次,并适当地为其命名(他们应该在被复制的时候被 Unity 命名过了):
Room for 2
Room for 3
Room for 4
查找以下位置、旋转和规模的变化,以加快这一重复过程。
Room for 2:
Room for 3:
地面大小缩放: 50,1,50
Room for 4:
地面大小缩放: 60,1,60
在编辑和发布时,项目的良好运行至关重要,我们需要在 Build Settings 中添加所有这些场景,以便 Unity 在构建应用程序时包含它们。
1. 通过 Unity 菜单"File/Build Settings"打开 Build Settings;
2. 如下图所示,拖拽所有的场景到列表,Launcher 场景必须保持在第一,因为 Unity默认将为玩家加载和展示列表中的第一个场景。
现在我们有了基本的场景设置,我们终于可以开始把一切串联起来。让我们在下一节做这个。
本节介绍了添加功能来处理基于目前在房间里玩的玩家数量来加载不同的关卡。
我们已经创建了 4 个不同的房间,我们以一定的规律来它们命名,场景名称的最后一个字符是玩家的数量,所以现在很容易把房间里的玩家数量和相关场景绑定起来。这是一个非常有效的技术被称为“convention over configuration”,就是方法匹配玩家数量到对应的房间。然后,我们的脚本将在该列表中查找,并返回一个场景,其中的名称并不重要。"Configuration"需要更多编写,这就是为什么我们会在这里使用"Convention",这让我们得到更快的编写代码,而无需在我们的代码中写无关的功能。
1. 打开 GameManager 脚本;
2. 让我们添加一个新的方法到一个新的专用于私有方法的区域。不要忘记保存GameManager 脚本;
- #region Private Methods //私有方法区域
- void LoadArena()
- {
- if ( ! PhotonNetwork.isMasterClient )
- {
- Debug.LogError( "PhotonNetwork : Trying to Load a level but we are not the master Client" );
- }
- Debug.Log( "PhotonNetwork : Loading Level : " +
- PhotonNetwork.room.PlayerCount );
- PhotonNetwork.LoadLevel("Room for
- "+PhotonNetwork.room.playerCount);
- }
- #endregion
3. 保存 GameManager 脚本。
当我们调用这个方法时,我们将根据我们所在房间的 PlayerCount 属性加载合适的房间。
这里有两件事值得注意,这是非常重要的。
只有在我们是主客户端的时候才能调用 PhotonNetwork.LoadLevel()。所以我首先使用 PhotonNetwork.isMasterClient 来检查我们是否是主客户端。检查这个是调用者的责任,我们将在这一节的下一部分详细讲。
我们使用 PhotonNetwork.LoadLevel()来加载我们想要加载的关卡,我们不会支持使用 Unity,因为我们想要依赖 Photon 来在所有已连接的该房间内的客户端上加载这个关卡 ,因为我们为这个游戏启用了
PhotonNetwork.automaticallySyncScene。
现在我们已经有了加载正确关卡的方法,让我们把这个和玩家的连接和断开连接绑定起来。
目前,我们的 GameManager 脚本是一个常规的 MonoBehaviour,我们已经在本教程的前面部分研究了得到 Photon 回调的各种方法,现在 GameManager 需要监听玩家连接与
断开。让我们来实现这个。
1. 打开 GameManager 脚本;
2. 把基类从 MonoBehaviour 修改成 Photon.PunBehaviour:
public class GameManager : Photon.PunBehaviour {
3. 让我们来添加下面的 Photon 回调信息并保存 GameManager 脚本:
- #region Photon Messages //Photon 消息区域
- public override void OnPhotonPlayerConnected( PhotonPlayer other )
- {
- Debug.Log( "OnPhotonPlayerConnected() " + other.NickName ); //如果你是正在连接的玩家则看不到
- if ( PhotonNetwork.isMasterClient )
- {
- Debug.Log( "OnPhotonPlayerConnected isMasterClient " +
- PhotonNetwork.isMasterClient ); //在 OnPhotonPlayerDisconnected 之前调用LoadArena(); //加载竞技场
- }
- }
- public override void OnPhotonPlayerDisconnected( PhotonPlayer other )
- {
- Debug.Log( "OnPhotonPlayerDisconnected() " + other.NickName ); //当其他客户端断开连接时可见
- if ( PhotonNetwork.isMasterClient )
- {
- Debug.Log( "OnPhotonPlayerConnected isMasterClient " +
- PhotonNetwork.isMasterClient ); //在 OnPhotonPlayerDisconnected 之前调用
- LoadArena();
- }
- }
- #endregion
4. 保存 GameManager 脚本
现在,我们有一个完整的设置。每当一个玩家加入或离开房间,我们就被告知,我们会调用我们刚刚在上面创建的 LoadArena() 方法。然而,只有使用PhotonNetwork.isMasterClient 确定我们是主客户端时才能调用 LoadArena() 。现在我们回到大厅,终于可以在加入房间时加载正确的场景。
1. 编辑脚本 Launcher;
2. 追加下面的代码到 OnJoinedRoom() 方法中:
- // #Critical | 极重要:只有第一个玩家才加载,否则我们依赖PhotonNetwork.automaticallySyncScene 来同步我们的场景实例
- if (PhotonNetwork.room.PlayerCount == 1)
- {
- Debug.Log("We load the 'Room for 1' ");
- // #Critical | 极重要:加载房间关卡
- PhotonNetwork.LoadLevel("Room for 1");
- }
3. 保存脚本 Launcher .
让我们来测试下这个脚本,打开场景 Launcher ,运行它。点击"Play",让系统连接并加入一个房间。就是这样,游戏大厅正在工作。但如果你离开房间,当回到大厅时你将注意到,它会自动重新加入…哎呀,让我们解决这个问题。如果你还不知道为什么,"simply"分析日志。我把简单地引用起来,因为分析日志需要实践和经验来获得概述问题的能力,并且知道到哪里去找,以及如何调试它。现在你自己尝试一下,如果你仍然无法找到问题的根源,让我们一起做这件事。
1. 运行 Launcher 场景;
2. 点击"Play"按钮,等待直到你加入了房间,并且"Room for 1"已被加载;
3. 清除 Unity 控制台;
4. 点击"Leave Room"按钮;
5. 研究 Unity 的控制台 , 注意到 "DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN"被记录了;
6. 停止 Launcher 场景;
7. 双击该日志条目 "DemoAnimator/Launcher: OnConnectedToMaster() was called by PUN",对应的脚本将被加载并指向对应调试调用的代码;
8. 嗯 … 所以 ,每次我们收到我们已连接的通知 ,我们会自动加入一个JoinRandomRoom方法。但那不是我们想要的;
要解决这个问题,我们需要了解上下文。当用户点击"Play"按钮时,我们应该发起一个标志来知道连接过程源于用户。然后我们可以检查这个标志在各种 Photon 回调中相应行为。
1. 编辑脚本 Launcher;
2. 在私有变量区域 Private Variables regions 内创建一个新属性;
- /// <summary>
- /// 跟踪当前进程。因为连接是异步的,且是基于来自 Photon 的几个回调,
- /// 我们需要跟踪这一点,以在我们收到 Photon 回调时适当地调整该行为。
- /// 这通常是用于 OnConnectedToMaster()回调。
- /// </summary>
- bool isConnecting;
3. 在 connect() 方法前面添加下列代码:
- // 跟踪玩家加入一个房间的意愿,因为当我们从游戏中回来时,我们会得到一个我们已连接的回调,所以我们需要知道那个时候该怎么做
- isConnecting = true;
4. 在 OnConnectedToMaster() 方法中, 用一个如下所示的 if 语句来包裹PhotonNetwork.JoinRandomRoom()
- //如果我们不想加入一个房间,我们不想做任何事情。
- //这种情况下 isConnecting 是 false,通常是当你掉线或退出游戏时,当这一关卡被加载,
- OnConnectedToMaster 将被调用,在这种情况下,我们不想做任何事情。
- if (isConnecting)
- {
- // #Critical | 关键: 我们首先要做的是加入一个潜在的现有房间。如果有,很好,否
- 则,我们将回调 OnPhotonRandomJoinFailed()
- PhotonNetwork.JoinRandomRoom();
- }
5. 保存脚本 Launcher
现在如果我们再次测试并运行 Launcher 场景,大厅和游戏之间来回切换,一切都很好 :) 为了测试场景的自动同步,你将需要发布该应用(发布为桌面版,这是最快的运行测试的平台),并在 Unity 中同时运行,所以你实际上有两个玩家将连接和加入房间。如果 Unity编辑器首先创建房间,它将是主客户端,并且你可以在 Unity 控制台得到证明:你将得到"PhotonNetwork : Loading Level : 1"以及接下来在你连接已发布实例应用时的
"PhotonNetwork : Loading Level : 2"。
很好!我们已经谈了很多,但这只完成了一半… :) 我们需要处理玩家本身了,让我们在下一节做吧。别忘了按时离开电脑休息一下,这样可以更有效地吸收我们解释的各种概念。
如果你对某个特定的功能有疑问,或者如果你在教程中遇到错误,或者遇到了在这里没有涉及到错误或问题,请不要犹豫在论坛上提问,我们将乐意帮助 :)
本节将引导你从零开始创建将在本教程中使用的玩家预设 Prefab,所以我们涵盖了创作过程的每一步。尝试并创建一个无需连接PUN就能工作的玩家预设Prefab常常是一个很好的途径,这样很容易快速测试、调试以及确保一切在没有网络功能的情况下也能工作。然后,你可以建立和慢慢修改,加入网络兼容的特性:通常情况下,用户输入只能在玩家拥有的实例上才能被激活,不在别人的电脑上。我们将在下面详细介绍。
你需要了解的第一且最重要的约定就是预设Prefab需要在网络上被实例化,它需要被放在 Resources 资源文件夹里,否则就不能被找到。第二重要的,在 Resources 资源文件夹里有预设 Prefabs 的副作用是,你
需要注意这些预设的名字。你不应该在你的资产下的 Resources 资源路径里有两个命名相同的预制件 Prefab,因为 Unity 你会选择找到的第一个,所以一定要确保在你的项目资产里,Resources 资源文件夹路径下没有两个预设 Prefabs的命名是相同的。我们很快就会讲到。我们将使用 Unity 提供的免费资源 Kyle 机器人来作为玩家预设。它是一个Fbx 文件,可以通过诸如 3ds Max、 Maya、 cinema4d 之流的 3d 软件来制作 generated。在这些软件上制作网格和动画已经超出了本教程的范围,但创造自己的人物和动画是必不可少。如下图所示,这个机器人 Kyle.fbx 位于/Assets/Photon Unity Networking/Demos/Shared Assets/ 路径下。
下面是开始使用 Kyle Robot.fbx 来制作玩家预设的一种方法:
1. 在你的项目浏览器里,在某个地方创建一个名为"Resources"的文件夹,通常建议你组织你的内容,所以可以像这样 'DemoAnimator_tutorial/Resources/'的路径;
2. 创建一个新的空场景,并将其保存在 /PunBasics_tutorial/Scenes/ 路径下,命名为Kyle Test ;
3. 把 Robot Kyle 资源拖拽到场景 Hierarchy 层级上;
4. 把你刚刚在 Hierarchy 层级中创建的游戏对象重命名为 My Robot Kyle;
5. 再把 My Robot Kyle 拖拽进 /PunBasics_tutorial/Resources/ 文件夹下。现在我们已经创建了一个基于 Kyle Robot Fbx 资产的预设 Prefab,并且我们在你的 Kyle Test 场景 Hierarchy 层级中有它的一个实例。现在我们可以开始完善这个预设了。
一、如下图右侧所示,我们来一起为 Hierarchy 层级中的 My Kyle Robot 实例添加一个 CharacterController Component 组件。你也可以直接在预设 Prefab本身上做这个操作,但我们需要对其进行调整,所以这种方式更快。
这些组件是一个由Unity为我们更快的创建使用Animator的典型人物角色而提供的非常方便的标准资产,所以让我们来一起利用这些伟大的Unity 功能。
二、双击 My Kyle Robot 来使场景视图 Scene View 聚焦到该对象上。如上图左侧所示,注意到 Capsule Collider 在脚的中心,我们实际上需要 Capsule Collider 适当地匹配角色。
三、如上图 31 右侧所示,在 CharacterController Component 组件里把Center.y 属性改成 1(是其 Height 属性的一半)。
四、如下图所示,点击 Apply 来应用我们所做的改变到预设上。这是非常重要的一步,因为我们完成 My Kyle Robot 预设实例的编辑,但是我们想要这些改变被应用到每一个实例上,不仅仅是这一个,所以我们点击 Apply 来应用。
Assigning an Animator Controller | 指派动画控制器
Kyle Robot Fbx 资源需要被 Animator Graph 控制,如下图所示。我们在本教程中将不包括创建此图表,所以我们提供了一个控制器,位于您的项目资Photon Unity Networking/Demos/PunBasics/Animator/ 路径下,叫做 Kyle Robot
如下图所示,要指派这个 Kyle Robot 控制器到我们预设 Prefab,简单地设置预设上的 Animator 组件的 Controller 属性指向 Kyle Robot 即可。
别忘了,如果你在 My Kyle Robot 的实例上执行该操作的话,你需要点击 Apply来让预设 Prefab 本身合并这些改变。
处理控制器参数 | Controller Parameters
要搞清楚 Animator Controller 的关键特性就是 Animation Parameters,通过该参数我们可以在脚本里控制我们的动画。在我们的例子中,我们有参数如Speed , Direction , Jump , Hi 。动画组件 Animator Component 的其中一个伟大的特点就是能够真正基于它的动画四处移动角色,这个功能被称为 Root Motion,并且在 Animator Component 上有一个属性 Apply Root Motion 默认为 true,所以我们保持默认即可。
因此,实际上,要有角色行走,我们只需要将 Animation Parameter 动画参数 Speed 设置为正数值,它就可以开始行走和向前移动。让我们就这样做!
Animator Manager Script | 动画管理脚本
让我们创建一个新的脚本,在该脚本中我们将根据用户的输入来控制角色。
1. 在 DemoAnimator/tutorial/Scripts/ 路径下创建一个名为 PlayerAnimatorManager 的 C#新脚本;
2. 将该脚本添加到 Prefab 预设 My Robot Kyle 上;
3. 如下面的代码所示,为你的脚本添加命名空间 Com.MyCompany.MyGame ;
4. 为了结构清晰起见,用 MONOBEHAVIOUR MESSAGES 域把 Start()和 Update()
包裹起来;
- using UnityEngine;
- using System.Collections;
- namespace Com.MyCompany.MyGame
- {
- public class PlayerAnimatorManager : MonoBehaviour
- {
- #region MONOBEHAVIOUR MESSAGES
- void Start () {
- }
- void Update () {
- }
- #endregion
- }
- }
5. 保存脚本 PlayerAnimatorManager。
Animator Manager: Speed Control | 速度控制
我们需要编码的第一件事就是获取 Animator Component 动画组件,这样我们才可以控制它。
1. 确保您正在编辑脚本 PlayerAnimatorManager;
2. 创建一个 Animator 类型的私有变量 animator;
3. 在 Start()方法内把 Animator Component 存储进这个变量。
- private Animator animator;
- void Start () {
- animator = GetComponent<Animator>(); //获取动画组件
- if (!animator) //如果没有获取到则报错
- {
- Debug.LogError("PlayerAnimatorManager is Missing Animator
- Component",this);
- }
- }
4. 注意,因为我们必需一个 Animator Component 动画组件,如果我们没有获取到,我们记录一个错误,这样可以及时发现问题并由开发者直接解决。你应该总是以别人会使用的方式来写代码 :) 这是乏味的,但长期来
看是值得的。
5. 现在让我们来监听用户输入 User Inputs ,并且控制 Animation Parameter 动画参数 Speed 。然后保存 PlayerAnimatorManager 脚本。
- void Update () {
- if (!animator) //如果没有获取到动画组件,则用 return 中断
- {
- return;
- }
- float h = Input.GetAxis("Horizontal");
- float v = Input.GetAxis("Vertical");
- if( v < 0 ) //数值校正
- {
- v = 0;
- }
- animator.SetFloat( "Speed", h*h+v*v ); //设置速度参数值
- }
6. 保存脚本 PlayerAnimatorManager
让我们来研究这个脚本在做什么:
由于我们的游戏不允许玩家后退,我们确保 v 不小于 0。用户正在按下‘↓’键或“s”键,我们不允许使用此值,并强制将值改成 0。你也会注意到,我们把两个输入都平方化处理,为什么?这样的话它总是一个正绝对值,以及增加一些 easing。很好的微妙技巧就在这里。你也可以使用Mathf.Abs() ,那也能正常工作。我们还把两个输入相加来控制速度 Speed ,以便当只按下左的右输入,我们仍然获得一些速度,因为我们转向。当然所有这一切对于我们的角色设计都很具体,取决于你的游戏逻辑,你可能想角色转变,或有往回走的能力,所以游戏的动画控制参数 Animation Parameters 总是非常具体。
Test, test, 1 2 3... | 测试
让我们来验证一下我们迄今所做的。确保你已经打开了 Kyle Test 场景。目前,在这个场景中,我们只有一个相机和 Kyle Robot 实例,我们不在乎场景的照明或任何花哨的效果,我们要验证我们的角色和脚本是否能正常工作。
1. 选择相机 Camera,并移动它来得到一个很好的总览视角。一个好的技巧是在场景视图 Scene View 中获取你喜欢的视角,选择相机,然后到菜单"GameObject/Align With View",这样像机将匹配场景视图的视角。
2. 最后一步,把 My Robot Kyle 在 y 轴上向上移 0.1,否则碰撞在开始时就丢失了,角色会直接穿过地面,所以总是在碰撞器之间预留一些物理空间,来模拟接触。
3. 运行场景,按下‘↑’或“A”键,角色正在行走!你可以用所有的键来验证测试。这很好,但仍然有大量的工作在我们面前,相机需要跟随,而且我们还不能转弯…如果你现在就像完成摄像机,可以直接跳到像机部分的内容,接下来的页面将完成动画控制和实现转弯。
Animator Manager Script: Direction Control | 方向控制
控制旋转会稍微复杂一点,我们不希望我们的角色在我们按左键和右键时突然旋转,我们希望平缓和平滑的旋转。幸运的是,一个动画参数 Animation Parameter 可以设置一些阻尼。
1. 确保您正在编辑脚本 PlayerAnimatorManager;
2. 在脚本的新的域"PUBLIC PROPERTIES"内创建公共浮点数变量DirectionDampTime。
- //#region PUBLIC PROPERTIES //公共属性域
- public float DirectionDampTime = .25f;
- //#endregion
3. 在 Update()函数末尾加上:
animator.SetFloat( "Direction", h, DirectionDampTime, Time.deltaTime );
4. 所以我们马上注意到 animator.SetFloat()有不同的用法。我们用来控制速度 Speed 的是简单直接的,但是对于这一个需要多两个参数,一个阻尼时间,一个是 deltaTime。阻尼时间是有意义的:它是需要多久才能达到期望值,但 deltaTime?它本质上让你写的代码独立于帧速率,因为Update()是依赖于帧速率的,我们需要通过使用 deltaTime 来计数。尽可能多地阅读关于这个主题知识,以及那些当你在网上搜索时你会发现的内容。只有在你之后理解这个概念后,当涉及到动画和随着时间推移值的一致性控制,你就可以充分利用到 Unity 的许多功能;
5. 保存脚本 PlayerAnimatorManager;
6. 运行您的场景,并使用所有的箭头键来看看你的角色是如何走动和转身;
7. 测试 DirectionDampTime 的效果:例如将其值改为 1,然后 5,并看看角色需要多长时间来达到最大弯度。你将发现转弯半径随着 DirectionDampTime 增加。
Animator Manager Script: Jumping | 跳跃
对于跳跃,我们需要更多的工作,因为两个因素。一、我们不希望玩家在没有奔跑的情况下跳跃;二,我们不想玩家循环跳跃。
1. 确保您正在编辑脚本 PlayerAnimatorManager;
2. 在方法里,在我们捕捉用户输入之前插入这些代码。
- // 处理跳跃
- AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
- // 只有当我们在奔跑的时候才能跳跃。
- if (stateInfo.IsName("Base Layer.Run"))
- {
- // 何时使用触发参数。
- if (Input.GetButtonDown("Fire2")) animator.SetTrigger("Jump");
- }
3. 保存脚本 PlayerAnimatorManager;
4. 测试。开始运行,按“Alt”键,凯尔应该会跳跃。好的,首先要了解我们如何知道动画是否在运行,我们使用stateInfo.IsName("Base Layer.Run") 来判断。我们只是想问动画当前活动状态是否是Run 。我们必须追加 Base Layer ,因为 Run 状态位于 Base Layer 中。如果我们在奔跑的状态中,那么就可以监听 Fire2 Input 输入,并且启用 Jump触发器是必要的。所以,这是目前的完整 PlayerAnimatorManager 脚本:
- using UnityEngine;
- using System.Collections;
- namespace Com.MyCompany.MyGame
- {
- public class PlayerAnimatorManager : MonoBehaviour
- {
- #region PUBLIC PROPERTIES //公共属性区域
- public float DirectionDampTime = .25f;
- #endregion
- #region PRIVATE PROPERTIES //私有属性区域
- private Animator animator;
- #endregion
- #region MONOBEHAVIOUR MESSAGES //Mono 行为消息区域
- void Start () {
- animator = GetComponent<Animator>();
- if (!animator)
- {
- Debug.LogError("PlayerAnimatorManager is Missing Animator
- Component",this);
- }
- }
- void Update () {
- if (!animator)
- {
- return;
- }
- //处理跳跃,获取当前动画状态
- AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
- //只有我们正在奔跑时才能跳跃
- if (stateInfo.IsName("Base Layer.Run"))
- {
- //何时使用触发器参数
- 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 脚本来完成摄像机对玩家的跟随。如果你想从零开始写 CameraWork ,请看下一部分,在完成后再回来看这一部分。
1. 添加 CameraWork 组件到 My Kyle Robot 预设;
2. 开启 Follow on Start 属性,这样可以使摄像机立即跟随角色。当我们将开始网络实现的时候我们将把它关掉;
3. 把 Center Offset 属性设置成 0,4,0 ,这可以使摄像机看得更高,这样可以给出更好的环境视角,而不是直接盯着玩家,我们将看到太多的地面;
4. 运行 Kyle Test 场景,让角色四处走走来测试摄像机是否恰当地跟随角色。
我们的机器人角色仍然缺少它的武器,让我们创造一些激光束从其眼睛内射出来。
Adding the Beams models | 添加射线模型
为了简单起见,我们将使用简单的立方体和将其缩放成非常薄和长。有一些技巧可以将其很快完成:不要直接将一个立方体添加为头部的子物体,而是单独创建它移动它,并单独进行缩放,然后附加到头部,它将防止猜测适当的旋转值,该值使您的光束与眼睛对齐。另一个重要的技巧是在两个光束中只使用一个碰撞器。这是为了让物理引擎的工作得更好,薄的碰撞器绝不是一个好主意,因为碰撞器太薄是不可靠的,所以我们要做一个大的碰撞器来使我们确定能够可靠地打击到目标。
1. 打开 Kyle test 场景;
2. 添加一个立方体到场景中,并命名为 Beam Left;
3. 修改它,使其看起来像一个长的光束,并妥善定位于左眼;
4. 在 Hierarchy 层级内选择 My Kyle Robot 实例;
5. 如下图所示,定位到 Head 子类;
6. 添加一个空对象作为 Head 游戏对象的子类,并命名为 Beams;
7. 把 Beam Left 拖拽进 Beams;
8. 如下图所示,复制 Beams Left , 并命名为 Beams Right;
9. 定位它,以便它与右眼对齐;
10. 从 Beams Right 中移除 Box Collider 组件;
11. 调整 Beams Left 的 Box Collider 中心和大小来封装两个光束;
12. 把 Beams Left 的 Box Collider 组件的 IsTrigger 属性改成 True ,我们只想在射线触碰到玩家时被通知,并且没有碰撞;
13. 创建一个新的材质, 命名为 Red Beam , 将其保存在'DemoAnimator_tutorial/Scenes/'路径下;
14. 把 Red Beam 材质指派给两个射线;
15. 把更改应用到预设上(点击实例上的 Apply 即可)。
如下图所示,你现在应该有这样的东西了:
Controlling the Beams with User Input | 用户控制射线
好吧,现在我们有射线了,让我们插入发射输入来触发它们。让我们创建一个新的 C #脚本,叫做 PlayerManager 。下面是第一个版本的完整代码,来使射线工作。
- using UnityEngine;
- using UnityEngine.EventSystems;
- using System.Collections;
- namespace Com.MyCompany.MyGame
- {
- /// <summary>
- /// 玩家总管,处理发射输入和射线。
- /// </summary>
- public class PlayerManager : MonoBehaviour {
- #region Public Variables //公共变量区域
- [Tooltip("The Beams GameObject to control")]
- public GameObject Beams; //要控制的射线
- #endregion
- #region Private Variables //私有变量区域
- //当玩家发射的时候为 true
- bool IsFiring;
- #endregion
- #region MonoBehaviour CallBacks //Mono 行为回调
- void Awake()
- {
- if (Beams==null) //如果射线为空,则报错
- {
- Debug.LogError("<Color=Red><a>Missing</a></Color> Beams
- Reference.",this);
- }else{
- Beams.SetActive(false);
- }
- }
- void Update()
- {
- ProcessInputs ();
- //触发射线的激活状态
- if (Beams!=null && IsFiring != Beams.GetActive ()) {
- Beams.SetActive(IsFiring);
- }
- }
- #endregion
- #region Custom //自定义区域
- /// <summary>
- /// 处理输入。当用户按下发射时,维护一个标志。
- /// </summary>
- void ProcessInputs()
- {
- if (Input.GetButtonDown ("Fire1") ) {
- if (!IsFiring)
- {
- IsFiring = true;
- }
- }
- if (Input.GetButtonUp ("Fire1") ) {
- if (IsFiring)
- {
- IsFiring = false;
- }
- }
- }
- #endregion
- }
- }
在这个阶段,这个脚本主要是激活或停用射线。当被激活时,射线将在与其他模型发生碰撞时有效地触发,所以我们将在触发后捕捉这些触发器来影响每个角色的健康值或体力。我们也暴露出公共属性 Beams ,将让我们引用在 My Kyle Robot 预设 Prefab 的层级内部的具体对象。让我们看看要如何办才能连接 Beams ,因为这在预设内部是棘手的,由于在资产浏览器里面,预设只暴露出第一个子类,并非其他子类,并且我们的射线确实是埋在预制体的层次中,所以我们需要做的是从场景中的一个实例上获取,然后将它应用到预制体本身。
1. 打开 Kyle test 场景;
2. 在场景层级中选择 My Kyle Robot;
3. 添加 PlayerManager 组件到 My Kyle Robot;
4. 把 My Kyle Robot/Root/Ribs/Neck/Head/Beams 拖拽进 PlayerManager 在 Inspector窗口里的 Beams 属性里;
5. 把实例上的改变应用到预设上。
如果你点击运行,并且按下 Fire1 Input 输入(默认是鼠标左键或左 Ctrl 键),射线将出现,并在你松开时立即隐藏。
让我们实现一个非常简单的健康系统,体力值会在射线击中玩家的时候减少。因为它不是子弹,而是源源不断的能量,我们需要用两种方法来计算伤害,当我们被射线击中,并且射线一直击中我们。
1. 打开 PlayerManager 脚本;
2. 把 PlayerManager 转化成Photon.PunBehaviour来暴露PhotonView组件:
public class PlayerManager : Photon.PunBehaviour {
3. 在 Public Variables 区域添加一个 Health 公共属性:
- [Tooltip("The current Health of our player")] //玩家的当前体力值
- public float Health = 1f;
4. 在 MonoBehaviour CallBacks 区域添加下面这两个方法,然后保存 Player Manager 脚本。
- /// <summary>
- /// 当碰撞器'other'进入触发器时调用的 MonoBehaviour 方法。
- /// 如果碰撞器是射线就会影响到玩家的体力值
- /// 注:当跳跃的同时射击,你会发现自己的射线和自身发生交互
- /// 你可以把碰撞器移动稍远一些,以防止这样的 Bug 或检查光束是否属于玩家。
- /// </summary>
- void OnTriggerEnter(Collider other) {
- if (! photonView.isMine) {
- return;
- }
- //我们只对敌人感兴趣,我们可以通过标签来区分,也可以简单地检查名称
- if (!other.name.Contains("Beam"))
- {
- return;
- }
- Health -= 0.1f;
- }
- /// <summary>
- /// 每一个'other'碰撞器触摸这个触发器的时候每帧调用的 MonoBehaviour 方法
- /// 当射线持续触碰玩家时我们将继续影响体力值。
- /// </summary>
- /// <param name="other">其他碰撞器</param>
- void OnTriggerStay(Collider other) {
- //如果不是本地玩家,什么都不做
- if (! photonView.isMine) {
- return;
- }
- if (!other.name.Contains("Beam"))
- {
- return;
- }
- //当射线持续击中我们的时候我们缓慢地影响体力值,这样玩家不得不移动来防止被击
- 杀。
- Health -= 0.1f*Time.deltaTime;
- }
5. 保存 PlayerManager 脚本。
首先,两方法几乎完全相同,唯一不同的是,我们在 TriggerStay 中使用Deltatime 来减少体力值,递减速度不应与帧率相关。这是一个重要的概念,通常适用于动画,但在这里,我们也需要这样,我们希望体力值在所有的设备上以可预见的方式减少,在一个更快的计算机上,你的体力值减少得越快 :) ,这是不公平的,Deltatime 来保证该值减少的一致性。如果你有问题的话可以给我们发邮件,通过搜索 Unity 社区来学习 DeltaTime,直到你完全了解 DeltaTime概念,它是必要的。第二重要的方面,那现在应该明白的是,我们只影响本地玩家的体力值,这就是为什么我们在 PhotonView 不是 Mine 时提前退出方法。
最后,我们只想如果击中我们的对象是射线时才影响体力值,所以我们使用标签"beam"来检查,这是我们如何标签我们的射线对象的。为了便于调试,我们使体力值作为一个公共浮点数,以便在我们等待 UI 被构建时检查它的值。
好吧,这看起来全都做对了吗?呃...健康系统没有考虑到玩家的游戏结束状态是不完整的,当体力值达到 0 时触发该状态,让我们现在来完成这个。
Health checking for game over | 检查体力来结束游戏
为了使事情简单,当玩家的体力值达到 0,我们就离开房间,如果你记得,我们已经在 GameManager 脚本里创造了离开房间的方法。如果我们可以重复使用这种方法,而不是为相同的功能编码两次,这才是最好的做法。为同样的结果重复代码是你应该不惜一切代价避免的。这也将是一个很好的介绍时机来引入一个非常方便的编程概念,"Singleton"。虽然这个主题本身可以填补一些教程,我们只会做"Singleton"最小化实现。要了解 Singleton,它们在 Unity 上下文中的变体,以及它们如何帮助创建强大的功能是非常重要的,将为您节省很多麻烦。所以不要犹豫在这个教程之外花更多时间来了解更多关于 Singleton 的知识。
1. 打开 GameManager 脚本
2. 在 Public Properties 区域添加这个变量
static public GameManager Instance;
3. 在 Start()方法中添加这个代码
Instance = this;
4. 保存 GameManager 脚本
注意我们用[static]关键字来修饰实例变量,这意味着这个变量可无需持有一个指向 GameManager 实例的指针就可用,所以你可以简单地在你的代码的任何地方使用 GameManager.instance.xxx() 。这确实很实际!让我们看看单例是如何在逻辑管理上适合我们的游戏。
1. 打开 PlayerManager 脚本
2. 在 Update()方法中,在我们 ProcessInputs 之后,添加下面这个判断,并保存 PlayerManager 脚本
- if ( Health <= 0f)
- {
- GameManager.Instance.LeaveRoom();
- }
3. 保存 PlayerManager 脚本
注意,我们考虑到体力值可能是负的,因为激光束造成的损害强度不同。注意我们没有得到任何组件就使用了GameManager实例的LeaveRoom()公共方法,我们只是依赖于这样的事实:我们认为在当前场景中的一个游戏对象上有一个 GameManager 组件。
好吧,现在我们进入网络!
本部分将引导您创建 CameraWork 脚本来让摄像机在你玩游戏时跟随你。
这一部分与网络无关,所以它会保持简短。
创建 CameraWork 脚本
1. 创建一个新的 C #脚本,叫做 CameraWork
2. 用下面的代码来替换 CameraWork 脚本里面的内容 :
- using UnityEngine;
- using System.Collections;
- namespace Com.MyCompany.MyGame
- {
- /// <summary>
- /// 相机的工作。跟踪目标
- /// </summary>
- public class CameraWork : MonoBehaviour
- {
- #region Public Properties //公共属性区域
- [Tooltip("在本地 X-Z 平面到目标的距离")]
- public float distance = 7.0f;
- [Tooltip("我们希望相机高于目标的高度")]
- public float height = 3.0f;
- [Tooltip("相机高度的平滑时间滞后.")]
- public float heightSmoothLag = 0.3f;
- [Tooltip("让相机可以从目标抵消垂直,例如提供更多的视野且更少的地面。")]
- public Vector3 centerOffset = Vector3.zero;
- [Tooltip("如果预设的组件正在被 Photon Networ 实例化把这个属性设置为false,并在需要的时候手动调用 OnStartFollowing()")]
- public bool followOnStart = false;
- #endregion
- #region Private Properties //私有属性区域
- //把目标的 Transform 缓存
- Transform cameraTransform;
- //如果目标丢失或相机被切换,请在内部保持一个标志来重新连接
- bool isFollowing;
- //表示当前的速度,这个值在每次你调用 SmoothDamp()时被修改。
- private float heightVelocity = 0.0f;
- //代表我们试图使用 SmoothDamp()来达到的位置
- private float targetHeight = 100000.0f;
- #endregion
- #region MonoBehaviour Messages //Mono 行为消息区域
- void Start()
- {
- //如果想要的话开始跟随目标
- if (followOnStart)
- {
- OnStartFollowing();
- }
- }
- /// <summary>
- /// 在所有 Update 方法被调用后才调用的 MonoBehaviour 方法。这对于让脚本按顺序执行是有用的。例如一个跟踪相机应该总是在 LateUpdate 里实现,因为它的追踪对象可能已经在 Update 中移动了。
- /// </summary>
- void LateUpdate()
- {
- //在关卡加载时目标 Transform 也许没有被破坏,因此,我们需要覆盖
- //小概率事件,每一次我们加载一个新场主摄像机是不一样的,在发生时重连
- if (cameraTransform == null && isFollowing)
- {
- OnStartFollowing();
- }
- //只在被明确声明时跟随
- if (isFollowing) {
- Apply ();
- }
- }
- #endregion
- #region Public Methods //公共方法区域
- /// <summary>
- /// 引发开始跟随事件。
- /// 当你不知道在编辑什么时候跟随时使用这个,通常实例由 photon 网络管理。
- /// </summary>
- public void OnStartFollowing()
- {
- cameraTransform = Camera.main.transform;
- isFollowing = true;
- //我们不平滑任何东西,我们直接找到正确的相机拍摄
- Cut();
- }
- #endregion
- #region Private Methods //私有方法区域
- /// <summary>
- /// 平滑地跟踪目标
- /// </summary>
- void Apply()
- {
- Vector3 targetCenter = transform.position + centerOffset;
- //计算当前与目标旋转角度
- float originalTargetAngle = transform.eulerAngles.y;
- float currentAngle = cameraTransform.eulerAngles.y;
- //当摄像机被锁定的时候适应真正的目标角度
- float targetAngle = originalTargetAngle;
- currentAngle = targetAngle;
- targetHeight = targetCenter.y + height;
- //对高度进行平滑缓冲
- float currentHeight = cameraTransform.position.y;
- currentHeight = Mathf.SmoothDamp( currentHeight, targetHeight, ref
- heightVelocity, heightSmoothLag );
- //把角度转化成旋转,这样我们就可以重置像机的位置了
- Quaternion currentRotation = Quaternion.Euler( 0, currentAngle, 0 );
- //把摄像机在 x-z 平面上的位置设置成:到目标的距离
- cameraTransform.position = targetCenter;
- cameraTransform.position += currentRotation * Vector3.back *
- distance;
- //设置摄像机的高度
- cameraTransform.position = new Vector3( cameraTransform.position.x,
- currentHeight, cameraTransform.position.z );
- //总是看向目标
- SetUpRotation(targetCenter);
- }
- /// <summary>
- /// 将摄像机直接定位到指定的目标和中心。
- /// </summary>
- void Cut( )
- {
- float oldHeightSmooth = heightSmoothLag;
- heightSmoothLag = 0.001f;
- Apply();
- heightSmoothLag = oldHeightSmooth;
- }
- /// <summary>
- /// 设置摄像机的旋转始终在目标后面
- /// </summary>
- /// <param name="centerPos">中心位置.</param>
- void SetUpRotation( Vector3 centerPos )
- {
- Vector3 cameraPos = cameraTransform.position;
- Vector3 offsetToCenter = centerPos - cameraPos;
- //只围绕 Y 轴生成基础旋转
- Quaternion yRotation = Quaternion.LookRotation( new
- Vector3( offsetToCenter.x, 0, offsetToCenter.z ) );
- Vector3 relativeOffset = Vector3.forward * distance + Vector3.down *
- height;
- cameraTransform.rotation = yRotation *
- Quaternion.LookRotation( relativeOffset );
- }
- #endregion
- }
- }
3. 保存脚本 CameraWork。
如果你刚刚开始实时 3D、向量和四元数为基础的数学,跟随玩家背后的数学是棘手的。所以我不会在这个教程内不遗余力地试图解释。但是如果你好奇并想学习,不要犹豫与我们联系,我们会尽全力解释。然而,这个脚本不只是关于疯狂的数学,重要的也是设置;当摄像机的行为应该积极地跟随玩家时有能力去控制。并且理解这一点很重要:为什么我们在摄像机跟随玩家时想要去控制。通常情况下,如果它总是跟随玩家,让我们想象会发生什么。当你连接到一个满是玩家的房间,在其他玩家上的每一个 CameraWork 脚本都会努力控制主摄像来让其看到自己的玩家…好吧,我们不想这样,我们只想跟随本地玩家。一旦我们定义了这个问题,我们只有一个摄像机,但有几个玩家的实例,我们可以很容易地找到几种方法去解决这个问题。
1. 只在本地玩家上依附 CameraWork 脚本。
2. 通过关闭 CameraWork 来控制其行为,并根据是否是本地玩家来进行跟随。
3. 把 CameraWork 依附到摄像机上并时刻注意场景中的本地玩家实例,然后只跟随本地玩家。
这 3 个选项并不详尽,可以找到更多的方法,但从这 3 个,我们将任意选择第二个。上述选项没有一个更好或更坏,但这可能是一个需要编码最少和最灵活… "有趣..." 我听到你说 :)
我们已经暴露了公共属性 followOnStart ,如果我们想用在非网络环境上就将其设置为 true,例如在我们的测试场景里,或完全不同的场景里。在我们的网络游戏运行的时候,当检测到本地玩家时我们会调用OnStartFollowing() 公共方法。这将在 Player Prefab Networking 章里创建并解释的 PlayerManager 脚本里完成。
本部分将引导您修改玩家预制体。我们首先创建了一个玩家,但现在我们要修改它使其在我们在 PUN 环境里使用的时候能够工作和兼容。非常轻量的修改,但概念是至关重要的。所以这部分确实很重要。
首先,我们需要在我们的预设上添加一个 PhotonView 组件。一个 PhotonView 就是将不同的实例在每台电脑上连接起来的组件,并定义观察什么组件以及如何观察这些组件。
1. 添加一个 PhotonView 组件到 My Robot Kyle
2. 设置 Observe Option 为 Unreliable On Change
3. 注意 PhotonView 提醒你,你需要观察的东西对其有任何影响
让我们一起来设置我们将要观察的,然后我们将会回到这个 PhotonView 组件并完成它的设置。
我们想要同步的一个明显的特征是角色的位置和旋转,这样当一个玩家移动时,其他计算机上的其他玩家代表以同样的方式移动和旋转。您可以直接观察到自己脚本中的 Transform 组件,但是由于网络延迟和数据同步的有效性,您会遇到很多麻烦。幸运的是为了让这个共同的任务更容易,我们将使用一个[Photon Transform View]组件,作为 Transform 组件和 PhotonView 之间的中间人。基本上,用这个组件把你要做的所有难点都解决了。
1. 添加一个 PhotonTransformView 组件到'My Robot Kyle'预设
2. 如下图所示,拖拽 PhotonTransformView 组件的标题将其放进 PhotonView 组件的第一个观察组件条目里(文中的 Gif 图请参考官网)
3. 现在检查 PhotonTransformView 里的 Synchronize Position
4. 在 Synchronize Position 里,选择"Lerp Value for Interpolation Option"
5. 设置 Lerp Speed 为 10 (这个值越大就能越快追上)
6. 如下图所示,检查 SynchronizeRotation
提示: 注意如下图所示的蓝皮书帮助链接。点击它揭示信息,了解所有的各种设置和它们的影响。
PhotonAnimatorView 组件也使得网络设置轻而易举,并且帮你省去了大把的时间和麻烦。它允许您定义哪些层权重和哪些参数要同步。层权重只需要在游戏中发生改变时同步,有可能逃脱而完全没有同步。参数也一样。有时它可能从其他因素中获得动画值。一个速度值是一个很好的例子,你不必需要精确地同步这个值,你可以使用同步位置更新来估计它的值。如果可能的话,尽量少同步参数。
1. 添加一个 PhotonAnimatorView 组件到 My Robot Kyle 预设
2. 把 PhotonAnimatorView 拖拽到 PhotonView 组件的新的观察组件条目上
3. 现在,在同步参数中,设置 Speed 为 Discrete
4. 设置 Direction 为 Discrete
5. 设置 Jump 为 Discrete
6. 设置 Hi 为 Disabled
每个值都可以被禁用,或同步(无论是离散或连续)。在我们的情况下,因为我们不使用 Hi 参数,我们将禁用它,并节省带宽。Discrete synchronization 离散同步意味着一个值每秒被发送 10 次(在OnPhotonSerializeView 里面)。接收客户端把值传递给本地的动画。
Continuous 连续同步意味着 PhotonAnimatorView 每帧运行。当 OnPhotonSerializeView被调用(每秒 10 次),该值自从上次被调用就开始记录,再一起发送。接收客户端然后按序列应用这些值,以保持平稳过渡。虽然这个模式是平滑的,它也发送更多的数据来实现这种效果。
在网络用户控制的一个关键方面是相同的预制件将在所有的玩家客户端上被实例化,但其中只有一个角色是代表在当前电脑前玩游戏的用户,所有其他实例代表在其他电脑上玩游戏的其他用户。因此,首先要考虑的就是输入管理。我们怎样才能使在一个实例上启用输入而不是其他,如何知道哪个是正确的?引入 isMine 概念。
让我们来一起编辑我们之前创建的 PlayerAnimatorManager 脚本。在当前形态下,该脚本并不知道其中的区别,让我们来一起实现这个功能。
1. 打开脚本 PlayerAnimatorManager
2. 把 PlayerAnimatorManager 类 的 基 类 从 MonoBehaviour 改 成Photon.MonoBehaviour,从而很方便地暴露 photonView 组件。
3. 在 Update() 调用里,在最开始插入下面的代码
- if( photonView.isMine == false && PhotonNetwork.connected == true )
- {
- return;
- }
4. 保存脚本 PlayerAnimatorManager
5. 好的,如果实例由'client'应用控制 PhotonView.isMine 将是 true,意味着这个实例代表的就是在这台电脑上玩这个游戏的本地玩家。所以如果它是 false,我们不想做任何事,并且像我们早先设置好的那样仅仅依靠 PhotonView 组件来同步Transform 和动画组件。
6. 但是,为什么又要在if语句里面判断 PhotonNetwork.connected == true ?呃 嗯 :) 因为在开发过程中,我们也许想要在没有连接的情况下测试这个预设。例如在虚拟场景中,只是创建和验证与网络功能无关的代码。所以有这个语句我们就可以允许在不联网的情况下输入。这是一个非常简单的技巧,将在开发过程中大大提高您的工作流程。
和上一部分讲的输入一样,玩家只有一个游戏视图,所以我们需要 CameraWork 脚本,来控制摄像机只能跟着本地玩家,而不是其他玩家。这就是为什么 CameraWork 脚本有定义何时跟随的能力。让我们一起修改 PlayerManager 脚本来控制 CameraWork 组件。
1. 打开 PlayerManager 脚本.
2. 把下面的代码插入到 Awake() 和 Update() 方法之间。
- 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);
- }
- }
3. 保存脚本 PlayerManager
首先,获取到 CameraWork 组件,我们想到,如果我们找不到它,我们记录一个错误。然后,如果 photonView.isMine 是 true ,这意味着我们需要跟随这个实例,所以我们调用_cameraWork.OnStartFollowing() ,该方法有效地让相机跟随场景中的对应实例。所有其他玩家实例将把它们的 photonView.isMine 设置为 false ,这样它们的对应_cameraWork 将什么也不做。使其运行起来的最后一个改动:
1. 如下图所示,在 My Robot Kyle 预设上的 CameraWork 组件上禁用 Follow on Start
这样就把跟随玩家的逻辑都交给了 PlayerManager 脚本,该脚本将按照上面描述的那样调用 _cameraWork.OnStartFollowing() 。
发射同样遵循上述的输入原则,它只在 photonView.isMine 是 true 的时候工作:
1. 打开脚本 PlayerManager
2. 用 if 语句来包裹输入调用。
- if (photonView.isMine) {
- ProcessInputs ();
- }
3. 保存脚本 PlayerManager
然而,在测试时,我们只看到了本地玩家射击。我们需要查看其他实例什么时候射击!我们需要一个同步整个网络发射的机制。要做到这一点,我们要去手动同步 IsFiring 布尔值,直到现在,我们得到了 PhotonTransformView 和 PhotonAnimatorView 来我们做所有变量的内部同步,我们只有调整那些通过 Unity 的 Inspector 方便地暴露出来的值,但在这里我们所需要的对于你的游戏来说是非常具体的,所以我们需要手动来做。
1. 打开脚本 PlayerManager
2. 实现 IPunObservable
3. 在 IPunObservable.OnPhotonSerializeView 里面加入下列代码
- if (stream.isWriting)
- {
- // 我们拥有该玩家:把我们的数据发送给其他玩家
- stream.SendNext(IsFiring);
- }else{
- //网络玩家,接收数据
- this.IsFiring = (bool)stream.ReceiveNext();
- }
4. 保存脚本 PlayerManager
5. 回到 Unity 编辑器,在资源里面选择 My Robot Kyle 预设,并在 PhotonView 组件上添加一个观察条目,把 PlayerManager 组件拖拽到该条目上。
没有这最后一步,IPunObservable.OnPhotonSerializeView 绝不会被调用,因为没有被 PhotonView 观察。
在这个 IPunObservable.OnPhotonSerializeView 方法里面,我们传递一个 stream 变量,这是将被通过网络发送的,并且如果我们进行读写数据也会调用。我们只有是本地玩家( PhotonView.isMine == true )时才写入,否则我们就读取。
由于 stream 类有助手,知道要做什么,我们仅仅依靠 stream.isWriting 来知道在当前实例想要什么。如果我们想要写入数据,我们把 IsFiring 值追加到数据流,使用 stream.SendNext() ,该方法非常方便地隐藏了所有数据序列化的艰苦工作。如果我们想读取,使用stream.ReceiveNext()。
好了,要完成为网络更新玩家的功能,我们将同步的健康值,使每个玩家的实例将有正确的健康值。这和上述讲的 IsFiring 值是完全相同的原则。
1. 打开脚本 PlayerManager
2. 在 IPunObservable.OnPhotonSerializeView 里面,在你 SendNext 和 ReceiveNext 变量IsFiring 之后,为 Health 执行一样的操作
- if (stream.isWriting)
- {
- // 我们拥有该玩家:把我们的数据发送给其他玩家
- stream.SendNext(IsFiring);
- stream.SendNext(Health);
- }else{
- //网络玩家,接收数据
- this.IsFiring = (bool)stream.ReceiveNext();
- this.Health = (float)stream.ReceiveNext();
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。