随着游戏行业的兴起,越来越多的游戏出现。游戏中又分各种各样的游戏类型,而基本上在国内的游戏环境中,最受欢迎的还是网络游戏。不知道大家有没有好奇过,游戏中是如何实现你和你的朋友能一起出现在游戏中并一起游玩的,而这背后的机制又是怎么样的?

首先看到这个标题大家不要害怕,这篇文章并不想讲无聊的技术,不会研究游戏服务器需要多少核多少内存,设计什么样的架构,能承载多少人,用什么语言,什么网络通信协议。而是想从一个产品的设计的角度,或者游戏框架的设计角度,来探讨一下如何设计一个多人的游戏场景。

游戏服务器

如果要问一款游戏都由什么组成的,你会怎么回答?
我猜大概你的第一反应是游戏的画面,画质,特效,帧率,音效?或者更深入一点,可能会想到玩法,操作,剧情,战斗?
不可否认的是,大家玩游戏的第一感受一定是这个游戏对我们的美术效果的呈现,再然后就是游戏的玩法和操作之类的互动体验。而还有一部分,是非常重要的,却经常被大家忽略的,那就是服务器,即那个把成千上万的玩家连接在一起的幕后“黑手”。

如果你玩的游戏是能和其他玩家一起组队或者对战,那你可能意识到了,它的背后还有一个游戏服务器。甚至很多游戏,你是看不到任何其他玩家的,它的背后也有服务器的存在。

当然了,最能让你注意到游戏服务器的,一定是“网络掉线”,“服务爆满”,或者是网络原因造成卡顿,这几种场景。

今天,我想讲讲最常见的场景,即多人组队,多人同屏对战的游戏服务器,也就是最常见的MMO类型的游戏,是如何跑起来的。简单来说,就是你和你的朋友们,你的网友们,是如何在游戏里一起愉快的玩耍的

虽然我做了7年的游戏服务器开发,但是今天,我想抛开具体的技术实现,从一个设计者的角度,来讲讲如何构建一个多人同屏的游戏服务器场景。

场景单位

首先,我们想一想,在一个大型MMO的游戏场景中,都有哪些要素组成呢?换成程序员的思维,即游戏场景中都有哪些对象呢?

我们最容易想到的,一定是玩家,怪物,宠物,坐骑,召唤物,陷阱,机关,NPC等等能直接看见的对象。这一类对象,我们统一归为场景单位对象。
除了场景单位以外,玩家在战斗中,释放的技能,产生的子弹,添加的buff,触发的被动,均是对象。

以上所说的所有对象,通通在被包在一个叫做游戏场景,或者叫游戏地图的对象中。

首先,我们让玩家进入游戏,玩家的对象实体,就被放在了这个游戏场景的不同的角落中。
好了,现在我们的游戏场景中有很多的玩家了,我们可以再让服务器刷出来一些怪物,并把它们种在地图上不同的位置
接下来,我们在场景中再放入一些机关,陷阱,NPC等等。

游戏场景

单位行为

现在我们的游戏场景有玩家有怪物有一切游戏单位,那么我们怎么让它们动起来呢?不知道大家小时候玩过需要上发条的玩具吗?这种玩具会有一个设定好的行为,只要我们拧动它的发条,它就可以按照设定好的行为行动起来。

在游戏场景中,我们所有的场景单位对象,其实都可以类比成这一类的发条玩具。
对于场景单位的行为这一点,我们可以稍微展开说一说:
一般来说,我们会对怪物,NPC,召唤物等指定一系列的行为,然后根据场景中的不同状态选择行为进行响应。一般这样的系列行为会被组织成一颗行为树或AI树。(当然,这里的AI和真正意义上的AI其实还差的很远)。
行为树

对玩家来说,玩家对象的行为则是由游戏前的玩家用双手去操作的。玩家的操作通过键盘、鼠标、或者手机屏幕的触摸、滑动传递到游戏客户端,游戏客户端再通过和服务器协定的通讯机制,把玩家的操作指令通过网络发送到服务器的,服务器再把收集到的指令转成玩家的行为,再赋予到玩家对象的身上。和其他单位相比,其他单位是由AI树提供行为,和玩家对象由玩家提供行为。
玩家操作

好了,目前为止,场景有了,场景单位有了,场景单位要执行什么行为有了,那么谁让他们动起来呢?

场景调度

我们都知道游戏的本质,其实就是计算机每帧计算出要呈现的画面,最后以每秒24帧,30帧,或者60帧,或更高的帧数连续播放出来。同理,服务器为了和客户端进行同步,也有一个每帧进行逻辑计算的过程,不同的是,客户端每帧只计算一个本地场景,而服务器要计算N个场景(这取决于一台服务器在保证计算效率的情况下能承载多少游戏场景)。

这个每帧进行计算的过程,我们称之为场景调度。简单来说,就是场景本身是不计算的,需要有人调度你的时候,你才能进行计算。当调度到你的场景的时候,就可以把场景里的所有单位的行为都执行一遍。

那要如何调度,才能保证我们能把所有场景都调度到呢,这就不得不让我们调度大哥出场了。让我们想象一下,当有多个场景的时候,这位调度大哥会指挥一堆场景进行行动。这个过程,大家是不是觉得有点熟悉呢?对!这不就是交通路口的交警叔叔所做的事情吗?

