【译】Unity3D 多人在线游戏综合开发文档(Photon Network)
原文:https://www.photonengine.com/zh-CN/PUN
Photon Unity Networking v1.78 Generated by Doxygen 1.8.7 Tue Nov 1 2016 09:53:23
前言:已发布过了 PUN教程 的译文,这里补发综合开发文档的译文,以便于开发者在开发中进行参考。
第一章 概述
Photon是一个快速、简洁和灵活的实时多人游戏开发框架。Photon包含一个服务器和几个客户端针对主要平台的SDKs。
Photon Unity Network (简称PUN)是一个特别的客户端框架,它的目标是重新实现和优化Unity内置的网络 (可能是Unet)。在引擎盖下,它使用Photon的通信和匹配玩家功能。
由于PhotonNetwork 的API和Unity的内置解决方案是非常类似的,有Unet使用经验的用户应该立即有如鱼得水的感觉。一个自动转换器可以帮助你把现有的多玩家项目等效地移植到Photon。
提供完整的源代码,所以你可以扩展这个插件来支持任何你需要的类型的多玩家游戏。
这个插件是和托管的Photon Cloud服务兼容的,Photon云服务器为你运行Photon服务器。 插件里面有一个安装窗口为你在一分钟内免费注册。
最显著的特征:
l 简单得要死的API
l 提供的服务器可以作为托管服务(开发免费)或作为“企业预置”
l 跨服负载均衡工作流程表(无需额外的开发)
l 性能卓越的Photon服务器
l 没有直接的P2P和没有NAT穿透需要
l 离线模式:在单人游戏模式中重用你的多玩家代码
如果你知道怎样使用Unity的网络,你应该会觉得如鱼得水。你也许想要在你的一个项目中运行转换器(快捷启动向导:ALT+P)并潜入代码和参考。
GeneralDocumentation (API) | 综合文档
继续阅读GeneralDocumentation.
MarcoPolo Tutorial | 马可波罗教程
或你可以先做MarcoPolo Tutorial。然后只需参考General Documentation即可。(马可波罗这个教程相当旧了,建议从PUN Basics Tutorial这个基础教程开始学习。PS:教程里的超链接的服务器都是国外的,建议使用翻墙软件!)
第二章 综合文档
Photon、订阅、托管选项和如何开始的概述。
2.1 Photon | 概述
不像Unity的内建网络,PUN(Photon Unity Network首字母缩写,后文同,不再重复解释)总是连接一个为玩家提供房间、匹配玩家并提供同房间通信的专用服务器。Photon Unity Networking在后台不止使用一个服务器:
几个“游戏服务器”运行特定的房间(匹配的),同时一个“主服务器”保持追踪各个房间并匹配玩家。
服务器端你有两个选项。
Exit Games Cloud是一个为你提供托管和负载均衡的Photon服务器(该服务器由ExitGames公司全局管理)的服务。提供免费试用,而且商业通途的订阅成本是相对低廉的。(这个云端类似于国内的阿里云和腾讯云,它们都是提供可托管的云服务器服务)
服务运行一个固定的逻辑,所以你不能实现你自己的服务器端游戏逻辑。相反,客户端需要被授权。
客户端通过“applicationid”分隔,这关系到您的游戏标题和“游戏版本”。有了这个,你的玩家不会与另一个开发或旧的游戏迭代发生冲突。
在资源商店购买的订阅
如果你是在资源商店(资源商店是指Unity里面的Asset Store,后文同)里购买的一个带Photon云服务订阅的插件包,跟着这些步骤:
l 注册Photon云端账户:exitgames.com/en/Account/SignUp
l 从仪表盘创建一个应用并获取你的AppID
l 发送一封带如下类容的邮件到developer@exitgames.com:
– 你的姓名和公司(如果有的话)
– 资源商店的发票/购买 ID(购买后会收到带发票的邮件)
– PhotonCloud AppID(仪表盘获得)
Photon Server SDK | Photon服务器软件开发工具集
作为替代的Photon云服务,你可以运行自己的服务器和开发服务器端逻辑在我们的“负载平衡”C#解决方案上。这将为您提供服务器逻辑的完全控制。
Photon服务器 SDK(SDK是Software Development Kit的首字母缩写,意思是软件开发工具包。)可以在这里下载: www.exitgames.com/en/OnPremise/Download
五分钟内启动服务器教程: doc.exitgames.com/en/onpremise/current/getting-started/photon-server-in-5min
PUN - 第一步
当你导入PUN时,“向导”窗口会弹出。输入您的电子邮件地址登记到云端,跳过这一步输入一个现有的帐户的AppID或切换到“自托管”Photon 输入您的服务器的地址。
如下图所示,这将在项目中(在PhotonServerSettings里)创建一个云服务或您自己的Photon服务器的配置。
PUN由相当多的文件组成,但只有一个真正重要:PhotonNetwork。这个类包含所需的所有函数和变量。如果你有定制要求,你也可以修改源文件-毕竟这个插件只是Photon的实现。
要从UnityScript中使用PUN, 把"PhotonNetwork" 文件夹和"UtilityScripts"文件夹都移动到Assets文件夹下。
为了向你展示这个API(API是Application Program Interface的首字母缩写,意思是应用程序接口)是如何工作的,这里马上就有几个例子。
2.1.1 主服务器和大厅
PUN总是使用一个主服务器和一个或多个游戏服务器。主服务器管理当前运行在不同的游戏服务器上的游戏,并在当你加入或创建一个房间时将提供一个游戏服务器地址。PUN(客户端)自动切换到游戏服务器。
个人匹配被称为房间。他们是相互独立的,并以名称为标记ID。房间被分组成一个或多个大厅。大堂是匹配的可选部分。如果你明确地不使用自定义大厅,PUN将使用一个由所有房间组成的单一的大堂。
默认情况下,在连接后PUN将加入默认的大厅。这个大厅向客户发送一个现有的房间列表,所以玩家可以选择一个房间(通过名称或一些列出的属性)。使用PhotonNetwork.GetRoomList()方法来进入当前列表。列表在时间间隔内被更新来保持低流量。
客户端不必加入一个大厅来加入或创建房间。如果你不想在您的客户端显示房间列表,在你连接之前设置PhotonNetwork.autoJoinLobby= false,你的客户端将会跳过大堂。
你可以使用一个以上的大厅来组织你游戏所需的房间列表。PhotonNetwork.JoinLobby是加入一个特定的大厅的方法。你可以在客户端上完成操作-服务器将会保持跟踪它们。只要名称和类型相同,对应的TypedLobby将对于所有的客户端都一样。
一个客户总是只在一个大厅,而在一个大厅里,创造一个房间也将涉及到这个大厅。多个大厅意味着客户端得到更短的房间列表,这是很好的。房间列表是无限的。
在JoinRoom,JoinRandomRoom和CreateRoom中的一个参数使你可以无需加入就可以选择大厅。
玩家不会注意到对方在大厅里,也不能发送数据(以防止当它会变得拥挤时发生问题)。
服务器都在专用的机器上运行-没有所谓的玩家托管的“服务器”。您不必费心记住服务器的组织,因为API为你都隐藏了。
PhotonNetwork.ConnectUsingSettings(“v1.0“);
上面的代码是任何使用PhotonNetwork功能的应用所必须的。它设置你的游戏客户端游戏版本和使用安装向导配置(保存在:PhotonServerSettings,上文中有截图)。当你自己托管Photon服务器时也可以使用该向导。另外,使用Connect()方法连接服务器时你可以忽略PhotonServerSettings文件。
版本控制
Photon的负载均衡逻辑是使用AppID区分你的和别人的玩家。同样的游戏版本将新客户端的和旧客户端的玩家区分开来。由于我们不能保证不同PUN版本之间相互兼容,我们在你的游戏版本发送之前添加PUN版本到你的游戏版本(从PUN v1.7版本开始)。
创建和加入游戏
下一步,你会想加入或创建一个房间。下面的代码展示了一些必要的功能:
1 2 3 4 5 6 7 8 9 10 | //加入一个房间 PhotonNetwork.JoinRoom(roomName); //创建这个房间 PhotonNetwork.CreateRoom(roomName); //如果该房间已经存在就会失败,并调用:OnPhotonCreateGameFailed //、尝试加入任何随机游戏 PhotonNetwork.JoinRandomRoom(); //如果没有匹配的游戏就会失败并调用: OnPhotonRandomJoinFailed |
目前正在运行的游戏列表是由主服务器的大厅提供的。它可以像其他房间一样被加入,但只提供和更新的房间列表。PhotonNetwork插件会在连接后自动加入大堂。
当你加入一个房间时,列表将不再更新。
显示房间列表(在大厅里):
1 2 3 4 | foreach (RoomInfo game in PhotonNetwork.GetRoomList()) { GUILayout.Label(game.name + " " + game.playerCount + "/" + game.maxPlayers); } |
另外,游戏可以使用随机配对:它会尝试加入任何房间,并且如果没有一个房间可以容纳该玩家就会失败。在这种情况下:创建一个没有名字的房间,并等待直到其他玩家随机加入。
高级匹配和Room属性
完全随机的配对并不总是玩家喜欢的东西。有时你只想玩一个特定的地图或只是2vs2。
在Photon云和负载均衡里,你可以任意设定房间的属性和那些在JoinRandom加入的玩家过滤器。
房间属性和大厅
房间属性是同步到所有房间中的玩家,并在跟踪当前的地图、回合、开始时间等信息时很有用。这些信息被当作字符串键值的散列表来处理。优选短键。
您也可以将选定的属性发送到大厅。这使得它们可以被列表列出,也可以被随机配对。并不是所有大厅里的房间属性都是有趣的,所以你在创建房间的时候定义一系列的属性。
1 2 3 4 5 | Hashtable roomProps = new Hashtable() { { "map" , 1 } }; //房间属性哈希表 string [] roomPropsInLobby = { "map" , "ai" }; //房间属性字符串数组 RoomOptions roomOptions = new RoomOptions() { customRoomProperties = roomProps,customRoomPropertiesForLobby = roomPropsInLobby } //房间选项 CreateRoom(roomName, roomOptions, TypedLobby.Default) //创建房间 |
请注意“ai”还不是房间属性里的一个键值。它不会出现在大厅,直到它在游戏里通过Room.SetCustomProperties()方法被设置。当你改变“map”或“ai”的值时,他们也会在大厅里在一个很短的延迟后被更新。
保持列表短以确保性能不会受到加载列表影响。
在JoinRandom中过滤房间属性
在JoinRandom方法中,你可以传递一个包含该方法所期望的房间属性和最大玩家值的哈希表。当服务器为你选择一个“合适的”房间时这些作为过滤器。
1 2 | Hashtable expectedCustomRoomProperties = new Hashtable() { { "map" , 1 } }; JoinRandomRoom(expectedCustomRoomProperties, 4); |
如果你传递更多的过滤器属性,一个房间与它们相匹配的机会较低。最好限制选项。
确保你从不过滤那些大厅没有的属性(见上文)。
MonoBehaviour回调函数
PUN使用几个回调让你的游戏知道状态的变化,如“连接”或“加入游戏”。所有您需要做的是在任何MonoBehaviour中实现合适的方法,并在事件发生时被调用。
为了获得一个良好的可用回调函数的总览,可以在Photon.PunBehaviour类中看一看。如果你让你的脚本继承PunBehaviour(代替MonoBehaviour),您可以很容易地重写个人回调函数。如果你开始键入“override”关键字,你的编码IDE应该为您列出了回调,所以它们在编码时也很容易找到。(IDE即Integrated Development Environment的首字母缩写,意思是集成开发环境,例如微软的VS,VS即Visual Studio)
这包括建立游戏房间的基本知识。接下来是游戏中的实际交流。
在房间里发送信息
在一个房间里,你可以发送网络信息给其他连接的玩家。此外,你能够发送缓冲的消息,缓存消息也将被发送到未来连接的玩家(以生成玩家为例)。
发送消息可以用两种方法来完成。无论是RPCs(RPC是Remote Procedure Call的首字母缩写,意思是远程过程调用,下文会有详细解释)还是利用PhotonView(Photon消息视图,详见后文)属性OnSerializePhotonView。虽然有更多的网络互动。你可以监听回调函数来检测特定的网络事件(如OnPhotonInstantiate, OnPhotonPlayerConnected)并且你可以触发它们中的一些事件(也就是说会调用实例化方法PhotonNetwork.Instantiate)。不要担心,如果你困惑的最后一段,下一段我们将解释每一个主题。
在PUN里使用组
当组在任何PhotonView上被改变时是不被同步的。保持photonviews在所有客户端上在同一组里是由开发人员决定的,如果需要这样的话。对同样的photonviews使用不同的组数在几个客户端会导致一些不一致的行为。(PhotonView是用于发送视图消息的脚本组件)
一些网络消息只在接收端检查他们的接收组,即:
l RPCS是针对单人(或主客户端)
l RPCS被缓冲(AllBuffered全部缓冲/OthersBuffered其他缓冲)
l 这包括 PhotonNetwork.Instantiate(因为它被缓冲)。
这是技术原因:Photon服务器只支持不被缓存且不是针对特定的演员的信息群体。这在未来可能会改变。
Photon视图
PhotonView是用于发送消息(消息是指RPCs和OnSerializePhotonView)的脚本组件。你需要将该脚本挂在你的游戏中的游戏对象上。请注意,PhotonView和Unity里的NetworkView很相似。
为了发送消息和可选的实例化/分配其他的PhotonViews,在你的游戏中每时每刻都需要至少一个PhotonView.
要添加一个PhotonView脚本组件到一个游戏对象上,简单地选择一个游戏对象并使用: “Components/Miscellaneous/PhotonView”。
观察变换
如果你将Transform挂到PhotonView的观察属性上,你可以选择同步Position、Rotation和Scale或者在玩家身上的这些字段组合。这对那些原型或小游戏可以是一个伟大的帮助。注:任何观察到的值的一个变化将发送所有观察到的值-不仅是单独改变了的值。此外,更新不是平滑的或以内插值替换的。
观察MonoBehaviour
一个PhotonView 可以被设置来观察一个MonoBehaviour。在这种情况下,该脚本的OnPhotonSerializeView方法将会被调用。这种方法被调用来根据该脚本是否被本地玩家控制来读写一个对象的状态。
下面简单的代码显示了如何在几行代码中添加字符状态同步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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(); } } |
如果你发送”ReliableDeltaCompressed”的东西,确保总是以同样的顺序写入数据到二进制流(二进制0和1所组成的数据像是溪流一样,所以在英文中被称为Stream)。如果你没有写入数据到PhotonStream,更新不发送。这在暂停时可能有用。现在,以另一种方式交流:RPCs。
2.1.2 Remote Procedure Calls | 远程过程调用
远程过程调用(RPCs)正是顾名思义:方法可以在同房间的远程客户端上被调用。要启用远程对MonoBehaviour中某个方法的调用,你必须应用注解:[PunRPC ]。
一个PhotonView实例需要在同一游戏对象上,来调用标记功能。
1 2 3 4 5 | [PunRPC] //在需要远程过程调用的方法前加该注解 void ChatMessage( string a, string b) { Debug.Log( "ChatMessage " + a + " " + b); } |
要在任何脚本中调用的该方法,你需要获取一个PhotonView对象。如果你的脚本是从Photon.MonoBehaviour派生的,它就会有一个photonView字段。任何MonoBehaviour或游戏对象可以使用:PhotonView。Get(this)方法来获得它的PhotonView 组件,然后在它上面调用RPC。
1 2 3 4 | //获取PhotonView组件 PhotonView photonView = PhotonView.Get( this ); //调用RPC方法来远程过程调用上面注解的ChatMessage方法 photonView.RPC( "ChatMessage" , PhotonTargets.All, "jup" , "and jup!" ); |
因此,不是直接调用目标方法,而是在一个PhotonView上调用RPC()方法。提供调用方法的名称,哪些玩家应该调用该方法,然后提供一个参数列表。
注意:用于RPC()方法的参数列表必须符合预期的参数个数!如果接收客户端找不到匹配的方法,则会记录一个错误。这个规则有一个例外:RPC方法的最后一个参数的类型可以是PhotonMessageInfo类型,这将为每个调用提供一些语境。
1 2 3 4 5 | [PunRPC] void ChatMessage( string a, string b, PhotonMessageInfo info) { Debug.Log(String.Format( "Info: {0} {1} {2}" , info.sender, info.photonView, info.timestamp)); } |
RPCs和负载水平的时机
RPCs在远程客户端的具体PhotonViews上被调用,并且始终以与之匹配的客户端为目标。如果远程客户端不知道匹配的PhotonView,RPC丢失。
丢失RPCs一个典型的原因是当客户端加载和设置等级时,一个客户更快或在房间里更长一段时间,并发送重要的RPCs到那些在其他客户端上还没有加载好的目标对象。同样的情况发生在RPCs缓冲时。
解决方法是在加载场景的时候暂停消息队列。下面的代码展示了你可以怎样操作:
1 2 3 4 5 6 7 8 9 10 11 12 | private IEnumerator MoveToGameScene() { //临时禁用远程网络消息的进程 PhotonNetwork.isMessageQueueRunning = false ; Application.LoadLevel(levelName); } |
或者你可以使用PhotonNetwork.LoadLevel. 它也会临时禁用消息队列。
禁用消息队列将会延迟进来的和输出的消息,直到消息队列被解锁。显然,当你准备好继续的时候解锁消息队列就非常重要了。
属于上一个加载场景但是仍然到达的远程过程调用RPCs将会在这个时候被丢弃。但是你应该能够用RPC来在两个场景间定义一个间断。
多个主题
和Unity网络之间的区别
1. 主机模型
l Unity网络是基于服务器-客户端(而不是P2P! P2P的意思是玩家对玩家或一对一,Player to player,Person to person)服务器通过Unity客户端运行(所以是通过其中一个玩家,也就是说客户端也可以是服务器,服务器也可以是客户端)
l Photon也是基于服务器-客户端,但是有一个专用的服务器;不再有由于主机离开而连接中断。
2. 连通性
l Unity网络与NAT穿透协作来提高连接:由于玩家托管网络服务器,常常连接失败是 因为防火墙/路由器等。连接永远不能被保证,有一个较低的成功率。
l Photon有一个专用的服务器,也不需要NAT穿透或其他概念。连接是100%保证。 如果,在罕见的情况下,一个连接失败,它必须是由于一个非常严格的客户端网络(例如一个 商业VPN,VPN即Virtual Private Network的首字母缩写,意思是虚拟专用网络)。
3. 性能
l Photon击败Unity网络性能。我们还没有数字来证明这一点,但库已经优化了几年了。此外,由于Unity服务器是玩家托管,延迟/信号查验通常是更糟;你依靠玩家作为服务器的连接。这些连接永远不会比你的专用Photon服务器的连接更好。
4. 价格
l 像Unity网络解决方案一样,PUN插件也是免费的。您可以为您的游戏订阅使用Photon云托管服务。或者,你可以租用自己的服务器并在上面运行Photon。免费许可证可以使多达100个玩家同时在线。其他许可证是一次性费用(因为您做托管)并且解除并发用户限制(当然,仍然会受到服务器和宽带的限制)。
5. 特点与维修
l Unity似乎并没有给他们的网络实现的太多优先。很少有功能改进和错误修正也是很少。Photon的解决方案是积极维护和部分是可用的源代码。此外,Photon已经提供了比Unity更多的功能,如内置的负载平衡和离线模式。
6. 主服务器
l Photon主服务器和朴素的UNet主服务器有一点不同的是:在我们的例子中,它是一个在所谓的“游戏大厅”里列出当前正在进行的游戏房间名称的Photon服务器。像Unity的主服务器,它将客户端转化成游戏服务器,实际的游戏在这些客户端服务器中完成。
2.1.3Instantiating Networked Objects | 实例化网络对象
在每一场游戏你需要为每一个玩家实例化一个或更多的玩家对象。下面列出各种要做的选项。
PhotonNetwork.Instantiate| 实例化方法
PUN可以通过传递一个起始位置、旋转和预设名到PhotonNetwork.Instantiate方法中来自动完成孵化一个对象。要求:预制应可以直接在Resources/文件夹下找到,这样才可以在运行时加载预置。注意网络播放器:在资源文件夹中的一切都会从默认的第一个场景加载为二进制流。在网络播放器设置下,您可以通过使用“First streamed level”指定第一级通过使用Resources文件夹中的资产。如果你为你的第一个游戏场景设置这个,如果你的预加载器和主菜单不使用Resources文件夹下的资源,它们将不会被减慢。
1 2 3 4 5 6 7 8 9 10 11 | void SpawnMyPlayerEverywhere() { //实例化方法,参数1是Resources文件夹下的预设名,2是Position,3是Rotation PhotonNetwork.Instantiate(“MyPrefabName”, new Vector3(0,0,0), Quaternion.identity, 0); //最后一个参数是可选组数,现在可以暂时忽略它 } |
那么,整个开发文档有三百多页,这里就不全部写出来了,请需要的朋友下载附件中完整翻译的PDF文档。