地图编辑器仿真 tick 重建抑制
UI 重建频率 = 事件分发的可靠性上限。 只要有一帧 SetRoot 销毁了正在处理 pointer 事件的控件,跨重建边界的 Down/Up 对就会被吞。高频仿真 tick 必须在 UI 边界按页面过滤。


关键代码(scripts/main.lua HandleUpdate)
四步走:先原地处理人员 → 聚合所有仿真 changed → 地图页强制压制 → 只放行显式/登录态变化。
lua-- main.lua (HandleUpdate) -- 0) 各仿真服务独立更新,各自返回 changed 标志 local dailyChanged = CampusDaily.Update(timeStep) local playerChanged = PlayerComponentService.Update(timeStep) local planChanged = PersonalPlanService.Update(timeStep) local explorationChanged= RoomExplorationService.Update(timeStep) local hygieneChanged = CampusHygieneService.Update(timeStep) local personnelChanged = CampusPersonnel.Update(timeStep) local behaviorChanged = BehaviorObservationService.Update(timeStep) local observationChanged= ObservationTaskService.Update(timeStep) -- 1) 人员移动在地图页内原地刷新,走独立通道,不重建 if personnelChanged and currentPage_ == "mapEditor" then MapEditorPage.UpdateDynamicCharacters() personnelChanged = false end -- 2) 聚合所有仿真 tick 的 changed 标志(电平型,NPC 移动/思考时会"长期 true") local simulationChanged = dailyChanged or playerChanged or planChanged or explorationChanged or hygieneChanged or personnelChanged or behaviorChanged or observationChanged -- 3) 地图编辑器页强制压制——编辑页绝不因后台仿真每帧重建 if currentPage_ == "mapEditor" then simulationChanged = false end -- 4) 只允许三类显式信号触发整树重建 if simulationChanged or context_.rebuildUIPending -- RequestUIRebuild() 显式调用 or uid ~= lastLoggedUserId_ -- 登录用户切换 or isAdmin ~= lastLoggedAdminState_ -- 管理员状态变化 then context_.rebuildUIPending = false RebuildUI() -- 内部调用 UI.SetRoot(root, true) end


原地刷新关键代码(地图编辑模式.lua)
人员位置每帧变,但不重建 UI,通过保留 widget 引用用 SetStyle 原地更新:
lua-- 已放置人员的 dot widget 在进入编辑页时就建好,缓存到 dynamicCharDots_ function M.UpdateDynamicCharacters() for id, dot in pairs(dynamicCharDots_) do local char = MapData.data.characters[id] local sx, sy = WorldToScreenLocal(char.x, char.y) dot:SetStyle({ left = sx, top = sy }) -- 原地改 transform,无重建 end end


显式重建入口(被业务代码在结构变化时调用)
luafunction RequestUIRebuild() context_.rebuildUIPending = true end -- 调用场景:新增/删除房间、切换工具、打开/关闭面板(结构性变化,非每帧)


根因(为什么"放完房间要等一会才能再点左栏")
| 层级 | 现象 |
| 仿真层 | CampusNavigation.Update:NPC 在校日常移动时每帧返回 changed=true |
| UI 层 | 每帧 UI.SetRoot(root, true) → 整棵 widget 树(含左栏 VirtualList)以 ~60 fps 销毁重建 |
| 事件层 | pointerDown 落在旧控件 A → 下一帧 A 被销毁 → pointerUp 到达时 A 不存在 → OnClick 不触发 |
| 用户感知 | 点"物品"按钮无响应;等待 1–2 秒后 NPC 恰好集体到达目的地,重建频率下降,点击恢复 |


新增仿真服务时的检查清单
- ☐ 该 changed 是否会在 NPC 持续移动/思考时"每帧 true"?是→并入 simulationChanged 聚合,不要单独加进重建条件
- ☐ changed 是否驱动视觉?位置→原地 SetStyle;文案/列表结构→走 RequestUIRebuild()
- ☐ 地图编辑器/菜单页等静态页一律压制 simulationChanged
- ☐ 如需"状态切换瞬间"刷新,让服务返回脉冲型 changed(切换瞬间 true,下一帧 false),脉冲可在所有页面放行


被否决方案(避免再踩)
- 只压制 dailyChanged,放过 explorationChanged/behaviorChanged → 治标不治本
- 让 CampusNavigation.Update 改成只在状态变化瞬间返回 true → 破坏仿真语义(其他页面依赖每帧 changed 刷 HUD)
- 给 VirtualList 加 key 复用 DOM 身份 → UI 库不支持跨 SetRoot 复用控件,事件监听器仍是新闭包



