« 梦幻西游服务器的优化 | 返回首页 | 洋画 »

网络游戏物品校验系统的设计

网络游戏若要有支持一个稳固的经济系统,服务器底层必须有一个可靠的数据服务。要设计出精简的数据协议可不容易。它需要保证在发生异常(可能是硬件异常,也可能是软件异常)时,不会出现物品/货币丢失,复制的问题。

使用带事务的数据库是一个不错的选择,但对程序员的要求较高。程序员需要仔细考虑每个数据操作如何编写事务才是安全的,还需要考虑数据操作的异步性。这对需求变化迅速,量又比较大的游戏,做的好真的是很困难。

我思考了很久,几经易稿,大约得到了这么一个东西:

数据存储和合法性校验应该分开,独立为不同的服务,这样才容易做的稳固。也就是说,数据服务不必做到完全的完备,简单的去读写修改数据即可。这部分,我倾向于用简单的 key/value 方式储存数据到数据库,可以自己实现,也可以用 redis 这样的现成产品。不必使用事务机制。

但是我们应该提供一个强的校验系统,所有的虚拟物品发放、转移,都应该经过这个校验系统。一切操作都需要经过事后的核对。由此系统来修正数据异常。


简单来说,我们需要保证的是游戏世界中的每件物品都有唯一的拥有者,如果不被玩家拥有,则被系统所有。虚拟货币也是如此。应该避免物品的蒸发(平白无故丢失)或是被复制。

物品和所有者的关系是简单的,单层的,不必实现多层次拥有的树状关系。每个玩家以及可以拥有货币和物品的 Entity 都有独立的帐号(用一个 64bit ID 表示)。而每件物品 Goods ,都有其唯一的 ID (64bit 整数),以及唯一的拥有者。

Entity 和 Goods 是独立正交的两个概念,一个 Entity 不可能是一个 Goods ,反之亦然。Goods 对 Entity 是一个 n:1 的关系。

这样看来,这个数据校验系统的 API 设计就可以比较简洁了。

本来,Entity 和 Goods 可以有相同的 id ,因为它们相互不影响(类型不同)。但为了实现方便,我们让 Entity 和 Goods 公用一个 id 空间,不会使用相同的 id 。

ApplyID(number)

申请一段 ID 备用。这是一个有返回值的 API 。多在服务启动时调用。数据服务返回 number 个空闲 ID 。这样,请求者之后,可以任意使用这段 ID 中的某一个,而不用担心和其他人冲突。一旦申请的 ID 接近枯竭,则可以提前申请下一批。0~1023 为保留 id ,通常 0 表示系统。

CreateEntity(id)

创建一个 entity ,赋予它一个空闲 id 。如果 id 是正确的通过 ApplyID 得到,这个 API 通常不会调用失败。

CreateGoods(id)

创建一个 Goods ,赋予它一个空闲 id 。它的所有者,默认为 0 ,即系统。

ExchangeGoods( { entity1 , funds , goods[] } , ... )

这个 API 接收若干组数据,每组数据包含 entity 的 id ,货币 funds 数量,以及它可以获得的 goods

这个 API 用于在几组物品以及货币的所有者间做交换。所有的 goods 的所有者必须存在于传入的列表内。 所有的 funds 总数必须为 0 。例如:

玩家 player1 用 1000 块钱,交换 player2 的编号为 12345 的物品,系统抽取 player1 的 10 块钱税。则可以表达为 ExcahngeGoods ( { player1 , -1010 , [12345] } , { player2 , 1000 , [] } , { 0 , 10 } ) 即,每组数据表达了某个 Entity 在这次交易中将获得什么。

系统发放(凋落)物品则可以先用 CreateGoods 创建出来,再用 ExchangeGoods 来发放。

VerifyGoods( entity , goods [] )

这个 API 用于校验 entity 所拥有的物品的合法性。API 传入他所拥有物品 id 列表 goods[]。校验服务校验完毕后,将告诉调用者,entity 拥有的物品是否有缺失,或是否有冗余。一般说来,再服务正常的情况下,这个校验是多余的。所以可以在服务维护时,离线跑一次。也可以在玩家上线时(或定期)做一次校对。

QueryGoods( entity )

可以获取 entity 所拥有的所有物品列表以及货币数量。这个 API 仅供调试使用。功能上和 VerifyGoods 有所重复。

如果一个玩家拥有的东西过多,可以考虑把仓库和背包分离成两个 Entity ,这样可以减轻 VerifyGoods 的负担(如果需要定期去做的话)


数据校验服务和数据存储服务是独立的,所以数据储存服务中还是记录有玩家对物品的拥有关系。游戏逻辑不应该依赖数据校验服务提供的物品列表,它只是用来保证游戏内的交易系统、怪物凋落系统都是正常工作的。并可以在异常发生时(硬件异常或软件异常),提供一份数据来修复。


1 月 13 日 补充:

关于一个玩家拥有多件相同物品的优化。

其实大多数物品并不具备唯一性,比如血瓶,材料等等。每个玩家都可能拥有多件。这种东西界于货币和特有物品之间。如果每个都为其分配一个 uuid ,可能会浪费大量的储存空间。

