查看原文
其他

快节奏网络游戏,该如何选择服务器编程模型?

矩阵游戏朱纯 腾讯GWB游戏无界 2022-08-30

本文作者为《Prank Bros(欢乐兄弟)》研发团队——矩阵游戏的朱纯,《Prank Bros》这款游戏曾在第三期腾讯游戏学院品鉴会上与WeGame达成签约协议,且已于近日上架WeGame。



以下为分享正文:


在过去几年的工作中,我接触了一些网络游戏编程方面的知识,但是大多数的文献和资料都来源于外国网站,中文版的信息寥寥可数,这次将一些资料做一个总结,希望大家喜欢。先来看看《Prank Bros(欢乐兄弟)》的节奏:


《Prank Bros(欢乐兄弟)》游戏视频


尽管后端编程一直给人一种非常晦涩的印象,入门相对来说却是挺简单的。从手游端的回合制游戏到PC端的FPS,虽然它们的游戏类型相去甚远,但其实它们都源自于屈指可数的几种网络编程模式,让我们先从最基础的3个网络协议开始。

 

IP


Internet Protocol,互联网协议。这是最为基础的一个协议,简单来说,这个协议仅仅确保数据能够被传输出去——想象一下你坐在教室里,有人给你递了一张小纸条,然后你再把纸条递出去,仅此而已!注意,对发件人来说这张纸条没有任何的保障性可言——他既不知道收件人收不收得到,递出去的纸条也可能会经由不同的路线中转,总结为一句话,只管收发就是了!然而就是这看似一点都不靠谱的传输方式,使得互联网具有了“自行组织”和“自行修复”的特性,任何一个网关坏了都没有问题,因为纸条会从其他路线中转。每个设备要做的事情也相当简单,只要把收到的数据交给跟自己连接的设备就可以了。

 

UDP


User Datagram Protocol,用户数据报协议。UDP是基于IP的一层非常精简的协议。它可以让你指定对方的IP地址和端口进行发送,也可以让你监听设备上的端口来接收数据。和IP一样,UDP也是无连接的,这意味着每次发送的时候你都要指定接收人的地址和端口。此外在网络状况良好的时候,尽管大多数你发出的包都会按顺序到达目的地,但是你完全不能依赖它的可靠性,UDP也是非可靠的。

 

TCP


Transimission Control Protocol,传输控制协议。尽管TCP也是基于IP,但与前两者不同的是,它被设计成了一种高大全协议:易于使用,有连接,可靠传输。TCP所传输的数据是可靠而且是按顺序排列的,并且非常易于使用——就像你用IO写入磁盘文件一样。同时它还会自动分割大块的数据,结合数据流自动控制发送(例如当你带宽是1Mbps的时候程序不会要求每秒发送1GB的数据),从某种意义上来说简直是完美!

 

在了解了这三种协议之后,我们会觉得TCP是对程序员非常友好的一种协议,那么我们就用它来做游戏吧!然而……并不行,或者至少对快节奏(Fast-Paced)游戏而言不行。这是因为TCP是一种可靠性优先的协议,而快节奏游戏往往是对时间非常敏感的游戏。想象这么一个情形,你在玩一个FPS,有个敌人从你面前走过并且没发现你,你希望先手攻击他。但是!就在这电光火石的一刹那,你的网络出现了丢包的情况,真糟糕!服务器本来要跟你说这个人在T0秒时刻,处于P1的位置,可惜你一直收不到这个数据,画面上的人物就卡在了原地——直至过了一段时间后你的网络恢复了,于是游戏在T1时刻显示这个人在P1的位置。然而说什么都太晚了,因为这是一个过期的数据,敌人已经走掉了——他已经在P2的位置了。如你所见,这种时候TCP高大全的特性就会让我们有点搬石头砸脚的感觉。所以我们在做多人快节奏游戏的时候,往往会自己基于精简的UDP去额外实现可靠传输和冗余传输。当然除了快节奏的游戏外——尤其是对游戏同步时间不敏感的游戏类型,TCP依然还是一个不错的选择。


 

接下来我们考虑该怎么实现我们的游戏吧。现在很多游戏引擎例如Unity和Unreal都提供了很方便的代码方式,例如Component System、蓝图等等。但是涉及网络模型时,我们需要到更底层的地方执行操作。为方便起见,假设我们完整的游戏程序是这么实现的:

 

Void Step(){    // 模拟一帧的游戏

Input();// 获取玩家的输入

Update();// 根据输入更新游戏世界

Render();// 显示游戏世界到屏幕上

}

 

你没有看错,虽然实际游戏引擎中,代码的执行顺序远比这个复杂得多,但是作为我们现阶段需要了解的内容而言,这几行代码已经足够简洁和精确了。

 

值得一提的是,在所有涉及快节奏游戏的网络模型的知识里,idSoftware都是一家无法绕开的公司。是他们最早在自己的Doom和Quake中使用了多种现代游戏的网络模型,并且将之开源。这种对程序界的震撼一直影响到现在,几乎每一个现代3A游戏的网络模型都是基于最早idSoftware提出的原型——换句话来说,只要了解了这些原型,那么就完全理解了现代网络游戏。


 

