
最近有空研究了下某游戏开发项目里的排行榜实现,发现虽然业务逻辑不复杂,但要做到高效、灵活、易扩展,还是有不少细节值得推敲。于是我尝试用 Lua 模仿实现了一个轻量级的排行榜功能,并在此记录下我的设计思路与实现过程。
排行榜几乎是所有带有竞争或成长元素的游戏标配功能,比如战力排行、等级排行、积分排行等。核心需求通常包括:
本文将介绍如何利用 Lua 的 table 特性,简单模拟实现一个灵活、高效的TOP N排行榜功能。
-- 模拟时间戳生成
local function get_timestamp()
local nowTime = os.time()
return nowTime
end
local MAX_SHOW = 5;
local MAX_BUFFER = MAX_SHOW * 2
-- 排行榜数据结构
local Rank = {
max_show = MAX_SHOW, -- 显示前MAX_SHOW名
max_buffer = MAX_BUFFER, -- 缓冲MAX_BUFFER名
list = {}, -- 排行榜列表
cache = {}, -- 排名缓存 playerName==>index
version = 0, -- 版本号(用于检测更新)
type = "" -- 排序类型
}
-- 排行榜构造函数
--- @param rank_type string
--- @return table
local function create_rank(rank_type)
local obj = setmetatable({}, {__index = Rank})
obj.type = rank_type
obj.list = {}
obj.cache = {}
obj.version = 0
return obj
end
-- 填充玩家数据到排行榜项
---comment
---@param player table
---@return table
local function fill_player_data(player)
return {
Name = player.Name,
Exp = player.Exp,
Job = player.Job,
CombatPower = player.CombatPower,
timestamp = player.timestamp or get_timestamp() -- 首次进入时记录时间戳
}
end
-- 比较函数:战力排序(相同则按时间戳)
---comment
---@param a table
---@param b table
---@return boolean
local function combat_power_cmp(a, b)
if a.CombatPower ~= b.CombatPower then
return a.CombatPower > b.CombatPower
end
-- 时间戳小的排在前面(先进入)
return a.timestamp < b.timestamp
end
-- 比较函数:经验排序(相同则按时间戳)
local function exp_cmp(a, b)
if a.Exp ~= b.Exp then
return a.Exp > b.Exp
end
return a.timestamp < b.timestamp
end
local typeCpm = {
CombatPower = combat_power_cmp,
Exp = exp_cmp
}
-- 获取比较函数
---comment
---@param rank_type string
---@return function
local function get_cmp_func(rank_type)
return typeCpm[rank_type]
end
---comment
---@param list table
---@param player_data table
---@param cmp function
---@return integer
local function find_insert_index(list, player_data, cmp)
local left = 1
local right = #list
local insert_pos = right + 1 -- 默认放在最后
while left <= right do
local mid = math.floor((left + right) / 2)
if cmp(player_data, list[mid]) then
-- 当前 mid 位置“弱”于 player_data,所以 player_data 应该在 mid 前面
insert_pos = mid
right = mid - 1
else
left = mid + 1
end
end
return insert_pos
end
--- 移除指定玩家
--- @param rank table 排行榜对象
--- @param player_name string 玩家名称
local function remove_player(rank, player_name)
local index = rank.cache[player_name]
if not index then return end -- 不在榜上
-- 从列表中移除
table.remove(rank.list, index)
-- 从缓存中移除
rank.cache[player_name] = nil
-- 由于删除了一个元素,后面的玩家索引都 -1,需要更新缓存中受影响玩家的索引
for i = index, #rank.list do
rank.cache[rank.list[i].Name] = i
end
-- 截断缓冲区检查
local total_max = rank.max_show + rank.max_buffer
while #rank.list > total_max do
local removed = table.remove(rank.list)
rank.cache[removed.Name] = nil
end
rank.version = rank.version + 1
end
-- 更新排行榜
---comment
---@param rank table
---@param player table
local function update_rank(rank, player)
local player_data = fill_player_data(player)
local cmp = get_cmp_func(rank.type)
local old_index = rank.cache[player_data.Name]
local total_max = rank.max_show + rank.max_buffer
-- 如果是老玩家,先删除旧数据(不管它原来在哪)
if old_index then
table.remove(rank.list, old_index)
end
-- 找到 player_data 应该插入的正确位置
local new_index = find_insert_index(rank.list, player_data, cmp)
-- 插入到新位置
table.insert(rank.list, new_index, player_data)
-- 更新缓存
rank.cache = {}
for idx, item in ipairs(rank.list) do
rank.cache[item.Name] = idx
end
-- 超出缓冲区则移除尾部
while #rank.list > total_max do
local removed = table.remove(rank.list)
rank.cache[removed.Name] = nil
end
-- 版本号更新
rank.version = rank.version + 1
end
--- 重建排行榜缓存用 - 日后可将update_rank改为更新局部缓存,只有需要精准查询时,再调用rebuild_cache
--- @param rank table 排行榜对象
local function rebuild_cache(rank)
rank.cache = {} -- 清空旧缓存
for idx, player in ipairs(rank.list) do
rank.cache[player.Name] = idx -- 重新构建索引
end
rank.version = rank.version + 1 -- 更新版本号
end
-- 打印排行榜
---comment
---@param rank table
local function print_rank(rank)
print(string.format("\n【%s排行榜】(版本:%d)",
rank.type == "CombatPower" and "战力" or "经验",
rank.version))
print("排名 | 名称 | 战力 | 经验 | 职业")
for i = 1, math.min(rank.max_show, #rank.list) do
local item = rank.list[i]
print(string.format("%d | %s | %d | %d | %s",
i, item.Name, item.CombatPower, item.Exp, item.Job))
end
end
-- 测试代码
local function test()
local data = {
nodes = {
{Name = "kk", Exp = 100, Job = "AK1", CombatPower = 300},
{Name = "lisi", Exp = 1000, Job = "AK2", CombatPower = 15},
{Name = "lufei", Exp = 200, Job = "AK3", CombatPower = 150},
{Name = "solong", Exp = 300, Job = "AK4", CombatPower = 2},
{Name = "nami", Exp = 500, Job = "AK6", CombatPower = 200},
{Name = "usopp", Exp = 800, Job = "AK7", CombatPower = 50},
}
}
local combat_rank = create_rank("CombatPower")
local exp_rank = create_rank("Exp")
for _, p in ipairs(data.nodes) do
update_rank(combat_rank, p)
update_rank(exp_rank, p)
end
print_rank(combat_rank)
print_rank(exp_rank)
-- lufei 战力提升
update_rank(combat_rank, {Name = "lufei", Exp = 200, Job = "AK3", CombatPower = 400})
print("\n--- lufei战力提升后 ---")
print_rank(combat_rank)
-- 新玩家 zoro
update_rank(combat_rank, {Name = "zoro", Exp = 500, Job = "AK5", CombatPower = 350})
update_rank(exp_rank, {Name = "zoro", Exp = 500, Job = "AK5", CombatPower = 350})
print("\n--- 新玩家 zoro 加入 ---")
print_rank(combat_rank)
-- solong 经验提升
update_rank(exp_rank, {Name = "solong", Exp = 1500, Job = "AK4", CombatPower = 2})
print("\n--- solong 经验提升后 ---")
print_rank(exp_rank)
-- 移除玩家 lufei
print("\n--- 移除玩家 lufei ---")
remove_player(combat_rank, "lufei")
print_rank(combat_rank)
-- 边界:尝试移除不存在的玩家
remove_player(combat_rank, "nonexist")
end
test()原项目的数据存储 也是使用lua table存储排行榜列表,每个元素包含玩家信息 排行数值 时间戳(用于同分排序)
缓存机制 缓存玩家id 到 排名的映射 避免每次查询排名时便利整个列表
排序逻辑 支持自定义比较函数,支持多维度排序(例如优先比较排行数值,数值相同时比较时间戳,确保排序唯一性) 实现玩家排名插入更新和位移,自动维护列表顺序。
实现版本号标记,延迟同步
实现注册回调功能,能监听玩家数据变更,自动同步到排行榜,保证数据一致性
能通过lua的热更新功能,随时加载玩家数据变更
因为没有框架相关,也没有数据库存储数据,和网络流带来的数据随时变动,故只是lua原生api模仿实现,热更新采用手动文件存储实现
用了 Lua 的 metatable + 面向对象风格,将排行榜封装为一个 TopRanking类,具有明确的构造函数 TopRanking.new(),以及一系列方法
local TopRanking = {}
TopRanking.__index = TopRanking
function TopRanking.new(max_display, max_store, compare_func)
local self = setmetatable({}, TopRanking)
-- 配置参数
self.max_display = max_display or 5
self.max_store = max_store or self.max_display * 2
-- 数据存储
self.players = {} -- 排行榜主列表,已排序
self.player_map = {} -- 玩家ID到数据的映射
self.rank_cache = {} -- 玩家ID到排名的缓存
-- 版本控制
self.version = 0
self.dirty = false -- 是否有未同步的更改
-- 回调函数
self.callbacks = {
on_update = nil,
on_rank_change = nil
}
-- 排序函数,默认按Exp降序,相同则按时间戳升序
self.compare_func = compare_func or function(a, b)
if a.Exp ~= b.Exp then
return a.Exp > b.Exp -- 经验值高的排在前面
end
-- 经验相同则时间早的排在前面
return a.timestamp < b.timestamp
end
return self
end
-- ...
-- 相关方法实现
-- ...
return TopRanking当有新玩家数据插入时,若其排名在 max_store之后(即连“存储榜”都进不去),则直接丢弃该玩家,避免无效数据占用内存。
同时,在插入和移除逻辑中,会严格保证 players列表长度不超过 max_store,从而实现展示与存储的分层控制,提升灵活性和内存使用效率。
function TopRanking.new(max_display, max_store, compare_func)
...
-- 如果超出存储上限,移除末尾的元素
while #self.players > self.max_store do
local removed = table.remove(self.players)
local removed_uid = generate_unique_id(removed)
print("移除玩家:", removed_uid, "Exp:", removed.Exp) -- 新增日志
self.player_map[removed_uid] = nil
end
...
end通过维护一个额外的哈希表 rank_cache,实现 O(1) 时间复杂度的排名查询
self.rank_cache = {} -- 玩家ID => 排名(从1开始)每当玩家数据发生插入、更新或排序后,_reorder()方法会重新计算所有玩家的排名,并更新该缓存:
self.rank_cache[uid] = i -- i 就是排名因此,当我们需要查询某个玩家的排名时,只需:
function TopRanking:get_player_rank(uid)
return self.rank_cache[uid] or -1 -- -1 表示未上榜
end排行榜的核心之一,就是保证玩家数据始终按照指定的排序规则(如经验值、战力等)有序排列。
排序逻辑通过用户传入的比较函数 compare_func定义(默认按经验值倒序,时间戳正序),并通过 Lua 原生的 table.sort()实现排序:
-- 内部函数:重新排序并更新排名缓存
function TopRanking:_reorder()
-- 排序玩家列表
table.sort(self.players, self.compare_func) -- 重点
-- 更新排名缓存
self.rank_cache = {}
for i, player in ipairs(self.players) do
local uid = generate_unique_id(player)
self.rank_cache[uid] = i
end
-- 标记为已修改
self.dirty = true
self.version = self.version + 1
-- 触发更新回调
if self.callbacks.on_update then
self.callbacks.on_update(self.version, self:get_top_players())
end
end
table.sort(self.players, self.compare_func)每次玩家数据发生变更(新增、更新),都会触发 _reorder()方法,该方法会:
对 self.players列表进行全局重新排序;
遍历排序后的列表,重新构建 rank_cache,确保每个玩家的排名信息是最新的;
更新版本号并标记数据为“脏”(dirty = true),以便触发回调或同步逻辑。
同时,所有相关的缓存(如 player_map和 rank_cache)也会在插入、删除、排序时自动维护,保证数据一致性,这样无需手动管理排序状态。
为了增强排行榜的灵活性和扩展性,提供了 事件回调机制,允许外部模块监听排行榜数据的变化,例如:
排行榜整体刷新(可用于 UI 重绘、数据同步)
某个玩家的排名发生变化(可用于成就触发、通知提示、弹窗提醒等)
通过 register_callback方法,可以分别注册两种事件:
-- 注册回调函数
function TopRanking:register_callback(event, func)
if self.callbacks[event] then
error("Callback for event " .. event .. " already registered")
end
self.callbacks[event] = func
end
ranking:register_callback("on_update", function(version, top_players)
-- 排行榜更新时触发
end)
ranking:register_callback("on_rank_change", function(uid, player, old_rank, new_rank)
-- 某玩家排名变化时触发
end)在数据变更后(如调用 update_player()导致排名或版本变化),TopRanking会自动检查是否有注册的回调,并传入相应的参数,例如新版次、玩家数据、排名变化等,便于业务逻辑快速响应。
实现本地文件加载排行榜数据,以及排行榜数据更新好保存到特定文件的功能
本地文件格式如下
return {
nodes = {
{ Name = "ace", Exp = 12000, ... },
{ Name = "sabo", Exp = 700, ... },
...
}
}读文件
function TopRanking:load_from_file(filename)
local file, err = io.open(filename, "r")
if not file then
print("Error opening file for reading:", err)
return false
end
local content = file:read("*a")
file:close()
-- 使用load执行文件内容
local func, parse_err = load(content, "=" .. filename)
if not func then
print("Error parsing data file:", parse_err)
return false
end
local success, data = pcall(func)
if not success or not data or not data.nodes then
print("Error loading data from file:", data)
return false
end
return self:hot_update(data)
end写文件/保存文件
function TopRanking:save_to_file(filename)
local file, err = io.open(filename, "w")
if not file then
print("Error opening file for writing:", err)
return false
end
-- 构建要保存的数据结构
local data = {nodes = {}}
for _, player in ipairs(self.players) do
table.insert(data.nodes, {
Name = player.Name,
Exp = player.Exp,
Job = player.Job,
CombatPower = player.CombatPower,
timestamp = player.timestamp
})
end
-- 将数据序列化为Lua代码
file:write("return {\n")
file:write(" nodes = {\n")
for i, node in ipairs(data.nodes) do
file:write(" {\n")
file:write(string.format(" Name = \"%s\",\n", node.Name))
file:write(string.format(" Exp = %d,\n", node.Exp))
file:write(string.format(" Job = \"%s\",\n", node.Job))
file:write(string.format(" CombatPower = %d,\n", node.CombatPower))
file:write(string.format(" timestamp = %d\n", node.timestamp))
file:write(" }")
if i < #data.nodes then file:write(",") end
file:write("\n")
end
file:write(" }\n")
file:write("}\n")
file:close()
return true
endreturn {
nodes = {
{
Name = "kk",
Exp = 100,
Job = "AK1",
CombatPower = 300,
timestamp = 1757746000
},
{
Name = "lisi",
Exp = 1000,
Job = "AK2",
CombatPower = 15,
timestamp = 1757746001
},
{
Name = "lufei",
Exp = 200,
Job = "AK3",
CombatPower = 150,
timestamp = 1757746002
},
{
Name = "solong",
Exp = 300,
Job = "AK4",
CombatPower = 2,
timestamp = 1757746003
},
{
Name = "zoro",
Exp = 500,
Job = "AK5",
CombatPower = 250,
timestamp = 1757746004
},
{
Name = "nami",
Exp = 400,
Job = "AK6",
CombatPower = 100,
timestamp = 1757746005
},
{
Name = "usopp",
Exp = 150,
Job = "AK7",
CombatPower = 50,
timestamp = 1757746006
},
{
Name = "sanji",
Exp = 600,
Job = "AK8",
CombatPower = 200,
timestamp = 1757746007
},
{
Name = "chopper",
Exp = 50,
Job = "AK9",
CombatPower = 10,
timestamp = 1757746008
},
{
Name = "robin",
Exp = 700,
Job = "AK10",
CombatPower = 180,
timestamp = 1757746009
}
}
}rank-V2.lua
-- rank-V2.lua
local TopRanking = {}
TopRanking.__index = TopRanking
-- 将时间戳转换为年月日时分秒格式
local function timestamp_to_datetime(ts)
local dt = os.date("*t", ts)
return string.format("%04d-%02d-%02d %02d:%02d:%02d",
dt.year, dt.month, dt.day,
dt.hour, dt.min, dt.sec)
end
-- 创建新的排行榜实例
function TopRanking.new(max_display, max_store, compare_func)
local self = setmetatable({}, TopRanking)
-- 配置参数
self.max_display = max_display or 5
self.max_store = max_store or self.max_display * 2
-- 数据存储
self.players = {} -- 排行榜主列表,已排序
self.player_map = {} -- 玩家ID到数据的映射
self.rank_cache = {} -- 玩家ID到排名的缓存
-- 版本控制
self.version = 0
self.dirty = false -- 是否有未同步的更改
-- 回调函数
self.callbacks = {
on_update = nil,
on_rank_change = nil
}
-- 排序函数,默认按Exp降序,相同则按时间戳升序
self.compare_func = compare_func or function(a, b)
if a.Exp ~= b.Exp then
return a.Exp > b.Exp -- 经验值高的排在前面
end
-- 经验相同则时间早的排在前面
return a.timestamp < b.timestamp
end
return self
end
-- 生成唯一ID
local function generate_unique_id(player_data)
return player_data.Name -- 使用Name作为唯一标识
end
-- 内部函数:重新排序并更新排名缓存
function TopRanking:_reorder()
-- 排序玩家列表
table.sort(self.players, self.compare_func)
-- 更新排名缓存
self.rank_cache = {}
for i, player in ipairs(self.players) do
local uid = generate_unique_id(player)
self.rank_cache[uid] = i
end
-- 标记为已修改
self.dirty = true
self.version = self.version + 1
-- 触发更新回调
if self.callbacks.on_update then
self.callbacks.on_update(self.version, self:get_top_players())
end
end
-- 添加或更新玩家数据
function TopRanking:update_player(player_data)
local uid = generate_unique_id(player_data)
-- 如果数据中已有时间戳则使用,否则生成新的
local timestamp = player_data.timestamp or os.time()
-- 保留原始数据并确保时间戳存在
local new_player = {
Name = player_data.Name or "",
Exp = player_data.Exp or 0, -- 增加默认值 0,防止 nil
Job = player_data.Job or "", -- 可选:为 Job 也加默认值
CombatPower = player_data.CombatPower or 0, -- 可选:为战力加默认值
timestamp = timestamp
}
local old_rank = self.rank_cache[uid]
local is_new = not self.player_map[uid]
-- 更新玩家数据映射
self.player_map[uid] = new_player
-- 如果是新玩家或不在当前列表中,添加到列表
local in_list = false
for _, p in ipairs(self.players) do
if generate_unique_id(p) == uid then
in_list = true
break
end
end
local total_max = self.max_store
if not is_new and not in_list then
-- 检查新玩家是否有资格进入排行榜(优于当前最后一名)
if #self.players >= total_max then
local last_player = self.players[total_max]
if not self.compare_func(new_player, last_player) then
-- 新玩家不足以进入排行榜,直接返回
return -1
end
end
end
if is_new or not in_list then
table.insert(self.players, new_player)
end
-- 重新排序
self:_reorder()
-- 如果超出存储上限,移除末尾的元素
while #self.players > self.max_store do
local removed = table.remove(self.players)
local removed_uid = generate_unique_id(removed)
print("移除玩家:", removed_uid, "Exp:", removed.Exp) -- 新增日志
self.player_map[removed_uid] = nil
end
-- 触发排名变化回调
local new_rank = self.rank_cache[uid]
if self.callbacks.on_rank_change and new_rank and (is_new or old_rank ~= new_rank) then
self.callbacks.on_rank_change(uid, new_player, old_rank, new_rank)
end
return new_rank
end
-- 检查玩家是否在排行榜列表中
function TopRanking:is_in_list(uid)
return self.rank_cache[uid] ~= nil
end
-- 获取玩家排名(从1开始)
function TopRanking:get_player_rank(uid)
return self.rank_cache[uid] or -1 -- -1表示不在排行榜中
end
-- 获取玩家数据
function TopRanking:get_player_data(uid)
return self.player_map[uid]
end
-- 获取排行榜前N名玩家
function TopRanking:get_top_players(n)
n = n or self.max_display
local top = {}
for i = 1, math.min(n, #self.players) do
table.insert(top, self.players[i])
end
return top
end
-- 注册回调函数
function TopRanking:register_callback(event, func)
if self.callbacks[event] then
error("Callback for event " .. event .. " already registered")
end
self.callbacks[event] = func
end
-- 同步数据
function TopRanking:sync()
if self.dirty then
print(string.format("Syncing ranking data, version: %d", self.version))
self.dirty = false
return true
end
return false
end
-- 热更新玩家数据
function TopRanking:hot_update(data)
if not data or not data.nodes then
return false
end
print("Hot updating ranking data...")
for _, player in ipairs(data.nodes) do
self:update_player(player)
end
return true
end
-- 从文件加载数据
function TopRanking:load_from_file(filename)
local file, err = io.open(filename, "r")
if not file then
print("Error opening file for reading:", err)
return false
end
local content = file:read("*a")
file:close()
-- 使用load执行文件内容
local func, parse_err = load(content, "=" .. filename)
if not func then
print("Error parsing data file:", parse_err)
return false
end
local success, data = pcall(func)
if not success or not data or not data.nodes then
print("Error loading data from file:", data)
return false
end
return self:hot_update(data)
end
-- 保存数据到文件
function TopRanking:save_to_file(filename)
local file, err = io.open(filename, "w")
if not file then
print("Error opening file for writing:", err)
return false
end
-- 构建要保存的数据结构
local data = {nodes = {}}
for _, player in ipairs(self.players) do
table.insert(data.nodes, {
Name = player.Name,
Exp = player.Exp,
Job = player.Job,
CombatPower = player.CombatPower,
timestamp = player.timestamp
})
end
-- 将数据序列化为Lua代码
file:write("return {\n")
file:write(" nodes = {\n")
for i, node in ipairs(data.nodes) do
file:write(" {\n")
file:write(string.format(" Name = \"%s\",\n", node.Name))
file:write(string.format(" Exp = %d,\n", node.Exp))
file:write(string.format(" Job = \"%s\",\n", node.Job))
file:write(string.format(" CombatPower = %d,\n", node.CombatPower))
file:write(string.format(" timestamp = %d\n", node.timestamp))
file:write(" }")
if i < #data.nodes then file:write(",") end
file:write("\n")
end
file:write(" }\n")
file:write("}\n")
file:close()
return true
end
-- 打印排行榜,包含时间戳信息
function TopRanking:print_ranking(detailed)
local top_players = self:get_top_players()
print("\n=== Top Ranking ===")
for i, player in ipairs(top_players) do
local line = string.format("%d. %s - Exp: %d, Job: %s, Combat: %d",
i, player.Name, player.Exp, player.Job, player.CombatPower)
if detailed then
line = line .. " (" .. timestamp_to_datetime(player.timestamp) .. ")"
end
print(line)
end
print("===================")
end
-- 打印完整存储的玩家列表(包括未进入前N名但在存储范围内的)
function TopRanking:print_all_stored()
print("\n=== All Stored Players ===")
for i, player in ipairs(self.players) do
local rank_str = i <= self.max_display and (i .. ".") or " "
print(string.format("%s %s - Exp: %d, Time: %s",
rank_str, player.Name, player.Exp,
timestamp_to_datetime(player.timestamp)))
end
print("==========================")
end
return TopRankingrankTest-V2.lua
-- rankTest-V2.lua
-- 导入排行榜模块
local TopRanking = require("rankTest.rank-V2")
-- 创建排行榜实例,显示前5名,存储最多10名
local ranking = TopRanking.new(5, 10)
-- 注册回调函数
ranking:register_callback("on_rank_change", function(uid, player, old_rank, new_rank)
local old_rank_str = old_rank and tostring(old_rank) or "none"
print(string.format("[回调] 玩家 %s 排名变化: %s -> %d (经验: %d, 时间: %s)",
uid, old_rank_str, new_rank, player.Exp,
os.date("%Y-%m-%d %H:%M:%S", player.timestamp)))
end)
-- 从文件加载数据
print("从文件加载初始数据...")
ranking:load_from_file("ranking_data.lua")
-- 打印初始排行榜(详细模式,显示时间戳)
print("\n=== 初始排行榜 ===")
ranking:print_ranking(true)
-- 打印所有存储的玩家(包括未进入前五但在存储范围内的)
ranking:print_all_stored()
-- 添加三个新玩家
print("\n=== 添加新玩家 ===")
-- 这个玩家经验很高,会进入前五
ranking:update_player({
Name = "ace",
Exp = 12000,
Job = "AK11",
CombatPower = 3500
})
-- 这个玩家经验与robin相同,但时间更新,会排在后面
ranking:update_player({
Name = "sabo",
Exp = 700,
Job = "AK12",
CombatPower = 320
})
-- 这个玩家经验较低,可能不会进入前五
ranking:update_player({
Name = "vivi",
Exp = 350,
Job = "AK13",
CombatPower = 80
})
-- 打印更新后的排行榜
print("\n=== 更新后的排行榜 ===")
ranking:print_ranking(true)
-- 打印所有存储的玩家
ranking:print_all_stored()
-- 保存数据到文件
print("\n保存数据到文件...")
ranking:save_to_file("updated_ranking_data.lua")
-- 同步数据
ranking:sync()
-- 查询特定玩家排名
local players_to_check = {"lisi", "robin", "sabo", "vivi"}
print("\n=== 玩家排名查询 ===")
for _, name in ipairs(players_to_check) do
local rank = ranking:get_player_rank(name)
local player = ranking:get_player_data(name)
if rank > 0 then
print(string.format("%s 的排名: %d (经验: %d)", name, rank, player.Exp))
else
print(string.format("%s 不在排行榜中", name))
end
end排行榜针对 “多类型 + 分服” 场景设计,通过多层级缓存实现精细化隔离:
-- 按服务器、排行榜id、玩家id三级隔离的缓存
local rankcaches = setmetatable({}, {__mode = "k"}) -- table<Server, table<number, table<number, number>>>
local function getrankcache(server, id)
local cache_map = rankcaches[server]
if not cache_map then
cache_map = {}
rankcaches[server] = cache_map
end
if not cache_map[id] then -- 同一服务器内按排行榜id隔离
cache_map[id] = {}
local rank = gettyperank(server, id)
if rank.list then
for index, item in ipairs(rank.list) do
cache_map[id][item.player.id] = index -- 玩家id映射到排名
end
end
end
return cache_map[id]
end这种设计支持同一服务器内存在多个不同类型的排行榜(如装备评分榜、宠物战力榜),且彼此数据完全隔离,满足游戏中多样化的排行需求。
针对不同类型排行榜的排序需求,通过rankcmp函数实现通用比较逻辑,并适配数值类型的特殊性:
-- 排序比较器
local function rankcmp(server)
local cmp = rankcmps[server]
if not cmp then
cmp = function(left, right)
if left.value == right.value then
-- 数值相同时,按时间戳排序(先达成者优先)
if left.timestamp.sec == right.timestamp.sec then
return left.timestamp.msec < right.timestamp.msec
end
return left.timestamp.sec < right.timestamp.sec
else
-- 使用bn.number处理大数值比较(支持高精度数值)
return (bn.number(left.value) or 0) > (bn.number(right.value) or 0)
end
end
rankcmps[server] = cmp
end
return cmp
end引入了bn.number(openssl 大数处理)支持高精度数值比较
排行榜更新采用 “二分查找定位 + 局部元素调整” 的高效策略,避免全量排序:
二分查找新位置:通过二分查找在当前排名区间内快速定位玩家应处的新位置
---@generic T
---@param a T
---@param b T
---@return boolean
local function defaultcompare(a, b)
return a < b
end
---@generic T
---@param value T
---@return T
local function defaultkeyof(value)
return value
end
---@generic K, V
---@param values V[]
---@param key K
---@param compare (fun(a: K, b: K): boolean)?
---@param keyof (fun(a: V): K)?
---@return number
function M.binarysearch(values, key, compare, keyof)
local l, r = 1, #values
compare = compare or defaultcompare
keyof = keyof or defaultkeyof
while l <= r do
local m = (l + r) // 2
if compare(keyof(values[m]), key) then
l = m + 1
else
r = m - 1
end
end
return l
end局部元素位移:仅调整受排名变化影响的元素,而非全量重排:
-- 上升排名时的局部调整
if index < old then
for i = old, index + 1, -1 do
local temp = cur_rank.list[i - 1]
cur_rank.list[i] = temp -- 元素后移
rankcache[temp.player.id] = i -- 更新缓存索引
end
cur_rank.list[index] = playerRank -- 插入新位置
rankcache[playerRank.player.id] = index
end缓存实时同步:通过rankcache实时更新玩家 ID 与排名的映射关系,确保查询复杂度为O(1)。
---@param server Server
---@param id integer
---@return table<number, number>
local function getrankcache(server, id)
local cache_map = rankcaches[server]
if cache_map == nil then
cache_map = {}
rankcaches[server] = cache_map
end
if cache_map[id] == nil then
cache_map[id] = {}
local rank = gettyperank(server, id)
if rank.list then
for index, item in ipairs(rank.list) do
cache_map[id][item.player.id] = index
end
end
end
return cache_map[id]
end排行榜与游戏活动深度绑定,通过事件驱动管理排行榜的生命周期:
-- 活动开始时初始化排行榜
unloads(event.on("activity.startserver", function(server, id, time)
if server.rank.typeranks then
for _, cur_rank in ipairs(server.rank.typeranks) do
local def = config.TypeRank.find(cur_rank.id)
if def and def.activity == id then
cur_rank.activity = id -- 绑定活动ID
cur_rank.last = nil -- 清空历史结算数据
end
end
end
end))
--活动结束,活动排行结算
---@param server Server
---@param id integer
---@param time integer
unloads(event.on("activity.finishserver", function(server, id, time)
-- 在活动结束时,对关联该活动的排行榜进行结算
-- 给榜上玩家发放对应排名的奖励,并留存结算时的排行榜数据。
end))活动期间的排行榜数据独立于非活动期间,活动结束后会触发结算逻辑(如发放奖励、保存历史排名),满足限时活动的排行需求。
通过ranks.sync监听玩家数据变更,自动更新排行榜中的玩家信息(如名称、头像、等级等):
-- 玩家数据同步
unloads(ranks.sync(function(player)
local server = servers.find(player.server)
if not server then return end
if server.rank.typeranks then
for _, value in ipairs(server.rank.typeranks) do
local playercache = getrankcache(server, value.id)
local index = playercache[player.id]
if index then
-- 通过相应的接口去更新排行榜中的玩家信息
ranks.fill(player, value.list[index].player)
end
end
end
end))当玩家数据(如名称、等级)变更时,排行榜会自动同步最新数据,保证展示一致性。
通过版本号控制数据同步频率,并支持活动结束后的排行结算:
-- 版本号更新
cur_rank.version = cur_rank.version + 1 -- 排名变化时版本号递增客户端可通过对比本地版本号与服务器版本号,决定是否拉取最新数据。活动结束时,会将当前排行数据保存至cur_rank.last,用于展示历史排名和发放奖励。
rankupdate)rankupdate是处理排名更新的核心入口,整合了插入、排序、缓存维护逻辑:
local function rankupdate(player, server, id, value, context)
local rankcache = getrankcache(server, id)
local old = rankcache[player.id] -- 查找旧排名
local cur_rank = gettyperank(server, id) -- 获取目标排行榜
local now = moon.now() / 1000 -- 当前时间戳
-- 1. 新玩家插入逻辑
if not old then
playerRank = protocol.TypeRankPlayerDB()
ranks.fill(player, playerRank.player) -- 填充玩家基础信息
playerRank.value = value
playerRank.timestamp.sec = now // 1
playerRank.timestamp.msec = (now - playerRank.timestamp.sec) * 1000 // 1
-- 首次插入列表
if not cur_rank.list then
cur_rank.list = {}
table.insert(cur_rank.list, playerRank)
rankcache[player.id] = 1
cur_rank.version = cur_rank.version + 1
return 1
end
old = #cur_rank.list + 1
rankcache[player.id] = old
else
-- 2. 已有玩家数据更新
playerRank = cur_rank.list[old]
-- 若新值不优于旧值,直接返回
if (bn.number(playerRank.value) or 0) >= (bn.number(value) or 0) then
return old
end
-- 更新数值与时间戳
playerRank.value = value
playerRank.timestamp.sec = now // 1
playerRank.timestamp.msec = (now - playerRank.timestamp.sec) * 1000 // 1
end
-- 3. 二分查找新位置
local range<close> = utility.slice(cur_rank.list, 1, old - 1)
local index = utility.binarysearch(range, playerRank, rankcmp(server))
-- 4. 处理相同数值的排名连续问题
while index < #cur_rank.list do
local checkPlayer = cur_rank.list[index + 1]
if checkPlayer.player.id == player.id or checkPlayer.value ~= value then
break
end
index = index + 1
if index == old then break end
end
-- 5. 局部调整排名与缓存
if index < old then -- 排名上升
for i = old, index + 1, -1 do
local temp = cur_rank.list[i - 1]
cur_rank.list[i] = temp
rankcache[temp.player.id] = i
end
cur_rank.list[index] = playerRank
rankcache[playerRank.player.id] = index
elseif index > old then -- 排名下降
for i = old, index - 1, 1 do
local temp = cur_rank.list[i + 1]
cur_rank.list[i] = temp
rankcache[temp.player.id] = i
end
cur_rank.list[index] = playerRank
rankcache[playerRank.player.id] = index
end
-- 6. 更新版本号
cur_rank.version = cur_rank.version + 1
return index
end通过ranktypemap建立 “排行类型→排行榜 id” 的映射,支持一种类型对应多个排行榜:
-- 配置加载与映射建立
local function reload()
ranktypemap = {}
for _, value in ipairs(config.TypeRank.values) do
if not ranktypemap[value.type] then
ranktypemap[value.type] = {}
end
-- 同一类型对应多个排行榜id
table.insert(ranktypemap[value.type], value.id)
end
-- 加载奖励配置...
end
unloads(config.subscribe(reload)) -- 配置变更时自动重载
reload()该设计允许同一类数据(如装备评分)在不同活动中对应多个独立排行榜,灵活应对业务需求。
处理客户端的排行榜查询请求,支持实时排行与历史结算数据:
-- 排行榜查询请求处理
unloads(network:typeRankReq(function(req, args)
local player = args.player
local server = servers.find(player.server)
if not server then return end
local cur_rank = gettyperank(server, req.id)
local rsp = protocol.CSTypeRankRsp()
rsp.id = req.id
rsp.rank = protocol.CSRank()
if req.settle then -- 请求结算数据
rsp.rank.version = req.version
if cur_rank.last then
-- 返回历史结算排名(限制数量)
for i = 1, math.min(params[config.ParamID.TOWER_RANK_LIMIT], #cur_rank.last) do
local item = protocol.CSRankItem()
item.player = cur_rank.last[i].player
item.value = cur_rank.last[i].value
table.insert(rsp.rank.list, item)
end
end
else -- 请求实时排行
if cur_rank.list and cur_rank.version ~= req.version then
rsp.rank.version = cur_rank.version
-- 返回前N名数据
for i = 1, math.min(params[config.ParamID.TOWER_RANK_LIMIT], #cur_rank.list) do
local item = protocol.CSRankItem()
item.player = cur_rank.list[i].player
item.value = cur_rank.list[i].value
table.insert(rsp.rank.list, item)
end
-- 返回玩家自身排名
local cache = getrankcache(server, req.id)
rsp.rank.self = cache[player.id]
if rsp.rank.self and rsp.rank.self > params[config.ParamID.TOWER_RANK_LIMIT] then
rsp.rank.self = nil -- 超出展示范围则不返回
end
end
end
network:typeRankRsp(rsp, args)
end))某游戏项目的类型排行榜实现针对 “多类型、活动关联、高精度数值” 场景优化,核心设计思路包括:
这种设计既满足了游戏中多样化的排行需求,又通过精细化的隔离和优化,保证了高并发场景下的性能和数据一致性。
通过这个简单的 Lua TOP N排行榜实现,可以得出以下几点实践心得:
我以前一直以为游戏服务器的排行榜都是redis的zset实现,现在才发现不是所有项目都会选用redis去实现,不过问了ai如果涉及跨服还是建议采用redis的zset方案的,如果以后接触到redis的zset实现,会补充实现思路。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。