最近由于遇到一些问题,老大们决定把场景显示相关的代码拆分出来用一个独立的线程去做(大概是实现一个独立的场景服务吧),感觉这样挺好的,毕竟这部分功能本来就较为独立。
我对这部分内容还挺感兴趣的,思考了一下,心里有一个感觉是比较好的解决方法,遂提笔记录下来:)
先简单说说背景:地图场景是按格子划分的,每个格子有若干属性(key-value对),这些属性随着场景事件的触发而改变。由于相同的格子在不同的玩家看到可能会有不同的显示,因此需要为每个玩家独立计算,并不能简单的把格子数据直接同步给client。值得一提的是,上一个版本就是这么做的,结果遇到了几个问题:
(1)有些时候client计算不太方便。比如这个计算依赖其它一些数据就不得不把这部分数据也同步过去,因此白白浪费了带宽。
(2)客户端在移动镜头的时候又要做复杂的逻辑计算会导致画面卡顿,感受不好。
因此决定了在server就把显示数据计算好再同步给client。
那么,这个场景服务如何实现呢?
首先看看它需要哪些功能呢?我觉得最基本的,它只需要做到以下两点:
(1)更新地图上每个格子的属性数据。
(2)提供一个类似 get_grid_show_data(role_id, grid_id) 的接口计算出玩家对某个格子的显示数据。
我的设想是这样的:逻辑线程将会导致地图上格子属性数据发生变化的事件,以消息的形式传递给场景线程,场景线程以此为驱动来更新格子的属性数据。而对于同步玩家格子的显示数据,以request-response模式工作,client定期(可以是1秒)请求server格子的显示数据。
为什么是request-response模式呢?
首先,这个模式server不用维护client的状态,因此可以实现的十分简单。
其次是,这种方式可以让client更灵活的选择更新格子显示的策略:
(1)比如说,当我打开了一个全屏的ui面板,这个时候其实就可以不需要关心格子的变化了因此可以简单的停止请求更新。
(2)又或者当client的网络不是很好,而这个时候server一股脑的将更新数据同步给client并无益处,更好的做法是client再上一次请求返回之后才允许发起下一次更新请求。
(3)再比如,当几个玩家正在某个格子正在进行一场激烈的战斗(或是其它行为),这个时候我们更应该优先关注这个格子的变化、而其他的一些格子倒是可以缓慢一些。此时client可以对这个关注的格子采用更为积极的更新策略(得到返回马上发起下一次查询),而其它不怎么关心的格子可以相对保守一点,如2秒3秒请求更新一次。
(4)最后还有一点,当玩家调整镜头远近导致可见格子数量变化后,client可以灵活的做出相应的调整,只请求更新当前可见的格子。
当然了,这种方式也是有缺点的。最为明显的就是,比订阅-发布这中模式浪费了更多带宽。这是因为发布订阅几乎就是做到了状态的精准同步,server只在数据发生改变的时候同步客户端,当然也就可以只同步最少的数据!
不过好在,这个缺点是可以在一定程度上规避的!
为了不同步重复的冗余数据,可以在格子上记录一个最近更新的时间戳。client请求格子显示数据时带上这个时间戳,如果这个时间戳大于格子上的说明client的已经是最新的了,无需再做同步。否则,server就根据格子数据计算显示数据并同步client,注意这个时候应当将时间戳也一并同步。client将收到的时间戳保存起来,下一次查询会再用到。
其实还可以再进一步优化。一般都是玩家镜头有一若干个格子,正常情况下我需要定时请求所有这些玩家可见的格子的数据。倘若一般手机屏幕能显示玩家周围4*4这么多格子,client以一秒为周期定时发起请求,那么每秒最少就需要发16条消息。这样显然是不科学的!
一个解决方法是,引入一个块(block)的概念。一个块就是一个格子集合,一般是一个固定大小的矩形。玩家周围的格子就可以看作是一个块。服务器除了在格子上记录更新时间戳之外,也在这个块上记录一个时间戳。块的时间戳等于其中包含的格子时间戳的最大值。也就是说,每当更新块中的格子时,都要更新块的时间戳。平时client在查询的时候时候可以一次查询整个块,参数就带上上一次服务器同步过来的块的时间戳。倘若这个时间戳没有变化,说明这段时间里这个块都没有数据变化,不做同步。若这个时间戳有变化了,则遍历块中的所有格子,查看其时间戳,如果比客户端传过来的时间戳大的,说明是最近更新的,计算其显示数据并同步。最后再把整个块的时间戳也同步给client。