下载 App

牌面文字反复重新布局和渲染

07/232 浏览

调查结论

问题已查清:牌面文字本身没有异常,也不是 NanoVG 重复绘制。真正原因是每名玩家或 AI 投币后,客户端都会销毁并重新创建整个战斗 UI。
左右牌文字、牌框、顶部头像、底部状态栏都在同一棵 UI 树中。因此本来只需要更新底部投币槽,却连带让红框内的牌面文字反复重新布局和渲染,表现为“闪烁几下”。

直接证据

Client.BuildPlayingUI() 每次先销毁现有根节点:
scripts/bullbear/ClientUIPlaying.lua:14
lua
function Client.BuildPlayingUI() if uiRoot_ then uiRoot_:Destroy() end
随后重新创建两张牌的标题和效果文字:
scripts/bullbear/ClientUIPlaying.lua:175
最后替换整棵根节点:
scripts/bullbear/ClientUIPlaying.lua:694
lua
uiRoot_ = UI.Panel { ... } UI.SetRoot(uiRoot_)
UI.SetRoot() 会把布局标为脏数据,下一次 UI 渲染时重新执行整树 Yoga 布局,因此文字会重新生成布局并绘制。
horizontal linehorizontal line

为什么真人投币会闪两次以上

真人点击牌面后的调用链为:
text
点击左牌或右牌 → Client.MakeChoice() → 本地把自己标记为已选择 → 向服务器发送 CHOOSE_CARD → Client.BuildPlayingUI()
本地第一次全量重建位于:
scripts/bullbear/ClientUIPlaying.lua:707-744
之后服务器确认选择并广播:
scripts/bullbear/ServerGame.lua:289-332
text
服务器收到 CHOOSE_CARD → BroadcastPlayerChoiceReady(slot) → 所有客户端收到 PLAYER_CHOICE_READY → Client.BuildPlayingUI()
客户端第二次全量重建位于:
scripts/bullbear/ClientCore.lua:777-786
lua
playerChoiceReady_[slot] = true if phase_ == Shared.PHASE.PLAYING then Client.BuildPlayingUI() end
这里没有判断该玩家是否已经处于锁定状态。因此本机刚刚做过乐观更新,收到自己的服务端确认后仍然会再销毁、重建一次。
所以单是本机投币,通常已经有:
  1. 本地点击后重建一次;
  2. 服务端确认本机选择后再重建一次。
horizontal linehorizontal line

为什么 AI 投币又会继续闪

每个 AI 完成选择时,服务器也单独广播一次 PLAYER_CHOICE_READY:
scripts/bullbear/ServerGame.lua:366-377
每条广播到达客户端后,都会进入同一个处理器并再次执行:
lua
Client.BuildPlayingUI()
也就是说,截图中的 1 名真人加 3 名 AI,理论时序可能是:
时序触发原因整树重建
1真人本地点击1 次
2服务器确认真人选择1 次
3AI P2 投币1 次
4AI P3 投币1 次
5AI P4 投币1 次
6全员选择完毕、飞币结束进入揭示1 次
AI 在真人投币后会被压缩为最多再等待 1 秒:
scripts/bullbear/ServerGame.lua:323-329
所以多个 AI 的确认事件会集中到较短时间内到达,视觉上正好就是牌面文字连续闪几下,而不是偶尔随机闪一下。
horizontal linehorizontal line

截图中几个红框为什么同步异常

红框标出的内容全部属于同一个 uiRoot_:
  • 左牌标题、说明、状态图标;
  • 右牌标题、说明;
  • P1~P4 底部投币槽。
PLAYER_CHOICE_READY 实际变化的只有某名玩家底部状态卡中的 isChoiceLocked:
scripts/bullbear/ClientUIPlaying.lua:500-513
但当前实现没有局部更新该状态卡,而是把中央双牌、顶部头像、全部底栏一起销毁重建。因此:
  • 底部投币槽发生变化是正常需求;
  • 左右牌文字跟着重绘是不必要的副作用;
  • AI 每投一枚币,左右牌又被重建一次。
horizontal linehorizontal line

与 NanoVG 的关系

目前没有证据表明牌面文字由 UI 和 raw NanoVG 重复绘制:
  • 牌名和效果说明是 UI.Label:scripts/bullbear/ClientUIPlaying.lua:175-195
  • raw NanoVG 主要负责航线、事件气泡、飞币动画等;
  • ClientRender*.lua 中没有再次绘制 leftName/rightName/leftEffect/rightEffect。
