传奇3 实战 · lua 实战 传3 版本制作
消费排行榜怎么做:统计、排序与刷新一条线
点这里打开这节课的课程页→这节课后半段落到消费排行榜上:拍卖行买进的钱要记下来,按玩家名字排个名,第二天按名次发奖励。课上是从「记录几个值」开始写的——当日消费榜、累计总购买,另留一个回收用的消费总额度。数据存进全局变量、取出来却是字符串、排序函数写错、比较对象取错层,这些坑都当场踩过一遍。这篇按课堂顺序把统计口径、排序写法、刷新下发和离线时间的处理重新理一遍,代码块都能照着改。
- 排行榜记两个值:当日消费(0 点到 24 点)与累计总购买,另留一个消费总额度,回收时按 30% 返元宝。
- 数据以玩家名字为主键存进全局变量,取出来是字符串,要先判断空再转成 table。
- 排序用
table.sort配比较函数,比较函数里不能拿整个元素比,要取到具体字段再比。 - 先迭代把名字和金额
insert进一个新数组再排序,降序用大于号,升序用小于号。 - 前端拿不到完整榜单,得自己写协议 107:服务端把表转成 json 字符串下发,客户端再转回 table。
- 消费满 1000 才有资格进榜,按名次领 10% 绑定元宝,但要第二天才能领;卖出不计入排行榜。
01先定口径:当日榜、累计总购买与回收额度
排行榜不是凭空算出来的,第一步是把要记的值定下来。课上把拍卖行里的钱拆成三份来看:一份是当日消费排行榜,只统计 0 点到 24 点这一段;一份是消费总购买,从开服累计到现在,永久保留;还有一份是消费总额度,专门留给回收用,将来做一键清理的时候,按这个额度给角色返 30% 的元宝。
为什么当日和累计要分成两个值?因为排行榜考核的是「这段时间买了多少」,回收算的是「历史上总共买了多少」,两个口径混在一起,第二天清零就会把回收的账一并抹掉。课上讲得很直白:当日榜给排名用,累计额度给返还用,两者不要互相覆盖。
这些钱从哪来?从拍卖行的买入走。谁买了东西,就往对应的值里加钱。买入的钱才进排行榜,卖出的钱不进。课上顺手把卖出的收益改成只有 50%,理由是买卖两边来回倒就能把榜单刷上去,收益砍半之后刷榜就不划算了。
记录的主体是玩家名字,不是账号、也不是道具。名字做主键,后面的排序、显示、发奖都靠这个键去找人。
课上把这一摊数据叫拍卖行买入,存的是这个玩家累计买入了多少。当日榜和累计榜共用同一套主键,只是写入的位置不同,读的时候要注意别拿错变量。
两个值的写入时机是同一个:玩家在拍卖行买下一件东西,钱扣掉的同时就往两个值里各记一笔。区别只在于当日值到点要被清空重来,累计值一直往上加。课上特意把这一点拎出来讲,因为这是「一次写入、两处记账」,而不是两套互不相干的逻辑;真写成两套,后面改规则时最容易改了一处忘了另一处。回收额度又是另外一个变量,它只在做返还的时候读,平时不参与显示。
课上把三个值的用途分得很清楚:当日榜只在排行榜界面上露面,累计总购买做长期统计,消费总额度压着不动,等一键清理功能上线时再结算返还。什么时候给总额度加钱,课上给的原话是「等你写那个一键清理的时候再往上加」,眼下先把排行榜这一路跑通。这样排期,排行榜当天就能验收,返还逻辑晚一步单独测,两边互不拖累。
02存进全局变量:取出来是字符串,先判断再转表
值定好了,接下来是怎么存。课上用的是一个全局变量,值的形态是一张表:主键是玩家名字,值是消费金额。写入的时候往表里加一笔,读取的时候把整张表拿出来。
这里出现了这节课反复卡壳的第一个点:取出来的值不是 table,是一个 string。老师当场把类型打印出来,屏幕上显示的就是 string——变量里存的其实是序列化之后的一串字符。所以拿到的值不能直接当表用,得先判断它是不是空,再决定要不要转换。
-- 全局变量里存的是一串序列化字符串,取出来是 string,不是 table
local rank = 读取全局变量("消费排行榜")
if rank == nil or rank == "" then
rank = {} -- 取到空,就给一张空表
else
rank = json2tbl(rank) -- 否则先转回 table 再用
end
-- 底表结构:主键 = 玩家名字,值 = 消费金额
rank[玩家名字] = (rank[玩家名字] or 0) + 本次买入
判断写成这样:如果取到的值等于空,就给一张空表;否则把它转成 table。课上在这一步来回改了好几遍,屏幕上出现过各种意料之外的返回值,老师边看边纠正「应该等于 table 才对」,到头来靠先打印类型才把顺序理顺。
空表判断最容易写错位置。课上把转换写在了判断之前,拿到的既不是空值也不是表,打印出来是个看不懂的返回值,来回对了好几次才定下来:先取值、再判断、再转换。顺序一颠倒,后面整段逻辑都会拿到脏数据。
转好之后,这张表就是排行榜的底表。往里面塞值的时候,键用玩家名字,值用金额累加,同名玩家自然就并到一行上,不需要额外的去重处理。
课上确认类型用的是打印,把值直接打到控制台,看它到底是 string 还是 table。这一步看着原始,却比猜快得多:类型对了才继续往下写,类型不对就先修类型。老师在这个环节反复念叨的一句话是「你从这看一下这是一个什么值」,说的就是别假设、先看。
测试这一步课上是真按出来的:买一笔看累计买入从 1000 涨到 2000,再买一笔到 4000,屏幕上的数字跟着走,写入才算通。老师顺带提了一句,取到的字符串不转表就往里塞值,塞进去的其实是字符拼接,看着也在变,结构却早就坏了,只有打印出来才发现不对劲。
03排序写错:拿整个元素比大小,名次一动不动
表有了,接下来是排序。课上用的是 table.sort,它需要第二个参数——一个比较函数,告诉它谁在前谁在后。老师第一次写的是 return a < b,看着挺对,跑起来点开界面,名次一点都没动。
为什么不排?因为这里的 a 和 b 不是金额,是表里的元素。拿两个元素直接比大小,比的根本不是消费额,比较结果和名次自然无关。老师想明白之后改成取字段再比:把 a 和 b 里代表金额的那个值取出来,两个值再互相比较。
-- 错的写法:a、b 是整张元素,比出来跟消费额没关系,名次不动
table.sort(list, function(a, b)
return a < b
end)
-- 对的写法:先取到金额字段,再比较;字段名按自己的表结构来
table.sort(list, function(a, b)
return a.value > b.value -- 降序用大于号,升序换成小于号
end)
课上为了确认这一点,专门把参与比较的两个值打印出来,看顺序有没有发生交换。打印出来的是名字,不是金额,一看就知道比错了对象。改成取值再比之后,名次立刻按预期排开。
比较函数是 table.sort 里最容易写错的一处:写 a 还是写 a.value,语法都合法、脚本都不报错,只是结果不对。判断方法很简单——在比较函数里把 a 打印出来,看到的是整张表就说明取错层了。
比较函数里取哪个字段,取决于底表怎么建。课上底表存的是「名字 → 金额」,迭代成数组时顺手把金额装进 value,所以比较函数写 a.value 与 b.value。如果当初装进去的字段叫别的名字,这里也得跟着改——比较函数和数据结构是绑在一起的,不能只改一头,改了数据不改比较,排出来的名次就是错的。
排错顺序课上走得很固定:先打印 table.sort 两个入参,确认拿到的是元素还是金额;再打印比较函数的返回值,确认方向没写反。名次迟迟不动,多半就出在这两处。老师没有一次改到位,而是每改一处点一次更新,看名次有没有反应,这样真排错了也清楚是哪一步引入的。
04先迭代 insert 再排序,降序用大于号
光有比较函数还不够,因为底表是「名字 → 金额」的字典,字典本身没有顺序,直接排序排不出名次。课上的做法是先迭代一遍:把底表里的名字一个个 insert 进一个新数组,元素带着名字和金额两个字段,再对这个数组排序。老师把这一步讲得很形象——先重新过一遍,让数组里有了带主键值的元素,这时候排序才有意义。
课上试了两个方向的比较:升序用小于号,降序用大于号。排行榜要的是降序,落到代码里定的是大于号。改完之后再点开查看,排在前面的是消费最高的那几个人,名次终于从高到低排下来了。
-- 第一步:把「名字 -> 金额」的字典迭代成数组
local list = {}
for name, value in pairs(rank) do
table.insert(list, {name = name, value = value})
end
-- 第二步:对数组排序,排行榜要降序
table.sort(list, function(a, b)
return a.value > b.value
end)
排完之后再迭代一次,输出的就是排好序的名次列表。老师在这一步说「这个代码写得太好了」,因为「迭代 → insert → 排序」这个套路不只用于消费榜,凡是字典要排名次都能套:投票、捐献、击杀数,换的只是字段名,流程一模一样。
数组排完不要直接丢掉原始字典。名次只是显示用的顺序,底表仍然要按名字保留,因为后面发奖、查某人消费多少,还得靠名字回查。
课上还验证过一次方向写反的后果:比较函数里写成小于号,名次就翻过来,该排在前面的跑到后面去了;改成大于号,顺序立刻回正。老师说这就是「本身就是降序」的意思,方向定对了,界面上显示的名次和后面按名次发奖才对得上。
05前端拿不到变量,就自己写一条协议下发
排好序的数据在服务端,可是真正要显示的是前端界面。课上试过前端变量推送、全局符号这些路子,发现数据不全,拿不到完整的排行榜。老师的结论是「没招了,就得自己去写协议」。
协议的做法很直接:打开排行榜界面的时候,客户端发一条请求给服务端,消息号取 107,含义是获取排行榜数据。服务端收到以后,把排好序的表转成 json 字符串再发回去;客户端收到字符串,再用转换接口转回 table,迭代渲染成一行行文本。
-- 服务端:收到 107 请求,把排好序的表转成字符串发回去
if msgid == 107 then
local data = tbl2json(list) -- 表 -> json 字符串
sendluamsg(actor, 107, 0, 0, 0, data, 0)
end
-- 客户端:收到字符串,先转回 table 再渲染
local list = json2tbl(data)
local n = 1
for i, v in pairs(list) do
-- 第 n 名:名字 v.name,金额 v.value
n = n + 1
end
客户端渲染的时候,课上用了一个局部计数变量,每渲染一行加一,名次号顺着往下写。位置按自己的界面调,重要的是数据先通。等到服务端打印出请求、客户端打印出收到的字符串,这条链路就算打通了。
协议号、消息号这些数字是课上现取的,换成自己工程里的空闲号即可。关键是约定两件事:前后端用同一个号,以及「服务端发字符串、客户端自己转表」这个分工。分工一旦定死,两边各写各的,联调时只看那一串字符串对不对。
课上先前试过两条现成的路:前端变量推送、全局符号,都因为拿不到完整数据被放弃,才落到自己写协议上。下发前服务端先确认一次请求,消息号命中 107 就取那份排好序的表,转成字符串塞进回包。把服务端打印的请求和客户端打印收到的字符串对着看,两边一致,才去写渲染。前端不猜数据、服务端不做显示,这条边界定下来,以后加字段只改一处。
06满 1000 进榜,按名次领 10% 绑定元宝
榜单排出来之后,课上一并定了几条规则。第一条是门槛:消费满 1000 以上才能进排行榜,低于这个数的记录不参与排名。第二条是奖励:进了榜的玩家,按名次可以领一份10% 的绑定元宝,名次越高领得越多。
第三条是时间:奖励不是当天发的,要第二天才能领取。老师的解释是「因为它有排行榜那个竞赛」,当天就派等于竞赛还没结束就开奖,所以隔一天再开领。统计本身按天跑,0 点到 24 点算当日消费榜,第二天刷新之后才进入可领取状态。
| 规则 | 取值 | 说明 |
|---|---|---|
| 进榜门槛 | 1000 以上 | 低于门槛的消费不参与排名 |
| 奖励比例 | 名次的 10% | 按名次计算,发放绑定元宝 |
| 领取时间 | 第二天起 | 有榜单竞赛,隔天再开领 |
| 统计口径 | 当日 0 到 24 点 | 卖出不计入,只算买入 |
领取同样走协议:客户端点领取按钮发一个请求,参数里带名次;服务端判断参数,命中之后就发奖励。课上到这一步,界面按钮上的文字还没补上,出现过按钮点不到的情况——那是按钮没设文字导致的定位问题,跟协议本身没关系,补上文字就好了。
门槛、比例、发放时间这三个值都是可配的。课上定的 1000、10%、次日领取只是这一版的数值,换成自家运营规则时,改的是这几个常量,不用动排序和下发那套逻辑。
领取的协议课上也是现写的:客户端点领取发一条请求,参数里带名次,服务端用条件分支接住,等于 1 走一条、等于 2 走另一条,命中就发对应奖励。老师在这里补了一句原则——前端发请求、后端做检测,检测命令不满足就直接返回。名次这东西到了服务端要再核一遍,确认这个人确实在榜、名次没被改过,再执行发放。
07离线时间换算:除 2 得 3 倍再除 60
排行榜按天统计,绕不开时间口径。课上在写这套逻辑的同一段里,专门把离线时间的处理方式算了一遍,因为它跟「按秒记、按天刷」是同一套时间概念。
需求是:离线时间也要算成 3 倍加成。老师先记一个离线时间差 offline = time,然后做换算——时间本身以秒为单位,除以 2 就得到 3 倍的时间;再除以 60 才是分钟。这里一开始是算错的,屏幕上出现过「这个值需要除以 400」的说法,老师自己马上否掉了,因为单位对不上,认准「现在就是秒」之后,先除以 2,再按需要除以 60。
-- 离线时间以秒为单位,先换算成 3 倍加成的时间,再转分钟
local offline = time -- 记录离线时间差(秒)
local bonus = offline / 2 -- 除以 2:秒数直接翻三倍
local minutes = bonus / 60 -- 再除以 60:换算成分钟
-- 上限:离线时间按分钟算,大于等于 1440 就不再往上加
if minutes >= 1440 then
minutes = 1440
end
还有一个上限要卡:离线时间大于等于 1440 分钟就不再往上加。课上讨论过上限判断该写在分钟这一层还是写在 3 倍加成之后,客户的提醒是「是 3 倍时间,不是离线时间」,也就是说判断之前先看清手上这个变量是哪一层的时间。老师现场直接用离线 10 秒来验证,改完立刻能看到结果,比干等 8 分钟省事得多。
单位不统一是这段最耗时间的地方:同一个变量,第一层是秒、第二层是分钟、第三层是 3 倍加成后的分钟。除法写在哪一层,结果就差出几十上百倍。课上就是靠不停打印、不停换算,把每一层的单位先对齐,再动公式。
换算写成代码就三行:先拿到秒,除以 2 得到 3 倍时间,再除以 60 换算成分钟。课上把这三层的数值一层层打印出来,对着屏幕确认每一层的单位都对上了,才去写那句上限判断。这套「先对齐单位、再动公式」的顺序,往后写任何跟时间有关的加成都能直接搬。
常见问题
排行榜的数据到底存在哪里?
存在一个全局变量里,主键是玩家名字,值是消费金额。取出来其实是一串序列化字符串,不是 table,得先判断是不是空,再用转换接口转回表才能用。
<code>table.sort</code> 排完名次没变,错在哪?
多是比较函数里拿整个元素在比。要先把金额字段取出来,写成 a.value 与 b.value 再比较;降序用大于号,升序换小于号。
为什么不能直接对字典排序?
字典是「名字 → 金额」的映射,本身没有顺序。要先 pairs 迭代一遍,把名字和金额 insert 进一个新数组,再对这个数组排序,名次才排得出来。
前端怎么拿到排行榜数据?
课上试过前端变量推送,数据不全,改成自己写协议 107:客户端发请求,服务端把排好序的表转成 json 字符串发回来,客户端再转回 table 渲染。
消费多少能进榜,能领多少奖励?
消费满 1000 以上才能进排行榜,进榜之后按名次领 10% 的绑定元宝。奖励要第二天才能领,因为当天还在榜单竞赛期内。
离线时间怎么换算成 3 倍加成?
离线时间以秒计,除以 2 就是 3 倍时间,再除以 60 换算成分钟;上限是 1440 分钟。判断上限前先确认手上这个变量是秒、分钟还是加成后的时间。