|
Game Network Sync GreenBook
简介:
1 本文规范定义了一些网络同步相关名词
2 同步方案中按照某些切入点进行了分类
3 阅读其他博客文章中发现一些疑惑点或者文章比较混乱,进行了验证和重定义;
4 历年游戏方案简略分析和商业方案技术选型推荐
参考
GameNetworkingResources
https://github.com/ThusSpokeNomad/GameNetworkingResources
同步发展 简介
https://blog.csdn.net/weixin_43679037/article/details/122848197#6Lag_Compensation_452 UE4
https://zhuanlan.zhihu.com/p/55596030 《守望先锋》回放技术-阵亡镜头、全场最佳和亮眼表现 https://gameinstitute.qq.com/community/detail/115186 https://zhuanlan.zhihu.com/p/130702310 https://zhuanlan.zhihu.com/p/164686867 https://zhuanlan.zhihu.com/p/336869551 https://www.udocz.com/apuntes/491/the-doom-3-network-architecture-2006 https://www.gamedevs.org/ https://docs.unrealengine.com/udk/Three/NetworkingOverview.html http://www.skywind.me/blog/archives/112 http://www.skywind.me/blog/archives/131 http://www.skywind.me/blog/archives/1343
https://fabiensanglard.net/
https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
零.概念说明
[状态网络同步]
State Network Synchronization
【状态网络同步】 简称 State Sync 或者 “'状态同步”
姑且分为2个大类别:快照流派和状态流派;这里按照客户端服务器处理力度,主动性和轻重划分。
- Authoritative Snapshot 权威快照
- State Replication Synchronization 状态复制同步
权威快照 派系下 :
由服务器处理大量运算,统一维护了世界的同步状态和一致性,甚至每个玩家的视角可见性渲染等,客户端的计算和权力较小。“重服务器轻客户端”。
状态复制同步 派系下 :
客户端各自维护状态更新的同步过程。“客户端主动性较强”。
[确定性网络同步]
Deterministic Network Synchronization
【确定性网络同步】 经常在国内被广泛(误)称为 “帧同步” 或者 “LockStep"
"LockStep" 是 确定性网络同步 中的一种。
本质上只有2种:锁步和回滚为互斥关系,其区别是对逻辑帧的处理,是顺序执行还是可以回退到之前再执行。
- Deterministic LockStep 确定性锁步
- Deterministic RollBack 确定性回滚
名称统一
【确定性网络同步】Deterministic Synchronization 名称由来 GDC 技术讲坛,各大商业解决方案,国外共识。 “frame synchronization”
是不存在这个说法的(中译英)。google搜索结果基本不相关。
"帧同步"叫法由来: 韦易笑2009年培训学生扩散。
https://zhuanlan.zhihu.com/p/165293116
Deterministic LockStep 确定性锁步
基本锁步 Basic LockStep
等待操作数据完成传输。在所有数据都到达前,什么都不做。这是锁步。

在一个游戏客户端上运行的锁步示例。客户端发送自己的角色输入,然后等待其他玩家(Remote Input)的输入(Remote Input),然后再处理游戏Tick。它会等待接收该数据所需的时间。
由于必须等待数据传输,锁步的基本版本有一个显着的限制:它需要所有客户端在下一个时间点发生之前完全完成他们的游戏时间和传输。如果甚至一个客户端由于延迟或机器速度慢而保持Tick,所有其他客户端(和服务器)都必须等待。游戏必须将其Tick速度降低到由最慢(或最迟)客户端决定的速度;如果网络中出现延迟峰值,那么所有游戏都将不得不暂时冻结(延迟)。

玩家 2(橙色)滞后的示例。尽管每个刻度的持续时间有所不同,但玩家 1(蓝绿色)客户端必须等待数据到达。两个客户都保持一致。
输入延迟锁步 Lockstep with Input Delay

三节拍输入延迟的示例。输入被传输,并在使用前缓冲 3 个Tick。这使数据有时间传输
通过不再需要等待,并期望输入在需要时到达,游戏可以根据需要以尽可能快的Tick率运行。只要输入数据在预定运行时间之前到达,游戏就不需要进入等待状态。

如果玩家 2(橙色)在 INPUT 6 发送后开始延迟,只要该数据在需要之前到达玩家 1 的客户端,延迟对游戏玩法没有影响。
其他锁步 Lockstep IDK
...
确定性锁步 举例 (Phonton TrueSync):
TureSync 已经废弃。这里跳过简介。
本身设计有一定问题,实际运行起来会产生DeSync。 第三方备份仓库:
https://github.com/zhu1987/TrueSync
Deterministic RollBack 确定性回滚
确定性回滚简介
游戏使用对输入可能是什么的预测来处理Tick,然后,当实际的真实输入稍后到达时,游戏状态将回滚到预测开始时,并在结合真实输入数据和当前数据的同时向前重新计算游戏状态换成了新的、重新处理过的状态。

回滚的一个例子:当没有接收到数据时,使用预测的输入进行Tick。当实际输入到达时,状态将回滚到该点,并与输入一起向前重新处理。
这样做的结果是可以减少本地玩家的输入延迟,代价是玩家在其他玩家(Remote Input)对该Tick的输入到达之前看到预测的游戏状态(这可能完全错误)并且游戏状态回滚并更新。这可能会导致诸如橡皮筋或传送之类的伪影,以及其他图形“故障”,当预测的游戏状态与实际状态不同时,就会发生这种情况。
为了减少这种瞬间错误的预测游戏步骤,可以将回滚与输入延迟相结合以分割差异:输入延迟少量,并使用预测/回滚来覆盖其余的延迟。这导致输入延迟和需要预测的刻度之间的折衷。

