首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >基于lua的table实现一个TOP N排行榜功能

基于lua的table实现一个TOP N排行榜功能

原创
作者头像
晨星成焰
发布2025-09-13 20:33:20
发布2025-09-13 20:33:20
1070
举报
文章被收录于专栏:Lua学习笔记Lua学习笔记

前言

最近有空研究了下某游戏开发项目里的排行榜实现,发现虽然业务逻辑不复杂,但要做到高效、灵活、易扩展,还是有不少细节值得推敲。于是我尝试用 Lua 模仿实现了一个轻量级的排行榜功能,并在此记录下我的设计思路与实现过程。

排行榜几乎是所有带有竞争或成长元素的游戏标配功能,比如战力排行、等级排行、积分排行等。核心需求通常包括:

  • 插入/更新玩家数据
  • 自动排序
  • 按排名展示(如 TOP N)
  • 快速查询某个玩家的排名
  • 支持不同排序规则(战力、经验等)

本文将介绍如何利用 Lua 的 table 特性,简单模拟实现一个灵活、高效的TOP N排行榜功能。

模拟实现rank排行榜功能

功能设计思路

  • 要实现一个排行榜,就要实现玩家数据的插入/更新、自动排序、以及按排名快速查询的核心操作。这也是为什么选择lua的table来做基础数据结构实现,因为table同时支持数组和哈希表特性,能满足排行的核心需求,非常适合做轻量级高性能排行榜。
  • 除了用lua的table还可以使用以下几种 • 数据库排序:灵活但查询慢,适合非实时榜 • Redis ZSET:高性能且支持范围查询,但依赖外部服务 • 跳表:高效但实现复杂,Lua 生态少
  • 排序逻辑基于经典的"键提取+比较函数实现",方便切换排序逻辑,提高复用性,有点类似策略模式
  • 实现缓存映射,通过玩家关键信息快速获取到排名,减少检索时间复杂度,这也是排行榜优化的基操,使用哈希表,将 O(n) 的查找优化为 O(1)。
  • 提供接口,将功能模块化,便于多处复用和测试,提高代码复用能力

简单实现

  • 使用 Lua table 同时作为数组(排行列表)与哈希表(玩家缓存)的基础数据结构
  • 支持 多类型排序策略(如战力、经验)
  • 引入 缓存索引,实现 O(1) 玩家排名查询
  • 使用 缓冲区机制优化性能,避免每次更新都全量排序
  • 模拟实现 版本号 仿客户端数据同步与变更追踪

相关代码

代码语言:lua
复制
-- 模拟时间戳生成
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(),以及一系列方法

代码语言:lua
复制
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,从而实现展示与存储的分层控制,提升灵活性和内存使用效率。

代码语言:lua
复制
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

精准、快速的排名查询(O(1))

通过维护一个额外的哈希表 rank_cache,实现 O(1) 时间复杂度的排名查询

代码语言:lua
复制
self.rank_cache = {}  -- 玩家ID => 排名(从1开始)

每当玩家数据发生插入、更新或排序后,_reorder()方法会重新计算所有玩家的排名,并更新该缓存:

代码语言:lua
复制
self.rank_cache[uid] = i  -- i 就是排名

因此,当我们需要查询某个玩家的排名时,只需:

代码语言:lua
复制
function TopRanking:get_player_rank(uid)
    return self.rank_cache[uid] or -1  -- -1 表示未上榜
end

自动排序与缓存同步

排行榜的核心之一,就是保证玩家数据始终按照指定的排序规则(如经验值、战力等)有序排列。

排序逻辑通过用户传入的比较函数 compare_func定义(默认按经验值倒序,时间戳正序),并通过 Lua 原生的 table.sort()实现排序:

代码语言:lua
复制
-- 内部函数:重新排序并更新排名缓存
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方法,可以分别注册两种事件:

代码语言:lua
复制
-- 注册回调函数
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会自动检查是否有注册的回调,并传入相应的参数,例如新版次、玩家数据、排名变化等,便于业务逻辑快速响应。

数据持久化:文件加载与保存

实现本地文件加载排行榜数据,以及排行榜数据更新好保存到特定文件的功能

本地文件格式如下

代码语言:lua
复制
return {
    nodes = {
        { Name = "ace", Exp = 12000, ... },
        { Name = "sabo", Exp = 700, ... },
        ...
    }
}

读文件

代码语言:lua
复制
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

写文件/保存文件

代码语言:lua
复制
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

以下是本地存储数据的lua文件

代码语言:lua
复制
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

代码语言: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

代码语言: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

某游戏项目rank实现思路及关键代码

核心设计思路

1. 分服隔离的排行榜架构

游戏通常采用分服运营模式,每个服务器的排行榜数据需要独立存储和计算。项目中通过servercachesrank.lua)实现了服务器级别的缓存隔离:

代码语言: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

这种设计确保了不同服务器的排行榜数据互不干扰,同时减少跨服数据交互的性能开销。

2. 多维度排序策略与比较器抽象

