更新时间:2026-03-26
为什么有这份文档:这次拍卖刷新不是“多调一个方法”就结束的小修小补,而是一段完整的逆向案例。
我们先后踩到了错误容器、错误关闭路径、错误时序、错误成功判定等一整串坑。把这段过程写下来,是为了让后续做黑市、锻造、原生通知、更多 QoL 功能时,不再靠记忆重复试错。
补充说明:如果你想看这条链路在
v1.8.15阶段的原始断点检查清单、探针设计和“尚未收口前”的判断标准,可以继续看:拍卖刷新断点研究。
表面现象其实很简单:
- 按
Alt+R有蜂鸣,说明热键触发了 - 拍卖“查看展品”有时不刷新
- 有时看上去刷新了,但其实只是复制 / 累计旧展品
- 有时又会变成“按一下关、按一下开”的开关行为
真正难的地方在于,这个问题同时跨了四层:
- 输入层:热键是不是进来了
- 业务层:随机拍品到底在哪一步生成
- 显示层:玩家眼前看到的展品 UI 到底挂在哪个容器
- 时序层:生成、清理、关闭、重开谁先谁后才不会串状态
如果只盯着其中一层,很容易得到“看起来合理、实际完全错位”的实现。
一开始最自然的想法是:
- 拍卖相关,应该就在
AuctionController - 展品相关,应该和
PlotController.plotItemGrid有关
这条判断只对了一半:
- 拍卖随机数据的业务链,确实和
PlotController.ShowAuctionItem()/GenerateAuctionItem()有关 - 但玩家眼前真正显示出来的那一层展品 UI,并不在
auctionPanel,也不在plotItemGrid
这就是后面所有“清理了但没用”“调用返回了但没关对”的根源。
我们一度尝试过走:
PlotBackgroundClicked()- 清
plotItemGrid - 清
plotInteractItem
这些动作都不是完全没效果,但都没有命中“当前这批展品图标实际挂在哪”。
结果就是:
- 数据层可能已经变了
- 旧的显示容器却没真正关掉
- 再开一次时,新旧内容叠在一起,看起来就像“复制”或“累计”
拍卖刷新最危险的一点在于:
- 多做一次
show - 多做一次
close - 多补一次兜底 reopen
都可能把状态从“差一点正确”直接推回“明显错误”。
这次我们后面反复验证出的结论很明确:
- 在这种 UI / 业务双层错位的系统里,步骤不是越多越稳
- 额外动作只会放大错误时序
最早的日志已经证明:
Alt+R能进PollInput- 蜂鸣能响
- 刷新分支确实被执行
所以这不是输入路径问题,也不是按键冲突导致完全没触发。
通过对日志里这几类信息的对照:
ShowAuctionItem pre/postGenerateAuctionItem pre/post- 展品前后内容摘要
可以确认一个关键事实:
PlotController.ShowAuctionItem()会走到GenerateAuctionItem()- 真正的拍品 reroll 业务入口就在这条链上
这一步非常重要,因为它把“是不是根本没重掷”这个疑问先排除了。
这次最值钱的探针不是单纯多打几条日志,而是做了运行时快照:
- 记录
AuctionController当前状态 - 记录
PlotController当前状态 - 枚举当前激活的
ItemIconController - 把它们的层级路径打印出来
最后看到的关键证据是:
auctionPanel active = falseplotItemGrid children = 0- 但真实展品图标还活着,而且路径在:
Canvas/ChoosePanel/ChoosePanelRoot/ChooseItemList/Viewport/Content/...
这一步直接推翻了前面的错误假设。
真正的结论是:
- 拍卖随机链在
PlotController - 展品显示链在
ChooseController / ChoosePanel
业务层和显示层根本不是同一个控制器。
确认到 ChoosePanel 之后,下一步不是继续盲调,而是去看 interop 元数据。
这次真正有价值的结论包括:
ChooseController.InstancechoosePanelchooseRootitemListHideChoosePanel()UnshowChoosePanel()
这一步带来的收益是:
- 我们终于能调用“真正的关闭入口”
- 不再需要拿
PlotController假装做 close
还有一个很关键的排除过程:
- 触发拍卖事件后先存档
- 读档后第一次查看展品与第二次查看展品结果不同
这说明:
- 随机结果不是在“事件触发那一刻”就固定好的
- 真正的掷骰子时机在“触发事件”和“查看展品”之间更靠后的位置
这也是为什么后面我们必须盯住 ShowAuctionItem -> GenerateAuctionItem 这条链,而不是去碰更早的事件入口。
截至 2026-03-26,拍卖刷新最终稳定下来的核心思路不是“模拟玩家乱点一串原生按钮”,而是做一个很小的目标态状态机:
- 不管当前“查看展品”是开着还是关着
Alt+R的最终结果都应该是:展品处于打开状态
pending reroll先挂起- 真正的成功判定,不看蜂鸣、不看 UI 闪动,只看
GenerateAuctionItem是否成功跑到 - 只有
GenerateAuctionItem成功后,才清掉挂起状态 - 显示层操作只针对真实
ChoosePanel - 所有时序都尽量压缩成最少步骤
从打开态进入时:
ResetPlotItemDisplay(...)- 延迟几帧
HideChoosePanel()- 再延迟几帧
EnsureOpen
从关闭态进入时:
- 直接进入
EnsureOpen
因为它描述的是“目标态”,不是“操作录制”:
- 不是单纯切一下开关
- 不是假设当前一定处于某个固定状态
- 而是每一步都先判断当前状态,再决定该做什么
这对拍卖这种“显示层和业务层分离”的系统尤其重要。
这次最根本的一条教训就是:
- 业务入口和显示容器可以完全不在同一个控制器上
如果这一点没先证明,后面所有“清空列表”“关面板”“重开界面”都可能打在假目标上。
对未来功能的直接启发是:
- 黑市
- 锻造
- 原生通知
- 商店 / 选择类界面
都要优先回答这个问题:
“真正显示给玩家看的对象,究竟挂在哪个控制器 / 哪个容器下?”
这次如果只是看:
- 按键响了
- UI 关了
- 面板开了
都会被误导。
真正可靠的成功点只有一个:
GenerateAuctionItem成功执行
因此后续同类功能都应该遵守:
pending状态只在真实业务成功点清理- 不要在“看起来差不多成功了”的中间态提前收尾
这次有用的探针只有两类:
- 能回答“真实 UI 在哪里”的快照
- 能回答“业务成功没成功”的链路日志
没用的探针特征则很明显:
- 高频刷屏
- 不能改变判断
- 只是让日志更长
所以项目里的经验可以沉淀成一句话:
- 诊断日志必须“问题驱动、按需开启、验证后收敛”
这次所有不稳定的版本,本质都像在做“操作录制”:
- 先点这个
- 再点那个
- 再补一次
但真正稳定下来的版本更像一个最小状态机:
- 当前是否打开
- 是否需要 reset
- 是否需要 close
- 最终是否已经回到打开态
这是一条非常可复用的工程经验。
我们最后稳定下来不是因为一上来就追求“一键丝滑无动画”,而是先接受:
- 先让它不复制
- 先让它不累计
- 先让它真实重掷
稳定之后,才继续收“从开态 / 关态进入都最终打开”的手感问题。
这条顺序非常重要:
- 先正确
- 再稳定
- 最后再优化体感
如果顺序反了,往往会回到“更丝滑,但更错”。
以后再做复杂 QoL 功能,推荐统一走这套顺序:
不要直接问“哪个方法有 bug”,先问:
- 输入进来了吗
- 真正的业务成功点是什么
- 玩家眼前看到的显示层是谁
- 哪一步时序会导致旧状态残留
只抓最有判别力的对象:
- 当前控制器单例
- 当前激活容器
- 当前关键图标 / 列表路径
这一步的目标不是“全扫场景”,而是尽快证伪错误假设。
一旦锁定到目标控制器,就去确认:
- 属性名
- 方法名
- 参数签名
不要继续凭印象猜字段。
优先回答:
- 需要哪些最少状态
- 成功点是什么
- 失败时怎么取消挂起
而不是一开始就抽象成大框架。
验证完成以后:
- 把高频日志收回
- 保留必要的摘要日志
- 不把临时探针和研究脚本混进正式功能路径
这次拍卖刷新复盘,对后面几个方向都有直接帮助:
优先检查:
- 随机结果在哪里生成
- 结果展示挂在哪个容器
- 原生关闭入口是不是和业务入口分离
这次已经证明一个原则:
- 不要一上来就自己注入 UI
- 应优先找原生提示链、原生显示控制器、原生 prefab
这次最终胜出的不是“大而全的一键脚本”,而是:
- 足够小
- 足够稳
- 每一步都有真实证据支撑
这也和当前项目的总原则完全一致:
- 稳定优先
- 低运行负担
- 尽量复用原生控制器
这次拍卖刷新最值得沉淀的,不是某一行代码,而是一套可复用的方法:
- 先证明真实显示层
- 再锁定真实业务成功点
- 用最小状态机描述目标态
- 挂起状态只在真实成功后清理
- 稳定后再优化体验,不要反过来
如果后面继续做黑市、锻造、原生通知、更多上下文刷新,这份复盘应该作为第一参考,而不是只把它当成“拍卖专属问题”的历史记录。