课程社群 跟着课程一起练,问题当场问 飞书申请加入课程
1 / 8

传奇3 实战 · lua 实战 传3 版本制作

元宝与物品怎么加:从配置到生效的完整流程

点这里打开这节课的课程页浮生梦 主讲 · 2025.07.24 · 4 小时 59 分

这节课是传奇3版本的实战课,浮生梦老师后半程带着把一个材料商人界面从零搭起来:NPC 怎么下发、物品怎么进列表、元宝和绑定元宝怎么区分、买入卖出怎么扣钱给东西。这篇只盯住「元宝与物品」这一条链路,把它讲成一条能照着走的流程:配置写在哪、代码改哪一处、以及怎么确认它真的生效。课上反复回扣的两句话会贯穿全文,一是物品用表驱动而不是写死,二是钱和物品的结算必须放在服务端,前端传过来的东西一律不能全信。

  1. 元宝和绑定元宝是两套独立的账,课上定价金条 1000 元宝、绑定金条 1000 绑定元宝,用绑定元宝买的东西会自动绑定。
  2. 物品清单做成一张表,加东西就加一行,遍历改成用迭代,不要再用写死次数的循环。
  3. 表里存的是物品名,真正创建前要先用「道具名词」把名字换成引擎认识的物品 ID。
  4. 物品靠列表容器挂出来,先创建容器、再设位置和大小、再挂到父节点,位置错了按钮就点不到。
  5. 买入先检测货币够不够再扣,卖出先检测背包数量再拿走物品,任何一步不满足就直接返回。
  6. 改完配置别急着测,先把前后端脚本和物品都更新一遍,再用打印看变量到底有没有变。

01先分清元宝绑定元宝两种货币

这节课要做的界面是材料商人,本质上就是一个拍卖行。玩家在上面买,在下面卖,买到的东西别人才能卖出去,所以它是个零和交易。落代码之前,浮生梦老师先把这个界面牵扯到的两种货币说清楚,因为后面所有的判断和扣除都围着它转。

界面上出现的两种东西叫金条和绑定金条,课上定的价格是 金条 = 1000 元宝绑定金条 = 1000 绑定元宝。看着只是名字差一个字,实际是两套完全独立的账:元宝和绑定元宝不能混着花,金条和绑定金条在系统里也被当成两种不同的物品,价格各算各的。

绑定这条线的规则是玩家提的,老师照着实现:用绑定元宝买到的东西会自动绑定,绑定之后不能交易、不能上拍卖行。所以一个物品要不要做一个绑定版,取决于你希望它在玩家之间能不能流动。能流通的走元宝,锁死的走绑定元宝,这条线定下来,后面的配置才有依据。

课上顺带聊了个取舍:这套东西是跟经济挂钩的,所以不建议直接拿去做跨服。老师的判断是人物不用跨过去,但数据可以联通,跨服本身功能不复杂,麻烦的是来回测,经济相关的模块一旦出问题就容易失控,所以他倾向先把本服的逻辑跑顺,再考虑要不要往外扩。这个判断也影响了后面很多细节,凡是跟钱和物品有关的,都往服务端收。

拍卖行的玩法逻辑也是在这里定下来的:上面是买、下面是卖,先有人买了,下面才有对应的数量可以被卖出去。所以它不是想挂就能挂,而是先有人买、别人才卖得掉。老师一句话概括,这就是个零和交易,数量的增减必须成对出现,这也是为什么后面要死死盯住检测和扣除的顺序。

货币 / 物品课上价格能不能交易
金条1000 元宝可以,属于普通流通物
绑定金条1000 绑定元宝不行,绑定后不能交易、不能上拍卖行
元宝用来买非绑定的东西可以流通
绑定元宝用来买绑定的东西不进流通
说明

自动绑定这件事不是写在物品表里的,而是在发放物品那一步顺手做掉的。也就是说,同一个物品用元宝买和用绑定元宝买,结果一个是可交易的、一个是锁死的,差别落在结算逻辑里。

02物品清单做成一张表,加东西只加一行

物品清单最忌讳一行一行写死。老师开的做法是把界面要卖的这些东西做成一张表,每一条就是「名字配上价格」,以后要加东西就往表里加一行,创建流程的代码一行都不用动。

课上还专门提醒了一句:表建好之后,遍历就不能再用固定次数的循环了。for i = 1, 10 那种写法是给「我就写死十个」准备的,一旦清单变成长度会变的表,就得换成迭代,按表里实际有几条来走。这条在课上被点得很重,因为界面后面还要继续加物品。