解决方法有三:

  1. 对于这个数据校验服务,不记录这些无关紧要的物品。

  2. 当同一件物品达到一定数量,以一定数量和系统兑换大面额 ID 。比如 10 个 id 兑换 1 个表示 10 个数量的 id 。这个方法不用为校验服务增加新协议,只需要在使用方约定即可。需要特殊处理的只是把系统 id 0 特殊对待,因其只换出不换入,就不需要把换掉的 id 进入数据库,写一下 log 即可。

  3. 保留的 1~1023 号 id 做特殊用途(当然从协议上说,不限于某一段 id ,每个 id 都可以允许多份,但不利于做一个简洁健壮的实现),每个表示特定物品。每个 entity 可以拥有这些小 id 多件。在校验服务的实现里,可以优化储存,保存 id 和数量即可。这个方法需要在 ExchangeGoods 里增加 Entity 失去的 id 列表,另外需要为实现做一定的优化。

Comments

我是做网游服务端架构的,校验肯定要做的,但有一点我不明白,按照你的方案,在数据存储的时候,如何避免数据异常?还是在校验正确的时候加锁?

我想是我理解错了,我把entity理解为玩家身上的道具容器之类。其实应该是容器(们)有对应的entity,用entity去验证道具容器吧?

对不起,我没表达好。我的理解是这样的,VerifyGoods的实现应该是判断所有entity的所有道具和货币是否守恒。
而疑问是,如果是这样做的话,要把所有entity在某个时刻全部加载进再做判断,这样的话消耗貌似不太好估计。还是我对这个方法本身理解错了?

@stephantan

这个问题我前面解释过了。如果每个 id 都是唯一的。那么系统知道每个 id 当下属于哪个 entity 。

一开始,所有 goods 都属于 entity 0 即系统。

所以只需要写每个人获得了什么,至于谁失去了什么,系统知道当前物品的所有者。

当然是所有者失去了它。在例子里,如果 12345 开始属于 player2 ,那么就是 player2 失去了 12345 ,这个是不需要在 api 里注明的。如果 player1 获得的 12345 不属于 player2 或系统,那么调用就是失败的。

一切 goods 最初都属于系统,由 CreateGoods 创建。

有2个疑问:
1.ExcahngeGoods这个方法的参数传入,如果像云风那样写的话,应该是返回false吧:
ExcahngeGoods ( { player1 , -1010 , [12345] } , { player2 , 1000 , [] } , { 0 , 10 } )
个人感觉应该是在第二个字典的物品那个list里写类似“-12345”的东西,否则无法守恒。
还是我没理解你的意思?

2.VerifyGoods这个方法,验证某个entity,实现里是否需要用到其他entity的数据?

设计很不错哎,考虑的挺全的

这种做法用来做物品交易或是物品掉落,确实很清晰。但是在具体游戏中,和物品相关的东西太多了,比如,物品的消耗,物品的合成,那么这些业务逻辑还要单独做吗?我觉得物品相关的条件判断的耦合还是很大,很难做到KISS。不过rollback通过log来做,我也支持这点

有没有什么游戏比较好玩的?

谢谢分享,有空再来玩!

--数据存储和合法性校验应该分开,独立为不同的服务,这样才容易做的稳固.

good idea

我之前遇到的设计和这个也差不多,装备之类不可堆叠物品的采用uuid,以及唯一的owner,但是同类可堆叠物品合并的时候会用加减数量的方式,数量为0就销毁物品。不过销毁的物品,并不会归还系统。但是在货币方面就只是在交换时记录一个来源而已。如果货币也采用这种方式校验的话,似乎只能采用特定id+数量的方式。

@Dante

如果,每个 id 都是唯一的。那么就不需要描述谁失去了什么。

交易是个零和游戏,如果每个 id 都有明确的所有者,那么就不需要这个信息。

后面我有所补充。如果每个 entity 可以拥有多个相同 id 的 goods ,那么就需要加上失去的 id 。

写的很不错,尤其是
ExcahngeGoods ( { player1 , -1010 , [12345] } , { player2 , 1000 , [] } , { 0 , 10 } )
启发很大。
我这边主要是做sns应用,其实sns中与好友的交互也是差不多的一个理念,我们这边也是对这种强事务不太看看好,觉得太重了。不过有一个小疑问,{ player2 , 1000 , [] }中第三个参数为空,不需要描述他失去了[12345] 吗?

另外,我的博客:www.vimer.cn
希望能交换链接。

现在哪款网络游戏最火

一些装备是可分解和合成的,并且因子很可能是原来的装备。比如a+b=c, c=a+b

系统本身也是entity,并不能解决脚本不小心写错了导致刷道具的情况。但是感觉思路和流程都非常清晰,易于自动化校验和倒流(追查或赔偿之类的)。

学习了。
谢谢。

@Liu Liu

对这个问题,我今天做了补充。

数据库崩溃,我们的装备是不是就没了?

@leesoft 设计上应该避免同一时刻多个entity试图和同一个goods建立拥有者关系。

问题在于数据冲突时以何依据去解决这些冲突。举例说: e1 claims g1 while e2 claims g1 in the same time, who is going to take priority, e1 or e2 or who has the latest timestamp?

很受启发。原来网游里面每个物品都是用uuid辨识的,而不是一类物品然后计个数量。这样的冗余设计的确能很大程度避免数据异常。只是这样设计会不会数据爆炸?从你们的实践上看的话。(Disclaimer:从来没玩过网游)

Post a comment

非这个主题相关的留言请到:留言本