最近有空研究了下某游戏开发项目里的排行榜实现,发现虽然业务逻辑不复杂,但要做到高效、灵活、易扩展,还是有不少细节值得推敲。于是我尝试用 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
end
return {
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 TopRanking
rankTest-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
游戏通常采用分服运营模式,每个服务器的排行榜数据需要独立存储和计算。项目中通过servercaches
(rank.lua
)实现了服务器级别的缓存隔离:
local servercaches = {} -- 按服务器隔离的排行榜缓存
local function getcache(player, rank, name)
local cache
local caches = servercaches[player.server] -- 以服务器为key
if caches == nil then
caches = {}
servercaches[player.server] = caches
end
cache = caches[name] -- 同一服务器内按排行榜名称隔离
-- ... 初始化缓存逻辑
return cache
end
这种设计确保了不同服务器的排行榜数据互不干扰,同时减少跨服数据交互的性能开销。
游戏中存在多种排行榜类型(如等级榜、Boss 伤害榜、战力榜),每种榜单的排序规则不同。项目通过比较器工厂函数实现排序逻辑的复用与定制:
-- 生成比较函数的工厂(rank.lua)
local function rank_compare(keyof, keycompare)
keycompare = keycompare or default_compare
local compare = function(left, right)
local leftkey = keyof(left)
local rightkey = keyof(right)
-- 优先按核心指标比较(如伤害值、等级)
local less = keycompare(leftkey, rightkey)
local greater = keycompare(rightkey, leftkey)
-- 核心指标相同时,按时间戳排序(先达成者优先)
if not (less or greater) then
if left.timestamp.sec == right.timestamp.sec then
return left.timestamp.msec < right.timestamp.msec
end
return left.timestamp.sec < right.timestamp.sec
end
return greater -- 降序排列(数值高的在前)
end
return compare
end
针对不同榜单类型,只需定制keyof
(提取排序键)和keycompare
(键比较规则)即可:
-- Boss伤害榜的键提取与比较(rank.lua)
local function bossdamagerankitem_keyof(value)
return value.damage -- 以伤害值为排序键
end
local function bossdamagerankitem_compare(left, right)
return (math.tointeger(left) or 0) < (math.tointeger(right) or 0) -- 数值比较
end
为避免每次更新都全量排序,项目采用二分查找 + 局部调整的方式优化性能:
utility.binarysearch
快速找到玩家在排行榜中的位置,时间复杂度为O(log n)
。-- 上升排名时的局部调整(rank.lua)
if index < old then
for i = old, index + 1, -1 do
local item = rank.list[i - 1]
rank.list[i] = item -- 元素后移
cache[item.player.id] = i -- 更新缓存索引
end
rank.list[index] = playerRank -- 插入新位置
end
cache
表(玩家 ID→排名)实现O(1)
时间复杂度的排名查询,避免遍历列表:-- 缓存结构(rank.lua)
cache[item.player.id] = index -- 玩家ID映射到排名
为减少高频更新带来的性能损耗,项目采用延迟批量更新策略:
-- 延迟更新逻辑(rank.lua)
unloads(ranks.sync(function(player, save)
if updates == nil then
updates = {}
moon.timeout(6000, save) -- 6秒后批量保存
end
updates[player.id] = player -- 暂存待更新玩家
end, function()
-- 批量处理所有待更新玩家
local old = updates
updates = nil
-- ... 批量更新排行榜数据
end))
通过moon.timeout
将 6 秒内的多次更新合并为一次批量处理,降低排序和 IO 操作的频率。
玩家数据(如名称、等级、头像)变更时,需要实时同步到排行榜。项目通过ranks.lua
的事件监听机制实现自动化同步:
-- 玩家数据变更事件监听(ranks.lua)
event.on("changename", function(player)
moon.async(dirty, player) -- 触发脏数据标记
end)
event.on("levelup", function(player)
moon.async(dirty, player)
end)
-- 其他事件:changeavatar、changehead、changeskin等
当事件触发时,dirty
函数会遍历所有注册的同步回调,更新排行榜中的玩家信息:
-- 同步回调触发(ranks.lua)
local function dirty(player)
local cursor = syncs.next
while cursor ~= syncs do
local next = cursor.next
xpcall(cursor.dirty, debug.traceback, player, cursor.save) -- 调用rank.lua中的更新逻辑
cursor = next
end
end
为避免客户端频繁请求全量数据,项目通过版本号机制追踪排行榜变更:
-- 版本号更新(rank.lua)
if index ~= old then
rank.version = rank.version + 1 -- 排名变化时版本号+1
end
客户端只需对比本地缓存的版本号与服务器版本号,即可判断是否需要拉取最新数据(如typerank.lua
中的排行榜请求处理)。
updaterank
)updaterank
是处理排行榜更新的核心入口,整合了插入、排序、缓存维护逻辑:
local function updaterank(name, rank, player, key, create, update, keyof, compare)
-- 1. 检查是否有资格进入排行榜(超出缓冲区则直接返回)
if rank.list and #rank.list >= 2 * max then
if not compare(keyof(rank.list[#rank.list]), key) then
return false
end
end
-- 2. 查找旧排名与新位置
local cache = getcache(player, rank, name)
local old = cache[player.id]
local index = utility.binarysearch(...) -- 二分查找新位置
-- 3. 局部调整列表与缓存
if index < old then ... end -- 上升调整
if index > old then ... end -- 下降调整
-- 4. 更新版本号与截断缓冲区
rank.version = rank.version + 1
while #rank.list > 2 * max do
table.remove(rank.list) -- 只保留2*max长度的缓冲区
end
return true
end
ranks.fill
)确保排行榜中的玩家信息与实际数据一致,包含基础信息和动态变更的属性:
-- 填充玩家数据到排行榜项(ranks.lua)
function M.fill(player, data)
data.id = player.id
data.name = player.name
data.level = player.level
data.avatar = player.avatar
data.head = player.headinfo.head
-- 处理皮肤信息(动态变更项)
data.skin = nil
if player.skininfo.skin and player.skininfo.unlockskins then
for _, value in ipairs(player.skininfo.unlockskins) do
if value.id == player.skininfo.skin then
data.skin = value
break
end
end
end
end
通过封装不同榜单的专属逻辑,实现对多种排行榜类型的支持。例如分支等级榜:
-- 分支等级榜更新(rank.lua)
local function branchlevel(player, type, value)
local server = servers[player.server]
local selected -- 查找当前服务器的对应类型榜单
if server.rank.branchlevel then
for _, v in ipairs(server.rank.branchlevel) do
if v.type == type then
selected = v
break
end
end
end
-- 不存在则创建新榜单
if not selected then
selected = protocol.BranchLevelRankDB()
selected.type = type
server.rank.branchlevel = server.rank.branchlevel or {}
table.insert(server.rank.branchlevel, selected)
end
-- 调用通用更新函数
updaterank("branchlevel_" .. type, selected, player, value,
protocol.RankItemDB, rankitem_update, rankitem_keyof)
end
该项目的排行榜实现围绕性能优化和可扩展性展开,核心亮点包括:
O(log n)
和O(1)
。这种设计既满足了游戏高并发场景的性能需求,又为后续功能迭代(如新增排行榜类型、调整排序规则)提供了灵活的扩展能力。
通过这个简单的 Lua 排行榜实现,可以得出以下几点实践心得:
我以前一直以为游戏服务器的排行榜都是redis的zset实现,现在才发现不是所有项目都会选用redis去实现,不过问了ai如果涉及跨服还是要采用redis的zset方案的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。