游戏中存在多种排行榜类型(如等级榜、Boss 伤害榜、战力榜),每种榜单的排序规则不同。项目通过比较器工厂函数实现排序逻辑的复用与定制:

代码语言:lua
复制
-- 生成比较函数的工厂(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(键比较规则)即可:

代码语言:lua
复制
-- 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

3. 高效的排名更新与缓存机制

为避免每次更新都全量排序,项目采用二分查找 + 局部调整的方式优化性能:

  1. 二分查找定位插入位置:通过utility.binarysearch快速找到玩家在排行榜中的位置,时间复杂度为O(log n)
  2. 局部元素位移:当玩家排名变化时,仅调整受影响的局部元素(而非全量重排):
代码语言:lua
复制
-- 上升排名时的局部调整(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
  1. 缓存索引映射:通过cache表(玩家 ID→排名)实现O(1)时间复杂度的排名查询,避免遍历列表:
代码语言:lua
复制
-- 缓存结构(rank.lua)
cache[item.player.id] = index  -- 玩家ID映射到排名

4. 延迟更新与批量同步

为减少高频更新带来的性能损耗,项目采用延迟批量更新策略:

代码语言:lua
复制
-- 延迟更新逻辑(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 操作的频率。

5. 事件驱动的数据同步

玩家数据(如名称、等级、头像)变更时,需要实时同步到排行榜。项目通过ranks.lua的事件监听机制实现自动化同步:

代码语言: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函数会遍历所有注册的同步回调,更新排行榜中的玩家信息:

代码语言:lua
复制
-- 同步回调触发(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

6. 版本号与数据一致性

为避免客户端频繁请求全量数据,项目通过版本号机制追踪排行榜变更:

代码语言:lua
复制
-- 版本号更新(rank.lua)
if index ~= old then
    rank.version = rank.version + 1  -- 排名变化时版本号+1
end

客户端只需对比本地缓存的版本号与服务器版本号,即可判断是否需要拉取最新数据(如typerank.lua中的排行榜请求处理)。

关键代码模块解析

1. 排行榜核心更新函数(updaterank

updaterank是处理排行榜更新的核心入口,整合了插入、排序、缓存维护逻辑:

代码语言:lua
复制
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

2. 玩家数据填充(ranks.fill

确保排行榜中的玩家信息与实际数据一致,包含基础信息和动态变更的属性:

代码语言:lua
复制
-- 填充玩家数据到排行榜项(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

3. 多类型排行榜适配

通过封装不同榜单的专属逻辑,实现对多种排行榜类型的支持。例如分支等级榜:

代码语言:lua
复制
-- 分支等级榜更新(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

总结

该项目的排行榜实现围绕性能优化可扩展性展开,核心亮点包括:

  1. 分服隔离:通过服务器级别的缓存隔离,适配游戏分服架构。
  2. 高效更新:二分查找 + 局部调整 + 缓存索引,将更新和查询复杂度降至O(log n)O(1)
  3. 延迟批量处理:合并短时间内的多次更新,减少性能损耗。
  4. 事件驱动同步:自动追踪玩家数据变更,保证排行榜信息实时性。
  5. 多类型适配:通过比较器工厂和通用更新函数,轻松扩展新的排行榜类型。

这种设计既满足了游戏高并发场景的性能需求,又为后续功能迭代(如新增排行榜类型、调整排序规则)提供了灵活的扩展能力。

总结

通过这个简单的 Lua 排行榜实现,可以得出以下几点实践心得:

  1. Lua table 是构建轻量级排行榜的绝佳工具,兼顾了顺序存储与快速查找的优势。
  2. 排序逻辑抽象为策略函数,使得排行榜支持多种排序类型,且易于扩展。
  3. 维护一个排名缓存(哈希表),极大优化了“查询某个玩家排名”的效率。
  4. 排行榜不一定要依赖 Redis 或数据库,在客户端、小游戏、模拟环境等场景中,Lua 实现足够高效和灵活。

我以前一直以为游戏服务器的排行榜都是redis的zset实现,现在才发现不是所有项目都会选用redis去实现,不过问了ai如果涉及跨服还是要采用redis的zset方案的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 模拟实现rank排行榜功能
    • 功能设计思路
    • 简单实现
      • 相关代码
    • 更贴合原项目实现思路实现
      • 面向对象思想
      • 支持最大展示数与最大存储数分离
      • 精准、快速的排名查询(O(1))
      • 自动排序与缓存同步
      • 回调机制:支持排行榜变更事件监听
      • 数据持久化:文件加载与保存
      • 以下是本地存储数据的lua文件
      • 以下是源代码
  • 某游戏项目rank实现思路及关键代码
    • 核心设计思路
      • 1. 分服隔离的排行榜架构
      • 2. 多维度排序策略与比较器抽象
      • 3. 高效的排名更新与缓存机制
      • 4. 延迟更新与批量同步
      • 5. 事件驱动的数据同步
      • 6. 版本号与数据一致性
    • 关键代码模块解析
      • 1. 排行榜核心更新函数(updaterank)
      • 2. 玩家数据填充(ranks.fill)
      • 3. 多类型排行榜适配
    • 总结
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档