这样做还有个额外好处:定价和创建被拆开了。策划想调价格,只改表里的数字;想加物品,只加一行。脚本里那段负责创建节点的逻辑保持不动,出问题的范围被控制得很小。

界面里还给后续物品预留了位置,所以这份清单注定还会变长。老师的意思很明确:这种会长大的清单,维护成本要压到最低,格式统一之后,加物品这件事交给谁都行,照着一行复制、改掉名字和价格就好,不用回头动流程那两段代码。他还顺手告诉了玩家该怎么照着写。

还有一点值得记下来:表里每一条都是独立的一份数据,创建的时候只关心名字和价格,界面怎么排、按钮放哪,跟表本身没有关系。这样一来,价格调整就退化成改一个数字这么简单的事,界面逻辑完全不受影响。

物品清单表lua
-- 要卖的物品做成一张表:名字配价格
local itemList = {
    { name = "初级书页", price = 1000 },
    { name = "生肖碎片", price = 1000 },
    { name = "科技碎片", price = 1000 },
    { name = "鉴定符",   price = 1000 },
}

-- 用迭代遍历,表里加物品不用改循环次数
for k, v in pairs(itemList) do
    release_print(k, v.name, v.price)
end
说明

老师给的维护规则很直白:以后加物品,就照着这张表的格式复制一行,名字和价格改掉,别去动遍历和创建那两段。谁照着写都行,反而不会出错。

03从物品名到 ID:走道具名词查表

表里存的是玩家看得懂的名字,比如「初级书页」「生肖碎片」,但引擎真正创建物品要的是物品 ID。这两者之间要靠「道具名词」去换:拿名字查一遍,查到对应的 ID 再往下走,而不是把 ID 直接写死在脚本里。

课上在这里踩过一次坑。某个物品名字写成了「低级书页」,数据库里其实叫「初级书页」,结果查出来是空的。名字对不上,拿到的就是空值,再拿这个空值去创建,界面上自然什么都不出。老师当场把名字改回数据库里真实存在的那个,问题就没了。

这也解释了为什么建议走查表而不是写死:写死的时候,名字和 ID 对不上只是脚本里的一句话,肉眼很难发现;走查表的话,查不到会立刻暴露成一个空值,反而好排查。

课上实际往清单里加过好几个物品,生肖碎片、科技碎片、鉴定符、特戒碎片这些。加的时候老师会先去数据库确认名字,有的名字和自己印象里的差一点,就用数据库里真实存在的那个。这份核对看着琐碎,但它直接决定后面查 ID 那一步顺不顺,跳过它,后面就是一连串的空值。

把名字和 ID 的关系交给查表之后,脚本里就不该再出现裸的物品 ID 了。这样即使以后数据库调整了物品名,出问题也只会在查表这一个地方暴露出来,而不是散落在几十行代码里,一个一个去找。

名字换 IDlua
-- 表里存的是名字,创建前先换成物品 ID
local idx = 道具名词(k)      -- k 是物品名
if idx == nil then
    release_print("没有这个物品: " .. tostring(k))
    return
end
-- 拿到 idx 之后,后面创建节点、发放物品都用它
踩坑记录

报「没有这个物品」或者拿到空值,第一件事不是改代码,而是把名字拿去对一遍数据库:数据库里到底叫「初级书页」还是「低级书页」,差一个字都查不到。

04物品挂进列表容器才看得见

物品有了 ID 还不能直接显示,它得先挂进一个列表容器。课上的顺序是:创建列表容器,指定位置和大小,把容器挂到父节点上,再把一个个物品节点塞进去。容器是壳,物品是塞进去的内容,两者分开建。

容器除了位置和大小,还有一堆可以配的东西:对齐方式、间隔、滚动方向、回弹、背景颜色。课上先用一个显眼的背景色把容器调出来,确认它确实存在、也确实挂在预期的地方,再往里塞物品。创建失败的时候方法会返回一个空值,老师在这里补了一句判空提示,省得后面对着「看不见的容器」查半天。

位置和大小是分两步设的,一个管坐标,一个管控件宽高。这两步课上反复调过,因为这个界面是横向排列的,位置差一点,按钮就会叠在一起,或者干脆跑到容器外面去,玩家点都点不到。

方向参数上还出过一段小插曲。老师照着说明书去设列表的方向,横排怎么都不生效,来回试了几次才发现是按说明书给的取值没对上,换了个参数写法才对。说明书也会写错,这种时候只能靠现场试出来。

