传奇3 实战 · lua 实战 传3 版本制作
点按钮发元宝:点击事件怎么绑,点了没反应怎么查
点这里打开这节课的课程页→
材料商人这个界面上的购买、卖出、绑元购买、绑元卖出四个按钮,全是脚本一行行建出来的,不是拖控件拖出来的。这篇只讲按钮这一条线:按钮怎么建、点击事件怎么绑上去、点击以后金额和动作号怎么传到服务端。课上在按钮上卡的时间不少,报过空值、报过重叠、还报过 ID 重复,这些错都留下了现场排查的顺序,跟着复现一遍比死记结论有用。
- 界面上的按钮是脚本创建的:给父节点、给位置、设置文字、挂进容器,少了哪一步按钮就不存在。
- 点击事件是一个回调,绑定的时候要送到按钮上,回调里带着 self 和组件对象一起传进来。
- 点击报「button 的值是一个空值」,先怀疑组件没被取到,也就是没有真正拿到那个按钮对象。
- 组件在父节点里找不到,就去打印它的结构,课上是换到 win 组件里才把方法找齐的。
- 按钮点不到、点错,常见就三种:没设置大小、位置重叠、ID 重复。
- 点击以后把动作号带过去走 106 协议,1 到 4 对应四种买卖,服务端只认这个编号。
01界面上的按钮,是脚本一步建出来的
开始写点击逻辑之前得先接受一件事:这个界面上的按钮不是在设计器里拖出来的,而是脚本运行时一个个建出来的。老师做的时候就是用新建按钮的方式,先把按钮对象拿到手,再设置文字、位置、大小,把它挂到父节点上。按钮对象存在,后面的点击事件才有地方绑。
创建的时候位置参数给的是坐标,课上有一次把参数传错了一个字母,按钮怎么都不对,改回来就正常了。这种低级错误在创建组件这一步特别多,因为参数一长串,顺序记混了很难看出来,只能对着说明书的参数表一个一个对。
挂节点那一步用父容器的方法,把按钮对象和一个位置参数丢进去,课上习惯写 -1 表示往末尾挂。这一步漏了,按钮就是悬空的,界面上看不到它,更谈不上点。老师建第二个按钮的时候就是把创建和挂载写在一起,复制一份改参数,比从头敲一遍稳。
按钮上的文字是单独设的,设置文字之后界面上才显示「购买」「卖出」这些字样。课上把按钮做成了四个:元宝购买、绑元购买放在一边,元宝卖出、绑元卖出放在另一边,颜色也分开,购买用红色、卖出用绿色。这不只是为了好看,是让人一眼分清哪个是买哪个是卖,少点错。
为什么非得用脚本建,而不是在设计器里摆好?因为这个界面的内容是会变的,卖什么、几组按钮、每个按钮叫什么,都得跟着配置走。脚本建的好处是数量可控:以后要再加一组买卖,复制一段、改个文字和位置就行,不用回设计器重新排版。老师说这类会长大的界面,用脚本建一开始麻烦,后面省事。
-- 按钮是脚本创建的:拿到对象、设文字、设大小、挂父节点
local btn = 新建按钮()
btn:设置文字("元宝购买")
btn:设置控件大小(72, 30)
-- 挂到父容器上,位置参数给 -1 表示往末尾挂
父节点:增加子节点(btn, -1)
-- 设置它的位置,坐标和上面的父节点对齐
btn:设置位置(500, 260)创建和挂载是两步,缺一不可。课上建第二个按钮时直接复制第一段再改文字和位置,很少出问题;手敲的话,参数顺序一乱就白折腾半天。
02点击事件绑在按钮上,回调带 self 和对象
按钮建好之后,要给它绑一个点击事件,不然点了什么都不会发生。课上的写法是把点击事件设成一个函数,函数被调用时会带进来两个东西:一个是 self,一个是组件对象。这两个参数是回调帮你送进来的,不用自己去取,但少了它们,回调里就不知道是谁被点了。
回调体里干的第一件事,通常是从组件对象上再取一次目标组件。老师用的是类似取组件的写法,把名字和那个对象一起传进去,让引擎在这棵组件树里找一遍,返回找到的那个组件,找不到就返回空值。找组件这件事是后面所有点击逻辑的地基,找错了或者没找到,接下来做全是白做。
课上现场讨论过用哪种绑定写法。最直接的是在点击事件里直接写回调,把要传的名字带进去;另一种是先拿到组件,再往上挂事件。两种都能用,老师的取舍是:逻辑简单就用直写的,需要复用组件实例就先把组件找出来再挂。他后来做「查看排行榜」那个按钮时,就是先取到组件、拿到实例,再在实例上挂事件,因为那个界面是要动态创建出来的。
这里还有个容易忽略的点:回调传进来的 self 不是随便什么值,它是这个组件自己的引用。后面调用组件上的方法、设置属性,都要靠它。课上就是因为在错误的位置去找 self,结果报了一个「空值」出来,绕了好几分钟才定位到是层级找错了。
回调体里该放多少逻辑,课上也有个尺度。老师倾向让点击事件只做一件事:把动作号和必要的名字发出去,剩下的判断交给服务端。理由是点击回调跑在前端,前端的东西都不可信,把结算逻辑堆在这里,等于把风险放在最容易改包的地方。回调负责表达意图,服务端负责决定结果,这条线划清楚,界面代码也干净。
-- 点击事件是一个回调,回调自带 self 和组件对象
btn.点击事件 = function(self, obj)
-- 拿名字和对象回组件树里找目标组件
local target = 取组件(obj, "元宝购买")
if target == nil then
release_print("没找到按钮组件")
return
end
-- 找到之后再往下做:发协议、改数量、刷新界面
release_print("点到了", "元宝购买")
end回调里的 self 是组件自己的引用,取组件那一步返回的才是真正的目标对象。这两个别混,课上就有一次混了,报的错看着像事件没绑上,其实是找错层级。
03点击报「button 是空值」到底是什么问题
课上最典型的一次报错是这样的:点一下按钮,直接弹出来说 button 的值是一个空值。老师当场给的判断很短:这句话的意思就是没有 call 到按钮,也就是那个位置上的按钮对象根本没建出来,或者没被取到。
换成人话:代码里以为手上有个按钮,其实是个空值,再去调它的方法自然就炸。原因基本逃不出两种。一种是创建那一步没执行到,按钮压根不存在;另一种是取了但没取到,比如名字对不上、层级找错了。这两个方向分清楚,排查能省一半时间。
老师当时没有急着改代码,而是先把按钮补上:加一个按钮进来、挂到父节点、设好文字,再点一次,问题就没了。他这么做的理由是,如果连对象都没有,后面所有的排查都是在猜;先把对象落实,报错会不会复现一眼就知道。
补对象这个动作还有个附带收获:它顺便验证了创建那段代码本身是对的。如果补上之后点击正常,说明问题只出在「对象没拿到」;如果补上还是报空值,那就得往取组件那一步查。等于用一次小实验,把问题的范围缩小了一半。
排查这类空值,手上可以有一条固定顺序。先看对象有没有被创建,再看它有没有被正确挂到父节点上,再看取组件那一步的名字和层级对不对。这三步走完,绝大多数「空值」都会现出原形,用不着去翻一大段代码。
- 确认对象存在:按钮有没有真的被创建,创建那几行有没有进到。
- 确认已经挂载:有没有把它塞进父节点,位置参数给对没有。
- 确认取得到:取组件时传的名字和对象对不对,层级是不是找错了。
- 打印一次:在报错行前面把对象打出来,是空值就继续往前找。
「button 的值是一个空值」几乎全是对象层面的问题,不是逻辑问题。别一上来就改判断条件,先确认你手上的按钮是不是真的存在。
04组件找不到,先把它的结构打印出来
比空值更绕的是「组件明明在,就是找不到」。课上也撞上了:取组件那一步返回空值,可界面上那个按钮明明显示着。老师没有硬猜,而是把整棵组件树打出来看结构,一层一层找它到底挂在哪里。
打出来一看就明白了:那个要找的方法并不在父节点上,而是在 win 组件里面。也就是说找组件的起点错了,从父节点往下找当然找不到,换到 win 这一层去找,立刻就找到了。找不到组件的时候,先相信打印,不要先信自己的记忆。
同一段时间还报过一个「link 的值是一个空值,第 15 行」。这句话看着像事件没绑上,实际上是回调里的 self 没拿到,还是层级的问题。老师处理的方式完全一样:先在这一层把值打印出来,确认结构,再决定从哪一层去找。
这几段排查连起来其实就是一条思路:对象是空值,就往创建和挂载上想;取组件是空值,就往名字和层级上想;self 是空值,就往回调绑定的位置上想。三种空值的来路不一样,但都能用打印定位,不靠猜。
-- 找不到就打印,看清它到底挂在哪个层级
release_print("父节点结构", 父节点)
-- 课上的结论:要找的方法在 win 组件里,不在父节点上
local win = 取组件(obj, "win")
release_print("win 里有没有这个方法", win)
-- 从对的那一层拿,再挂事件,self 才不会是空值
local target = 取组件(win, "元宝购买")报「link 的值是一个空值」时,很多人第一反应是事件没绑上,其实多数是 self 没取到。先把结构打出来,确认从哪一层找,再动事件绑定的代码。
05按钮点了没反应,先查这三个地方
按钮建出来、事件也绑了,点上去还是没反应,这种时候别急着怀疑协议。课上踩过的坑集中在三个地方,而且都能在界面上直接看出来,不用翻日志。
第一个是没设置大小。老师有次把组件的位置调好了、挂也挂了,就是点不动,查下来是大小没设,引擎里这个组件的可点区域是空的。把宽高一补上,配合位置一起调,立刻就能点了。他当时的原话是「是因为我没设置大小吗?是的」,所以这条被排在了前面。
第二个是位置重叠。两个按钮挨太近,点上去落在重叠区里,点到的是另一个。课上有一次就是「其实在这重叠了」,靠把两个按钮往上下分开、一个加 40 一个减 30 才解决。位置上的 40、30 这些微调,本质是在划分可点范围。
第三个是 ID 重复。课上直接报了一句「ID 重复了」,组件之间撞了标识,取组件时拿到的可能是别人。这个问题在复制粘贴建多个按钮时最容易出现,因为复制出来的那段常常把 ID 也一起带过去了,忘记改就撞上。
| 现象 | 常见原因 | 课上怎么改 |
|---|---|---|
| 按钮看得见,点不动 | 没设置控件大小,可点区域为空 | 补上宽高,和位置一起调 |
| 点 A 却触发了 B | 两个按钮位置重叠 | 把位置错开,一个加、一个减 |
| 取组件拿到别的对象 | 复制按钮时 ID 没改,重复了 | 每个按钮给不同的 ID |
这三条的共同点是都能在界面上看出来:大小对不对、有没有叠在一起、ID 有没有撞。先看界面,再翻代码,顺序反过来会多花很多时间。
06点击之后,把动作号带到服务端
点击事件真正要干的活是告诉服务端「谁被点了」。课上是在回调里把动作号和一个名字一起发出去,走的是 106 号协议。这个编号很小但信息量够:1 代表买入,2 代表卖出,界面扩成四个按钮之后,顺着排成 1 到 4,分别对应元宝买、绑元买、元宝卖、绑元卖。
回调里带出去的值,课上写成了一个参数,老师口头上叫它 ARG。它可能是按钮对象、也可能是一个动作编号,具体看这次点击要表达什么。定下来之后,服务端只要判断这个值是 1 还是 2、是 3 还是 4,就能知道玩家想干什么,不用再去猜。
一开始老师想连物品名一起传过去,后来发现编号本身已经说清了三件事:动作是什么、买的是什么、用的哪种货币。编号能表达的信息,就不要重复再传一遍,传得越多,被改包的余地越大。课上只留了一个必要的值过去。
四个按钮的颜色和位置也是为这一步服务的。购买用红色、卖出用绿色,绑元那两组再往两边分开放,玩家点的时候不容易串。课上跟玩家来回对位置对了好几轮,玩家一句「免得点错了」,就把按钮又往上抬了一点。这属于界面细节,但它直接决定点击进来的动作号对不对。
绑元那两个按钮走的是同一条协议,只是动作号不同。也就是说服务端不用为绑元单独写一套入口,收到的还是 1 到 4 里的某一个,区别只在分支里用哪种货币扣、给不给绑定。老师把这条也压进编号里,界面这一侧就不用关心货币,只管把按钮对应的号发对。按钮多不代表协议要多,编号排清楚就够。
-- 点击事件里把动作号发出去,走 106 协议
btn.点击事件 = function(self, obj)
-- 1 元宝买 2 绑元买 3 元宝卖 4 绑元卖
local 动作号 = 1
-- 名字已经能说明买什么、用哪种货币,其他参数就不传了
发送协议(106, 动作号)
end动作号压在 1 到 4 之间,一个编号就把动作和货币类型都说清楚了。老师专门强调过,能被编号表达的信息不要再单独传,前端传得越多,服务端要防的就越多。
07服务端收到动作号之后怎么处理
点击发出去只是上半场,值到了服务端才算数。课上的写法是先把收到的值存下来,再判断:等于 1、2、3、4 就直接往下走,等于其他值就只提示、不做任何事。这个兜底很重要,等于给协议加了一道门,异常的值进不来。
动作号的含义在服务端要写死清楚。老师当时一边写一边念:1 和 2 是加一,也就是买入方向;否则就是减一,卖出方向。一和二要加一,其他走减一,这个判断被单独拎出来写,免得和后面的分支缠在一起。
确认按钮真的把值发出去了,还是靠打印。老师点了「元宝卖出」,打印里刷出来一串,确认发过去的就是想要的编号,前端这一侧就算通了。要是打印里没有,说明问题在前端,回到点击事件那一层继续查;要是打印里值不对,就是动作号算错了。先分清是没发出去,还是发错了,再决定往哪查。
这一步走通之后,整条链路就闭合了:点按钮、发动作号、服务端判断、扣钱或给钱。后面再扩按钮,只要把新的编号排进 1 到 4 的规则里,服务端的判断逻辑基本不用重写。
兜底那半句也别省。老师写的是「等于其他的直接提示」,看起来只是防手滑,其实也挡住了改包的尝试。正常点按钮发出来的号一定在 1 到 4 之间,出现别的值就说明来的不是界面,服务端提示一下、记一笔就够了,不需要为它设计一套流程。
- 存值:把收到的动作号存到局部变量里,避免后面取错。
- 判断:等于 1 到 4 往下走,其他值直接提示并返回。
- 分流:1、2 走加一,3、4 走减一,方向别搞反。
- 验证:在服务端入口打印收到的值,确认发出来的编号是对的。
点击发出去的值到了服务端没打印出来,不代表按钮坏了,先分清是前端没发、还是服务端没收到。分开看,比在两头来回改快得多。
常见问题
界面上的按钮是拖出来的还是脚本建的?
是脚本创建的。课上用新建按钮的方式拿到对象,再设置文字、位置、大小,再挂到父节点上。少了挂载这一步,按钮就是悬空的,界面上看不到。
点击事件为什么要带 self 和对象?
self 是这个组件自己的引用,对象是回调帮你送进来的上下文。回调里要靠它们去取目标组件、调组件方法,少了就不知道谁被点了。
提示 button 的值是一个空值,先改哪里?
先确认按钮对象到底有没有建出来、有没有挂到父节点上,再确认取组件时名字和层级对不对。这条报错基本都属于对象层面的问题,不是逻辑问题。
组件明明在,取组件却返回空值怎么办?
把组件结构打印出来看。课上就是这么发现的:要找的方法挂在 win 组件里,不在父节点上,换到对的层级找立刻就找到了。
按钮看得见却点不动,是什么原因?
课上最常见的是没设置控件大小,可点区域是空的。另一类是两个按钮位置重叠,点到的是另一个;再就是复制时 ID 没改,撞了标识。
点击以后参数要怎么传到服务端?
把动作号通过点击事件发出去走 106 协议,1 到 4 对应元宝买、绑元买、元宝卖、绑元卖。编号能表达的信息就别重复传,服务端按编号分流即可。