MeteorCat / 状态同步(一)

Created Thu, 02 May 2024 01:59:24 +0800 Modified Wed, 29 Oct 2025 23:24:59 +0800
1621 Words

状态同步

网络游戏的同步策略是作为服务端的必修课, 并且在其中接触到以下两种同步方式:

  • 状态同步: 相对来说宽泛的同步条件, 对于数据同步精度不需要超高进度级别情况
  • 帧同步: 适合少数人竞技短时局势的对局游戏, 要求对局所有人都有高精度高频率数据交换

帧同步在 FPS|格斗|MOBA 中都有更好的表现, 游戏选手操作能够达到媲美高精度电脑级别, 所以对于数据同步要求极高

帧同步由于高频率高精度的同步请求导致性能消耗极高所以没办法持久常年运行, 帧同步只接收客户端推送操作来让服务端演算, 帧同步的游戏更像是将游戏当中客户端负责部分移交给到服务端, 所以在操作|响应方面体验更加贴近客户端体验.

但是帧同步带来更大的知识点: 定点数, 内|外插值补偿帧, 同步步骤记录

目前就我接触到有以下方案来处理服务端状态同步:

  • 自己从头编写 物理碰撞|寻路算法 在服务端处理方式使得客户端和服务端计算结果一致, 客户端负责提交操作让服务端一起计算结果保存在服务器.
  • 依靠游戏服务端自带的 DedicatedServer, 在服务端上挂载业务游戏自带的场景服务器, 用户提交的数据依靠自己编写服务端构建重新包装推送到游戏自带服务器.
  • 纯客户端实现的服务端, 基本比较知名的就是 Unity3D 的 ET(基于C#) 的集成开发服务端, 内部算法数据直接延用游戏引擎内部的方法

目前主流游戏开发引擎都自带 DedicatedServer 用来方便服务端挂载, Unity3d|UE|Godot 都支持挂载

从头实现在服务端实现客户端所需的 物理碰撞|寻路算法 之类对于服务端功底要求很高, 从数学到程序方面都要去从头开始设计;

而如果对游戏实时性要求不是那么高的话, 可以优先选用 状态同步 来构建, 所有这里着重讲解的就是 状态同步.

帧同步 涉及的知识点太过高深, 我至今都没办法说已经入门, 更别说深入了解构建样例.

状态同步

首先如果要做状态同步要准备构建出场景地图, 场景地图一般是为 2D(x,y)|3D(x,y,z) + block(碰撞不允许进入地区) + path(场景路线,玩家寻路直接检索路线最短点移动过去执行路线移动); 这里场景地图甚至需要客户端编辑器做好地图编辑器工具方便导出给服务端使用, 后续还有物理碰撞需要在服务端的 Update 做更新同步处理, 让服务端也可以模拟物理碰撞从而同步客户端的操作.

常规精度服务端的 Update 我看普遍推荐 15s/20次 = 0.75s/1次, 也就是设定 750ms/1次 的定时器做更新帧调用

状态同步主要就是 双端执行各自的 Update 帧更新, 只有客户端触发状态变化事件就需要进行双端通讯变动:

  1. 客户端按下 W(上)S(下)A(左)D(右) 进行位移时, 这就构成 状态触发 需要准备服务端同步
  2. 客户端将位移的向量 velocity(x,y) 封包推送给服务端, 该值作为 归一化( normalized, 只需要 1,0,-1 标识方向) 传递给服务端
  3. 服务端接受到位移确认位移合法性, 确认没有碰撞和超出场景边界的时候作为 状态触发 广播给同个场景玩家( 包括自己客户端 ).
  4. 所有同场景的客户端( 包括自己 )接收到服务端状态事件开始同步数据, 也就是将所在触发对象(指定UID玩家)开始移动

这种同步方式更类似 消费生产者模式(Productor|Comsumer), 流程时序图类似如下:

image

这里以 Godot官方样例(2D位移) 篇章为例.

func _process(delta):
	var velocity = Vector2.ZERO # The player's movement vector.
	if Input.is_action_pressed("move_right"):
		velocity.x += 1
	if Input.is_action_pressed("move_left"):
		velocity.x -= 1
	if Input.is_action_pressed("move_down"):
		velocity.y += 1
	if Input.is_action_pressed("move_up"):
		velocity.y -= 1

	if velocity.length() > 0:
	    # 这里就是需要推送给服务端的位移状态推送, 应该把位移 velocity.normalized() 交给服务端计算记录
		velocity = velocity.normalized() * speed
		
		# 这里的动画播放应该等服务端广播状态变动的时候确定执行
		# 而且服务端应该会返回 UID 或者其他标识身份的, 多人游戏都是以该标识选中并播放动画和执行位移逻辑		
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()
		
	# 后续这里也不是由客户端来掌握, 而是由服务端推送过来处理
	position += velocity * delta
    position = position.clamp(Vector2.ZERO, screen_size)

# 采用物理更新帧来获取服务端消息并且队列从而让 update 去做动画逻辑
func _physics_process(delta):
    pass

这里推荐学习 Godot向量数学, 内部包含游戏比较基础会用到数学计算流程和应用, 无论客户端还是服务端都是很值得学习的样例.

本篇章仅仅以状态同步来做初步讲解实现, 这里面实际上很大很碎的知识点, 而且游戏服务状态同步实现在网上都是各自 不传之秘, 所以这篇是想到什么就写上的初步印象, 可以当作实现自己游戏同步方案的论据并补充自己观点.