下载 App

弹窗内做"搜索 + 分类 + 网格"

horizontal linehorizontal line

经验 1:弹窗内做"搜索 + 分类 + 网格",用局部刷新保住输入焦点

为什么有效:搜索框是 UI.TextField,如果每次输入都 RebuildUI 重建整棵树,焦点会丢、光标乱跳。正确做法是只把"网格容器"这一棵子树的子节点 ClearChildren + AddChild,搜索框和分类标签不动。模块级变量 objectCategory_ 跨弹窗开关不重置,天然实现"记住上次分类"。
关键代码scripts/页面/地图编辑模式.lua):
lua
-- 模块级状态:跨弹窗持久,满足"记住上次分类" local objectSearchQuery_ = "" local objectCategory_ = "全部" local objectGridContainer_ = nil -- 网格容器引用,供局部刷新 local objectOverlayPanel_ = nil -- 外壳引用,供动态改高度 local objectCatTabRefs_ = {} -- 分类标签引用,供改高亮 local OBJECT_CATEGORY_ORDER = { "全部", "家具", "教学", "生活设施", "清洁", "随身物品", "电子设备" } -- 局部刷新:只动网格 + 改标签色 + 改外壳高度,绝不 RebuildUI RefreshObjectGrid = function(context) if not objectGridContainer_ then return end local filtered = FilterObjectItems(PALETTE_ITEMS.object or {}) objectGridContainer_:ClearChildren() for i = 1, #filtered do local card = CreatePaletteCard(filtered[i], i, context, 0) -- 0 = 相对 grid 定位 card.props.zIndex = 3 objectGridContainer_:AddChild(card) end local gridH = math.max(1, math.ceil(#filtered / 4)) * 82 objectGridContainer_.props.height = gridH if objectOverlayPanel_ then objectOverlayPanel_.props.height = OBJECT_HEADER_H + gridH + 12 end for _, tab in ipairs(objectCatTabRefs_) do local activeTab = tab._cat == objectCategory_ tab:SetStyle({ -- 原地改色,不重建 backgroundColor = activeTab and { 74, 126, 164, 246 } or { 52, 50, 44, 236 }, borderColor = activeTab and { 255, 232, 110, 255 } or { 120, 104, 74, 255 }, }) end end
搜索框 + 分类行的构造(isObject 分支内):
lua
overlay:AddChild(UI.TextField { position = "absolute", left = 12, top = 10, width = contentWidth - 24, height = 30, fontSize = 13, value = objectSearchQuery_ or "", placeholder = "搜索物品...", onChange = function(self, value) objectSearchQuery_ = value or "" RefreshObjectGrid(context) -- 只刷网格,焦点不丢 end, }) -- 分类标签:onPointerDown 切分类后只刷网格 local tab = UI.Panel { onPointerDown = function(event) if event then event:StopPropagation() end -- 见经验 6 的 onClick 守卫区别 objectCategory_ = catName RefreshObjectGrid(context) end, children = { UI.Label { text = catName, pointerEvents = "none", ... } }, }
踩坑:分类行 / 搜索框这类"覆盖在地图上的悬浮 UI",onPointerDown 必须 StopPropagation,否则事件会穿透到底层地图触发平移/放置。
horizontal linehorizontal line

经验 2:flex 列里的 Label 想水平居中,必须 width="100%" + textAlign="center"

为什么有效UI.Label 不传 width 时走 autoWidth,宽度=文字实际宽,放进 flex 列里默认左对齐,于是所有卡片名称偏左。
关键代码CreatePaletteCard):
lua
UI.Label { text = item.text, width = "100%", -- 禁用 autoWidth,撑满父宽 textAlign = "center", -- 此时居中才有可视效果 fontSize = 14, fontColor = { 246, 236, 200, 255 }, maxLines = 1, },
踩坑textAlign 只在 Label 宽 > 文字宽时才有用,所以 width 不可省。同理定高小容器里的 Label 还要控制 fontSize,否则触发 nvgTextBox 自动换行被裁切。
horizontal linehorizontal line

经验 3:运行时拾取"零代码自动接通"——靠 text == name 逐字匹配