容器和物品分开建,还有一个现实好处:这个界面里物品是要跟着清单长的,而容器本身的位置、大小、方向是固定的。清单变长的时候,动的只有往容器里挂节点这一段,容器那几行完全不用碰。课上就是把这两件事拆开写,所以后面往清单里连加好几个物品,容器一次都没重调。

创建并挂载列表容器lua
-- 创建列表容器:位置和大小分开设
local list = 列表容器(500, 200, 400, 200, 1)
list:设置位置(500, 200)        -- 坐标
list:设置控件大小(400, 200)    -- 宽高
list:设置方向(2)               -- 课上备注:2 水平,1 垂直

-- 把物品节点挂到容器里
list:增加子节点(itemNode, -1)
说明

挂节点用的是父容器的方法,把子节点和一个位置参数丢进去。课上先用一个写死的文本节点把容器调通,确认能显示,再换成真正的物品节点,这样排查的时候能分清是容器的问题还是物品的问题。

05买入卖出:先检测再扣除,顺序不能反

前端把「我要买什么」发过来之后,真正决定给不给东西的是服务端。老师定的顺序是先检测、再扣除、然后给物品,任何一步不满足就直接返回,绝不让后面的逻辑继续跑下去。

买入这条线是检测货币够不够,够就扣元宝、再给物品;卖出是反过来,先检测背包里这个物品有没有、够不够,够就把物品拿走、再给钱。课上把「检测并扣除」封成了一个小函数,省得每个分支都抄一遍相同的判断。

封装之后返回值的处理很关键。检测不通过时函数返回空值,调用方拿到空值就提示「绑定元宝不足」然后返回;检测通过时函数已经把货币扣掉了,调用方只需要负责给物品。检测和扣除绑在一个函数里,就不会出现只检测没扣、或者扣了没给的情况

卖出这条线还多了一层规则:用绑定买到的物品,不允许再用元宝卖出去,只能走绑定那条线卖掉。也就是说,货币和物品的绑定属性要对得上,不能拿锁死的东西换出可以流通的货币。老师把这条也归到结算逻辑里来处理,而不是塞进物品表,因为它是“钱怎么走”的规则,不是“物品是什么”的数据。

不管是买还是卖,都会落到同一句话上:钱和物品的进出只在服务端发生,前端只负责把动作号发过来。这句话在前面讲协议的时候还会再出现一次,因为它决定了这段逻辑该写在哪一侧。

检测并扣除货币lua
-- 封成一个函数:检测并扣除,返回是否成功
local function checkTakeMoney(player, moneyName, num)
    if player:getMoney(moneyName) < num then
        return nil                     -- 不够就直接返回
    end
    player:setMoney(moneyName, player:getMoney(moneyName) - num)
    return true
end

-- 买入:够就扣钱给物品,不够就提示
if not checkTakeMoney(player, "绑定元宝", 1000) then
    player:提示("绑定元宝不足")
    return
end
player:给予物品("初级书页", 1)
  1. 取数据:拿到物品名和数量,先把物品名换成 ID。
  2. 检测:买入查货币够不够,卖出查背包数量够不够。
  3. 扣减:不满足直接返回并提示,满足才扣钱或拿走物品。
  4. 发放:买入给物品(绑定元宝买就顺手绑定),卖出给钱。

06106 协议:一个参数分买入卖出

买卖两类动作走同一条协议,课上是 106 号。用一个参数就把动作区分开了:1 是买入,2 是卖出。后来界面扩成四个按钮,对应关系就顺着排成 1 到 4,分别覆盖元宝买、绑定买、元宝卖、绑定卖。

这条协议是「前端发、后端收」。前端点击按钮时把动作号发出去,服务端收到后先判断这个号在不在允许范围内,再走到对应的分支。界面本身挂在一个窗口里,前面那个 101 号窗口就是承载它的那层壳,协议负责把「点了哪个按钮」这件事告诉服务端。课上还顺手做了一层防御:如果收到的号不在 1 到 4 之间,说明是异常的封包,可以直接记日志甚至封号。

价格也带来一个扩展:因为金条和绑定金条是两种物品、两种价格,所以买和卖要各带一个价格。老师一开始想只传物品名,后来发现编号本身已经说明了是什么动作、买的是什么,就干脆把多余的参数去掉,只留必要的几个。

把动作压成一个编号还有个好处:服务端收到之后不用去猜玩家买的是哪一样、用的是哪种货币,编号本身已经把动作和货币类型都说清楚了,剩下的信息就没必要再传一遍,传得越少,被改包的余地就越小。

