前言的前言
在讲述万人同屏的技术问题前。一定要先回答一个问题,游戏中万人同屏究竟有什么意义?但这并不是一个问题而是三个问题他们分别是。
1,显卡绘制1万个高质量模型有什么意义?
2,游戏服务器承载1万个玩家互动有什么意义?
3,在客户端或单机中1万个高智能的游戏角色有什么意义?
显卡绘制1万个高质量模型的意义无需多说了。人的眼睛有多挑剔,对显卡性能的要求就有多高。高性能显卡为什么卖的比低性能的贵。用过苹果的人绝对不会再去用诺基亚。一场宏大绚丽的场景可以产生一半的游戏内容。所以好的游戏对显卡性能的追求就像吊丝对女神的向往。
游戏服务器承载1万个玩家互动的意义。玩家的互动是网游的灵魂。游戏的内容总会有被玩尽的时候。一个游戏能存活20年依靠的就是玩家的互动。互动产生的乐趣才是无穷无尽的。人和人之间的斗争才是无穷无尽的。这就是为什么围棋被玩了几千年的原因。在几十个人的战场里玩家被裹挟着只能冲锋!冲锋!再冲锋!但在几百个人的战场里,决定胜负的可能只是偷得一把大门钥匙。
在客户端或单机中1万个高智能游戏角色的意义。网络还是单机游戏不一定都是千篇一律的场景。在随机地图中来一次玩家独特的大冒险。参与到两个部落之间旷日持久的大混战中。让每个玩家拥有自己独特的经历。只有数量庞大的沙盒游戏才能做到。
万人同屏技术与其说是一种技术,不如说是对游戏上限的突破。除非你的游戏完全由剧情组成。否则绚丽的视角,复杂的互动,高随机的可玩性。都是在从技术角度极大扩展了游戏的内容。给了游戏更高更广的设计空间。技术上限的提高代表着更多的可能。也代表着你会做出一款特别的游戏。
前言
3D游戏的万人同屏是非常古老的话题。从网游的传奇,永恒之塔到单机的战争模拟器,骑马砍杀,刺客信条。是什么限制了3D游戏的万人同屏。为什么万人同屏很难做。问题出在哪?怎么去解决?要实现3D游戏的万人同屏,就必须解决以下2个问题。
第一个问题,万人同屏受限于显卡的渲染能力。
第二个问题,万人同屏受限于对1万个人的逻辑处理能力。
渲染的瓶颈
哪么我们先来看第一个问题。什么是显卡的渲染能力呢?我们知道3D环境下最小绘制面积单位是三角面。显卡以每秒60次的频率将这些三角面投影到2D平面。而CPU同样需要以每秒60次的频率将要显示的数据准备好。并提交给显卡进行处理。
我刚巧有一台10年前的笔记本。也就是和永恒之塔同时代的笔记本。这台笔记本的渲染能力是以每秒60次的频率绘制2万个三角面。2万个三角面是什么概念呢?一个游戏角色大概要500到1千个三角面。哪么2万个三角面可以绘制40个左右的玩家。而游戏里最基本的正方形需要12个三角面。如果使用这台笔记本玩万人同屏的游戏。哪么你只能看1万个三角形在蓝色背景下奔跑。或者看见1666个正方形在蓝色背景下奔跑。
而在最新版的AMD R7-4800U内置的显卡里可以每秒60次的频率绘制500到1000万个三角形。勉强可以实现1万个游戏角色在蓝色背景下狂奔。在显卡性能允许范围内万人同屏是质量和数量的选择。万人同屏相当于把显卡性能平分1万份。这样每份的质量肯定会比较差。而在10年前把显卡性能平分1万份,就只能保证1万个三角形的同屏。在10年前只有高端显卡可以在质量和数量上满足万人同屏。所以万人同屏的技术在10年前只有少数高端玩家可以体验。而随着硬件技术的发展,显卡性能的显著提升。今天就是CPU的内置显卡也能在满足一定程度的万人同屏。
既然现在的硬件条件已经有很大的改善,为什么能够做到万人同屏的游戏还哪么少呢?
这主要是3个方面的问题。
第一个原因是虽然硬件性能不断提升,玩家对画质的要求也在不断提升。在质量和数量的比较下,策划选择了游戏质量。在游戏中技术是对策划和美术的基本支持。游戏选择质量还是数量都是策划对游戏玩法的设定。无论策划做出那种选择都能够实现是公司技术实力的体现。技术给了策划更多的选择,就意味着策划有更大的舞台,能够做出更内容更丰富的游戏。
第二个原因是网络游戏服务器的“相位技术”并不普及,只被魔兽和EVE等少数厂家垄断。导致网络游戏的承载人数非常有限,大多数游戏公司的技术只能满足5百到1千人左右的水平。受到服务器技术水平的限制,导致网络游戏的客户端无法实现万人同屏。
第三个原因是单机技术水平不足。单机游戏显然并不受到服务器处理能力的限制。显卡能力也可以得到充分释放。但要满足万人同屏需要每秒钟处理60亿条以上的游戏数据。普通的游戏厂商没有这个技术实力。
网络游戏中的难点
稍后我们来详细了解第三个原因。先简单的说下第二个。网络游戏的特点就是所有数据的处理都在服务器完成。一个是为了能够同步给其他的玩家,也是为了数据的安全。整个处理过程是玩家通过键盘鼠标生成请求命令并发往服务器。服务器接到命令后处理相关数据,并生成返回消息发给玩家和相关的玩家。玩家客户端接到命令后通过3D引擎设置相关数据,并显示到屏幕上。例如玩家移动的消息。键盘控制玩家角色向前移动,这个消息发送给服务器后,服务器记录移动数据,并将数据转发给周围的玩家。这样周围的玩家才能看这个玩家在移动。所以网络游戏中客户端功能被分为了三大部分。第一接收键盘鼠标的输入。第二网络发送并接收服务器数据。第三根据返回数据调用相应的资源并显示在屏幕。哪么能否万人同屏就受到两个条件的制约。第一个是显卡能否按游戏要求的品质显示出足够数量的对象。第二个服务器能否支持足够多的玩家在线。
我们通过前面对渲染的介绍可以知道。显卡降低游戏品质哪怕是只显示1万个三角形。也是可以实现万人同屏的。所以今天的游戏引擎和显卡性能都不是制约网络游戏中万人同屏技术的关键。只要游戏服务器能够支撑足够多的玩家就可以实现客户端的万人同屏。在上一篇文章中,我介绍了游戏服务器《使用redis实现5万人同服的“相位技术”》的文章。有兴趣的同学可以去了解下。
在单机中的难点
下面我要重点介绍单机3D游戏如何实现万人同屏。下面默认的语言环境是使用go语言。包括go语言中消息队列和服务的概念,不熟悉的同学也不用着急,下一篇文章中我会来解读示例代码。
前面我介绍了渲染并不是制约万人同屏的关键。在单机环境下制约万人同屏技术的是对游戏数据处理的能力。顾名思义万人同屏是指1万个游戏角色在屏幕中移动。如果仅仅是移动非常的容易我写了示例test_move给同学们参考。
https://github.com/surparallel/unity_example_of_pelagiagithub.com/surparallel/unity_example_of_pelagia
技术困难的是在移动的过程中要不断处理各种逻辑。例如攻击附近的对象,跟随的对象,技能的触发,任务的触发,物品的拾取,攻击伤害,血量的扣减等等。这就要求游戏角色能够感知周围的其他游戏角色。显然万人同屏的另一个意义就是这1万个游戏角色要拥挤在很小的地图空间。又要感知周围游戏角色的状态进行逻辑判断。极端情况下就意味着每帧,也就是每秒60次对周围1万个角色的状态进行过滤。哪么在极端情况下,意味着1万个角色以每秒60次的频率对其他1万个角色状态进行过滤。也就是每秒钟要进行60亿次的数据筛选。假设我们所有数据处理都是在map类型中完成了。对map类型进行10万次插入需要0.016秒。每秒钟可以处理625万次。map类型读取的性能稍好可以处理1百万需要0.078秒。每秒钟可以处理1282万次。
我们的需求是每秒钟提交60次角色当前位置,并试图获取周围角色的位置用于逻辑分析。因为游戏角色被分配到不同的平行空间,也就是不同的线程。然后从不同的线程中获取当前角色周围玩家数据用于逻辑分析。这与我们在服务中使用的相位技术是一样的。哪么通过下面公式就可以得到每个角色在给定条件下能够处理的数据量为。
每个角色能够处理的数据量=(cpu的核数*每秒每线程读取能力)/(总角色数量*帧数)
假设在极端情况下,要每个角色要处理1万个其他角色的数据。例如对同屏所有人的范围伤害,被所有角色同时触发。哪么所需要的cpu核数为。
1万=(cpu的核数*1282万)/(1万*60)
cpu的核数=(1万*60*1万)/ 1282万
cpu的核数=468核
也就是如果想达到万人同屏的开发自由,当前cpu频率不变的情况下,需要有468核。这个硬件可以满足万人的现代战争的射击游戏。当然这个硬件目前还不存在。在这个公式里面除了每秒每线程的读写能力无法改变,因为取决于cpu的主频。其他的参数我们都可以适当的调整。例如保持60帧的前提下我们的射击类游戏可以做到多少角色混战呢?哪就假设总角色数量和每个角色处理的数据量相同。cpu为当前最高的128个。帧数为60帧。则计算如下。
x = (128*1282万)/(x*60)
x=5229.65
也就是枪战类的万人同屏目前的硬件还不能满足需求。但5千人左右的同屏还是可以满足的。如果我们降低精度要求小于60帧,也可以进一步提高游戏逻辑的处理能力。
如果我们时光倒退到10年前,为什么现在有骑马砍杀类的万人同屏游戏,10年前没有这类游戏呢?因为10年前的渲染能力不够。CPU的计算能力也不够。我们计算4核的处理器的逻辑处理能力如下。
x=(4*12820000)/(x*60)
x= 924
其实4核处理器的理论处理能力应该在7161个左右。因为2d策略类游戏要求并不高,可以把帧数设定为1。但10年前客户端的并行处理能力也非常的差。钢铁雄心在这类游戏算是佼佼者,单核处理能力的极限也就是1千个单位的同屏幕混战。当然P社的技术能力肯定不是最强的。对几万个角色进行军事推演的价值不言而喻,这里就不做过多的展开了。
这里我们通过锚定map类型的读写能力,将CPU的处理能力进行了量化。并将这种量化的CPU分配到1万个游戏角色上。每个角色能够分配到的CPU算力显然非常的有限。在有限的算力下又需要满足各种逻辑的处理。还要尽最大可能降低延迟到60帧,避免影响用户的体验。
在上述公式里我们忽略了角色的写入需求。因为相对于读取需求写入需求极少。假设每帧每个用户需要写入一个数据,哪么1万个用户60帧需要写入数据为。
总用户数量*帧数*写入需求=1万*60*1=60万/秒
相比于map写入能力625万/秒,写入需求可以忽略不计。在实际计算中可以对原有公式进行修正创建联立方程。分别得到写入上限和读取上限。然后反推计算,cpu核数的需求或者每秒的读写能力的需求或对帧数的需求。
(角色读取上限)=(cpu的核数*(每秒每线程读取能力))/(总角色数量*帧数)
(角色写入上限)=(cpu的核数*(每秒每线程写入能力))/(总角色数量*帧数)
单机的挑战就在于CPU的核数是无法改变的,又很难像服务器一样通过分布式进行扩展。要充分利用硬件资源的前提,是首先要搞清楚需要多少硬件资源。设计软件的目地,就是充分利用硬件资源满足软件需求。搞不清自己的软件需要多少硬件资源,这样会让软件设计陷入迷茫。在这个小结中我们通过计算得到万人同屏所需要的硬件条件。和软件频率的需求,将并行开发进行了量化计算。和所有工程学一样,计算工程的上限是性能设计的前提。尤其在并行开发中,并行开发的本质就是均衡分配硬件资源。
到这里我将万人同屏的问题拆解为三个问题。并给出了每种问题的解决方向。
1) 渲染问题,和显卡硬件挂钩显卡性能不提升无法解决,网游和单机都会受到渲染问题的影响。单位时间绘制的三角面数决定了显卡的渲染能力。
2) 网游问题,主要瓶颈在服务器的万人同步。
3) 单机问题,主要瓶颈在于角色逻辑刷新的数量和频率以及硬件的限制。1万个人每秒刷60次,每次刷1万条就是60亿次数据处理。
在下一篇中我将通过示例代码,一步一步讲解如何解决单机开发中万人同屏问题。
在我之前发内容:使用redis实现5万人同服的“相位技术”中我介绍了基于九宫格和相位技术的空间管理技术。这里我们也要借鉴游戏服务器中“服务”的概念。可能有些同学没有接触过游戏服务器,对服务的概念不是很熟悉。服务可以看做是一个独立的线程环境。这个线程监听着一个消息队列。其它的服务可以发送消息给他。这种方式在服务器开发中的go语言,erlang语言,skynet框架中被广泛应用。消息队列保证了服务所创建的数据是私有并且多线程安全的,只能通过消息通讯的方式进行修改。服务的概念为多线程下使用数据的安全问题提供了保护。通过消息通讯建立了在多线程下的秩序。但这种方式在客户端使用的并不多。各种服务的框架也都是在服务器端使用的多。
客户端使用多线程开发管理1万多个线程将会是一场噩梦。而管理1万多个服务对技术水平要求也还是比较高的。针对客户端没有多线程的服务框架问题,我开发了pelagia框架。借用“服务”的概念来管理客户端多线程。通过内嵌kv数据库和预判以及服务私有数据的概念彻底消灭多线程死锁和依赖的问题。因为只有解决多线程的安全问题。才能进一步思考如何优化通信和计算以及存储的平衡问题。安全问题不解决所有的优化问题就会是空中楼阁。
项目地址:
https://github.com/surparallel/pelagiagithub.com/surparallel/pelagia
以每秒60次的速度更新AI有意义么?
如果在游戏中有射击的动作哪么是非常必要的。如果是其他类型游戏可以适当减少刷新的速度。把标准提高才有进行优化的空间。如果一开始就是按1秒钟来设计,哪么就无法支撑动作类玩法。这里每秒60次是一个运行的速度单位。把他看作消息系统还是帧系统都是一样的。以消息系统的角度来分析。相当于消息系统中单位时间发送消息的频率为每秒60次。如果单独看移动是不需要这么高频率的发送消息。但是玩家在战斗中移动并释放技能。哪么玩家在单位时间内发送消息的频率会远远高于60次。所以消息可以看作是非固定频率的刷新,而帧是一种固定频率的刷新。所以无论哪种方式都要对发送的频率提出一个技术上限。并在设计的时候要达到这个上限的标准。60次是一个很高的标准,拿来做系统设计的基准非常合适。
服务的本质是什么?
在这里我没有使用多线程而是使用了服务。因为我们要知道多线程的本质是什么。多线程的本质是操作系统为了CPU的重复使用而发明的概念。操作系统虽然发明的多线程但没有发明消息通讯。而且在服务器软件中为每个服务请求都启动一个线程,系统开销还是非常大的。所以发明了服务的概念来替代多线程。服务和多线程的区别在于两点。
1,多个服务同时使用线程池,这样就不用频繁的创建和消耗线程。
2,为每个服务配置了消息通讯队列,解决逻辑执行上的依赖关系。
服务是对线程概念的升级。但本质和线程是一样的,运行在不同CPU上的代码。服务虽然是在线程上概念的扩展,但很少有人在本质上去分析服务。线程,进程和服务都是非常相似的概念。所不同的是进程是操作系统所有。开发多进程软件会和操作系统发生冲突。线程虽然受到开发者的控制,但缺乏进程一样的管理手段。所以服务的概念补充了线程的不足。也可以理解为我们在系统进程的内部实现了一套自己的进程管理系统。他们的区别仅仅在于一个受系统控制,一个受开发者控制。当管理非常复杂的软件,由大量多线程组成的软件。引入服务的概念就非常的必要。服务还有管理器,多线程连管理器都没有,简直就是恶梦。就更别说服务还有隔离,通讯,监控,日志等等保障系统稳定运行的功能。
服务的特性有哪些?
既然服务是仿照进程的一种内部实行方式。哪么服务有着和进程相似的特点。首先必须有名字。就像进程的名字一样,服务也有自己的名字。可以是文件名称或者通过配置文件自定义的。例如pelagia就可以通过json文件定义服务。
"player":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"player",
"weight":1
},
上面json就定义了一个服务,这个服务通文件3drpg2.lua中的函数player启动。
每次通过名字我们都可以创建一个新服务,或则通过设置不创建新服务只调用已经存在的服务,也就是全局单例。服务的特点就是既可以是单例也可以是多例。在服务之间会有一些共享的数据。用于服务之间的数据交换。例如全局的配置信息。哪么数据和服务之间就会形成这样的对应关系。
单例服务,无共享数据。
单例服务,有共享数据。
多例服务,无共享数据。
多例服务,有共享数据。
如果有一个全局配置信息,这个配置信息所有的服务都可以读,但只能通过一个单例服务进行修改。哪么这个共享信息是多线程安全。也就是我们常用的单点写多点读的模式。哪么我们再来分析上述4个对应关系。单例服务无共享数据,因为不存在共享数据。所以是多线程安全的。多例服务无共享数据。因为也不存在共享数据所以是多线程安全的。而多例服务有共享数据必然会导致多线程不安全。在多例服务,无共享数据中,所有服务的私有数据只能通过消息通讯进行修改。通过上述描述我们发现,多例服务有共享数据和单例服务非单点写入的模式是多线程不安全的。多线程不安全就代表着服务模式的安全边界被突破了。我们引入服务模式的一个重要原因就是服务比多线程更加的安全。如果有模式导致服务不安全,哪么就说明这两种模式不应当被使用。我们不应当让服务因为不安全的原因退化回多线程编程。包括再引入额外的锁来保护服务,这是一种严重退化。
由上述描述我们知道服务中存在两种不安全的模式。这两种模式不应当出现在服务里。所以需要重新定义服务的安全边界。并将不安全的模式彻底排出。在这里使用order重新定义了服务中安全的部分。用户在定义order时有共享数据就不能定义多例。有多例就不能定义共享数据。并且共享数据只能单点写入多点读取。来保证用户使用时的多线程安全。这点保证了我们在开发大量服务时,不用提心吊胆多线程安全的问题。而把精力更多的投入到性能的优化上。
为什么要使用服务?
在这种高强度的并行软件中直接使用多线程开发简直就是恶梦。服务为我们提供了必要的基础设施。因为过去很长一段时间里客户端并不需要如此高强度的软件。服务的概念通常是被使用在服务器软件中。在这里我们要创建管理1万个游戏角色的服务。已经负责分发和管理的服务。由上面的定义我们还知道服务还分为全局单例服务和普通服务。还要处理各种服务之间的通信。
这里为什么不使用go语言?go语言在服务方面非常有经验。但go语言更多的是关注语言方面的特性。将服务的其它支持交给了集群中的其他组件。这是开发服务端软件的便利,运行环境和开发环境可以分开考虑。但在客户端就不一样了,客户端需要一个完整的服务框架。需用日志,监控,路由,调度等等。显然go语言在客户端方面的支持非常的不足。
示例代码的导读
示例代码为开源软件,使用unity在win系统上运行地址为:
surparallel/unity_example_of_pelagiagithub.com/surparallel/unity_example_of_pelagia/tree/master/3D_Ten_Thousand
这里我使用unity实现了一个万人同屏的示例。准确的说应该是1万个正方形同屏的示例。万人同屏的渲染优化已经很成熟了。远处用贴图,削减材质等等。这里就不再展开讨论。示例代码主要展示了在服务模式下,如果开发1万个人同屏的AI。如何通过服务模式充分发挥CPU并行的优势。
创建服务先要通过json文件定义服务及其使用的数据。通过3drpg2.json定义了所要使用的服务。他们是manager负责管理配置和消息路由功能。role的角色管理服务。space负责空间管理的服务。test为通过控制台调用测试所需要的入口。
{
"dbPath":"",
"core":1,
"maxTableWeight":100,
"luaLibPath":"./lua5.1.dll",
"logLevel":2,
"logOutput":"print",
"manager":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"manager",
"weight":1,
"global":{
"weight":1
},
"global_str":{
"weight":1
},
"space":{
"weight":1
}
},
"role":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"player",
"weight":1
},
"space":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"space",
"weight":1
},
"test":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"test",
"weight":1
}
}
其中dbPath为数据库文件的保存路径。core为设定的cpu核数也就是在pelagia中线程池分配的数量。maxTableWeight为每个文件表的宽度。pelagia中表是数据的基本单位,每个表都有自己的设定宽度。如果想让表独享文件就把表的宽的设高,这样在一个文件就只能容纳一个表。宽度是一个参数和实际文件长度无关。luaLibPath为lua虚拟机的路径。logLevel为错误输出的等级为1到5。logOutput为错误输出的方式分别为打印到屏幕print和输出到文件的file。
"dbPath":"",
"core":1,
"maxTableWeight":100,
"luaLibPath":"./lua5.1.dll",
"logLevel":2,
"logOutput":"print",
manager就是典型的全局单例服务,在单例服务中可以使用数据库表。在manager中被写入的数据库表global,global_str,space要单独声明,如果是读取的表不需用声明。这里也定义了入口的类型为lua脚本,已经文件和函数。这里也有一个宽度,表示在线程中的宽度。所有的服务都会依据宽度被平均分配到线程池中的线程中。如果一个服务非常宽例如999,哪么他就可能会独占一个线程。我们看到global表中也有一个宽度的设定,这个设定和文件宽度设定相对应。如果设置的较大就会有独占文件的可能。
"manager":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"manager",
"weight":1,
"global":{
"weight":1
},
"global_str":{
"weight":1
},
"space":{
"weight":1
}
},
这一段是关于role服务的定义。在role服务中没有使用任何表。这个服务就是上面我所讲到的多例服务并且无共享数据。意味着每次调用服务都会被随机分配到线程池中任意线程中运行。这个类型的服务可以读取数据库中的数据但不能写入数据。space的定义和role是一样的,空间服务接受role服务提交的空间数据并提供查询。
"role":{
"orderType":"lua",
"file":"3drpg2.lua",
"fun":"player",
"weight":1
},
踩过的坑?
开发客户端的服务和服务器的服务完全不同。服务器的服务可以使用分布式的方式进行扩展。并且用户的行为是多变且无法预测的。客户端的通信也是需要cpu处理,并且随着服务的增加导致成本高昂。最开始我考虑像服务器一样为每个用户创建一个服务。这样用户之间有了数据隔离并且功能相对简单。但1万个用户服务同时活动造成了通信风暴。主要包括用户心跳和空间数据的更新。这迫使我开始优化服务之间的通信。
用户服务的创建
首先是在每个线程创建了用户服务,并将用户随机分配到这些服务上。然后统一处理一个服务中所有用户的心跳信息。这样减少心跳通信的发送。
local ret = pelagia.RemoteCallWithMaxCore("role", json_arg);
RemoteCallWithMaxCore会在每个线程执行一次初始化服务。然后通过随机的方式将角色分配到角色服务上。空间服务也是采用类似的方式。
--随机分配到一个role服务
local rolekey;
local limite = math.random(1, role_count);
for key, value in pairs(role_server) do
rolekey = key;
limite = limite - 1;
if limite == 0 then
break;
end
end
local player_Arg = {};
player_Arg.cmd = "create";
player_Arg.name = dvalue.name;
player_Arg.x = dvalue.x;
player_Arg.z = dvalue.z;
player_Arg.space = space_server;
player_Arg.space_count = space_count;
player_Arg.event = event;
local json_arg = json.encode(player_Arg);
pelagia.RemoteCallWithOrderID(rolekey, "role", json_arg);
角色服务包括服务的初始化,创建角色,心跳三个功能。通过创建orderid的方式来使用服务的私有数据。每次返回服务后通过pelagia.OrderID();可以重新获得。
local orderid = pelagia.CreateOrderID();
_G[orderid] = {};
_G[orderid]["role"] = {};
角色的具体功能处理是由roleproc完成的。
function roleProc(cmd, orderid, rolename, value, dvalue)
创建角色功能为角色随机分配一个空间服务。并生成移动的路径,包括开始位置,结束位置,开始时间。然后将移动的路径发往空间服务和3D引擎。3D就可以驱动角色开始移动。空间服务可以根据移动数据更新九宫格数据。
space_Arg.x = value["x"];
space_Arg.z = value["z"];
space_Arg.dx = value["dx"];
space_Arg.dz = value["dz"];
space_Arg.time = value["time"];
在角色服务内每次心跳都会计算最新的位置。用于判断移动是否已经结束。如果结束就重新生成目的位置。
角色服务最重要的功能是在每次心跳时从空间服务获取周围角色的信息。拿到周围角色信息后才能开始各种的逻辑计算。因为我们有多个空间服务。所以每次要从多个空间服务获取信息。整合在一起后才是角色完整的周围信息。每次获得空间服务信息的处理公式为
用户数量*空间服务数量*帧数*(json的加解码等操作的时间)
如果不对获取行为进行限制,假设有4个空间服务,1万人每秒60帧,就会有240万次的查询这样会导致服务之间的通信风暴。
--随机发起查询周围玩家进行逻辑处理
if math.random(1, 100) == 0 then
local space_Arg = {};
space_Arg.cmd = "read";
space_Arg.name = rolename;
space_Arg.orderid = orderid;
space_Arg.x = x;
space_Arg.z = z;
local json_arg = json.encode(space_Arg);
pelagia.RemoteCallWithOrderID(value["in_space"], "space", json_arg);
end
对于周围角色的数据查询可以有很多种优化操作。例如只有在释放特定技能时查询一定数量的角色,随机返回一个角色等等。这样都可以限制通信风暴的产生。
通过调整定时器参数可以设定刷新的帧率。日志输出的等级以及移动的速度。
frame_rate = 1/2;
log_level = 2;
speed = 5;
使用服务替代多线程开发对于客户端是全新的概念。虽然在服务器领域这个概念已经相当的成熟。相对于多线程的方式,服务的线程池,消息通讯,私有数据,内嵌kv数据库有效的隔离了多线程的不确定性。优化服务的通信可以有效的降低cpu的使用率。增加服务的缓存也可以降低cpu的使用率。这些新的优化手段使万人同屏的开发扫清了障碍。
后记
我垂涎3D游戏的万人同屏技术很久了。2009年的韩国网游《永恒之塔》让万人同屏技术第一次出现在公众视野。那年我刚入游戏行当不久。还在摆弄2百多人就卡爆了2D手游服务器。第一次看到满屏幕玩家的视频,我只能用震惊来形容。在国外骑马砍杀,刺客信条等等万人同屏的游戏已经大量普及。国内对于万人同屏的技术资料依然非常稀缺。一方面这些技术都是顶尖的技术,只存在于部分的公司和开发人员手中。另一方面这些技术也都是在探索阶段,本身也是对软件能力上限的探索。希望这两篇浅析万人同屏的文章和示例能够对你有所启发。
来源知乎专栏:分布式系统学说