这个周末很神奇的状态很好,只睡了10个小时,完全沉迷学习无法自拔。
深刻的自我检讨了一番,技术方面的能力还是太浅了,之前的一些想法和行为都很羞耻,啼笑大方。
一天多看完了《网络游戏核心技术与实战》 (以下简称《网》),大为叹服。清晰说明了网络游戏的主要架构不说,关键是这是第一次看到有完整的网络游戏整个研发运营过程的完整介绍,甚至包括商业部分,而且说得还很对……
6个月前就在考虑,万一最后还是决定需要强联网的话,至少应该先把这本看完,现在看完也不晚,感觉正是时候。
网游开发其实最主要的问题感觉是难入门。市面上能找到的资料其实我也看了,但总有种盲人摸象之感,难以串起来。偶有几篇讲述“帧同步”与“状态同步”的文章,都已经是极好的了。这点上,总有点感觉只要是国外不用的技术,就别想指望在互联网上能学到了,至少是要花费大量精力。如此,就更令我敬佩那些具有分享精神的前辈们了,这些经验的分享,真的是无价之宝。
下面还是具体的转述一下书中的一小部分概念,至少可以系统性的归纳一下“网络游戏”的架构方案。
当前国内的说法
我在阅读这本书之前,综合之前看的一些分享,是把网络游戏的架构分成这么几种情况:
- 状态同步
- 服务器计算
- 客户端计算
- 帧同步
- 客户端计算
先说帧同步,原理很简单,主要是基于一个确定性假设:初始状态一致,帧率一致、每帧输入一致的情况下,理论上可以保证一致。也就是说所谓帧“同步”,其实根本没有保证什么同步,只是“碰巧”罢了。用象棋举个例子好了,帧同步就是说,假设我们有两张棋盘,两个人下棋,但是你们俩都不许碰棋子,只能由第三个人(充当网络)来进行。来我们开始,只要遵守规则,那么初始的状态肯定是一致的。你先开始,想来一个当头炮,所以吼了一句——炮二平五,这时候,第三个人听到了,就分别给你们两摆上个当头炮。轮到对方了,对方也不甘示弱,也来了个——当头炮——开玩笑怎么可能,当然是要来个马来跳,于是他说:马8进7,第三个人也负责给摆上了,就这么下去,就是个“帧同步网络游戏”啦。
听上去挺简单,但是感觉有点悬么不是?对方万一瞎走直接将军那怎么办?第三个人你是要假设他可不会下棋的(网络部分只负责同步,没有逻辑判断),直接给你老车贴脸。再来,那万一那个人忙中出错,给你和对方棋盘上摆得不一样,那可糟了,后面就全乱了。现代游戏基本都很依赖物理引擎,受限于精度,这每次运算的结果可都不一样。虽然可能只是差了0.000000000000001,但是根据Chaos(混沌,好像有人会读作【敲死】,应该是读【K奥斯】,我也觉得有点奇怪)原理,或者你喜欢说“蝴蝶效应”,一点点的误差慢慢积累,都会导致结果的巨大不同。最后的一击打没打中,很可能直接导致了生死只差呀,一次团灭与否,很可能就是胜负之差,这可怎么玩呢。具体当然是有做法,主要就是随机数全部在开始约定种子,自己实现定点数库,消灭一切不确定因素,理论上,就可以保证一致的同步了。现实中,很多RTS都是使用这种方案,像魔兽争霸、星际争霸,当然还是大家喜闻乐见的——王者荣耀。
状态同步呢,实际上是要分两种的,当然这是我自己这么分的,大多数资料上面好像一般都只说了一种情况,我也是因为用了Photon以及UNET后,感觉他们的方案明显是状态同步,但是又明显和一般说的状态同步不太一样,所以还是分开表述。
状态同步嘛,同步的当然就是状态。我觉得最好的理解方式其实就是,其实就是无线手柄分屏本地多人嘛,想象一下,用四块屏幕,大家在房间的四个角玩。游戏主机其实就是服务器,手柄无线连接上去可以当做网络,显示器也假设是网络连接上游戏机的(或者直接理解为远程桌面)。好了,搞定。现实中的MMORPG也大多确实就是这样的原理,整个游戏实际上都是在服务器上跑的,只是不需要渲染,所以可以节省大量的资源。然后你的客户端其实就是个显示器,只是从服务器获取你需要看到的内容,并渲染出来给你看。你的一切操作,都是直接被转发到服务器作为输入的。当然这也就会有很大的延迟,所以一般客户端都会做一定的“预测”,预测对了皆大欢喜,如果不对,那还是会被强制和服务器的情况保持同步,也就是一般大家经常在网络不好的时候见到的“鬼跳”了。
另一种呢,其实说白了,就是有一名玩家作为服务器,其他人都同步他的结果。这种情况,其实可以简单的当做在一台电脑上同时运行了服务端和客户端两个软件,本机的客户端自己实际上还是使用和其他客户端一样的方式(比如Socket通信)与本地的服务端连接的。这么来看,实际上也就把这种情况和上面说的服务端计算类型划了等号了。又或者每个客户端都控制由自己创造的物体,比如自己的角色等等,自己控制的物体,就是自己控制状态并同步给其他人,这样来分担运算压力,以及保证每个人自己主观的流畅度(当然还有其他原因,就不在此赘述了)。
好了,这就是目前国内能找到的资料对于同步问题的表述。大体上这么分类我觉得是没有问题的,《网》这本书,则是更加完整得给了一个回答。
《网》中对同步问题的分类
书中首先先介绍了一下基本网络拓扑结构。而具体的架构分类,则从两个角度描述,物理架构和逻辑架构。
基本网络拓扑结构
首先基本的网络拓扑结构,计算机网络基础我就不赘述了,结果上来说,网络游戏中使用一般只有:
- 星形(和作为应用的总线型)
- 全网状
解释一下,因为总线型可以理解为不存在中央节点的星形,所以被包含在了星形的应用中。
物理架构
- C/S架构
- 纯服务器型
- 反射型
- P2P架构
- 同步方式
- 异步方式
- C/S + P2P混合架构
- ad-hoc模式
我现在了解的情况,似乎国内是有人用C/S架构代指状态同步,不准确,而且误导性极强。
一般的时候提到C/S架构(客户端 / 服务器架构),主要是对比B/S架构(浏览器/服务器架构),强调用户端是浏览器还是一个Native的客户端。而到了现代的网页前端,基本都接受了RESTful的API规范,实际上完全是把前端项目完全当做一个独立端了,与服务器也是通过Ajax传递数据的形式更新。所以实际上,我也已经很多年没听过这个词了。而在这里用到,是用来和P2P的客户端到客户端区分,强调的是“服务器”的有无。
纯服务器型和反射型的区别,其实也只是是否对数据包的合法性进行校验(反射型不校验)。至于为什么要专门强调乃至于可以据此分类,我目前还理解不能。
P2P,顾名思义,就是没有服务器,数据完全是客户端之间自己交换的。这里不禁怀念一下童年,红警、帝国时代、CS。忽然想起,大学刚报到的时候,大家还没办网,于是几个已经非法带电脑来了的同学(没错,我校规定大一不许带电脑)就把电脑搬到同一个寝室,建了个无线热点,痛快地打了个通宵CS。当然现在看来,这种“局域网游戏”是不能称作“网络游戏”的(各种对战平台实际上是自己做了一个大局域网,现在想想还真的挺有想法的),真正想通过P2P的方式做一款网络游戏,就得考虑蛋疼的NAT穿透问题了……暂且不论。
同步和异步方式的问题,暂且留到逻辑架构中描述(因为书中就是这个奇怪的顺序啊,所以也是我觉得有必要自己总结一下的原因……也确实是网络同步方案之间关系密切,很难有一个方法清晰的分类)。简单理解,P2P同步基本等同于帧同步,P2P异步,则可以理解为客户端计算的状态同步。
至于C/S + P2P混合架构,书中没有详细介绍,我印象里好像确实有这样用的,在有中心服务器保证可用性的前提下,在可行的情况下(可以NAT穿透,并且玩家物理网络连接良好),使用P2P直传数据降低服务器压力,提升玩家体验。
ad-hoc 我看书中的意思也没有很清楚,大概是说所有设备都是无线连接的移动设备。感觉上面我说的无线热点打CS就算,当然明显的例子应该是用3DS面连打怪物猎人。因为是通过热点直连。而因为某台设备掉线的概率很大,所以比较适合异步方式的P2P架构,都不是主机,大家随便掉(当然想连回来也相对不容易……)
逻辑架构
好了逻辑架构书中的分类就更迷了,竟然分为了MMO和MO架构……区别还真的就是是否Massively……按照规模分的……虽然我很能理解不同规模会直接导致方案的决定,但是规模本身不该是核心分类依据吧……
吐槽归吐槽,这章其实主要还是在论述
- 星形结构
- 全网状结构
与
- 同步方式
- 异步方式
的各种搭配情况。
前面有提到,同步方式就可以理解为帧同步,异步方式就可以理解为状态同步。但是不论怎么同步,总归大家还是要交换数据的吧,这个肯定跑不了。那么怎么交换呢?我们来想象大家上课穿小纸条吧。一般都是想传给谁,就直接说给谁对吧,也就是说,每两个人之间都是直接传递过去的,有一条“专线”,这就是全网状结构了。但是如果你想专递给隔壁班暗恋的可爱的蓝孩子(老脸一红),那就不好办了。结果你机智的发现,走廊上竟然有个值日生在打扫卫生(不要问我为什么他不用上课,我高中还真的每学期每个班有两天不上课全校打扫卫生的……),于是你机智的让他帮忙传递过去。也就是说,所有纸条,都必须先传递给这位勤劳的值日生,然后再由他传递给收件方。这就是星型结构。在这个问题上,那个值日生自己是否参与传纸条就不是核心问题了,也就是说,星型结构的中心节点可以是一台专门的服务器,也可以由其中一台客户端担任。甚至可以在当做中心节点的客户端网络条件突然下降的时候,自动由另外的客户端接任。
来想想这两种方式各有什么优劣吧,全网状结构,首先的问题就是线路过多,某条线搞不好就从老师眼皮底下过呢?在有15个人传纸条的情况下,就需要105条直连的线路了,如果说在10分钟不出问题的概率是90%的话,那么105条线路10分钟都不出问题的概率……已经基本为0了。所以参与人数增加,网状结构靠谱度指数级下降,不适合玩家过多的情况。而星型结构呢?倒没有这个问题,多一个人也就多一条线而已,这个时候,最大的风险就是,这一切传递都依赖那位勤劳的值日生啊,万一他傲娇一下不传了,那整个网络就崩坏了。
那么怎么选择呢?
全网状结构,只适合玩家人数很少,可以确保网络条件良好(如局域网内),而对延迟又非常敏感的情况使用。
星型结构的主要问题是响应慢(必须经过转发)、节点一旦中断,游戏就无法恢复、需要转发,逻辑上稍微麻烦一些、作为节点的玩家传输负荷比其他玩家高,不甚公平。
所以总的来说,主要影响因素是玩家规模,次要因素是对延迟的容忍程度。
至于同步方式与异步方式,或者说帧同步与状态同步,只要抓住一个重点就好理解了。同步方式同步的是输入,事实上不保证状态一致。异步方式同步的是状态,输入是客户端自己处理的。
好啦,方案基本都说完了,但是实际上具体还是有非常多的细节在里面的,十分推荐真的有兴趣的话还是仔细阅读一遍《网》,只需要一个周末的时间,真的是收获非常大。
RPC与共享内存
还有一点想特别提到的就是这个问题。之前一直知道网络游戏的通讯方式嘛,肯定是Socket了,至于TCP和UDP的问题,书上说有一小部分(1%)路由器不允许UDP通过,所以全部使用TCP。目前我了解的情况是国内主流还是推荐使用UDP的,快嘛。还有一些传说中的有人自己定的一些更厉害的协议,这个有兴趣的话还是可以研究一下的。
Socket实际上只是发送byte[]而已,具体使用的时候主流做法还是依靠RPC(远程过程调用)封装。我们把“进程之间”调用的函数接口(RPC接口)称作“协议”。
协议设计的原则主要有这几点:
- 后端实现基本的、通用的功能,前端实现专用功能
- 前端依赖后端架构
- 协议是无状态和简单操作的集合
- 在一个地方接受外部的异常状态
这我不一一解释了,有兴趣可以看书。
我觉得特别重要的一个思想是,函数接口应该明确的分为
- 单向消息 (one-way message)
- 查询 (query)
- 查询结果 (query result)
- 通知 (notification)
单向消息就是一个无返回的函数,例如使用鼠标移动角色的时候,调用:
void move(int x, int y);
需要返回值的接口,称作“查询”,比如打开物品栏时,需要获取物品列表:
void get_inventory_list();
inventory是个游戏术语,代表所持物品的列表,这个词真的有用,感谢作者,虽然你自己在书上还拼错了(虽然更有可能是译者的锅……)
此外,肯定有人发现了,明明是“需要返回值的接口”,为什么是void的?
这是因为RPC中所有的接口都应该是异步的,所以“需要返回值”的函数都明确的以get作为前缀,而且会有一个对应的查询结果接口:
void inventory_list(int item_id[]);
最后一类,有可能需要把一些信息从服务端推送到客户端,这就是通知了。书上建议是否一定需要通知是需要仔细思考的,因为一旦需要通知,接口实际是写在客户端的,但是调用方是服务端,这样会极大增加debug难度,毕竟如果这里出现问题,因为实际调用这个函数的代码在网络的另一头,你是无法追踪下去的。
通知的例子,服务器向客户端通知敌人的行动:
void notify_move(int id, int x, int y);
接口命名的时候以notify作为前缀,就很清楚这是服务端发的通知了。
如果你问,那如果通知需要返回值怎么办呢?
好问题。这种纯RPC方式一般用于纯服务器型的C/S架构,也就是说,所有的资料其实都是服务器计算出来的,客户端只是一个“显示器”或者“浏览器”而已,当然不可能有什么事情是需要服务端向客户端请求的啦。
那么对于反射型的C/S架构呢?换句话说就是我所谓的“客户端计算的状态同步”,有这种实际上是数据双向共享需求的情况下,就很合适使用这种称为“共享内存”的方式了。
刚开始看到这个词的时候我还吓了一跳,我们在谈网络通讯的问题,跟共享内存有什么关系?结果仔细看了看,还确实……没关系……作者也明确表达了和POSIX的shm_函数完全没关系,只是为了和RPC比较。
根本区别是:在游戏逻辑中,游戏进度数据的保存处理(覆盖或者不覆盖数据)所发生的时间和地点不同。换言之,RPC传递的是变化的请求,而共享内存传递的是变化的结果。从表现上看,RPC是可以像调用本地函数一样调用远端程序的函数,共享内存则是允许某个变量在端与端之间永远保持同步,这么看还确实有点“共享内存”的意思。
看到这里,UNET使用中一直困扰我的ClientRpc Calls、Commands和SyncVars到底该如何使用的问题就迎刃而解了,原来UNET只是提供了所有需要的功能任君选择罢了,原来我一直很纠结实现一个需求的方式太多到底哪个好……再次感谢作者,并且感叹一下UNET库比我想象中更加优雅。
到目前为止的新想法
上面说到现在我实在觉得UNET不错了,昨天也仔细查了查 .NET Core的情况,也安装了SDK试了试,但是毕竟和Unity实现的标准还是不一致的,这么一想,其实继续使用Unity也是个不错的选择,实际上Unity自己做服务端是可以实现上述全部情况的,而且还都挺方便。只是感觉服务端代码和客户端代码肯定是要分两个项目了,不然岂不是打包的时候全部暴露了?但是如果分成两个项目,目前我还不清楚UNET是怎么处理跨项目的RPC调用的,同一个项目中是自动生成的列表并通过Attribute直接调用函数,这个可能还得思考并尝试一下。
此外,对于现在这个项目的架构也有了比较明确的认识,首先肯定不是P2P,而且需要服务器,所以物理架构肯定是算C/S架构,逻辑架构上,星形没得说,也不做同步,肯定是异步。之前因为觉得没有同步,但是有很多数据肯定是要存在服务端的,所以非常纠结这到底算什么。其实很简单,就是个不M不M但是O的RPG嘛,除了不用考虑玩家同步的问题,其他按照MMO的思路就好了。
今天来本来的计划是,因为昨天查了登陆以及社交等服务器Asset Store里面是有看上去很靠谱的包卖,所以想看一下,还有几个网络库也想看一下,毕竟如果不用UNET,还是需要实现RPC的,还是需要找找靠谱的库。结果这一写就花了5个小时(再次感谢乐于分享的大大们)……当然总结一下对自己还是很有帮助的XD
于是现在就去实现计划吧~
P.S. 《网》里面还是有很多对我启发很大的方面,以后有机会遇到实际问题肯定要再多聊聊的!