参数取值动作说明
1买入用元宝购买
2买入用绑定元宝购买,物品自动绑定
3卖出卖出换元宝
4卖出卖出换绑定元宝
踩坑记录

课上原话说得很重:绝对不能相信前端。金额、数量这些值如果直接拿前端传的来用,对方改个包就能白拿东西。所以数量要按配置来,钱和物品的结算全部压在服务端。

07改完怎么确认真的生效

配置和数据都改完,不等于它生效了。课上每改完一处都会走一遍固定动作:更新前后端脚本、再更新一次物品,然后进游戏点一遍。少更新这一步,很常见的现象是「代码明明改了,怎么没反应」,其实是引擎里跑的还是旧的。

真要确认某个值对不对,还是靠打印。老师习惯用 release_print 把变量打出来,先看它到底是什么类型、是不是空值,再判断逻辑错在哪。比起盯着界面猜,打印一次能省很多来回。

还有一类问题不在逻辑里,而在类型上。值从别处传过来时可能是字符串,直接拿去参与计算就会出错,需要先转成数字。课上就因为这个卡了一会儿,还是靠打印类型才发现。

还有一类更隐蔽的问题:改了配置,但界面上的数量没跟着变。原因是打开界面时数据只推了一次,买卖动作做完之后要再推一次刷新。老师专门在协议处理完之后补了一次刷新,保证界面上的数字和服务端变量是一致的,不然玩家看到的是旧数据,会以为没生效。

离线相关的加成也在这里调过:按秒记录离线时长,三倍加成就是把秒数除以二,超过设定的上限就不再继续累加。这类数值改完之后,同样是先更新再进游戏测,别指望它自己刷新。老师对这类「看着像没生效」的问题有个固定判断:先按更新、再按刷新两条路各查一遍,多数情况就落在这两条上。

  1. 存盘:改完脚本先保存,关闭其他无关的窗口,避免改的是同一处。
  2. 更新:更新前后端脚本,再更新一次物品数据。
  3. 进游戏:点开 NPC,走一遍买入和卖出。
  4. 打印:用 release_print 看关键变量的类型和值。
踩坑记录

「没有这个物品」「类型不对」「绑定元宝不足」这三类报错,基本都落在「名字和数据库对不上」「字符串没转数字」「检测没过」上。挨个对一遍,比改代码快。

常见问题

金条和绑定金条到底有什么区别?

课上是两种物品:金条用 1000 元宝买,绑定金条用 1000 绑定元宝买。绑定金条不能交易、不能上拍卖行,价值被锁在账号里。

物品怎么加才不用每次改代码?

做成一张表,每行一个「名字 + 价格」,创建流程用迭代遍历这张表。加物品就加一行,遍历和创建那两段不用动。

为什么创建物品前要先查一遍名字?

表里存的是物品名,引擎要的是物品 ID。不查表直接写死 ID,名字一对不上就会静默出错;走道具名词查一下,查不到会立刻变成空值暴露出来。

买入和卖出的检测顺序能调换吗?

不建议。买入要先确认货币够再扣钱给物品,卖出要先确认背包数量够再拿走物品给钱。顺序反了容易扣了钱没给东西,或者东西没了没给钱。

绑定元宝买的东西为什么不能交易?

这是课上按玩家要求加的规则:用绑定元宝购买时,物品在发放那一步被自动绑定,所以不能交易、不能上拍卖行。规则落在发放逻辑里,不在物品表里。

代码改了界面没反应,先查哪里?

先确认有没有更新。前后端脚本更新了、物品数据也更新了没有,很多「没反应」其实是引擎里还跑着旧脚本。确认更新之后,再用 release_print 打印关键变量定位。

关于本文

本文整理自 2025.07.24 的课程妙记「lua 实战 传3 版本制作」。这节课里浮生梦老师是边讲边做的路子,把材料商人这个拍卖行界面从 NPC 下发一直写到买卖结算,中间哪个参数设错了、哪句说明书靠不住,都当场试给你看。他讲课最舒服的地方在于不藏步骤,物品怎么加、钱怎么扣、为什么不能信前端,都是先说结论再动手验证,跟着走一遍就能自己复现。正文里的写法取自课上的现场演示,接口名以 996 引擎说明书为准,逐字稿里对不上的细节以实际运行结果为准。

课程原页:飞书妙记 · lua 实战 传3 版本制作

本系列其他篇目