标签归档:sync

网络游戏的移动同步(二)状态更新及航位预测法

引言

前一篇文章将基本的网络同步例子制作出来,虽然还有很多问题,但是我们已经在网络问题很简易的摆放在我们面前,不需要经过服务器,就可以简单的模拟经常出现的网络延迟现象,那对于之前演示的问题,如何解决呢?今天就解决第一步,发包的问题。如果能够节省发包量?

状态更新

因为每个客户端都有自己的逻辑运算,所以我们很明显的就可以想到,只需要在状态更新时发送即可,对于一般的游戏这个方案都可以解决,比如我们修改速度的时候,游戏点击目标点的时候,这些时间点发送即可,对于之前的例子,我们只需要做如下判断就可以节省大量的通讯量。

// 获取输入
// 在通过键盘获取输入更新最新速度前
var lastVelocity:Vector2 = localPlayer.velocity.clone();
var lastAcceleration:Vector2 = localPlayer.acceleration.clone();

if (lastVelocity.x != localPlayer.velocity.x ||
    lastVelocity.y != localPlayer.velocity.y ||
    lastAcceleration.x != localPlayer.acceleration.x ||
    lastAcceleration.y != localPlayer.acceleration.y) {
    update = true;
}

if (update) {
    var p:PlayerStatePack = new PlayerStatePack();
    p.velocity = localPlayer.velocity.clone();
    p.position = localPlayer.position.clone();
    p.acceleration = localPlayer.acceleration.clone();
    p.time = getTimer();
    client.send(p);
}

我们再看现在的demo,通讯量明显下来了,而且网络场景走得也不差。

Get Adobe Flash player

当然,这个方案也是有其局限性的,比如如果加速度存在的话,那速度每时每刻是不一样的,所以发包频率还是非常频繁,还有一种更极端的情况,如果你加入了物理引擎,或者使用了自治智能体,此时连加速度都每时每刻在变化的,有没什么更好的方式来优化通讯量呢?下面引出下一个优化方案,航位预测法(DeadReckoning)。

航位预测法(DeadReckoning)

这种算法比较的主要是位置,而非上面的速度以及加速度,所以从通用性上来说,会好很多。具体的细节为,当本地个体与记录的上次本地个体(即其他网络场景看到的自己)的位置超过某个阈值,则发送更新状态包,当此阈值越小,则发送包频率越快,此阈值越大,则发送包频率越慢,也越节省。

基本的原理可参考如下图(从网络获取)

dr

基本实现代码如下

// dr位置
var drPosition:Vector2 = new Vector2();
if (lastState) {
    var e:Number = (getTimer() - lastStateTime) / 1000;
    // s = v0t + 0.5 * a * t^2;
    drPosition.addVectors(lastState.position, lastState.velocity.clone().multiply(e).add(lastState.acceleration.clone().divide(2).multiply(e * e)));
}
// 阈值
var threshold:Number = 5 * 5;
if (localPlayer.position.distSQ(drPosition) > threshold) {
    update = true;
}

代码很简单,就是牛顿力学的内容,位移,加速度,速度的公式,然后算出dr位置(即网络位置),如果超过阀值,则发送更新包,另外记录上次发送状态(为了计算下一次dr位置)。

演示如下

Get Adobe Flash player

应用

在游戏中,状态更新及航位预测可混合使用,如在键盘更新时,即可将状态更新包发出,而非一定要等到位置偏移超过一定值,这样网络网络场景看到的会更即时些。本章将发包的问题解决了,但是你可以在演示中看出,当网络延迟波动较大时,网络场景的物体拖拉很生硬,因为我们直接设置了网络场景的位置到了当前位置,下一部分介绍如何用插值平滑将网络延迟做进一步的遮掩。

参考文献

网络游戏的移动同步(一)网络同步演示

引言