回滚系统的另一个好处是游戏现在可以容忍意外的网络延迟——如果输入没有在需要的时间到达,而不是像在具有输入延迟的纯锁步中那样冻结直到接收到数据方法,游戏只是继续做出预测的游戏状态,直到数据到达。
确定性回滚 举例 (GGPO):
1 回滚如何实现
(回到历史游戏进程状态)
A1:存储所有历史帧操作,从第0帧开始算,算到目标帧号。(问题很多:需要处理的事物很多,性能也可能是瓶颈, 断线重连做一次是可以的,但是回滚是频繁发生的,高频从头计算到目标帧不可行)
A2 : 存储游戏状态,内存拷贝和跳转到目标帧号。 (GGPO方案)
GGPO gameState:
//GGPO vertexWar 打飞船Demo(雷电类似)。
// GameState 定义 : 地图飞船等相关基础属性
struct GameState { void Init(HWND hwnd, int num_players);
void GetShipAI(int i, double *heading, double *thrust, int *fire);
void ParseShipInputs(int inputs, int i, double *heading, double *thrust, int *fire);
void MoveShip(int i, double heading, double thrust, int fire);
void Update(int inputs[], int disconnect_flags);
int _framenumber;
RECT _bounds;
int _num_ships;
Ship _ships[MAX_SHIPS]; };
//gameState #define MAX_PLAYERS 64
GameState gs = { 0 };
NonGameState ngs = { 0 };
Renderer *renderer = NULL;
GGPOSession *ggpo = NULL;
// 保存 gameSate
bool __cdecl vw_save_game_state_callback(unsigned char **buffer, int *len, int *checksum, int)
{ *len = sizeof(gs);
*buffer = (unsigned char *)malloc(*len);
if (!*buffer) { return false; }
memcpy(*buffer, &gs, *len);
*checksum = fletcher32_checksum((short *)*buffer, *len / 2);
return true; }
//载入 gameSate 回滚到目标游戏状态
bool __cdecl vw_load_game_state_callback(unsigned char *buffer, int len)
{
memcpy(&gs, buffer, len);
return true;
}
3 回滚的目的
- 减少体验反馈延迟

GGPO 不是等待来自远程玩家的输入,而是根据过去的输入预测其他玩家可能会做什么。它将预测输入与玩家 1 的本地输入相结合,并立即将合并的输入传递给游戏引擎,以便它可以继续执行下一帧,即使尚未收到包含来自其他玩家的输入的数据包。
3 回滚触发和纠错
当远程 input 没达到的时候,P1玩家本地逻辑帧 结合 预测 input输入。
当远程 input到达时候进行校验。然后回滚,进行input 合并。 依次循环

同步对象
同步对象用于跟踪游戏状态的最后 n 帧。当其嵌入的预测对象通知它预测错误时,Sync 后端会将游戏倒回到更正确的状态并向前单步纠正预测错误。
输入队列对象
InputQueue 对象跟踪本地或远程播放器接收到的所有输入。当被要求输入它没有的输入时,输入队列会预测下一个输入,并跟踪此信息以备后用,以便同步对象知道在预测不正确时回滚到哪里。如果请求,输入队列还实现帧延迟。
4 回滚执行的流程

