我是lua后端老师!
小仙,你遇到的这个问题确实很典型,核心在于**全局变量 `A15` 的并发读写冲突**和**逻辑判断的时序问题**。当多个玩家快速点击、采集、取消时,就会出现数据不一致。
**核心问题分析:**
1. **全局变量竞争**:`A15` 是一个系统变量,所有玩家共享。玩家A在读取 `tbl` 并准备修改时,玩家B可能已经修改并写回了 `A15`,导致A的修改覆盖了B的,或者判断失效。
2. **采集状态与怪物对象绑定不牢**:你的逻辑是先记录 `mon_obj` 到表里,再判断是否有人采集。但在高并发下,`mon_obj` 可能因为怪物被杀死而失效,或者判断时状态已变。
3. **采集中断处理不彻底**:`caiji_false` 函数清空了整个 `A15` 表,这会误伤其他正在进行的合法采集。
**优化方案:使用“玩家-怪物”双键锁 + 原子操作思路**
我们重构一下逻辑,核心是 **`A15` 表结构改为:`[玩家ID .. "_" .. 怪物UID] = 时间戳`**。这样每个采集任务都是独立的条目,互不干扰。
```lua
-- 采集前触发
function collectmonex(actor, mon_idx, mon_name, mon_uid)
func_collectmonex(actor, mon_idx, mon_name, mon_uid)
end
-- 采集跳转
function func_collectmonex(actor, mon_idx, mon_name, mon_uid)
local playerId = getbaseinfo(actor, 3) -- 获取玩家账号ID,作为唯一标识
local lockKey = playerId .. "_" .. mon_uid -- 构建唯一的锁键
-- 灵兽蛋采集逻辑
if mon_name == "灵兽蛋[初级]" or mon_name == "灵兽蛋[中级]" or mon_name == "灵兽蛋[高级]" then
-- 1. 检查每日次数
if getplaydef(actor, "J14") >= 5 then
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>今日抓捕次数已达上限!!</font>","Type":9}')
return
end
-- 2. 原子化读取与检查A15表
local t = getsysvar("A15")
if t == "" then
t = "{}"
end
local tbl = json2tbl(t)
-- 3. 检查这个怪物是否已被其他玩家锁定
for key, timestamp in pairs(tbl) do
-- 如果键中包含当前怪物的UID,说明已被占用
if string.find(key, "_" .. mon_uid) then
-- 可选:可以检查时间戳是否超时,如果超时则清理旧锁
-- if os.time() - timestamp > 30 then ... end
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>当前灵兽蛋已被其他玩家锁定!!</font>","Type":9}')
return
end
end
-- 4. 检查这个玩家是否已经锁定了一个怪物(防止同时采多个)
for key, timestamp in pairs(tbl) do
if string.find(key, playerId .. "_") then
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>你已经在采集一个灵兽蛋了!!</font>","Type":9}')
return
end
end
-- 5. 获取怪物对象并检查是否存在
local mon_obj = getmonbyuserid(getbaseinfo(actor, 3), mon_uid)
if not mon_obj or mon_obj == "" then
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>灵兽蛋已消失!!</font>","Type":9}')
return
end
-- 6. 一切就绪,写入锁定记录(使用时间戳)
tbl[lockKey] = os.time() -- 记录锁定时间
setsysvar("A15", tbl2json(tbl))
-- 7. 存储采集信息到玩家变量,用于进度条回调
setplaydef(actor, "S$采集地图id", getbaseinfo(mon_obj, 3))
setplaydef(actor, "S$采集地图x", getbaseinfo(mon_obj, 4))
setplaydef(actor, "S$采集地图y", getbaseinfo(mon_obj, 5))
setplaydef(actor, "S$采集怪物UID", mon_uid) -- 关键:存储怪物UID
-- 8. 开始采集进度条
local tiem = DATA.hd_info["抓捕灵兽"].tiem_num[mon_name]
showprogressbardlg(actor, tiem, "@caiji_true", "灵兽蛋采集中...%s%%", 1, "@caiji_false")
end
-- 酒神材料采集逻辑(相对独立,可以保留,但建议也加入锁机制)
if mon_name == "千年灵芝" or mon_name == "紫水晶矿" or mon_name == "荧光草" then
local mon_obj = getmonbyuserid(getbaseinfo(actor, 3), mon_uid)
if mon_obj and mon_obj ~= "" then
setplaydef(actor,"S$酒神怪物采集",mon_obj)
showprogressbardlg(actor, 5, "@caiji_jiushen", "采集中...%s%%", 1, "")
end
end
end
-- 灵兽蛋采集成功触发
function caiji_true(actor)
local playerId = getbaseinfo(actor, 3)
local monsterUid = getplaydef(actor, "S$采集怪物UID")
local lockKey = playerId .. "_" .. monsterUid
-- 1. 原子化读取A15表
local t = getsysvar("A15")
if t == "" then
t = "{}"
end
local tbl = json2tbl(t)
-- 2. 验证锁是否仍然有效(防止回调时锁已超时或被清理)
if not tbl[lockKey] then
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>采集锁已失效或超时!!</font>","Type":9}')
-- 清理玩家变量
setplaydef(actor, "S$采集怪物UID", "")
return
end
-- 3. 获取怪物对象并再次验证
local mon_obj = getmonbyuserid(playerId, monsterUid)
if not mon_obj or mon_obj == "" then
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>灵兽蛋已消失!!</font>","Type":9}')
tbl[lockKey] = nil -- 清理无效锁
setsysvar("A15", tbl2json(tbl))
setplaydef(actor, "S$采集怪物UID", "")
return
end
-- 4. 执行采集成功逻辑
local mon_name = getbaseinfo(mon_obj, 1)
setplaydef(actor, "J14", getplaydef(actor, "J14") + 1)
killmonbyobj(actor, mon_obj, false, false, false) -- 杀死怪物
local t_data = DATA.hd_info["抓捕灵兽"]
if t_data.ran[mon_name] then
local color = ransjstr(t_data.ran[mon_name], 1, 3)
local ran = math.random(1, #t_data.mon_itme[color])
local item_name = t_data.mon_itme[color][ran]
giveitem(actor, item_name, 1)
guildnoticemsg(actor, 255, 249, "{【" .. getbaseinfo(actor, 1) .. "】|254:0:1}成功抓捕{ 灵兽蛋 |251:0:1}获得{ " .. item_name .. "|242:0:1}", 1)
-- 采集后特效
mapeffect(106, getplaydef(actor, "S$采集地图id"), getplaydef(actor, "S$采集地图x"), getplaydef(actor, "S$采集地图y"), 61242, 2, 0, nil, 1)
end
sendmsg(actor, 1, '{"Msg":"<font color=\'#00FF00\'>抓捕成功!!今日抓捕次数(' .. getplaydef(actor, "J14") .. '/5)</font>","Type":9}')
-- 5. 清理:移除锁,清空玩家变量
tbl[lockKey] = nil
setsysvar("A15", tbl2json(tbl))
setplaydef(actor, "S$采集地图id", "")
setplaydef(actor, "S$采集地图x", "")
setplaydef(actor, "S$采集地图y", "")
setplaydef(actor, "S$采集怪物UID", "")
end
-- 灵兽蛋采集失败/中断触发
function caiji_false(actor)
local playerId = getbaseinfo(actor, 3)
local monsterUid = getplaydef(actor, "S$采集怪物UID")
if monsterUid == "" then
return
end
local lockKey = playerId .. "_" .. monsterUid
-- 只清理自己的锁
local t = getsysvar("A15")
if t == "" then
t = "{}"
end
local tbl = json2tbl(t)
if tbl[lockKey] then
tbl[lockKey] = nil
setsysvar("A15", tbl2json(tbl))
sendmsg(actor, 1, '{"Msg":"<font color=\'#ff0000\'>采集中断!!</font>","Type":9}')
end
-- 清理玩家变量
setplaydef(actor, "S$采集地图id", "")
setplaydef(actor, "S$采集地图x", "")
setplaydef(actor, "S$采集地图y", "")
setplaydef(actor, "S$采集怪物UID", "")
end
-- 可选:增加一个定时器,定期清理A15表中的过期锁(例如超过30秒的锁)
function clear_expired_locks()
local t = getsysvar("A15")
if t == "" then
return
end
local tbl = json2tbl(t)
local now = os.time()
local changed = false
for key, timestamp in pairs(tbl) do
if now - timestamp > 30 then -- 30秒超时
tbl[key] = nil
changed = true
end
end
if changed then
setsysvar("A15", tbl2json(tbl))
end
end
-- 可以在startup触发中设置一个全局定时器来调用 clear_expired_locks
```
**优化点总结:**
1. **唯一锁键**:使用 `玩家ID_怪物UID` 作为键,隔离每个采集任务。
2. **原子操作**:在关键逻辑点(检查、加锁、清理)都重新读取 `A15` 表,避免使用陈旧的本地副本。
3. **双重验证**:在 `collectmonex` 和 `caiji_true` 中都验证怪物对象是否存在。
4. **精准清理**:`caiji_false` 只清理自己的锁,不影响他人。
5. **状态绑定**:将关键信息(怪物UID)存入玩家变量,确保回调时能找到正确的采集目标。
6. **超时机制**:锁中加入时间戳,可通过定时任务清理“僵尸锁”,防止玩家掉线后锁永远存在。
这套逻辑能极大减少并发错误,但请注意,在极端高并发下,`setsysvar` 写入仍可能存在微小竞争窗口。对于要求绝对严谨的场景,可能需要引擎提供更底层的原子操作支持。你先试试这个方案,应该能解决大部分问题!