网络游戏的同步处理是在制作网络游戏中独有的处理方案,现在网络游戏几乎都是C/S架构,也就是说需要同步其他主机发送给服务器,并转发回本机的包,这里就涉及到如何发包给服务器,发哪些包,另外就是接收到这些包之后,客户端如何处理这些包的问题。

在不进行示例之前,如果简单的思考一下这些问题,一般人都会想到的是,其实只需要发布初始状态,然后每次变更发送更新状态即可。为了使这个问题简单化,本文的处理主要是移动同步,这个也是最常见,并且包量最大的一个协议,下面我们先创建这个更新包。

简单的更新协议

public class PlayerStatePack
{
	public var velocity:Vector2;
	public var position:Vector2;
	public var acceleration:Vector2;
	public var time:uint;
}

3个2维向量分别是位置,速度,加速度,此示例以2维为主,一般游戏可能用加速度比较少一些,但是为了更好的模拟更多的游戏,比如一些赛车或者炮弹之类的,这里的更新包中还有加速度。时间是发送包的时间,加这个主要是为了接收方能预知此时的真实位置。因为接收到的时候,发送方实际已经不在那个位置。

虚拟服务器

为了在客户端中直接模拟服务器延迟,可以直接做个简单的类模拟延迟。

public class Client
{
	// 模拟收到的网络包
	public var packets:Vector.;

	public function Client()
	{
		packets = new Vector.();
	}

	public function send(p:PlayerStatePack):void {
		// 延迟加入队列
		var delayTime:int = MathUtil.rand(Global.delay - Global.range, Global.delay + Global.range);

		setTimeout(sendNow(p), delayTime);
	}

	private function sendNow(p:PlayerStatePack):Function
	{
		return function():void {
			packets.push(p);
		}
	}
}

类的功能很简单,就是发送的时候增加setTimeout,另外这里有两个配置量,延迟量以及延迟波动量,这两个即可比较好的模拟出真实的网络环境,接收方处理的时候,直接从packets数组中获取包处理即可。

第一次尝试

假设这是一个简单的键盘移动游戏,因为鼠标移动同步相对比较简单,甚至于包都可以不需要那么复杂,只需要目标点就可以。
我们开始写发送及接收处理的代码。主循环代码如下

// 一般的线性移动
localPlayer.velocity.x = 0;
localPlayer.velocity.y = 0;
if (InputManager.instance.keyDown(Keyboard.LEFT)) {
	localPlayer.velocity.x = -localPlayer.speed;
}
if (InputManager.instance.keyDown(Keyboard.RIGHT)) {
	localPlayer.velocity.x = localPlayer.speed;
}
if (InputManager.instance.keyDown(Keyboard.UP)) {
	localPlayer.velocity.y = -localPlayer.speed;
}
if (InputManager.instance.keyDown(Keyboard.DOWN)) {
	localPlayer.velocity.y = localPlayer.speed;
}
// 发送协议
var p:PlayerStatePack = new PlayerStatePack();
p.velocity = localPlayer.velocity.clone();
p.position = localPlayer.position.clone();
p.acceleration = localPlayer.acceleration.clone();
p.time = getTimer();
client.send(p);

while (client.packets.length > 0) {
	var rp:PlayerStatePack = client.packets.shift();
	// 直接拉扯
	if (smoothMethodCb.selectedIndex == 0) {
		netPlayer.position.x = rp.position.x;
		netPlayer.position.y = rp.position.y;
		netPlayer.velocity = rp.velocity;
	}
}

// 更新场景
localScene.update();
netScene.update();

看这个简单的游戏主循环,主要的流程是获取输入->本地玩家更新->发送更新包->接收更新包->处理游戏逻辑。为了使整个游戏代码清晰,我清理了不太需要的细节代码。

这个简单的网络同步演示如下

Get Adobe Flash player

这个演示明显有几个问题,比如发送包太频繁(停止也在发送,因为是每帧发送),另外接受包的处理也很生硬,当网络延迟波动过大,拉扯现象非常明显。