因此这不是:
  • DPR 或分辨率模式错误;
  • 字体每帧重新创建;
  • 两个渲染层同时绘制相同文字;
  • Update 每帧调用 BuildPlayingUI()。
它是事件密集期间反复替换 UI 根节点造成的。
horizontal linehorizontal line

后续修复方向

推荐优先级如下:
  1. 最佳方案:保持中央牌面 UI 稳定,只局部更新对应玩家的投币槽。
  2. 通过控件引用或 FindById() 更新某个 P 位;
  3. 不再让 PLAYER_CHOICE_READY 调用 BuildPlayingUI();
  4. 这是从根本上消除闪烁的方案。
  5. 对服务端确认做幂等去重。
  6. 若 playerChoiceReady_[slot] 已经为 true,不再刷新;
  7. 能消除本机“本地更新 + 服务端回执”的重复刷新;
  8. 但其他 AI 投币时仍会重建,所以只能缓解,不能根治。
  9. 保留揭示阶段的一次完整切换。
  10. 飞币结束后进入 REVEAL 的一次重建属于阶段切换,可以保留;
  11. 需要避免的是每个 PLAYER_CHOICE_READY 都重建中央牌面。
  12. 次选方案:合并同一帧或短时间内的多条刷新请求。
  13. 可以减少 AI 集中投币时的连续重建;
  14. 但仍然比局部更新代价大,不建议作为最终方案。
本轮仅调查,没有修改任何文件,也没有执行构建。

第一项“普通遭遇投币槽局部更新”已完成。

实现内容

  • 为每个玩家投币槽增加稳定 ID:
  • scripts/bullbear/ClientStatusCards.lua:190
  • 格式为 playing_coin_slot_<slot>
  • 自定义投币槽和 fallback 控件都支持定位。
  • 新增局部刷新接口:
  • scripts/bullbear/ClientUIPlaying.lua:14
  • 通过 uiRoot_:FindById() 定位指定 P 位投币槽;
  • 只调用 SetProp("spinning", false) 停止铜钱旋转;
  • 不销毁状态栏,不重建左右牌面。
  • 本机普通遭遇点击改为局部更新:
  • scripts/bullbear/ClientUIPlaying.lua:753
  • 普通遭遇不再调用 BuildPlayingUI();
  • Buff、技能节点仍保留全量更新,因为它们会改变实际卡片内容。
  • 服务端投币确认改为局部更新并增加幂等保护:
  • scripts/bullbear/ClientCore.lua:777
  • AI 或其他真人投币时,只更新对应玩家投币槽;
  • 重复收到同一 P 位确认不会再次刷新。

调用链复核

现在普通遭遇投币链为:
text
本机点击 / AI 投币确认 → playerChoiceReady_[slot] = true → RefreshPlayerChoiceCoin(slot) → coinSlot:SetProp("spinning", false)
不再经过:
text
uiRoot_:Destroy() → 重建左右牌文字 → UI.SetRoot()
揭示完成、玩家爆仓、Buff/技能节点等确实改变界面结构的路径仍保留完整重建。

验证结果

  • Lua LSP:0 Error
  • 官方多人构建:成功
  • 多人配置保持正确:
  • background_match=true
  • free_match_with_ai
  • immediately_start=true
  • match_timeout=3
  • player_number=4
  • 单机截图验证工具不支持多人项目,因此未用无服务端环境制造无效测试结果。
