Leader Board Design In Lua
Introduction
在多人在线游戏中,排行榜是很重要的一个功能。多年游戏经验告诉我,排行榜不仅是对自身游戏角色实力的一种评判,还是一种让用户加大投入时间,甚至充值的驱动力。想一想,如果你离排行榜第一名只差一点点,这不爆肝一晚冲榜首
排行榜很重要,但排行榜却不是那么容易设计的。每个用户的得分都在实时变化,并且一般还得提供不同维度的排名,当用户群体一多,数据更新的操作就多了。如何保持高效的数据更新,便显得尤为重要
Simple LeadBoard With Advance
抽象而言,排行榜的最简形态无非就是每个玩家两个字段,用于区分不同玩家的Id字段以及用于排序的Score字段
- 根据Id获取玩家得分
- 根据得分进行排序
- 将结果输出到排行榜
- 当玩家得分变化时更新得分并重新排序
- 新玩家进入时则增加一条记录
以上思路基本可以完成排行榜的各项需求,而用LuaTable实现起来也是相当简单
function LeaderBoard:ctor()
self.rank = {}
end
function LeaderBoard:addNewPlayer(entid, name, score)
self.rank[#self.rank + 1] = {["entid"] = entid ["name"] = name, ["score"] = score}
table.sort(self.rank)
end
function LeaderBoard:updateLeaderboard(entid, newScore)
for i = 1, #self.rank do
if self.rank[i]["entid"] = entid then
self.rank[i]["score"] = newScore
end
end
table.sort(self.rank)
end
function LeaderBoard:queryPlayerRank(entid)
local score = getPlayerScore(entid)
for i = 1, #self.rank do
if self.rank[i]["score"] = score then
return i
end
end
local name = getPlayerName(entid)
self:addNewPlayer(entid, name, score)
return -1
end
上述实现的思路是当每次数据变动时都将Rank进行排序以保持Rank按得分有序,在查询时只需找到对应的得分在第几个即可
但仔细思考发现,要想获得自己的排名,根本没必要保持Rank有序,只需要遍历Rank求出得分比自己高的玩家个数即可,因此,可以就此优化添加和更新接口的实现:
function LeaderBoard:ctor()
self.rank = {}
end
function LeaderBoard:addNewPlayer(entid, name, score)
self.rank[entid] = {["name"] = name, ["score"] = score}
end
function LeaderBoard:updateLeaderboard(entid, name, oldScore, newScore)
self.rank[entid]["score"] = newScore
end
function LeaderBoard:queryPlayerRank(entid)
local rank = 0
local myScore = self.rank[entid]["score"]
for k, v in pairs(self.rank) do
if v["score"] > myScore then
rank = rank + 1
end
end
return rank + 1
end
比对两者,可以发现优化后的算法查询排名复杂度不变O(N),但添加和更新都变成了O(1)操作,大大加快了执行效率
Segment Tree
虽然优化后的排名查询效率已经达到O(N),但当玩家数量庞大时每次可能有十万级的查询请求,O(N)的复杂度显然不能满足要求
注意到,一般玩家的得分在一段时间内总是在一定范围内的,不可能有一天增长100倍的情况,于是我们可以把得分划分区间,借助线段树优化查询效率
- 构建一棵从1到最大值的线段树,最大值可以预估未来几个月玩家得分的上限,比如10万分
- 线段树的每个节点由Start(区间左值), End(区间右值), Count(得分处于此区间的玩家人数)组成,且每个节点的左孩子节点的区间为当前区间的左半边,右孩子为当前区间的右半边,叶子节点则左值与右值相等,整棵树相当于把原始区间不断二分
- 当增加一个新得分时,即插入一个节点,沿根节点不断选择得分所处区间的节点直至叶子节点,将这条路径上的每个节点的Count加1
- 删除同理,即当Count大于0时将Count减一,其余与插入一样
- 更新操作则为先删除后插入
- 可以看出每个节点的Count代表着处于此区间的玩家人数,因此每次选择子节点若选择左孩子子节点,则代表当前有着右孩子节点Count的人数分数比你高,根据这条性质,查询排名时从根节点向下遍历到叶子节点,每次要往左走时则加上右孩子节点的Count,求和后即得所有分数比当前分数高的人数,即得排名
local sgmentTree = {}
function sgmentTree:node(min, max)
return {Start = min, End = max}
end
function sgmentTree:new(min, max)
if