idSoftware公开的第一种出现的联机方式叫Lockstep,早出现在Doom里,所有玩家都是网状连接在一起的,每一回合玩家都发送自己的输入(Input)给所有人,当收到所有玩家的输入后就各自开始Update和Render。这个方法非常简单,而且数据量非常小,因为只有输入数据在网络间传输。但是它有三个明显的不足之处:

 

1. 游戏需要具有100%的确定性,通常来说这非常困难,需要你做出很多功能性的取舍。


2. 所有人的网络延迟都是一样的,等于“网络最卡的那个玩家的延迟”!想象一下31个50ms延迟的玩家在每帧都要等待1个500ms延迟的玩家的输入到达后才能开始模拟,太可怕了!


3. 玩家无法在中途加入游戏(虽然理论上是可以的)。

 

尽管这些不足之处是如此的让人没法接受,Lockstep在局域网条件,或者是只有2-4人联机的情况下依然是很理想的,易于开发,逻辑简单,而且数据量超小——微软的帝国时代在同步1500个单位的时候只需要28.8kbps的带宽。作为对比,如果使用其它模型而不启用数据压缩/发送策略的话,那么主机很可能需要超过100mbps的带宽。所以,时至今日Lockstep依然是很多RTS游戏的首选联机方式。

 

idSoftware充分认识到了Lockstep在快节奏游戏——尤其是FPS上的不足,于是在Quake中,它们使用了另一种联机方式: Client/Server。这一次所有玩家(Client)都连接到一个主机(Server)上,传输的对象也不再是输入(Input)而是状态(State)。因此网络延迟也不再取决于最卡的那名玩家,而是各自连接到主机的延迟。游戏过程中,玩家不断的向主机发送输入,主机模拟后将游戏世界的所有信息,例如位置、HP等不断发送给玩家,而Client本身不需要执行游戏逻辑。所以这种模式下的客户端又叫做Dummy Client。在这种模式的支持下,玩家也可以随时加入/退出游戏了。

 

尽管对比Lockstep有长足的进步,它仍然有些显著的缺点:


1. 玩家所有的输入都需要等待服务器响应,例如一名100ms延迟的玩家A在按下攻击后,服务器要在100ms后才能接收到他的输入并将新的状态发送A,而A需要在200ms后才能在屏幕上看到自己开了一枪!这个时间又被称为RTT, Round Trip Time,即数据包在Client/Server之间走一个来回的时长。


2. 发送的数据量大。相对于输入而言,常规游戏里的状态(State)可能会大上几百倍,而且联机人数越多/需要同步的状态越多,网络数据量就越大。游戏开发者需要非常谨慎的对数据进行精简和压缩,否则在游戏上线后可能会付出高昂的带宽费用。而数据的精简和压缩方法之复杂,本身又足以让游戏开发者学习很长时间了。

 

如果说原始的Client/Server发送数据量大的问题还能克服的话,那么时长等同于RTT的输入延迟是很多FPS很难接受的。于是在QuakeWorld中,idSoftware又加入了一种新的技术:Client-Side Prediction, 客户端预测。这一次客户端不再是一台Dummy Client,在发送完一个输入后,它会马上在本地执行Update和Render,让玩家马上看到自己开了一枪,完美!虽然这个方法彻底解决了输入延迟的问题,但是它有自己的技术难点——当服务器发来正确数据的时候,客户端需要针对有冲突的数据进行调解(Reconciliation),所以它必须存储好输入和状态的历史纪录,当服务器的数据到达时,找到对应时刻的状态,将它们设置为服务器的数据,之后再一直根据输入记录不断执行Update将游戏状态更新到当前时刻,最后再执行Render告诉玩家正确的客户端预测结果。如果说这个过程听着已经够麻烦了,那么一些例外情形的处理就更让人头大了,例如状态的冲突:某个玩家在本地播放了死亡动画但是在稍后数据包到达时,服务器却告诉你其实他还活着;玩家被车辆撞飞了但是服务器却告诉你其实他还在原地。想要非常自然的处理这些数据冲突需要开发者付出很多的努力并积累实战经验,但是客户端预测带来的好处是毋庸置疑的,这就是为什么时至今日,几乎每一个联机FPS都使用了这个技术。

 

好了,一口气写了这么多,希望能帮助大家打开网络游戏这扇晦涩的大门。


矩阵游戏目前正在制作一款新的联机FPS游戏,欢迎有兴趣的技术开发同学联系我们。联系邮箱:479428092@qq.com


今日推荐  

·MOBA类游戏该如何优化AI?

·大数据会说谎吗?腾讯游戏学院品鉴会专家团解读背后的秘密

·腾讯游戏学院品鉴会丨《中国式家长》认为立项差异化是小团队突围关键

↓↓↓点击阅读原文,了解更多。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存