这是本次最有价值的发现。新物品想运行时可拾取,不用改交互服务、不用在编辑器目录写 interactable 字段,只要满足两个条件:① 物品注册表有条目;② 编辑器目录的 text 与注册表 def.name 逐字相等。
自动接通的链路NormalizeObjects 在启动/进图时跑):
lua
-- scripts/数据/地图/校园主地图.lua function M.NormalizeObjects() for i = 1, #map.objects do local object = map.objects[i] local itemId = ItemRegistry.ResolveItemId(object.type) -- object.type = 编辑器目录的 text if itemId then object.interactable = true -- 强制补齐,不看编辑器目录 object.carryable = true object.interactionType = "pickup" object.itemId = itemId end end end -- 匹配桥梁:中文名或 itemId 都能命中 function ItemRegistry.ResolveItemId(objectType) for id, def in pairs(ITEMS) do if def.name == objectType or id == objectType then return id end end return nil end
补齐后 CreateObjectDot 自动加金色圆圈 + zIndex=20 + onClick 拾取,PickupAny 过门槛后查注册表执行 on_pickup/on_throw/on_use。固定家具(课桌/黑板/摄像头…)故意不进注册表,所以保持不可拾取——这是设计,不是 bug。
致命约束:一旦为显示把 text 改名(如"数学书"→"数学课本")而注册表 name 没同步,ResolveItemId 返回 nil,物品静默变不可拾取(无金圈、点击无反应、不报错)。所以新增物品时两处名称必须复制粘贴保证一致。
horizontal linehorizontal line

经验 4:手持/地面双尺寸,用一个"尺寸漏斗"收敛

为什么有效:地面要"看得见点得到"(≥3×3),手持要"和身体成比例"(2×2 起步)。两套尺寸不应让渲染层去判断,而是收敛进 GetHeldItemVisual 这一个函数,渲染层 (CreatePlayerMarker) 完全无感知。
关键代码scripts/系统/主角交互服务.lua):
lua
-- 单一漏斗:held 优先 → 地面回退 → 兜底 3 function PlayerInteractionService.GetHeldItemVisual() local player = GetPlayer() if not player or not player.heldObjectId then return nil end local object = FindObject(player.heldObjectId) if not object or not object.image then return nil end local def = ResolveItemDef(object) local w = (def and def.heldVisualWidth) or object.visualWidth or 3 local h = (def and def.heldVisualHeight) or object.visualHeight or 3 return { image = object.image, visualWidth = w, visualHeight = h } end
注册表只加两个字段,想让某件物品手持=地面大小,直接不写这两个字段(回退链自动用 object 的地面尺寸)——这就是经验 5 扫把的处理方式,零特殊分支。
lua
screwdriver = { ..., heldVisualWidth = 2, heldVisualHeight = 4, ... }, -- 放大 2 倍 broom = { ..., /* 不写 heldVisual,回退地面 4×4 */ },
horizontal linehorizontal line

经验 5:手持垂直定位用"躯干锚点",别用几何中心

为什么有效:角色头占顶部约 30%,若物品中心取容器几何中心 ((contH-itemH)/2),正好落在脖子/上胸,视觉"怪"。改成锚定角色高度的 65% 处即躯干中部。
关键代码scripts/页面/校园地图.lua CreatePlayerMarker):
lua
local CHAR_W, CHAR_H = 36, 36 local HELD_ITEM_Y_ANCHOR = 0.65 -- 0.5=几何中心,0.65=躯干,0.7=腰,0.8=腿侧 local itemLeft, itemTop = 0, 0 if heldVisual then itemLeft = math.floor((contW - itemW) / 2) -- 水平仍居中 local anchorY = charTop + CHAR_H * HELD_ITEM_Y_ANCHOR -- 锚到躯干 itemTop = math.floor(anchorY - itemH / 2) -- 物品中心对齐锚点 end
一个常量管全部手持物品的垂直位置,微调只动它,转向不跳变(水平居中 + 容器尺寸逻辑没动)。
horizontal linehorizontal line

经验 6:Lua 前向声明必须 name = function,绝不能 local function name

本次真实崩溃RecordObjectUse 在 line 94 做了 local RecordObjectUse 前向声明,但定义处写成 local function RecordObjectUse(...),这新建了一个同名局部变量,遮蔽了前向声明,导致 line 122 的 SelectPaletteItem 调用到的还是 nil → 运行时 attempt to call a nil value,"最近使用排序"也从未生效。
正确写法
lua
local RecordObjectUse -- line 94:前向声明,值 nil ... RecordObjectUse = function(itemText) -- 定义处:赋值,复用上面的 upvalue if not itemText then return end for i = 1, #objectRecentUse_ do if objectRecentUse_[i] == itemText then table.remove(objectRecentUse_, i); break end end table.insert(objectRecentUse_, 1, itemText) if #objectRecentUse_ > 20 then objectRecentUse_[#objectRecentUse_] = nil end end
附带的"最近使用优先"排序("全部" + 无搜索时生效):
lua
if cat == "全部" and query == "" and #objectRecentUse_ > 0 then local rankMap = {} for i = 1, #objectRecentUse_ do rankMap[objectRecentUse_[i]] = i end table.sort(out, function(a, b) return (rankMap[a.text] or 9999) < (rankMap[b.text] or 9999) end) end
horizontal linehorizontal line

复用清单:新增一件"可拾取物品"的标准 4 步