// CheckSimulation
void
Sync::CheckSimulation(int timeout)
{
int seek_to;
if (!CheckSimulationConsistency(&seek_to)) {
AdjustSimulation(seek_to);
}
}
// find first incorrect frame number
bool
Sync::CheckSimulationConsistency(int *seekTo)
{
int first_incorrect = GameInput::NullFrame;
for (int i = 0; i < _config.num_players; i++) {
int incorrect = _input_queues.GetFirstIncorrectFrame();
Log(&#34;considering incorrect frame %d reported by queue %d.\n&#34;, incorrect, i);
if (incorrect != GameInput::NullFrame && (first_incorrect == GameInput::NullFrame || incorrect < first_incorrect)) {
first_incorrect = incorrect;
}
}
if (first_incorrect == GameInput::NullFrame) {
Log(&#34;prediction ok. proceeding.\n&#34;);
return true;
}
*seekTo = first_incorrect;
return false;
}
// AdjustSimulation
void
Sync::AdjustSimulation(int seek_to)
{
int framecount = _framecount;
int count = _framecount - seek_to;
_rollingback = true;
/*
* Flush our input queue and load the last frame.
*/
LoadFrame(seek_to);
/*
* Advance frame by frame (stuffing notifications back to
* the master).
*/
ResetPrediction(_framecount);
for (int i = 0; i < count; i++) {
_callbacks.advance_frame(0);
}
_rollingback = false;
}
void
Sync::LoadFrame(int frame)
{
// Move the head pointer back and load it up
_savedstate.head = FindSavedFrameIndex(frame);
SavedFrame *state = _savedstate.frames + _savedstate.head;
//载入 gameSate 回滚到目标游戏状态
_callbacks.load_game_state(state->buf, state->cbuf);
// Reset framecount and the head of the state ring-buffer to point in
// advance of the current frame (as if we had just finished executing it).
_framecount = state->frame;
_savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames);
}
//
void
Sync::IncrementFrame(void)
{
_framecount++;
SaveCurrentFrame();
}
//
void
Sync::SaveCurrentFrame()
{
/*
* See StateCompress for the real save feature implemented by FinalBurn.
* Write everything into the head, then advance the head pointer.
*/
SavedFrame *state = _savedstate.frames + _savedstate.head;
if (state->buf) {
_callbacks.free_buffer(state->buf);
state->buf = NULL;
}
state->frame = _framecount;
_callbacks.save_game_state(&state->buf, &state->cbuf, &state->checksum, state->frame);
Log(&#34;=== Saved frame info %d (size: %d checksum: %08x).\n&#34;, state->frame, state->cbuf, state->checksum);
_savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames);
}
总结和对比
Deterministic LockStep 确定性锁步 | 概括 | 说明 | 基本锁步 Basic LockStep | 等待Remote Input的达到,然后再处理游戏Tick,期间暂停逻辑更新 Logic Simulation | 一旦网络RTT加长和客户端卡顿,会加剧卡顿和延迟感 | 输入延迟锁步 Lockstep with Input Delay | 缓存本地和远程input, 通过 Tick index 顺序滞后执行;只要输入数据在预定运行时间之前到达,游戏就不需要暂停逻辑更新 Logic Simulation | 人为制造输入延迟;来平衡可能遇到的“忽快忽慢”;
任何玩家输入的延迟感都会被放大;并且低延迟的玩家也会和高延迟方对齐。 | 其他... | Deterministic RollBack 确定性回滚 | 当Remote Input到达时,游戏状态将回滚到预测开始之前,并在结合真实输入数据和当前数据的同时向前重新计算游戏状态换成了新的、重新处理过的状态。 | 减少本地玩家的输入延迟;尽量不暂停逻辑更新;
代价是玩家在其他玩家对该Tick的输入到达之前看到预测的游戏状态(可能完全错误,硬拉,&#34;鬼影&#34;等)并且游戏状态回滚并更新 |
流程简述
Deterministic LockStep 确定性锁步
- {方案1} [ 本地帧操作 => 服务器 => 远程所有帧操作回包 => 逻辑层 => EventMgr => 表现层 ]
- {方案2} 逻辑层 :[ 本地帧操作 => 服务器 => 远程所有帧操作回包 => 逻辑层 ]
表现层 :[ 表现层 =(查寻)> 逻辑层 ]
- 架构流程是单链流水线线性式结构,环节间是相互高依赖的,有一个环节有断连和延迟都会影响后面的环节。{方案2} 可以缓解延迟反馈感受。
Deterministic RollBack 确定性回滚
- 玩家1 : [ 本地帧操作 => 逻辑层先行计算 => 表现层先行计算 ]
[ 本地帧操作 => 服务器 => 远程帧操作回包 => 帧号比较与回滚逻辑帧 => 回滚表现层再更新 ]
- 玩家n... : [ 本地帧操作 => 逻辑层先行计算 => 表现层先行计算 ]
[ 本地帧操作 => 服务器 => 远程帧操作回包 => 帧号比较与回滚逻辑帧 => 回滚表现层再更新 ]
关键词说明 [确定性网络同步]
帧 / 逻辑帧 Logic Tick | 建议统一命名为 Tick, 对应 LogicManager ,更新方法一般命名为Sim/ Simulation | 渲染帧 RenderingFrame | 建议统一命名为 Frame, 表现层更新,一般为Update | “追帧” Tick-CatchUp | 本地帧操作号,Tick Num 落后于远程操作帧号 ,开始加速模拟Loop | 锁步 LockStep | 玩家均保持和等待本地帧号和远程帧号一致,从而达到&#34;锁步&#34; | 回滚 RollBack | 本地逻辑帧先行,对逻辑层帧回退到历史帧号重新开始再模拟 | 步长 step | 客户端预测Client-Side Prediction | 本地帧号或者状态在远程帧号或者状态之前做的一些弥补“RTT空闲”情况的措施;一般用于移动预测(较安全),如果是不连续大位移和变化处理事项较多。 | 战斗验证BattleValidation | 逻辑层单独剥离出来,平台化处理后放到服务器作为战斗结果验算。 | 不同步 deSync | deSync,客户端各自运算产生不同步结果,常见于确定性网络同步 | Server Reconciliation
服务器同步协调 | 可以用于物理计算同步,但不是唯一解释:
客户端执行去自己的物理模拟,会在不同客户端之间同步上产生精度区别。但是如果所有的物理物件在中央服务器以及处理更新状态,并发送回给所有客户端,来确保他们是一致的。 | CheckSum 校验和Fletcher&#39;s checksum | 校验和是从另一个数字数据块导出的小数据块,用于检测在传输或存储过程中可能引入的错误。校验和本身通常用于验证数据完整性,但不依赖于验证数据的真实性 |
关键词说明 [状态网络同步]
SnapShot 快照 | 记录客户端同步性必要参数的状态序列化集合 | SnapShot Buffer 快照缓冲
Incremental SnapShot 增量快照(n)Delta SnapShot 增量快照Snapshot Interpolation 快照差值 | 快照缓冲;
任意2个快照间的差值。具体指新快照相对旧快照的增量差值。
任意2个快照间的差值补间变化值。 | State Consistency 状态一致性 | State Consistency 状态一致性 | Eventual Consistency 最终一致性 | 最终一致性允许部分或者剔除后的状态传输,这可能是实现感兴趣区域所必需的.但保证最终一致性 | 客户端预测Client-Side Prediction | 本地状态在远程状态之前做的一些弥补“RTT空闲”情况的措施;一般用于移动预测(较安全),如果技能连续动作处理事项较多。 | Dead Reckoning航位推测法 | 航位推测法(简称:DR)是一种利用当前位置及位置的航行技术,迅速推定应用至定位,从而影响准确度,游戏中用于载具对象的行驶预测。 | Tick-based Clock 基于刻度时钟 |
历年游戏同步方案图

(原图片源自 曾志伟,有修改)
(拳头公司在2016~2017年间,将英雄联盟的服务器框架改为成确定性;而客户端仍保持状态/事件/预测式的c/s架构,主要考量是录像回放和比赛进度闪回,这里暂时也认定他也是确定性)
游戏 | 简介 | 游戏类型 | 网络拓扑 | 特点 | 说明 | 确定性网络同步 | Doom (1995) | 确定性网络同步,确定性锁步 | FPS | P2P | Mortal Kombat | 确定性网络同步,确定性回滚 | ACT | P2P | 16 毫秒内8帧 回滚 | Serialization Restore;开发商在开发过程中将同步架构从Lockstep 调整为 RollBack; | For Honor | 确定性网络同步,确定性回滚 | ACT | P2P | 确定性回滚 | Rewind / History Buffers
确定性AI 和 复制 (replicated)AI并存 | StarCraft II | 确定性网络同步, 确定性锁步 | RTS | C/S | 多单位阵型确定性;强大的录像功能。 | 星际争霸2的录像可以播放和任意跳转,还可以从中任意一时间点开始从录像切为实时对战。录像文件相对较小,150kb左右 | League of Legend (2017年左右之后) | 状态同步 +
确定性服务器 (比较怪) | MOBA | C/S | 拳头公司在2016~2017年间,将英雄联盟的服务器框架改为成确定性。主要考量是录像回放和比赛进度闪回,这里暂时也认定他也是确定性
LOL录像文件格式是 Rofl ,解析其中的可以看到Keyframe 和 Chunk。 | 状态同步 | Quake | 状态同步,采用全快照的全状态同步 | FPS | C/S | 全快照同步,客户端是输入器和渲染器 | “快照” 在Quake里 成为了 状态同步保证的 唯一手段。 | QuakeIII ( 1999 ) | 状态同步,采用增量快照的状态同步 | FPS | C/S | 增量快照同步,客户端是输入器和渲染器 | “快照” 在Quake III 里 成为了 状态同步保证的 唯一手段。快照相对Quake 改为了 增量快照 | OverWatch (2015) | 状态同步,采用ECS架构和预测,回滚的增量状态同步 | FPS | C/S | 采用ECS架构 方便做回滚和实时死亡回放等 | “快照” 在OverWatch里 是用于实体状态数据记录的手段,方便回滚,死亡回放等。
整场Replay回放(不是高光时刻)保存在服务器,最近10场。 | Dota2 | 状态同步 , ??? | MOBA | C/S | 录像文件相对较大. *dem 大约33mb 左右。 | League of Legend (2017年之前) | 状态同步 , ??? | MOBA | C/S | 早年版本的LOL录像文件格式是
LRF ? 和 LPR ?。解析其中只有 Chunk Data |
一. 历年游戏分析
1. [DOOM] 同步框架
[Doom] Github(Linux)
https://github.com/id-Software/DOOM
信息总览
项目代码结构
File | D_* | G_* | I_* | M_* | P_* | R_* | V_* | W_* | Z_* | Info | Initialisationgeneral code | Main game loop/control | System-specific code | Miscellaneous (includes the menu) | Game logic/behaviour | Rendering engine | General graphic rendering | WAD file loading | Zone memory allocation system |
网络信息简介
网络同步类型 | 网络通信拓扑 | 网络通信协议 | 确定性同步 LockStep锁步 | P2P | UDP |
确定性锁步
定点数
m_fixed.c
m_bbox.c
// m_fixed.h
// Fixed point, 32bit as 16.16.
//
#define FRACBITS 16
#define FRACUNIT (1<<FRACBITS)
typedef int fixed_t;
fixed_t FixedMul (fixed_t a, fixed_t b);
fixed_t FixedDiv (fixed_t a, fixed_t b);
fixed_t FixedDiv2 (fixed_t a, fixed_t b);
TickCMD操作命令
d_ticcmd.h
// The data sampled per tick (single player)
// and transmitted to other peers (multiplayer).
// Mainly movements/button commands per game tick,
// plus a checksum for internal state consistency.
typedef struct
{
char forwardmove; // *2048 for move
char sidemove; // *2048 for move
short angleturn; // <<16 for angle delta
short consistancy; // checks for net game
byte chatchar;
byte buttons;
} ticcmd_t;
//================================================================
i_net.c
void PacketGet (void)
// byte swap
netbuffer->checksum = ntohl(sw.checksum);
netbuffer->player = sw.player;
netbuffer->retransmitfrom = sw.retransmitfrom;
netbuffer->starttic = sw.starttic;
netbuffer->numtics = sw.numtics;
for (c=0 ; c< netbuffer->numtics ; c++)
{
netbuffer->cmds[c].forwardmove = sw.cmds[c].forwardmove;
netbuffer->cmds[c].sidemove = sw.cmds[c].sidemove;
netbuffer->cmds[c].angleturn = ntohs(sw.cmds[c].angleturn);
netbuffer->cmds[c].consistancy = ntohs(sw.cmds[c].consistancy);
netbuffer->cmds[c].chatchar = sw.cmds[c].chatchar;
netbuffer->cmds[c].buttons = sw.cmds[c].buttons;
}
网络及帧数据处理
d_net.c
ticcmd_t localcmds[BACKUPTICS];
ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS];
int nettics[MAXNETNODES];
boolean nodeingame[MAXNETNODES]; // set false as nodes leave game
boolean remoteresend[MAXNETNODES]; // set when local needs tics
int resendto[MAXNETNODES]; // set when remote needs tics
int resendcount[MAXNETNODES];
int nodeforplayer[MAXPLAYERS];
int maketic;
int lastnettic;
int skiptics;
int ticdup;
int maxsend; // BACKUPTICS/(2*ticdup)-1
//=================================================================
//
// TryRunTics
//
int frametics[4];
int frameon;
int frameskip[4];
int oldnettics;
extern boolean advancedemo;
void TryRunTics (void)
{
int i;
int lowtic;
int entertic;
static int oldentertics;
int realtics;
int availabletics;
int counts;
int numplaying;
// get real tics
entertic = I_GetTime ()/ticdup;
realtics = entertic - oldentertics;
oldentertics = entertic;
// get available tics
NetUpdate ();
lowtic = MAXINT;
numplaying = 0;
for (i=0 ; i<doomcom->numnodes ; i++)
{
if (nodeingame)
{
numplaying++;
if (nettics < lowtic)
lowtic = nettics;
}
}
availabletics = lowtic - gametic/ticdup;
// decide how many tics to run
if (realtics < availabletics-1)
counts = realtics+1;
else if (realtics < availabletics)
counts = realtics;
else
counts = availabletics;
if (counts < 1)
counts = 1;
frameon++;
// wait for new tics if needed
while (lowtic < gametic/ticdup + counts)
{
NetUpdate ();
lowtic = MAXINT;
for (i=0 ; i<doomcom->numnodes ; i++)
if (nodeingame && nettics < lowtic)
lowtic = nettics;
if (lowtic < gametic/ticdup)
I_Error (&#34;TryRunTics: lowtic < gametic&#34;);
// don&#39;t stay in here forever -- give the menu a chance to work
if (I_GetTime ()/ticdup - entertic >= 20)
{
M_Ticker ();
return;
}
}
// run the count * ticdup dics
while (counts--)
{
for (i=0 ; i<ticdup ; i++)
{
M_Ticker ();
G_Ticker ();
gametic++;
// modify command for duplicated tics
if (i != ticdup-1)
{
ticcmd_t *cmd;
int buf;
int j;
buf = (gametic/ticdup)%BACKUPTICS;
for (j=0 ; j<MAXPLAYERS ; j++)
{
cmd = &netcmds[j][buf];
cmd->chatchar = 0;
if (cmd->buttons & BT_SPECIAL)
cmd->buttons = 0;
}
}
}
NetUpdate (); // check for new console commands
}
}
doomstat.h
// Needed to store the number of the dummy sky flat.
// Used for rendering,
// as well as tracking projectiles etc.
extern int skyflatnum;
// Netgame stuff (buffers and pointers, i.e. indices).
// This is ???
extern doomcom_t* doomcom;
// This points inside doomcom.
extern doomdata_t* netbuffer;
extern ticcmd_t localcmds[BACKUPTICS];
extern int rndindex;
extern int maketic;
extern int nettics[MAXNETNODES];
extern ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS];
extern int ticdup;
执行流程





网络包收发


Demo Windows
Windows Build https://www.chocolate-doom.org/wiki/index.php/Downloads
Doom WADs Files
https://www.pc-freak.net/blog/doom-1-doom-2-doom-3-game-wad-files-for-download-playing-doom-on-debian-linux-via-freedoom-open-source-doom-engine/ Windows Build Demo
Port 设置:
Windows 10
- Go to Control Panel --> Systems and Security --> Windows Defender Firewall
- Select Allow an App through Windows Firewall
- Select Advanced Settings --> Inbound Rules
- Create a New Rule
- Port (click next) --> UDP
- Specify port 2343(click next)
- Allow Connection (click next)
- Rule Applies should have { Domain, Public, Private } all checked (click next)
- Name this rule &#34;xxx&#34;


2. [QuakeIII] 同步框架
[Quake-III-Arena] Github
https://github.com/id-Software/Quake-III-Arena
信息简介
项目代码结构
Dir | code/ | q3asm/ | q3map/ | q3radiant/ | libs/ | File | SV_* | CG_* | AI_* | G_* | TR_* | Info | Server code | Game Client code | Bot AI | Gameplay
Logic | Render | map compiler | ( .map -> .bsp ) | Q3Radiant map editor build 200f | Third party lib: jepg6; pak ... |
网络信息简介
网络同步类型 | 网络通信拓扑 | 网络通信协议 | 状态同步 | C/S | UDP |
- 增量快照
- &#34;回包用最新的&#34;
分析和图解
看这个blog:
https://fabiensanglard.net/quake3/network.php

代码查看
cg_public.h
typedef struct {
int snapFlags; // SNAPFLAG_RATE_DELAYED, etc
int ping;
int serverTime; // server time the message is valid for (in msec)
byte areamask[MAX_MAP_AREA_BYTES]; // portalarea visibility bits
playerState_t ps; // complete information about the current player at this time
int numEntities; // all of the entities that need to be presented
entityState_t entities[MAX_ENTITIES_IN_SNAPSHOT]; // at the time of this snapshot
int numServerCommands; // text based server commands to execute when this
int serverCommandSequence; // snapshot becomes current
} snapshot_t;
server.h
typedef struct {
int areabytes;
byte areabits[MAX_MAP_AREA_BYTES]; // portalarea visibility bits
playerState_t ps;
int num_entities;
int first_entity; // into the circular sv_packet_entities[]
// the entities MUST be in increasing state number
// order, otherwise the delta compression will fail
int messageSent; // time the message was transmitted
int messageAcked; // time the message was acked
int messageSize; // used to rate drop packets
} clientSnapshot_t;
Calls-SV_BuildClientSnapshot

Butterfly-SV_WriteSnapshotToClient

Calls-CG_DrawActiveFrame

CalledBy-CG_TransitionSnapshot

Calls-CG_TransitionSnapshot

3. [英雄联盟] 同步框架
LOL的变迁
拳头公司在2016~2017年间,将英雄联盟的服务器框架改为成确定性;
拳头公司考量具体有流量问题,服务器承载问题,比赛错误下的“时光回溯”的精确度问题灾难恢复 (DDR) ,录像回放便利性等。 具体可以看拳头官方的文章:
https://technology.riotgames.com/news/determinism-league-legends-introduction
https://technology.riotgames.com/news/determinism-league-legends-implementation
https://technology.riotgames.com/news/determinism-league-legends-unified-clock
https://technology.riotgames.com/news/determinism-league-legends-fixing-divergences
&#34;THE ORIGINS OF SERVER DETERMINISM&#34;
It may come as a surprise that determinism in League of Legends was not inspired by Project Chronobreak. Rather, it was inspired by a desire to create fast, repeatable tests driven from a set of recordable inputs. This tool was dubbed Delta Checker, and it required a level of client-server determinism to operate reliably.
&#34;GOING DEEPER&#34;
We haven’t really plumbed too deeply into the technical details of the project just yet, but stay tuned for the next posts in this series, where I’ll cover how we transformed the League of Legends codebase to be deterministic.
&#34;TIME&#34;
We played it safe by retaining the variable frame delta times, which in turn helped us deliver DDR in a shorter timeframe. However, this is something I’d like to explore further in the future. With a fixed timestep, we may be able to improve the knowability and testability of numerous game systems. It may also simplify the creation of a debug-network-lock-step-mode to game clients, which enables some valuable debugging and profiling functionality.
“检测分歧”
分歧检测的核心是能够比较两次独立运行的游戏结果。理想情况下,我们将原始游戏的结果与服务器网络记录的回放进行比较。
我们选择的解决方案是手动将[游戏状态]记录为文件中的一组分层键值对。这有助于我们用一块石头杀死两只鸟:我们可以比较这些日志的输出,我们可以使用这些日志来帮助我们查明代码库中可能发生分歧的位置。
游戏状态日志包含英雄联盟服务器中不断变化的游戏状态的基于 JSON 的表示。我们通常在每一帧的末尾捕获这些日志,尽管我们有能力降低我们的采样率或将日志记录限制到特定的帧片段。
为了节省空间,我们不会每帧记录每个游戏变量的状态,而只记录值发生变化的帧。这种关键的优化极大地提高了 I/O 性能,代价是在日志写入时运行值与现有状态的比较。即使进行了这种优化,服务器上的游戏状态记录也可能会产生大量的性能开销。因此,我们还有两个级别的游戏状态记录细节:

我们编写了一个 Python 脚本来比较游戏日志并返回与第一个已知分歧有关的详细信息的简洁“差异日志”。正如在之前的博文中所讨论的,只有第一次分歧才是真正重要的,因为在一次分歧之后,游戏状态将很快陷入混乱。
测试和调查差异的标准工作流程如下:
- 在其中一个 Riot 环境中记录服务器网络记录和基本游戏状态日志以进行真实游戏
- 在单独的电脑上,播放服务器网络记录并创建一个新的基本游戏状态日志
- 比较两个基本的游戏状态日志
- 如果基本游戏状态日志不同,请使用详细日志重新运行两次播放,将详细日志相互比较
- 从发散的变量开始调查发散
定义确定性
验证确定性
识别输入
记录输入SNR
控制输入
时间
服务器启动值
随机数发生器
未初始化的变量
异步处理
非确定性系统状态泄漏
检测分歧
自动化测试
案例研究:解决分歧
<hr/>
ROFL 文件解析
ROFLPlayer
ROFLPlayer 是一个简单的 Windows 程序,用于查看和播放英雄联盟的回放文件。
读取录像文件的游戏版本号,去下载不同LOL版本的游戏补丁,甚至5年前的版本录像。
早年版本的LOL录像文件格式是 LRF ? 和 LPR ?。解析其中只有 Chunk Data
https://github.com/fraxiinus/ReplayBook
https://github.com/fraxiinus/ROFL-Player

播放器读取录像文件的游戏版本号,去下载不同LOL版本的游戏补丁; 解析出比赛结果和比赛数据统计;
ROFL 文件由 明文Json 和 Byte[]组成; 其中Json部分记录了比赛结果和玩家数据统计;
Byte[]部分是由Keyframe, Chunk Data组成。
目前 Chunk Data 具体值没有被解析出来。
A complete ROFL parser for C#
https://github.com/fraxiinus/roflxd.cs
public class ChunkHeader {
public uint ChunkId { get; private set; }
public ChunkType ChunkType { get; private set; }
public uint ChunkLength { get; private set; }
public uint NextChunkId { get; private set; }
public uint Offset { get; private set; } }
public class Chunk {
public uint Id { get; private set; }
public ChunkType Type { get; private set; }
/// Unknown format, presented as raw bytes
public byte[] Data { get; private set; } }
public enum ChunkType { Keyframe, Chunk }
游戏同步类型确定方法:
- 官方开发者讲坛;
- 版本更新时候观察录像文件是否删除;
- 录像文件大小或者格式解析;
- 网络收发包抓取和分析;
- 其他游戏对战特征 (重连,弱网等);
总结:
League 客户端不使用确定性同步模拟。它是一种状态/事件/预测风格的客户端-服务器架构。确定性仅存在于服务器上。用于录像快速回放和确定性数据倒退。
架构比较奇怪/特别,目前难以准确量化和思考出对应结构图。
4. [守望先锋] 同步框架
采用State Replication Synchronization 状态复制同步; 客户端代码架构基于ECS:
实体是由组件组成的,由System负责更新, 有些System是序列化过程的参与者(participants),它们负责同步游戏的某些方面(aspect)给接收者(receivers)或者复制目标(replication targets)。接受者一般就是需要接收数据的活跃玩家或者观战者(spectator)。最后,复制目标会收到关于其他实体的更新消息。 通常会把这一切简言之为:参与者把网络相关实体数据打包并发给接收者.
OverWatch框架流程图概要
GDC译文链接
https://gameinstitute.qq.com/community/detail/115186
框架流程图概要如下

客户端
客户端有2个域 Domain,相当于平行世界。 一个是 Gamplay,用于游戏战斗; 一个是 Replay,用于回放所有类型的录像; 他们都是基于相同的ECS框架,是2个不同的实例化。大部分运行代码都是相同的; 不同之处大致有: 序列化器; 网络接受者;
网络Sender; 其他...
增量复制

服务器
从c2s网络包中维护和处理 每帧脏数据集合(per frame dirty set) 以及 永久脏状态集合(lifetime dirty set),并且从中处理delta snapshot 和下发给客户端s2c网络包。 服务器有内存缓冲,用于缓存每帧快照 (per snapshot )和增量快照 (delta snapshot )
具体可以看上面的流程图.
Replay 回放
生成回放“卷”(replay reel)
卷”由一个快照 (snapshot x)加上一系列递增的Delta (snapshot delta x+1 +n...)组成。



插入更多快照!
可扩展的观战功能。增加一个代理或者服务器,与实时游戏服务器独立开来,专门接收带Delta的回放快照即可。越来越多的玩家都想要在诸如锦标赛之类的比赛中实时观战,只需要下发快照给这些玩家,然后不断塞Delta数据流给他们就行了。所有这一切都发生在一个隔离的服务器上,完全不用担心实时游戏服务器过载.

OverWatch 的录像数据在服务器,磁盘占用较大,有一定的存储压力和带宽压力。
逻辑和上层
角色技能逻辑用的可视化编程工具 StateScript Editor. 和 Unreal 的 Blueprints 有很多类似之处。 stateScript 最早开发源自 《Titan 》
编辑器 | 持久资源文件 | Node 节点 | StateScript | stateScript Editor | *.stu可编译成自动生产代码;支持属性标签和运行时反射; | Entry入口;Condition 条件;Action 动作;State 状态;Variables 变量; |
StateScript 简介




StateScript 同步





和 unreal 有类似之处。在可视化编辑器内 可以指定 Server 和 Client 执行权威。

【属性标签】 [ ...SYNC_ALL] 自动同步 StateScript 的 State 和 Action等; 类似Unreal 的UPROPERTY( replicated )
https://docs.unrealengine.com/4.27/en-US/InteractiveExperiences/Networking/Actors/Properties/Conditions/

&#34;自动同步是好的结果。手写很糟糕&#34;
StateScript 包传输

StateScript Deltas StateScript Ghost
StateScript Packets
StateScript Deltas | 最近变更的StateScript 的 Action 和 States 以及 Variables;
创建/销毁; | StateScript Ghost | stage Group deltas 每个客户端需要知道和跟踪的指针集合 | StateScript Packets | 可重用 playloads client boud data 相对应到一个 或者多个 deltas |






5. 历年游戏分析小结
LOL
LOL这种用户量的游戏,对上线产品进行了同步框架的完全变更;
&#34;&#34;最大的胜利也可能是最难量化的:我们显着提高了游戏中许多低级系统的代码质量、可维护性和可靠性。这不仅提高了我们作为游戏开发者的敏捷性,而且让我们有信心在英雄联盟继续发展的过程中承担更大的游戏玩法和功能风险。对我来说,这是最令人兴奋的方面——凭借强大的技术基础,我们可以提供更具影响力的游戏功能。
&#34;我们将这个项目的成功归功于一个非常强大的开发团队和明确的产品目标。但是,我想传授一个关键的知识,它在完成这个功能方面发挥了重要作用。[可以对大规模发布的游戏进行大胆的更改,但 需要能够与传统技术并行推出系统替代品]。我们不能犯需要重新部署才能修复的错误;我们的成功取决于学习如何有效地编写新系统,同时使用久经考验的技术。&#34;
OverWatch
“设计网络同步模型时就考虑到回放的需求”;
守望先锋在开发期间对客户端-服务器框架的选型是ECS框架。
不相关的执行域提供了更大的灵活性和隔离性;、网络带宽优化重于回放文件尺寸优化,能省一个字节就省一个字节;
社区都很喜欢分享亮眼表现和全场最佳,能在Reddit和其他论坛上看到这些分享,感觉真的很棒。
守望先锋对回放( 死亡回放,高光时刻,全场最佳,整场录像 )的处理花费的经历和投入是很大的。

二. 优化技术手段对比
同步优化算法和手段
同步在这里是泛化的。 网络游戏内所有
同步性 => “流畅”
&#34;延迟感&#34; => “卡”
优化算法 | 优化实施者 | 具体手段 | 优化手段本质 | 优化目的和特点 | 优化对象 | 差值 | 客户端 | 内插 | 滞后 | 平滑数据和表现 | 外推 | 高速连续运动的物体轨迹 | 影子跟随 | ... | 服务器 | 快照差值 | 性能优化 | 优化内存,流量和带宽 | 预测 | 客户端 | 表现层预测;
Input输入预测;
载具轨迹预测;
物理预测;
... | 提前 | 减少输入反馈延迟和空闲时间或者断触感 | 延迟补偿 | 服务器 | 延迟补偿 | 人工干预和平衡 | 让客户端之间延迟感相对平衡 | 将服务器状态回滚到延迟前,再进行运算 | AOI 控制 | 服务器 | Unreal ReplicationGraphUnreal Relevancy and Priority | 性能优化 | 优化内存,流量和带宽等 | 客户端 | ... | 性能优化 | 客户端AOI主要是性能优化,和同步优化相关性不大。 | ... | 优化手段 | 优化实施者 | 具体手段 | 优化手段本质 | 优化目的和特点 | 优化对象 | 客户端性能优化 | 客户端 | 客户端性能优化 | 性能优化 | 减少渲染和逻辑的开销,从而减少阻塞网络通信的占用的发生概率 | 客户端整体流畅度 | 服务器折中部署 | 开发商 | 服务器折中部署 | 网络连接速度和稳定性 | 选取目标市场的地理或者网络中间节点物理服务器地址。 | 网络传输RTT | 双线加速 | 开发商 | 移动端双线加速 | 网络连接速度和稳定性 | 优化网络连接速度和稳定性 | 玩家设备游戏网络连接 | 加速器 | 玩家 | 开加速器 | 网络连接速度和稳定性 | 优化网络连接速度和稳定性 | 玩家设备游戏网络连接 | 框架选型 | 开发商 | 选择适合项目和团队的框架 | 选择 |
三 商业网路同步解决方案对比
方案对比
Mirror https://github.com/vis2k/Mirror
Unity NetCode https://docs-multiplayer.unity3d.com/netcode/current/about/index.html
Phonton https://www.photonengine.com/
DarkRift2 https://www.darkriftnetworking.com/
商业同步方案 | 文章时间 2022.9.1 | 类型 | 拓扑 | 特点 | 完成度 | 商用推荐 | 价值 | MLAPI | 状态同步 | 已废弃 | Unity NetCode | 状态同步 | C/S;
Relay; | ECS ,预测和回滚,暂时没有 Compoent的增删 | Preview 0.6进度较慢 | 不推荐 | 开源免费;ECS ,预测和回滚. 无头服务器整合代码 | Mirror | 状态同步 | C/S; | 服务器和客户端是一个项目 | Public维护力度较高 | 一般推荐
MMO
RPG | 开源免费;已经有若干发布的游戏 | DarkRift2 | 状态同步 | C/S; | 多线程https://www.darkriftnetworking.com/ | Public维护力度未知 | 一般推荐MMO
RPGFPS | 开源免费;已经有若干发布的游戏 | FishNet | 状态同步 | C/S; | 性能较好 | Public维护力度未知 | 一般推荐/可以尝试 | 开源免费 | Phonton PUN,Bolt | 状态同步 | 已废弃 | Phonton Fusion | 状态同步 | C/S;
Relay; | 预测和回滚,增量快照状态同步。 | Public维护力度较高设计和代码质量高 | 推荐RPG
FPS | 半开源不免费;较完善的增量快照状态同步方案 | Unreal MultiPlayer | 状态同步 | C/S;
Relay; | 快照状态同步。 | Public维护力度较高设计和代码质量高 | 推荐RPG
FPS | 开源半免费;较完善的快照状态同步方案 | smartfoxserver | 状态同步 | 暂未研究过 ... | DOTSNET | 状态同步 | C/S; | ECS, 预测和回滚. 无头服务器整合代码 | Public维护力度较高设计和代码质量高 | 可以尝试
MMO | 开源收费;适合大型多同屏MMO | ggpo C++ ggpo C# wrapper | 确定性回滚 | p2p | input预测和回滚 确定性回滚 | Public维护力度未知 | 可以尝试ACT | 开源免费;有已经发布的游戏 | Phonton TrueSync | 确定性锁步 | 已废弃 | FP 库被广泛使用 | Phonton Quantum | 确定性回滚 | p2pC/S;
Relay; | 确定性回滚;
录像和回放; | Public维护力度未知设计和代码质量高 | 可以尝试ACT
MOBA | 半开源挺贵;较完善的确定性回滚同步方案 |
游戏方案选型:
 |
|