游戏服务端究竟解决了什么问题-UNITY3D游戏外包

游戏服务端究竟解决了什么问题-UNITY3D游戏外包

2 years ago 0 12829

1、写在前面

写这篇文章之前也挺纠结的,一方面是因为游戏服务端其实不论架构上还是具体一些逻辑模块的构建,都属于非常成熟的技术,举个简单的例子,像端游的多zone/scene/game进程+单全局进程架构,网上随便一搜能搜出来几十篇内容差不多的。另一方面是因为中国特色MMO基本上把服务端程序员整成了业务逻辑狗,很多明星团队的业务狗基本上从入职第一天开始就成天写lua、写python,纯写lua/python,你是完全无法辨别一个程序员的vision强弱区别的,结果论资排辈导致vision弱的上去了。(也许vision强的出去创业了?)你就会发现,游戏服务端的话语权到底是被谁占据了。

在我看来,游戏服务端程序员容易陷入两个误区:

第一,游戏服务端实际上要解决的并不是性能问题。一方面,即使是千人同屏的端游(姑且不论这千人同屏是不是一个中国特色的伪需求,反正我是没法将千人同屏跟游戏乐趣联系在一起的),其服务端如果进程划分得当,一个场景进程也至多只有千级别entity的压力,性能问题退化为了逻辑狗的业务素养问题。另一方面,现在端游MOBA和手游时代,开房间式场景同步已经成为主流,各种逻辑狗进化来的资深人士不需要也没必要将性能挂在嘴边了。

第二,大部分游戏服务端所谓框架的定位有误。服务端框架的设计有好有坏,判断一个设计好不好没有普适统一的标准,但是判断一个设计烂不烂一定是存在一个标准线的。简单列举几种烂设计:

烂设计基础版本。帮你定义好框架中的几种角色,你要么全盘接受,要么全不接受,不存在中间状态。但是,提供一种简单的通信机制,以及外部与框架通信的clientLib。或者能让你定制开发其中一种角色,可以写外部driver。这样,虽然架构丑一点,至少还能提供一定程度的扩展性。

烂设计进阶版本。除了满足基础版本的定义之外,还具有一些额外的烂特点:框架中的角色定义的特别二逼,举个例子,基础版本的烂设计在角色定义上可能只是大概区分了Db代理进程、Gate进程、逻辑进程,但是进阶版本会对逻辑进程进行区分,定义了不同的逻辑进程角色。这意味着什么?意味着我想写一个简单的单逻辑进程游戏是没办法用这个框架的,因为框架默认就集成进来了一堆莫名其妙的东西。更有甚者,我想要添加一种角色,是需要动手去改框架的。

说实话,正是由于这类设计的存在,我在看到类似于“游戏服务端技术含量不高”这类论断的时候,总感觉辩无可辩,因为就这两种设计而言,我甚至除了代码逻辑复杂度之外看不到跟本科毕设级别的游戏服务器有什么区别。

不知道算是不幸还是幸运,前段时间亲眼目睹了上述提到的某种设计的从无到有的过程。当然,今天写此文的目的不是为了将这种设计批判一番,每种设计的诞生都是与各种因素相关的,我们不能站在上帝视角去评判这个过程。今天写此文,是希望对自己这整整一年半的游戏服务端编码历程中的一些所思所惑做个整理,希望能带各位看官从另一个思路看游戏服务端。

2、游戏服务端究竟解决了什么问题?

从定义问题开始,简单直接地说,一套游戏服务端开发框架应该具有下面两种能力:

定义了client到server、server到client、server到server的消息pipeline。

描述了游戏世界状态的维护方式。

下面就从这两点来展开这篇文章。

3、消息pipeline

3.1 经典消息pipeline

3.1.1 场景同步

当讨论到游戏服务端的时候,我们首先想到的会是什么?要回答这个问题,我们需要从游戏服务端的需求起源说起。

定义问题

游戏对服务端的需求起源应该有两个:

第一种是单机游戏联网版,实现为主客机模式的话,主机部分可以看做服务端。

第二种是所有mmo的雏形mud,跟webserver比较类似,一个host服务多clients,表现为cs架构。

第一种需求长盛不衰,一方面是console游戏特别适合这一套,另一方面是最近几年手游起来了,碎片化的PVE玩法+开房间式同步PVP玩法也得到验证,毕竟MMO手游再怎么火也不可能改变手游时间碎片化的事实的,最近的皇家冲突也证明,手游不会再重走端游老路了。

第二种需求就不用说了,网上大把例子可以参考。最典型的是假设有这样一块野地,上面很多玩家和怪,逻辑都在服务端驱动,好了,这类需求没其他额外的描述了。

但是,解决方案毕竟是不断发展的,即使速度很慢。

说不断发展是特指针对第一种需求的解决方案,发展原因就是国情,外挂太多。像war3这种都还是纯正的主客机,但是后来对战平台出现、发展,逐渐过渡成了cs架构。真正的主机 其实是建在服务器的,这样其实服务器这边也维护了房间状态。后来的一系列ARPG端游也都是这个趋势,服务端越来越重,逐渐变得与第二种模式没什么区别。 同理如现在的各种ARPG手游。

