Post

属性同步--MMO服务器开发技术点记录

属性同步--MMO服务器开发技术点记录

在MMO游戏中,玩家实体必然存在大量的状态,这些状态被称为属性状态,如hp、mp、攻击力、防御力等。不同的实体的组件会影响到这些属性状态的值,需要进行同步。不能说在每次修改某位玩家实体的属性状态后就要发送一个rpc去通知客户端。每个属性的变化都去进行一条rpc的通知,在实现上也是比较麻烦。

比较好的方案是提供属性同步功能,当属性状态发生改变时,就主动地通知;

属性同步设计

属性同步分为两个阶段去完成,第一阶段是识别,识别到哪些属性发生了改变;第二阶段是通知,将这些变化的属性进行序列化然后发送给客户端;

识别有两个方案:第一个是服务端手动通过对属性置脏标记来识别哪些属性发生变化,第二个是外包给客户端引擎,客户端比较发送过来的全量属性状态和先前的状态有什么不同;我们的MMO服务器主要使用第一种方案,因为这样可以减少网络IO压力;

在之前的文章中我们有提到,我们的组件具备序列化的能力,这些组件包含的数据其实就是玩家实体的属性状态。玩家实体的序列化方法就是负责去选择变化的组件属性状态去序列化到输出流 / OutputBitStream中。

这里的序列化方法包括两种,一种是全量序列化,一种是增量序列化:

1
2
 void NetSerialize(OutputBitStream& output) const;
 bool NetDeltaSerialize(OutputBitStream& output);

识别能力

这里详细讲下,我们如何让net_delta_serialize具有识别脏数据并将其序列化进输出流的能力。假设现在我们有下面这样的结构体:

1
2
3
4
5
6
7
struct PlayerState
{
    int hp;
    int mp;
    int attack;
    int defense;
};

这里有四个属性,我们需要精确的识别到里面的某一个字段是脏的;我们需要添加一个字段用于储存这个信息dirty, 从0位开始依次表示每个字段是否被置脏。同时需要四个掩码来方便我们对某一位进行操作:

1
2
3
4
uint32_t hp_mask = 1 << 0;
uint32_t mp_mask = 1 << 1;
uint32_t attack_mask = 1 << 2;
uint32_t defense_mask = 1 << 3;

至于置脏的操作,可以去了解位运算。我们可以通过对dirty进行位运算来标记某个字段为脏,比如:

1
2
3
dirty |= hp_mask;      // 标记hp为脏
dirty |= mp_mask;      // 标记mp为脏
dirty &= ~attack_mask; // 清除attack的脏标记

服务端需要传输什么数据给客户端,使得客户端知道自己要如何更新属性呢?脏码和脏数据;只要客户端和服务端协商好使用相同类型和大小的脏码和属性字段,就可以知道如何从输入流中读取脏数据。前提是对于一个组件类型,客户端和服务端要使用同样的结构;这样客户端才知道要将读多少数据进某个类型字段中。输入流提供了读取数据填充到类型中的能力,这里就发挥作用了。

发现如果每次新建一个可序列化类,就要为其编写序列化方法和反序列化方法,就比较麻烦和费事。可以考虑使用考虑构建类型信息来减少代码量,这部分我打算单独放一篇文章来讲解。

最后需要解决的事情就比较简单了,就是在场景服务中的update中调用这些序列化方法,将脏数据分发给客户端即可。在每个update中,对于每个实体,首先对自身属性状态进行增量的同步,这里的属性分为两种,一种是需要自所有人可见的属性,一种是仅自己可见的属性,分开同步给自己和其他玩家;

会发现,在update这里,如果玩家实体数量太多的情况下,会极大的增加服务器的网路IO负担,这就是我们下一篇需要介绍的模块–AOI,决定对于每一个实体,我们要将这些属性同步给哪些玩家。

This post is licensed under CC BY 4.0 by the author.