传奇3 实战 · lua 实战 传3 版本制作
服务端变量怎么改才生效:从声明到排查
点这里打开这节课的课程页→材料商人要记住每个玩家买了几次、卖了几次,还要维护一张全服共享的消费榜,这些都是变量在扛。这篇把课上用到的变量写法拎出来讲:变量分哪几类、怎么声明和初始化、存表的时候为什么老取到空表、取到 nil 又该往哪查。课上最花时间的不是写,而是「改了不生效」和「取回来是空值」这两类问题,老师是一边打印一边定位的,这套排查顺序值得抄下来。
- 服务端变量至少分三类:全服一份的全局变量、跟着角色的个人变量、走引擎接口的系统自定义变量。
- 自定义变量要先声明 set 和 get、再初始化一次,漏了初始化,取回来就是空值。
- 存表比存数字麻烦:取到的可能是空表,要先判空,是空表就新建,否则把 json 转回 table。
- 取到 nil 或空值,先怀疑没存过、名字拼错、以及存进去的值本身就是空的。
- 变量里的值有类型,取出来常是字符串,直接拿去加一会报错,要先转成数字。
- 系统按固定频率落盘,课上提到一秒存一次;改了不生效,多半是没读对或没刷新下发。
01服务端变量,课上用到三类
先把这个界面上要用到的变量分清楚,不然后面很容易用错。课上至少动了三类:全服一份的全局变量、跟着角色走的个人变量,还有引擎提供的系统自定义变量。每一类负责的场景不一样,混着用就会出怪问题。
全局变量一个服务器只有一份,谁都能读能写。消费排行榜这种东西就得用它,因为榜单是全服拼出来的,不能只有一个玩家看得到自己。老师在课上专门说过一句:这个要做成全局的。
个人变量是每个角色各持一份的。买卖次数这种数据要按人分开记,就必须落在个人变量上。用错地方的后果很直接:本来该记在某个玩家头上的数字,被塞进了全局变量,结果一个人买了,全服都跟着算数。
系统自定义变量是读写都走引擎接口的那一类,读用取、写用设,名字自己拼。课上拼名字的方式是「拍卖行」加上一个字段,一个字段对应一个计数,这样以后加字段只要改后半段,前缀不用动。名字拼得整齐,后面排查时一眼能看出这个变量是干什么的。
课上还确认了一件容易搞混的事:这个消费榜只统计买入,不统计卖出。卖出走的是另一套账,收益还要打折,跟排行榜分开算。所以变量在职责上得切干净,榜单的变量只管买入累计,别把卖出的数字混进去,混了之后排名就说不清了。
| 变量类型 | 作用范围 | 课上用来做什么 |
|---|---|---|
| 全局变量 | 全服共享一份 | 消费排行榜,所有玩家的消费额度拼在一起 |
| 个人变量 | 每个角色各一份 | 某个玩家买了几次、卖了几次 |
| 系统自定义变量 | 按名字区分,走引擎接口 | 用「拍卖行 + 字段」拼出来的各类计数 |
判断该用哪一类,就问一句「这个值是不是每个玩家都不一样」。是,就用个人变量;不是,才考虑全局。这个判断能挡掉大部分用错地方的问题。
02自定义变量:先声明,再初始化一次
自定义变量不是写个名字就自动存在的。课上第一件事是声明 set 和 get,把这套读写接口立起来,之后才谈得上取值和存值。声明没做,后面 get 出来的就是空值,一运算就报错。
声明的下一步是初始化。老师会在第一次触发的时候,把变量写上一个默认值,比如次数类的给 0。这一步看起来多余,其实是把「变量到底存不存在」这件事一次性解决掉,后面所有取值都能假定它已经有值。
初始化很容易漏。漏了之后的现象是:第一次进来 get 到空值,代码没防,直接拿着空值去加一,报错。老师处理这类问题的习惯是把初始化和取值绑在一起写,形成一个固定入口,谁进来都先过这一遍。
课上把这段包成了一个小函数,参数是名字,第一次进来声明并写入默认值,之后都走取值。这样调用方不用关心它到底是不是第一次,代码里也不会到处散落「判空 + 初始化」的重复片段。把边界收在一个地方,比在每个用到的地方各防一次省事。
课上还强调了一句:变量名要提前定好。同一个计数的名字,写入和读取必须是同一个字符串,不能靠记忆临场拼。老师把它们收成统一前缀加字段的写法,就是为了让名字只有一处来源,改的时候改一处,不用满脚本找。
-- 名字用「拍卖行 + 字段」拼出来,一个字段一个计数
local function 取计数(player, 字段)
local 名 = "拍卖行" .. 字段
-- 先声明并写入默认值,再取回来
player:setSysVR(名, 0)
return player:getSysVR(名)
end
-- 调用方不用关心是不是第一次
local 次数 = 取计数(player, "买入")上课用的接口名以 996 引擎说明书为准,这里重点是顺序:先声明 set/get,再初始化一次,之后统一走取值。顺序对了,空值问题少一半。
03存表最麻烦:取到的可能是空表
存数字相对简单,存表就麻烦多了,因为取出来的东西有可能是空表。排行榜用的就是一张表,每个玩家的名字配上他的消费额度,这张表存进去、取出来,中间要判断好几次。
课上的判断是这样写的:取到的值如果等于空表,就说明还没存过,先给它一个新表;如果不是空表,说明存过,把里面存的 json 转回 table 再用。判空这步不能省,省了后面往表里塞值就会报错。
这里踩了一个很典型的坑。老师一开始把转换那一步写错了位置,结果取回来的是个 new 出来的空值,转过来转过去都不对,打印出来一堆看不懂的东西。绕了几分钟才定位到:应该先取到值、再转,顺序反了就白转。
定位这类问题靠的是先打印结构。老师会把取到的值和它的类型一起打出来,看一眼它到底是 table 还是别的,再决定下一步怎么转。看着像是多余的一步,实际省掉了大量来回试的时间。
课上还撞见一个现象:全局变量存进去明明是张表,读出来却是字符串。老师打印了它的类型,看到是 string 才反应过来,得先把它解析回表才能往里塞值,不然看着像表,实际用不了。类型这件事,存进去和取出来不是一个形态,心里得先有这个底。
-- 取出来的可能是个空表,先判空
local t = player:getSysVR("消费排行榜")
if t == 空表 then
t = {} -- 没存过,给一张新表
else
t = json_to_table(t) -- 存过,json 转回 table
end
-- 先转换成 table,再往里塞值
t["玩家名"] = 额度课上把转换写错了位置,结果取回来是个 new 出来的空值,怎么转都不对。正确的顺序是先取到、再转换。顺序反了,转多少次都是白的。
04取到 nil 或空值,往这三个方向查
「取到 nil」是课上出现频率最高的报错之一。变量存得好好的,取出来却是空的,这种感觉最磨人。老师把来路归成了三种,按这个顺序查,基本都能落地。
第一种是没存过、也没初始化。变量名第一次被用到,之前从来没写过值,取出来当然是空。这类的解法就是把初始化补上,让它在第一次被用之前有个值。
第二种是名字对不上。课上专门试过,名字里多打一个空格,取出来的就是另一个变量,压根取不到想要的值。名字是拼出来的,更容易出错:前缀改了、字段写错一个字、中间少了个点,都会变成另一个名字。拼名字的地方,值一定要核对一遍。
第三种是存进去的值本身就是空的。这看着矛盾,实际常发生:上一次存的时候算出来的结果就是空,于是把空值写了进去,这次取出来自然还是空。课上有一次就是取回来是空,追下去发现是写进去的时候那个值就没算出来。
那个空格的例子值得单独记一下。老师本来以为引擎会忽略名字里的空格,实测发现不会,多一个空格就是另一个变量,取出来当然是空的。越是觉得是小事的地方,越是这类问题的高发区。
查这类问题还有个特别顺手的办法:把当前能取到的源变量的 key 全量打印出来。名字到底存不存在、拼出来落在哪一个,一眼就能对上,比在代码里猜哪个名字写错了快得多。变量一多,靠猜不如靠打。
- 打名字:把拼出来的变量名先打出来,确认它就是想要的那个。
- 打值:在同一处把取到的值打出来,看它是不是空。
- 打类型:把值的类型也打出来,空值和空表是两回事,处理方式不一样。
- 往上追:如果取的时候就是空,去看写入那一步,值有没有真的存进去。
课上一个常见的误会是「我明明存了」。存和取用的名字如果不完全一样,在引擎看来就是两个变量。先把名字打出来对一遍,比反复怀疑代码快。
05变量里的值有类型,加一之前先转换
变量里存的是有类型的。数字存进去、取出来,看着像数字,实际可能是个字符串。直接拿它去加一,就会报一个让人摸不着头脑的错,课上的原话是「这个值不算数」,翻译过来就是类型不对,得先转。
老师当时的处理是单独封了一个转换,取出来先转成数字,再做加减。这一步本身很简单,麻烦的是不转的时候不报在显眼的地方,而是在某个运算里炸,看着和变量八竿子打不着。
类型这类问题,光看界面几乎发现不了。因为界面上的数字看着都对,只是算出来的结果不跟着变。老师是靠打印类型定位的:把值加类型一起打出来,一眼看到是字符串,问题就清楚了。
转换之后再存回去,也会顺带把类型统一了。下次取出来就是数字,不用每次都转。老师把这归成一句话:写进去的时候把类型定死,取出来的时候少一半意外。
转换的位置也很讲究。课上把转换写错了一次:本想着一拿到值就转,结果转的位置不对,取回来还是空的。改法其实简单,先把值取出来握在手里,再决定要不要转、转成什么。转换本身不复杂,麻烦的是放错地方之后,问题会藏得更深。
-- 取出来可能是字符串,直接加一会报类型错
local v = player:getSysVR("拍卖行" .. 字段)
-- 先转成数字,再做加减
local n = number(v)
player:setSysVR("拍卖行" .. 字段, n + 1)
-- 不放心就把类型打出来看看
release_print("值是", n, "类型是", type(n))报「这个值不算数」这类错,十有八九是字符串没转数字。界面上数字看着没问题,但运算结果不动的时候,先把类型打出来。
06什么时候读、什么时候存
变量的读写时机,课上是有固定套路的。买卖动作发生的时候,先把旧值取出来,加一或者减一,再把新值写回去。读、运算、写,三步挨在一起,中间不插别的逻辑。
存盘的频率也得知道。老师提到系统是按固定节奏把变量落盘的,一秒一次,也就是说改完之后不是立刻写进存储,中途出问题可能丢一点点。知道这个节奏,就不会去纠结「我明明改了怎么没存住」,也会明白没必要每次改动都自己手动存一遍。
界面这一侧是另一次读写。打开界面的时候读一次,把当前值填进去;买卖动作做完再读一次,刷新界面上的数字。课上特意在协议处理完之后补了一次刷新,不然玩家看到的还是打开那一刻的旧数据,会以为操作没生效。服务端的值变了,界面不一定跟着变,得有人去推。
改完变量之后要更新前后端脚本再进游戏测,这一步课上反复强调。很多时候「改了不生效」不是逻辑错,而是引擎里跑的还是旧脚本,更新一次就好了。
读和写的顺序还有个小讲究:先读后写,别先写后读。先写会把旧值覆盖掉,再读出来就只能看到刚写进去的值,等于把自己的历史抹了。课上的计数都是先读旧值、再写新值,这个顺序错不了。
读写还要分清服务端和前端两套。服务端这边是入库的正式值,前端那边是拿来展示的副本。老师会把两边分开看:服务端读出来不对,是存储的问题;服务端对了但界面不对,就是下发或刷新的问题。先框定是哪一侧出问题,再去查,比一上来就全链路翻要省事。
课上有人问能不能改一次存一次,老师的意见是不用。系统自己按节奏落盘,一秒一次已经够密,手动再存一遍,除了多写代码没有额外好处。真正要盯的是「改了之后到底有没有生效」,而不是存得够不够勤。
- 读旧值:动作发生时先把当前值取出来。
- 算新值:按买入加一、卖出减一的规则算。
- 写回:把新值存回去,把类型也统一好。
- 刷新:动作做完再读一次,推给界面,别让玩家看旧数字。
07变量存好了,怎么下发到前端
变量在服务端存好了,前端要显示出来,还得有一次下发。课上找了一圈,发现传3 这边没有现成的「服务端下发变量改变」可以直接用,翻了几个位置都没对上的方法。
没现成的就自己写一条。老师的做法是:打开界面的时候,前端主动发一个请求给服务端,服务端收到请求,再把排行榜数据回过来。这样即使没有自动推送,数据也能到前端,界面照样能刷新。
请求走的是 107 号协议,界面本身挂在 101 那层窗口里。前端发请求,服务端按编号识别出来,把处理好的数据发回去。课上还提了一句,能直接用的通道就别绕服务端,比如那个「查看排行榜」的新界面是直接加载的,不需要再走一遍协议。
编号也不用自己编。课上用的 107 本来就是现成的,往上挂一个「获取排行榜数据」的用途就行,前端发、服务端按编号认、再把数据回过去,这套收发结构都是现成的,省下来的是自己造轮子的时间。
传值的时候还是要注意类型。服务端回过去的是拼好的 json,前端收到之后要转成表才能用。老师在这一步也打印了一次,确认收到的就是想要的数据,前端这一侧才算通。
数据在传输前后形态也会变。服务端发出去的是拼好的字符串,客户端拿到之后要再转成表才好遍历,这样才能按名次一条条摆到界面上。课上专门把这一步做了,不转直接往界面塞,会卡在第一个节点上。
收到的那一刻也要打印。老师在客户端把收到的值直接打出来,确认它和发送端一致,才继续往下写界面。收到的不对,前面发得再对也没用。两头都打,整条链路才有底。
确认整条链路的办法还是打印。服务端发的时候打一次,客户端收的时候打一次,两头都对上了,说明变量从读、到存、到下发这条线全通了。要是某一头没打印出来,问题就落在那一段,不用全链路去猜。
变量改对了但前端没变化,别急着怪变量,先看下发有没有走通。课上就是没有现成的下发方法,自己补了一条 107 请求才把数据送到前端。
常见问题
服务端变量有哪几类,怎么选?
课上用到三类:全服一份的全局变量、按角色分的个人变量、走接口的系统自定义变量。判断标准是看这个值是否每个玩家都不一样,是就用个人变量,否则才用全局。
自定义变量为什么必须先初始化?
因为它不会自动存在。课上先声明 set 和 get,再在第一次触发时写入默认值。漏了初始化,取出来就是空值,拿去运算直接报错。
存表的时候为什么取回来是空表?
取出来的值可能是空表,代表还没存过。课上先判空:是空表就新建一张,否则把存进去的 json 转回 table。判空和转换的顺序都不能少。
取到 nil 一般是哪里错了?
课上归了三种:没存过也没初始化、变量名拼错了(多一个空格都不行)、以及存进去的值本身就是空的。按这个顺序查,先打印名字和值。
变量取出来加一报错是什么原因?
多半是类型问题。取出来的值可能是字符串,直接加一会报「这个值不算数」。先转成数字再运算,写回去的时候顺手把类型定死。
变量改了前端没反应怎么办?
先确认下发有没有走通。课上发现没有现成的下发方法,就自己补了一条 107 请求,打开界面时向前端要数据。再看有没有更新脚本和刷新界面。