说发展速度很慢特指针对第二种需求的解决方案,慢的原因也比较有意思,那就是wow成了不可逾越的鸿沟。bigworld在wow用之前名不见经传,wow用了之后国内厂商也跟进。发展了这么多年,现在的无缝世界服务端跟当年的无缝世界服务端并无二致。发展慢的原因就观察来说可能需求本身就不是特别明确,MMO核心用户是重社交的,无缝世界核心用户是重体验的。前者跑去玩了天龙八部和倩女不干了,说这俩既轻松又妹子多;后者玩了console游戏也不干了,搞了半天MMO无缝世界是让我更好地刷刷刷的。所以仔细想想,这么多年了,能数得上的无缝世界游戏除了天下就是剑网,收入跟重社交的那几款完全不在一个量级。

两种需求起源,最终其实导向了同一种业务需求。传统MMO架构(就是之前说的天龙、倩女类架构),一个进程维护多个场景,每个场景里多个玩家,额外的中心进程负责帮玩家从一个场景/进程切到另一个场景/进程。bigworld架构,如果剥离开其围绕切进程所做的一些外围设施,核心工作流程基本就能用这一段话描述。

抽象一下问题,那我们谈到游戏服务端首先想到的就应该是多玩家对同一场景的view同步,也就是场景服务。

本节不会讨论帧同步或是状态同步这种比较上层的问题,我们将重点放在数据流上。

如何实现场景同步?

首先,我们看手边工具,socket。

之所以不提TCP或UDP是因为要不要用UDP自己实现一套TCP是另一个待撕话题,这篇文章不做讨论。因此,我们假设,后续的实现是建立在对底层协议一无所知的前提之上的,这样设计的时候只要适配各种协议,到时候就能按需切换。

socket大家都很熟悉,优点就是各操作系统上抽象统一。

因此,之前的问题可以规约为:如何用socket实现场景同步?

拓扑结构是这样的(之后的所有图片连接箭头的意思表示箭头指向的对于箭头起源的来说是静态的):

场景同步有两个需求:

low latency

rich interaction

要做到前者,最理想的情况就是由游戏程序员把控消息流的整套pipeline,换句话说,就是不借助第三方的消息库/连接库。当然,例外是你对某些第三方连接库特别熟悉,比如很多C++服务端库喜欢用的libevent,或者我在本篇文章提供的示例代码所依赖的,mono中的IO模块。

要做到后者,就需要保持场景同步逻辑的简化,也就是说,场景逻辑最好是单线程的,并且跟IO无关。其核心入口就是一个主循环,依次更新场景中的所有entity,刷新状态,并通知client。

正是由于这两个需求的存在,网络库的概念就出现了。网络库由于易于实现,概念简单,而且笼罩着“底层”光环,所以如果除去玩具性质的项目之外,网络库应该是程序员造过最多的轮子之一。

那么,网络库解决了什么问题?

抛开多项目代码复用不谈,网络库首先解决的一点就是,将传输层的协议(stream-based的TCP协议或packet-based的UDP协议)转换为应用层的消息协议(通常是packet-based)。对于业务层来说,接收到流和包的处理模型是完全不同的。对于业务逻辑狗来说,包显然是处理起来更直观的。

流转包的方法很多,最简单的可伸缩的non-trivial buffer,ringbuffer,bufferlist,不同的结构适用于不同的需求,有的方便做zero-copy,有的方便做无锁,有的纯粹图个省事。因为如果没有个具体的testcast或者benchmark,谁比谁一定好都说不准。

buffer需要提供的语义也很简单,无非就是add、remove。buffer是只服务于网络库的。

网络库要解决的第二个问题是,为应用层建立IO模型。由于之前提到过的场景服务的rich interaction的特点,poll模型可以避免大量共享状态的存在,理论上应该是最合适场景服务的。所谓poll,就是IO线程准备好数据放在消息队列中,用户线程负责轮询poll,这样,应用层的回调就是由用户线程进入的,保证模型简单。

而至于IO线程是如何准备数据的,平台不同做法不同。linux上最合适的做法是reactor,win最合适的做法就是proactor,一个例外是mono,mono跑在linux平台上的时候虽然IO库是reactor模型,但是在C#层面还是表现为proactor模型。提供统一poll语义的网络库可以隐藏这种平台差异,让应用层看起来就是统一的本线程poll,本线程回调。

网络库要解决的第三个问题是,封装具体的连接细节。cs架构中一方是client一方是server,因此连接细节在两侧是不一样的。而由于socket是全双工的,因此之前所说的IO模型对于任意一侧都是适用的。

连接细节的不同就体现在,client侧,核心需求是发起建立连接,外围需求是重连;server侧,核心需求是接受连接,外围需求是主动断开连接。而两边等到连接建立好,都可以基于这个连接构建同样的IO模型就可以了。