我们就以交警叔叔为例子,假如我们有一个十字路口,有上下左右四条大道,中间有一个交警叔叔,来指挥四条大路,四条大路不断来往车辆。
交警类比图

交警叔叔每隔一段时间,就指挥一次上下左右四条大路进行通过,指挥完了之后,就原地等待车辆通行。等到下一波车辆过来,交警叔叔再重复上面的动作。
在我们的游戏场景中,场景调度器就相当于我们的交警叔叔,每一辆准备通行的车辆,就相当于一个个游戏场景,为了让多辆车同时通行,我们有四条大路,也就相当于用来运行场景的4根线程组成的线程池。交警叔叔每指挥一次就等待一段时间,等待车辆通行,这个过程就相当于一次场景的调度,而调度完了之后,就会等待每帧运行所需的时间,再进行下一帧调度。

场景运行器

一切都很顺畅,交通井然有序,直到交警叔叔发现大家的车辆速度都不太一样,有的是一脚油门就起飞的跑车,有的是装满砂石的货车,有的是速度一般的家用车,大家的通过速度都不一样。所以很快,交通发生了拥堵。
交警类比图
交警叔叔很头疼,怎么办呢,交警叔叔自有办法
交警叔叔把不同的车分成了不同的车道,规定同类型的车只能走同一条车道,并且交警叔叔还能规定每一种类型的车只能有多少辆。这样一来,交警叔叔就能很轻松的把控每一条车道的运行速度。比如砂石车跑得慢,那么同一条车道上,就只能出现4辆砂石车,而跑车跑得快,那么同一条车道里,就可以有更多的跑车。
当然了,为了控制这个交通路口的运行效率,交警叔叔一定要保证所有的车辆都能高效有序的通行,比如当发现有三条车道都是砂石车道的话,就不然允许新的砂石车进来了,等等限制措施。
这样一来,在每一波车辆来了之后,交警叔叔都能通过自己的判断,让所有车辆的都能高效有序的通行
交警类比图

那这样的高效运行的交通规则又如何应用到游戏场景中呢?这就要提到我们的场景运行器了。
理论上来讲,有一个场景调度器,不停的调度我们的场景,把场景直接扔到一个多线程池子里,至于它具体在哪根线程上执行,我们也不需要关心,这样一来,我们游戏服务器上的场景就已经可以跑起来了。
那这样会有什么问题呢?和上面的交警叔叔的类比一样,我们的场景中的有的运行快、有的运行慢,直接这样无序的投递线程池,我们对于整体的运行效率是无法把控的,假如游戏要求一秒运行30帧,那么一帧的运算速度必须控制在33毫秒以内,那么如何分配场景,能让这根线程上的所有场景在33毫秒内执行完,就是需要我们做的优化。

在上面的举例中,四条方向的大道我们可以认为是线程池中的四根线程。为了让每根线程在一次调度中把需要运算的场景执行完,我们就需要计算好一帧的时间能够运行多少场景:

  • 分配多了,那么这个线程这一帧还没运算完,下一帧将要运算的场景又开始排着队了;
  • 分配少了,那么这个线程就会有一段时间的空闲,造成线程资源的浪费。

当我们仔细分析就会发现,同样是游戏场景,不同场景的复杂度,其实是不一样的,比如场景中我们又可以分为副本场景,工会场景,野外场景,主城场景等。我们完全可以根据不同场景类型,选用不同的场景运行器,比如单人副本的场景,一个场景只有玩家一个人,那么我们就可以按人数来分配场景运行器,同一个场景运行器,就可以多容纳更多的单人副本场景。而主城的场景中,一个场景里面里可能会有N个玩家,那么我们就可以限制一个场景运行器中主城场景的数量,来保证这个场景运行器的运行速度。最后,我们把分配好的场景运运行器作为一个整体,再交给我们的场景线程池去执行,这个时候,我们就基本能保证这个场景运行器能在指定时间内运算完所有的场景了。我们需要做的事情,就是为了能够合理分配我们的游戏场景,让场景调度器在一次调度中,以满足游戏逻辑运算的速度来高效运行

合理分配游戏场景,充分榨干多核CPU的性能,是每一个游戏服务器应尽的责任

场景线程模型

我们通过以上对于游戏中的场景单位,单位行为,场景,场景运行器,以及场景调度器的分析,最后,我们得到一个这样的线程模型。当然了,这肯定也不是最优的游戏场景线程模型,这只能说是其中一种能够高效利用多核CPU的场景线程模型​。一个问题肯定会有着不同的解答,这才是设计模式的魅力所在
场景线程模型

现在再回到我们最初的问题:你和你的朋友们,你的网友们,是如何在游戏里一起愉快的玩耍的?,没错,你们就是存在于服务器上的每一个游戏场景中,而所有其他的玩家,也都和你们一样,被分配在一个个的游戏场景中,被一个叫做场景调度器的老大哥以每秒30帧调度运行。

后话

不知道这篇文章下来,我有没有把游戏中的场景线程模型通过这样一个白话文讲清楚。写这篇文章的最初,只是因为我觉得,任何一门技术都不是单独存在的,在开发中,有很多的设计模型,其实都是来源于生活,或者说有很多开发中的设计模型,其实也能应用于生活中。

仔细想想,如果是商店,银行,游乐场,是否也可以用类似的模型来设计一套接待系统?

Q.E.D.