需要在多人预览中实际投一次币观察:预期底部对应铜钱停止旋转,左右牌文字保持稳定,不再随真人和三个 AI 的投币逐次闪烁。
【更新】增加了一些任务截图
【更新】增加了一些任务
我从故事便签墙中,看到了大多数玩家都在和野兽、疾病博弈中倒下 我这两天补充了一些任务,有关合成物品的为主 在此感谢大伙儿的积极反馈
1 赞
4 回复
01:39
【新游】友谊的小船……望官方首推截图
【新游】友谊的小船……望官方首推
Tap平台有大量的‌投资模拟类游戏,谁不想在‌市场中当常胜将军。 但现实中是残酷的,希望这个游戏能让玩家先锻炼好心态,再去博弈 游戏简介: 该游戏是一款以投资市场为背景的心理博弈桌游,核心设计围绕人性弱点(贪婪、恐惧、从众、侥幸、冲动等) 游戏通过大量映射现实市场中的事件,将投资心理陷阱(幸存者偏差、囚徒困境、羊群效应、赌徒谬误、沉没成本、从众心理、社会懈怠、过度自信、锚定效应、确认偏误等)
1 赞
卡牌:市场调研
skill_market_research 当前仅完成了: 加入随机技能池; 可以在技能节点被选中; 有新绘制图标; 有 Tooltip 设计说明; 能作为技能徽章显示。 但“选择后显示实时人数,并允许一次改选”的玩法逻辑尚未接入。 代码证据 1. Tooltip 明确标记为“设计中” scripts/bullbear/Client.lua:830 当前内容仍是: 技能 · 侦察 + 改选 · 设
官方
UI按键动画设计
一、本次反复失败的根因(三个坑) 现象 根因 按钮完全不可见(空白/小白点) 入场动画把 scale/alpha 初始设 0,靠 Update 驱动 Tween 补到 1;但该模板页面级 Update 时序不可靠,补间不推进 → 永远停在 0 背景/按钮颜色反转 用了 nvgRGBA(r,g,b,255) 整数版,出现通道映射异常 矩形在、文字消失 文字色≈填充色;或 DrawStrokedTex
官方
【牌】兴登堡凶兆
这张卡的主题很适合《友谊的小船》,但按原始数值直接落地会有一个关键问题:右牌严格优于左牌,四人局会趋向全员选右,博弈不成立。 先评估原方案 选择 未扎堆 同侧≥3人 结论 左:追高 -15万 -40万 始终比右牌更痛 右:抄底 -10万 -25万 任意人数下都更优 无论同侧人数是多少,右牌都少亏: 未扎堆:右牌少亏 5万 扎堆后:右牌少亏 15万 所以理性玩家没有选择左牌的理由;最终通常会四人全选
官方
理性值跨局持久化
1. 数据常量(RationalityData.lua) M.INITIAL_VALUE = 100 -- 新玩家初始理性值 M.MAX_VALUE = 100 -- 理论上限 M.REMEDY_MAX_VALUE = 90 -- 药剂恢复封顶 M.REMEDY_REQUIRED_MAX_VALUE = 89 -- 药剂可用阈值(理性≤89) M.REMEDY_COST = 3 -- 药剂价格(宝
官方
闪跳问题
下面是这轮三个问题(音量滑块、拖动闪跳、选角页闪跳/白屏)的可复用经验与对应关键代码。代码均为磁盘最终落地版本。 经验一:拖动控件别在回调里重建整棵树 滑块/开关这类"边拖边回调"的控件,onChange 里只能原地改值,绝不能触发 BuildXxxUI() 整树重建——否则正在拖的滑块会被销毁,拖动中断。音量百分比标签也是同理,用闭包持有引用做原地 SetText。 scripts/bullbe
官方
大厅校准模式
一、整体架构 校准系统由四层组成: 层 文件 职责 状态容器 Client.lua 保存 lobbyCalibrationMode_/Drag_/Slots_/RestorePending_/SaveStatus_、路径常量、管理员标记 拖拽 + UI ClientUILobby.lua 指针事件包装、归一化坐标、实时预览、设置面板 序列化 ClientStorage.lua 本地 pref 读写
官方
WASM 崩溃
nvgCreateImage 在 WASM 环境下无法正确加载图片文件(中文路径/资源未打包进虚拟文件系统),返回看似正数但实际无效的纹理句柄。Lua 中 0/-1 都是 truthy,if 判断通过后把无效句柄传给 nvgImagePattern,C 层访问空指针 → WASM 内存越界。 彻底解决方案:纯矢量绘制,零文件 I/O 完全放弃图片加载,用 NanoVG 基础图元(圆/椭圆/描边)绘
官方
背景音乐持久化方案
问题演进 轮次 方案 失败原因 1 File("save.pref", FILE_WRITE) WASM MEMFS 刷新即丢失 2 clientCloud:SetInt("music_enabled", 1/0) 写 0 被后端吞掉;HTTP 请求刷新时被浏览器取消 3 clientCloud:Set("music_enabled", "off") 同上,HTTP 不可靠 4 serverClo
官方