title: 2020游戏开发入门-05(游戏逻辑,主要是状态同步和帧同步)
date: 2020-05-31 23:09:24
tags:
- 游戏开发
- Unity3D
- Python
- 服务端
categories: 游戏开发
目录
概述
-
客户端项目地址:DTSGameClient
-
服务端项目地址:DTSGameServer
Unity3D + C# +Python 2.7 。服务端框架都是自己写的。啥第三方库都没有。资源文件太大。客户端项目里面是Assest/script
文件夹下面的代码。完整项目在里面有个云盘链接。
在windows下直接打开客户端。如果有python环境(我测试的时候是py 2.7。理论上3也可以只是我没全面测试)也可以跑起来服务端。然后就可以登入进去玩了。
玩法大概就是登入后在一个匹配房间。点匹配会在服务端的匹配列表里面。人够了就一起丢到一个场景。按吃鸡的规则最后一个活下来的胜利。
ps: 初学者写的框架。python 2.7 写的。性能不好。仅供学习使用。
游戏逻辑
仔细想想游戏逻辑其实很多也很杂。基本没什么需要特别讲的。无非就是模块划分。就讲几个我写代码的时候的坑点吧。
推荐一本书:
书名:游戏编程模式作者:[美]Robert Nystrom 尼斯卓姆
主要讲设计模式在游戏方面的应用。
我的角色行为状态机模式就是参考里面的算法。
Update函数里面的输入。前进后退什么的。压成二进制int的某一位。然后作为状态机的输入。
然后按状态机写个状态机顺带维护下动画系统就好了。
还有就是注意不要在Update里面new对象。大量new对象会很影响性能的
服务端那边用共享内存。运行时维护下玩家数据。房间数据好像也没啥了。
然后主要是同步算法了。
游戏同步算法
游戏同步算法保证的是多客户端的表现一致。
比如在我搞的这个吃鸡Demo里面。需要同步的就是玩家所在的位置(Transform属性)。角色做动作,射击,蹲下,跳跃,所有客户端都要反馈。A玩家打了B文件一枪扣血要能正确计算。
游戏同步算法主要分状态同步和帧同步。
我的理解:
状态同步同步状态。帧同步同步操作。
玩家A从P1点向前走到P2点。
状态同步需要直接告诉服务端玩家A现在在P2点了。然后所有客户端都知道A在P2点了。
帧同步需要告诉客户端玩家A向前走这个操作。然后服务端广播操作。所有服务端都知道玩家A向前走了。
下面是一些游戏用到的同步算法。
到底用那种同步和游戏有关。但是也不绝对。 比如星际争霸的RTS。如果服务端维护所有控制单位的位置。那网络带宽就不够了。如果用帧同步,同步鼠标从哪里拉到哪里。点了哪里。需要同步的数据就少很多。
同步频率:我写的时候是15个包。也就是66ms一个包。我了解了下大概一秒10~15个包把。这个要自己测。我感觉差不多。
写累了,把我毕设论文粘过来了。。。o(︶︿︶)o
同步算法的存在是为了减少网络波动对于玩家游戏体验的影响,在对实时性有着较高要求的游戏中有着较大的影响。在同步算法的划分上主要分为两种。一种是状态同步,另外一种是帧同步。
状态同步,指的是客户端向服务端上报玩家的状态,可能是血量,空间坐标等信息。这些信息可能由于玩家操作而改变,但是客户端只对最终的状态进行上报。由服务端对所有玩家的数据进行维护,计算。在同步给所有的客户端。计算量大部分在服务端。
帧同步,指的是客户端上报玩家操作,称为逻辑帧。服务端收集所有客户端在某一帧的操作。然后广播给所有客户端。只要所有客户端接收的指令相同,那么所有玩家的表现就是一致的。其中大部分的计算量落在了客户端。只要保证在不同客户端环境下接受相同的指令。最终的计算结果一致即可。因此一般需要做到以下几点。
多客户端在使用随机数之前,需要同步相同的随机种子。
保证不同客户端物理引擎的计算结果一致。Unity3D的物理引擎是英伟达公司的Phys X,是无法保证这一点的。
不能因为某些无序容器的不一致导致结果不一致。比如哈希表在不同客户端的某些算法下哈希码的计算差异性。
浮点数运算本身就存在一定的误差。需要实现定点浮点数。
以上是对于状态同步算法和帧同步算法的概述。但是实际的同步算法一定是和业务息息相关的。下面开始接受本系统使用的同步算法与变化。由于以上四个条件客户端无法全部满足,本系统不适用完全的帧同步算法。
首先是渲染帧和逻辑帧的概念上的不同。本文渲染帧指的是客户端的画面在渲染上更新的频率。更新率越高画面就越流畅。比如60FPS,表示每秒更新60次画面。但是我们的网络数据包不能以这么高的频率更新。于是一些地方提出了逻辑帧的概念。比如每秒发10个消息。那么两次发包的时间间隔就是100毫秒。这个间隔与网络状况和网络类型,以及服务器实现都有着关系。本系统经过测试将使用66毫秒作为发包的间隔。也就是每秒15个数据包。
在帧同步类中,维护着两个字典。放在一个大小为2的列表中。当前帧指针值为0或1,通过与1异或来切换。当前逻辑帧接受所有用户上报的操作,当操作接收满后当前帧指针异或1。同时服务器维护的逻辑帧增加。当前帧变为上一帧。收集完全的所有操作现在允许被所有用户查询。
注意只有当服务端收到所有客户端某一帧的操作时才运行当前帧的查询。只有在客户端查询到了某一帧后才会进入下一帧的上报。而在一帧的上报未完成之前,服务端就不会允许客户端的查询操作。客户端处于哪一逻辑帧会在与服务端的不断通信中被修正。上报快的客户端的操作会被忽略。而上报慢的客户端,则会引起所有客户端的等待,因此客户端没有操作时也需要发一个没有操作的包。但是当客户端卡顿时,包没有发送出去,还是会出现卡顿现象。这确实是一个帧同步算法本生存在的一个缺点。
总体来说,上述算法有两个可能存在的问题。
一个是忽略用户某些操作帧是否会给玩家带来影响。答案是影响很小,因为逻辑帧的频率是66毫秒。具体体现为在这66毫秒内玩家没有操作。但是需要对一些操作做插值平滑化处理。
第二个客户端卡顿造成的影响,当某一个客户端迟迟没有发送数据包时,所有的玩家都会停止等待。运行较快的客户端操作会被忽略,而等待卡顿玩家上报操作。
对于卡顿带来的影响,本系统通过以下几个手段进行了优化。
使用心跳包将卡顿玩家踢出房间。客户端需要每隔一段时间上报自己任然在线。当在误差范围时间限制之外,且没收到某个客户端消息时,将其踢出房间。清空所有内存中的用户数据。战斗系统将不再等待该玩家。当客户端再次连接上来后。发现服务端将自己踢出房间。则显示网络连接中断。
不使用完全的帧同步,在角色Transform属性选择直接广播给所有用户。由于客户端物理引擎无法保证多客户端计算结果的一致性,也不能使用完全的帧同步。玩家的操作,影响角色行为状态机。直接影响角色移动。而其他玩家同步其操作后,只表现角色动画效果,不影响位置。位置信息直接通过获取Transform属性来修改。这样,在其他玩家卡顿时,当前玩家的操作依旧能够直接执行。卡顿的客户端在偶然的网络波动后,会重新开始同步数据。
到此,我们的同步算法已经成型。每个玩家在加入游戏的时候,通过上文提到的游戏控制器。请求服务端分配游戏房间。服务端以每一个游戏房间为单位。划分战斗系统。同步玩家数据。客户端通过一定频率发送数据包。主要为玩家操作数据包,和玩家位置信息数据包。服务端收集数据,并允许客户端查询。
客户端以Json格式上报位置的请求参数如下。
{
"position": "0.00;0.00;0.00",
"rotation": "0.00;0.00;0.00",
"user_id": "1",
"time": "0"
}
其中position, rotation是Unity3D GameObject 中的Transform 组件的数据。由客户端保留两位小数后,带上角色信息user_id。不断更新服务端所存储的角色位置。
用户同步的操作信息,请求响应数据包如下所示。
请求数据包: {"action": 10, "frame": 0, "user_id": "1"}
响应数据包: {"frame": 0, "err_msg": "", "ret": 0}
在请求数据包中。ret和err_msg参数表示请求是否出现未知错误。frame表示发送的操作发生在哪一逻辑帧。响应数据包的frame数值为服务端所在的逻辑帧值。用于修正中途加入的玩家的所在逻辑帧。
action是一个整形值。用户每一帧操作,经过上文的用户角色状态机中提到的压缩算法。将用户行为压缩到一个actionSign的客户端整形变量。在网络传输阶段以数据包的action值表现。
我开始写同步的时候。
玩家在加载出来的时候,当前玩家会挂一个脚本,按66ms的数据向服务端上报 transform的position, rotation。
如果是其他玩家角色。会挂在另一个脚本。按照66ms的数据查transform。
这样其实就能移动了。不过只能动。。。
然后两个思路。
按状态同步。应该是比如A点走到了B点。我拿到的是先后两个坐标。然后我再去算他移动的方向。去驱动他的动画系统???高度变高了播放下跳跃动画。
好像有点麻烦。,,诶
然后我就按帧同步写了。主要是试一试。
在Update函数里面:
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
四个值传给服务端。然后WASD,空格,蹲之类的行为压成一个int。省带宽。一样传给客户端。
客户端维护一个第几帧,服务端也有一个第几帧。
所有客户端在第一帧做的操作要全部告诉服务端后。服务端才允许客户端上报下一帧。太快的客户端报快了怎么办。服务端要忽略。另一个是客户端同一帧可以控制下别上报两次。
而服务端查到所有客户端第n帧上报完后。客户端才会做第n帧的事情。然后把第n+1帧的操作上报。客户端是不会跳帧上报的。
客户端在上报第n帧的时候,客户端另一个接口一直在查其他客户端第n帧的操作。但是如果所有客户端没上报完。服务端不让他查。就查失败了,就一直查。直到成功。成功后,服务端告诉它下一帧是那一帧(而不是客户端自己帧数加一,这样也解决了中途加入游戏的问题了)
另外的问题的卡顿。万一某个客户端卡了一下,卡0.5秒都很明显的。还有就是鼠标视角变换如果是服务端同步完才响应影响会很明显。
所以我又改了一点。当前玩家的操作直接影响角色状态机。反正最终角色的坐标是直接同步位置的。这样某个玩家卡了,对当前玩家没影响。只是它眼中的其他玩家全卡了一下而已。另外,卡太久的玩家可以直接踢下线。用心跳包机制。