传奇3 实战 · lua 实战 传3 版本制作
Lua 括号与运算优先级:元宝计算里的隐形坑
点这里打开这节课的课程页→这节课的后半段几乎都在跟元宝打交道:买、卖、扣钱、发奖励,每一处都离不开括号和加减乘除。也正是在这些地方,最容易冒出「少一个括号整行报错」和「算式顺序写反结果差几十倍」的问题。这篇把课上现场踩过的几类坑梳理一遍:括号怎么配对、先算哪一步、单位是秒还是分、逗号该放在哪、循环时间与列表刷新怎么配合。
- 报「缺少括号」时,先从调用那一行往回数,看冒号后面的括号有没有成对。
- 括号决定先算哪一步,离线时间的换算顺序写反,结果会差几十倍。
- 时间存的是秒还是分要先确认单位,再写算式,不然公式全废。
- 逗号有两个身份:分隔参数,以及混进变量名就变成报错源。
- 元宝读写走同一套货币写法,括号写全才拿得到对象里的值。
- 上限判断要写在你手上那一层的时间上,课上的提醒是「是 3 倍时间,不是离线时间」。
01少一个括号,整行代码就废了
课上写到排行榜那一段时,界面一点就报错,提示是「缺少括号」。老师的反应很直接:哪找括号了?就在这。顺着那一行往回看,果然是函数名后面那一对括号只写了左半边。
这类错误的特点是不看你后面写了多少逻辑,只要这一行的括号不成对,整行都过不去。它也不会聪明到告诉你缺在哪个字后面,只给一个大致的位置,剩下靠自己数。
数括号的顺序可以固定下来:先找到这一行最外层的函数名,再找它紧跟的冒号或者括号,然后一层层往里拆,每拆一层就划掉一对,划到结尾看还剩没剩左括号。
课上的修法很朴素:把已经写对的那一段复制过来,照着补齐少掉的那半边。「冒号,然后把那个复制过来」说的就是这个动作——与其在出错的那一行硬找,不如先找一份写对的当参照。少括号这种事几乎不会只发生一次,同一种调用写熟了,后面就顺手了。
-- 少了一个右括号,整行都过不去
按钮:onclick(function(self)
发送(107, 1)
end课上还遇到过一次同源的问题:按钮绑定事件时漏了一个逗号,实际报错的位置和真正写错的位置差了好几行。报错给的行号是「第一个不满意的地方」,不一定是「你写错的地方」,所以别只在提示的那一行盯死。
括号问题在事件绑定那一块出现得最多。按钮的回调、点击的处理、发送消息的调用,几乎每写一行就是一个嵌套,层数一多,最外层那个右括号反而最容易被忘掉。课上那次报错的正好就是最外层,里面几层反倒是齐的。
- 定位函数:找到这一行最外层的调用名和它后面的冒号
- 拆括号:从外往里,每层划掉一对括号
- 看收尾:划到结尾还多出左括号,就是这一行缺了右括号
- 补完再跑:补齐后单独点一次验证,不要连同别的改动一起测
02括号决定先算哪一步:除以 400 那版为什么被否
算离线加成的时候,第一版算式是拿离线时间去除以 400。老师刚写完自己就否了:不对,这也不对,现在就是秒。这个 400 是怎么凑出来的,其实是想一步把秒换成分钟再叠上三倍,把两次除法硬塞进了一个数字里。
正确的顺序是分两步走:先拿秒数除以 2 得到 3 倍加成的时间,再拿这个值除以 60 换成分钟。两步之间没有括号,靠的是从左往右的顺序;一旦把两步揉成一个系数,顺序就被写死了,后面再也调不动。
老师在这里停了一下,问了一句「我在这算不行吗?我在加的时候算不行吗?」——这问的是算式该写在读的地方还是写的地方。他的取舍是先把值算清楚再分层用,别等到用的那一刻临时拼系数。
还有一处容易自作聪明:课上把「这个时间除以 2,就是 3 倍加成」直接当成定义用了,没有再去推一遍倍数关系。这种地方照抄课上已经对齐的口径就好,自己在中途换成乘以 3,看着结果差不多,但跟你后面写 1440 上限时的口径就对不上了。
删掉 400 那一版之后,老师是重新从头写了一遍的,边写边把每一步的单位念出来:这个是秒,除以 2 是 3 倍,除以 60 是分钟。写一行报一次单位,比事后补注释管用,因为注释是给以后的人看的,念出来是给当下的自己听。
-- 离线时间拿到的是秒
local offline = time - 上次离线时间
-- 第一步:秒 ÷ 2,得到 3 倍加成的时间
local bonus = offline / 2
-- 第二步:再 ÷ 60,换成分钟
local minutes = bonus / 60课上把「除以 400」那一版当场删掉了。它的毛病不在于数字大小,而在于把两个不同量纲的换算压成了一个常数,读代码的人再也看不出这里原本发生过两次除法。
03单位不对齐,公式全白写:秒、分与 1440 上限
第二个坑是单位。同一个 time 到底是秒还是分,课上反复确认了两遍:这里是以秒计算的,秒计算就放到这,那就是除以 2。之所以要确认,是因为当成分钟来算的话,除以 2 得到的根本不是 3 倍时间,而是一个没有意义的数。
单位对齐之后再加上限。客户的提醒很关键:是 3 倍时间,不是离线时间。也就是先看清手上这个变量是哪一层的时间,再决定拿谁去跟 1440 比。写成「离线的时间大于等于 1440」就是比错了对象。
上限的作用是防止加成无限往上叠。课上提到「是怕他超过 1440 还会加的意思」,所以判断放在算完之后,一旦超过就压回 1440,不再往上走。
把判断放在分钟这一层,还有一个附带的好处:读的人一眼就能看出 1440 是分钟口径的封顶。要是把它挪到秒那一层去比,同一个上限就得写成 86400,数字一换,后面接手的人多半还要再查一次这个数字是怎么来的。
上线之前还得确认这个上限是卡给谁的。课上问的原话是「离线加时间的上限」,那么它就是加成的封顶,不是对玩家长时间的限制。玩家离线多久都要如实记下来,只是发出去的加成有上限,这两件事不能混成一件。
local offline = time - 上次离线时间 -- 单位:秒
local bonus = offline / 2 -- 3 倍加成的时间
local minutes = bonus / 60 -- 换成分钟
-- 上限判断要用 3 倍时间这一层,不是原始离线时间
if minutes >= 1440 then
minutes = 1440
end单位混用最典型的症状是「数值看着合理,量级不对」。那版除以 400 算出来的加成时间也是个正数,不会报错,只有拿一个小值真去测一次才发现跟预期差了几十倍。换算完一定要用小值试一次,别等上线。
04逗号的两个身份:分隔参数与名字里的地雷
逗号的第一层身份是参数分隔符。课上每加一个参数都要顺手补一个逗号,最典型的一句就是「传递 self 和 object c 加个逗号」。少写一个,参数就被粘成一个表达式,报错的位置还常常飘到下一行去。
参数一多,逗号就容易漏。课上写循环的时候是一边念一边敲的:for i 逗号,i 等于一逗号 10 度——把每个逗号都念出来,漏一个当场就能听出来。这个办法听着笨,但在参数长的调用里确实有效。
另一个高发点是货币相关的调用。写扣钱那一段时,货币类型后面紧跟着逗号和数量,念出来就是「绑定元宝 逗号 加多少值」。逗号一旦漏掉,类型名和数量会被当成一个整体,报出来的错也看不出根子在这里。
逗号的第二个身份就没那么显眼了:它要是混进了变量名,就会变成一颗地雷。课上打印的时候发现「当前累计买入」后面莫名其妙多了一条下划线,回头翻代码才确认,是这个变量名里带了逗号。
两种情况出问题的位置不同,处理方式也不一样,放在一起对照会更清楚。
| 出现位置 | 本该起的作用 | 出问题时的表现 |
|---|---|---|
| 参数之间 | 分隔前后两个参数 | 少一个,参数被粘在一起,报错行漂移 |
| 变量名中间 | 不该出现 | 名字被切断,打印出来多一条下划线 |
课上那句「除什么?加逗号」不是口误,是在数参数:这一处算式写完了,还差一个逗号才能接下一个参数。写多参数的调用时,边写边数逗号比写完再回头查快得多。
想少漏逗号,有个笨办法:把长调用拆开写,一个参数一行。课上那些写得很长的调用是横向铺在屏幕上的,参数一多就顶到边,改的时候根本看不出逗号落在哪一层。拆成一行一个之后,少了哪个,上下扫一眼就能对上。
05元宝扣减:括号写全才取得到对象里的值
元宝和绑定元宝在课上走的是同一套货币写法,区别只在传进去的类型名。写扣钱的时候,老师专门强调了括号的位置:先用货币接口查到余量,判断够不够,够了再扣。
课上还有一句提醒很关键:这个配置得在这边重新配,不能太相信前端。能不能扣、扣多少,判断必须在服务端这一侧重新算一遍,前端传来的值只当参考。买卖两边都会用到这套判断,所以课上干脆收成了一个函数。
扣减本身就是一个带负号的量,类型名和数量都要包在括号里,别让负号跑到括号外面去,不然传进去的就不是「减十」而是别的东西。
卖出那一侧是反过来的,课上明确说这边逻辑是反的:先检测背包里有没有这件东西,数量小于一就直接把「物品不足」丢回给玩家,满足条件才给钱。给钱时同样是货币类型加数量,中间用一个逗号隔开。
判断不通过时怎么返回也有讲究。检测函数返回假值,就把提示直接返回去,不再往下走;扣减那一步只要执行成功,后面的逻辑就不用再重复校验一次,省掉一趟多余的分支。课上把这段判断收成一个函数,图的就是买卖两边共用同一套顺序。
-- 查询绑定元宝余量
local 余量 = player:money("绑定元宝")
if 余量 < 10 then
return "绑定元宝不足"
end
-- 扣减:类型名和数量都写在括号里
player:money("绑定元宝", -10)课上把这个流程收成了一个函数,名字取的是「检测并拿走」的意思:先查够不够,够就直接扣,省得每次写两遍判断。名字沿用课上写法 checktakemoney,参数是一个货币名加一个数量。
把「检测够不够」和「扣掉」合成一步,调用处就只剩一件事要关心:成没成功。失败的时候拿返回值去做提示,成功就往下走。代价是成功那一侧没有明确的返回值可用,所以两边的约定要写清楚,别一边按真值判断、一边按缺省继续跑。
06循环时间与列表刷新:算好的值放在读的地方
榜单界面的列表容器有几个跟时间相关的属性,课上点到过两个:点击时间是绑定时间,还有一个是循环时间。它们决定列表滚动和回弹的节奏,跟数据本身无关,但配错了界面就会一直动,或者干脆不动。
跟循环时间配套的还有「跳到某一个位置、一定时间内回转」这类容器行为。这些都不用自己写计时器,靠容器的属性就能配出来,课上把这一串属性一次性过了一遍。
真正要自己掌握的是把算好的值放在读的地方。课上问过「我在这算不行吗?我在加的时候算不行吗?」——如果每次用到都重算一遍,后面口径一改就得满地图找地方改。算好存下来,读的时候直接用。
容器的滚动方向也属于这一类配置。课上试的时候把方向值设成了水平滚动,点一下界面直接崩了,回头把方向改回默认值才正常。这类属性没有中间状态,值不对就是崩,所以每配完一项都要立刻点一次确认。
刷新时机还决定要不要循环。数据是整批下发的话,一次加载就够,用循环把节点铺开;要按位置更新某一行,就得走迭代去找对应的那条。课上这两种都用到了,前者铺整个榜单,后者改单条记录。
-- 列表容器的时间属性:滚动节奏靠容器配,不用自己写循环
local 列表 = 创建列表(父节点, "排行榜", x, y, 宽, 高)
列表:设置间隔(10) -- 行与行之间的间隔
列表:设置回弹(true) -- 拖到头之后的回弹- 先定容器属性:滚动方向、间隔、回弹、循环时间按需要配
- 再灌数据:用循环或者迭代把节点一条条加进列表
- 再接刷新:等服务端下发之后再重新加载列表,不要在前端自己攒数据
课上写榜单时,前端并没有直接去读服务端的变量,而是等服务端把整理好的数据下发过来再渲染。前端拿不到的变量,就让它走一条消息下发过来,这样刷新时机也一并有了。
课上这一整段是穿插在别的功能里写的,界面、事件、排行榜来回切,直播里这么写能理解。落到自己的项目里,建议先把容器属性配好再灌数据,别一边调属性一边塞节点——属性一改,之前看到的渲染结果就全不作数了,等于白看。
07小数点与临时属性:离线 10 秒测出来的那点尾数
换算写完总得验一次。课上没有等一整天,直接说「我就离线个 10 秒钟不就好了吗」,用最短的路径把整条链路跑通。这也是排查时间类问题的常规做法:把时间缩短到能立刻看到结果。
一测就测出问题:秒数除下来带小数点。老师当场追问「有小数点到底行不行?它会不会自动给我转成整数?」这属于引擎行为,靠猜没用,只能试。试的办法是写死一个不能被整除的值扔进去跑,看它能不能加进去。
试出来的结果是能加,而且加的是临时属性,课上那句「差不多 4.5 秒后,一周以后就消失了」说的就是这个临时效果。能加不等于该这么写,带小数的值要不要保留,取决于这个属性本身吃不吃小数,得看具体需求。
加进去之后还得确认它落到哪了。课上顺口问了一句「是在 BUFF 那里吗」,得到的答复是这属于临时增加的属性,会自己到期消失。也就是说它不是一个写进角色面板就固定下来的数值,用的时候得按临时效果来安排。
挑测试值也有讲究。课上特意避开了能整除的数:十一这种一除就干净的没用,得换成十九、九十九这种除不尽的,跑一遍才知道小数点会不会被引擎吃掉。用一个「本来就不该出小数」的值去测,等于什么都没测。
别用 10、20 这种能整除的数去验证小数问题。课上特意挑了一个不能被整除的值,就是为了让小数点暴露出来;拿整数去测,永远测不到这一类问题,等玩家真离线了才炸。
同一个变量在第 2 节里当秒用、在第 3 节里当分钟用,所以每次换手之前都值得问一句「我现在手上这一层是哪个单位」。课上的两处返工,根子都在这句话上。
常见问题
报错写「缺少括号」,我该从哪一行开始看?
从报错提示的那一行往回数,先找最外层的函数名,再从外往里一层层配对括号。课上那次就是最外层少了一个右括号,补齐后就好了。注意报错行号只是「第一个不满意的地方」,真实的错误点可能在前几行。
离线时间的换算为什么不能一步写完?
因为里面其实是两次除法:先 ÷ 2 拿到 3 倍加成的时间,再 ÷ 60 换成分钟。课上第一版把它压成了一个除以 400 的常数,当场就被否掉,原因是两步揉成一个数之后,读代码的人看不出顺序,后面也没法单独调。
上限判断该拿哪个值去比 1440?
拿你手上那一层的时间去比。课上有明确的提醒「是 3 倍时间,不是离线时间」,也就是先看清变量是原始离线秒数还是加成之后的分钟数,再决定跟 1440 比的是谁。比错了对象,上限就形同虚设。
变量名后面莫名多一条下划线是怎么回事?
课上查到过这个现象,原因是变量名里混进了逗号。逗号在 Lua 里是参数分隔符,出现在名字中间就会把名字切断,打印出来就多一条下划线。把名字里的逗号改掉即可。
元宝扣减为什么要在服务端再判断一次够不够?
课上的原话是「不能太相信前端」。前端传来的只是一个参考值,能不能扣、扣多少必须在服务端用货币接口重新查一遍余量,够才扣,不够直接返回提示。买卖两边都按这个顺序写。
循环时间是什么,为什么不用自己写计时器?
循环时间是列表容器自带的时间属性,跟点击时的绑定时间一样,属于容器的配置项,用来控制滚动和回弹这类节奏。课上提到容器还有「跳到某一个位置、一定时间内回转」这类行为,都是配属性就行,不需要自己写循环去计时。
本系列其他篇目