【游戏开发】TPS游戏网络同步总结
本次Demo是C/S一体化的设计,即服务端也是Unity做的。网络模块采用了UDP KCP,即先前BNB的强化版,而之所以没用UNet是因为之前搞出了乌龙所以换了现在这套,但序列化部分还是用的UNet。
实现思想
如果你涉及到这方面,那你必须对什么是状态同步有一个大致的了解。市场上的大多数文章都认为它与帧锁定同步相反,但我不认为它们是相反的。这篇文章非常清楚这一点,希望读者不要拘泥于形式。在详细阐述实施思路之前,我们先来看看FPS/TPS游戏的需求:
-
非常迅速的操作反馈(若采用服务器应答后方有反馈的设计,很难达到要求,尤其是操作镜头) → 本地先行
-
个人体验第一(对于是否命中敌人与被命中不是很敏感) → 玩家之间看到画面情况不一致 ACT元素低(不存在ACT游戏的打击控制链,不需要帧判定) → 不需要精确到帧的同步
-
服务器权威(命中判定由服务器决定) → 服务端模拟游戏世界、同步验证 房间战斗(玩家人数不多) → 与MMORPG同步不同
-
相对同步(玩家之间的时间差不可拉得太大) → 追赶进度
Well done,由以上几点需求已经得出了TPS游戏同步的实现思想,下文将根据实现思想阐述具体实现细节。
快照
在探究同步流程之前,首先要了解同步的核心:快照。换言之,也就是我们所同步的内容。快照(Snapshot)通俗来讲就是玩家的操作指令与相关数据的集合,由于需要做同步验证,所以将数据分为必要数据(Must)与验证数据(Check),先来看看移动的快照数据结构吧:
// Actor/Common.cs
public class Move {
public string fd; // Address:Port(Must)
public int frame; // Game Frame(Must)
public bool fromServer; // It is from server, or client?(Must)
public Vector3 velocity; // Moving Velocity(Must)
public Vector3 position; // Position before moving(Check)
}
如上文所示,position
为移动前的坐标,这样的数据客户端不需要上传,只需要与服务器发送的快照进行同步验证。
同步流程
由于服务端模拟游戏世界,所以采用了C/S一体化的设计。在代码层面上则是分为ServerMgr
与ClientMgr
两个MonoBehaviour
,ServerMgr负责收集客户端的快照并整合下发,而ClientMgr负责发送快照与模拟来自服务端的快照以驱动同步单位的运行。如下图所示:
图中的同步快照是一个特殊的快照列表。它由服务器的每一帧打包,包括多个客户端的一帧快照。客户机可以通过模拟其他客户机所代表的对象来驱动它们。此同步过程只能确保在客户端生成在同一帧上生成的快照,并将其打包在服务器上的同一个同步快照中。除了不需要精确同步到帧之外,没有任何保证(不考虑快照之间帧间距的执行)。
追赶进度
在正常的同步过程中情况总是理想的,但是一旦出现网络延迟或卡住的话,在恢复之时便会面临大量的快照,那么按照现有的做法便会导致与其他玩家的时间轴拉得太远(看到的画面是很久以前的了),这便需要设计追赶进度的机制。需要注意的是,追赶进度是服务端与客户端都需要的(服务器也有网络延迟和卡住的可能),客户端的追赶处理相当简单,同步快照超过一个数量则循环模拟:
// ClientMgr.cs
if (this.syncList.Count > 0) {
this.Simulate();
// SYNCMAX = 15
while (this.syncList.Count > SYNCMAX) {
this.Simulate();
}
}
本地先行
本地先行可谓这类同步最玄学之处,不过只要了解其原理倒也无甚。需要本地先行的理由在上文已经阐述,由于是以服务端权威且不那么介意判定的问题,所以是可以允许玩家之间看到画面情况不一致这种情况的。况且在大多数场合下,玩家先行并不会造成什么问题(最终的结果趋于一致),但假设在这么一个场合下:玩家A一直行走,在玩家B的视角里对玩家A进行了眩晕。如此便会造成不同步了,所以需要进行同步验证以将问题修正。
要实现同步验证的思路倒也朴素:就是用一个验证列表将快照保存,当收到同步快照列表时就进行逐个对照(对比它们的验证数据,见前文),一旦发现不一致之处,就以当前位置开始,循环模拟同步快照,然后再继续循环模拟验证列表里进度比目前快的快照,追上最新进度:
// ServerMgr.cs
var list = new List < Snapshot > (); // sync-snapshot
// Foreach all clients.
foreach(var i in this.unitMap) {
int frame = -1;
var sl = i.Value.list;
// INTERVAL = 10, i.Value.count that is count of frame.
while (sl.Count > 0 && (i.Value.count > INTERVAL || (frame == -1 || sl[0].frame == frame))) {
var s = sl[0];
list.Add(s);
sl.RemoveAt(0);
if (frame != s.frame) {
frame = s.frame;
i.Value.count--;
}
}
}
服务端权威
从上文可以看出,本地先行会修正的范围只有本地玩家而已,回到之前的例子:在玩家B的视角里对玩家A进行了眩晕,假设这个行为在服务端上并没有达成(玩家A闪现走了),那么该如何修正呢?很显然可以选择搞个更大的修正系统,但我认为这样并不符合业界的常规做法,所以我给出的答案是: 眩晕行为需要在服务端触发了,然后由服务端将其作为快照,以正常同步的形式在诸客户端上展示。
事实上在网络正常的情况下,这样的间隔最多也只是0.1x秒左右而已,完全可以接受。当然这么做对于玩家B而言肯定会发生修正(眩晕按理来说是之前的事了),所以我对此作了个措施: 为快照设计了fromServer
属性,一旦是fromServer = true
且属于本地玩家的快照,本地玩家会直接模拟而不会将其进行修正对比。这也可以看出这套同步的一个规则:会影响他人的操作,都需要由服务端发起。
后记
很显然,目前这个demo仍很不成熟,不少地方在业界应该会有更好的处理,如CS的射击纠正(服务端根据客户端的射击时间回滚之前的场景进行判定)。如此只能算是一个雏形,还是缺少实战项目的淬炼,先根据接下来的项目看看效果吧。
好啦,以上就是今天要分享的内容!
转载声明:本文来源于网络,不作任何商业用途。
全部评论
暂无留言,赶紧抢占沙发