把上面 6 条经验压成一张可照做的清单:
  1. 物品注册表加条目:id / name / image / category / 效果积木块,需要手持缩小时加 heldVisualWidth/Height,想手持=地面就不写
  2. 编辑器目录 object 表加条目:text 必须逐字等于注册表 name,加 category(控制分类标签),imageimage/交互/*.png不要写 interactable。
  3. 若分类是新的,在 OBJECT_CATEGORY_ORDER 追加名称。
  4. LSP 0 Error → 官方 Build。运行时拾取/金圈/效果全部自动生效,无需改交互服务或渲染层。
horizontal linehorizontal line
一句话总纲:本次能又快又稳,核心是"数据驱动 + 单一漏斗 + 局部刷新"三件事——可拾取性靠 NormalizeObjects 数据自动接通,手持尺寸靠 GetHeldItemVisual 一个函数收敛,搜索/分类靠只刷网格子树保住焦点;而两个最容易静默失败的坑(text≠name 断链、local function 遮蔽前向声明)都已写进项目避雷清单。
槽位变更只走原子方法
经验 下标 = 视觉格子,空槽是合法状态。定长 9 槽,inventory[i]=nil 即第 i 格空着,中间允许空。任何"把物品往前挪/压缩/重排"的操作都会让"你看到的格子"和"数组真实下标"脱节 → 物品消失。渲染直映 inventory[i],数据层绝不重排。 稀疏表禁用 #。带 nil 洞的表 #inv 是未定义的(可能=0 直接读成空包,或=洞前边界)。所有遍历上界一律 Player
官方
4 赞
穿透层修复
经验一:CSS transform 穿透层修复模式 问题本质 当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。 受影响的场景 地图编辑器(右键平移/滚轮缩放后)
官方
4 赞
地图编辑器「墙」全链路增强
经验 1:子编辑器"连续操作" vs "一次性操作"——提交后别清空激活态 现象:放完一段墙,提示"可继续拖拽放置",但再拖没反应。 根因:CommitWall 提交后把 selectedItem=nil、tool="select",导致 IsActive() 返回 false,下次 HandlePointerDown 第一行就 return。提示语与行为矛盾。 模式:连续工具(墙、擦除)提交后只
官方
4 赞
【日常更新】支持2人、3人、4人对战截图
【日常更新】支持2人、3人、4人对战
优化了排行榜显示玩家昵称
3 赞
右键拖拽平移
一、问题本质 不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加: 陷阱 现象 根因文件 OnPointerMove 不向父冒泡 右键按下 isPanning_=true,但 move 事件永远到不了 mapView urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) H
官方
4 赞
地图编辑器仿真 tick 重建抑制
UI 重建频率 = 事件分发的可靠性上限。 只要有一帧 SetRoot 销毁了正在处理 pointer 事件的控件,跨重建边界的 Down/Up 对就会被吞。高频仿真 tick 必须在 UI 边界按页面过滤。 关键代码(scripts/main.lua HandleUpdate) 四步走:先原地处理人员 → 聚合所有仿真 changed → 地图页强制压制 → 只放行显式/登录态变化。 -- ma
官方
4 赞
修复"地图编辑器刷新数据丢失"
问题诊断过程 用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。 关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。 这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。 根因 地图编辑模式.lua 的 EnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutClear
官方
4 赞
地图编辑器地面交互与视觉优化
一、问题链 用户截图反馈了三个递进的问题: 问题 1:点击地面类型(如食堂地砖),弹窗不关闭,挡住画布无法操作 ↓ 修复 v0.10.94 问题 2:地面贴图太大,一块地砖比小人还宽,比例失调 ↓ 修复 v0.10.96(改配置值) 问题 3:改了配置值后刷新仍然太大——旧存档的 tileSize=256 优先级高于新 config ↓ 修复 v0.10.97(反转渲染优先级) 二、关键代码
官方
4 赞
图片加载三层优化设计
目标场景:地图编辑器"物品/墙体/地面/房间/人员"调色板弹窗的图片首帧显示 文档目的:沉淀"图片为什么慢、怎么治、发布后玩家体验如何、未来加资源时 preload_groups 怎么取舍"的完整结论,避免重复踩坑。 一句话结论 WASM 下图片"慢"不是单一问题,而是下载 / 解码 / 上传 GPU 三件事全堆在"玩家点弹窗那一刻"做导致的。优化的本质是把这三件事提前到启动期做(让玩家点的时候图
官方
1 赞
以鼠标为锚点缩放
核心数学原理 缩放前后,鼠标指向的那个世界坐标点必须保持在同一屏幕位置。 已知:旧缩放 oldZoom、新缩放 newZoom、鼠标锚点屏幕坐标 (ax, ay)、当前偏移 (panX, panY) 步骤1:算出旧缩放下鼠标指向的世界坐标 wx = (ax - panX) / oldZoom wy = (ay - panY) / oldZoom 步骤2:新缩放后反解新偏移,让 wx/wy
官方
1 赞