下载 App

拾取 / 丢弃物品

07/27综合

拾取的坑是「点击被遮挡」,丢弃的坑是「坐标反解会偏」。两者解法都是绕开复杂机制、用最直接的方式。
问题根因解法
走到物品旁点不动、捡不起来物品命中层和主角标记同层、都无 zIndex,数组顺序靠后的主角标记盖在上面,点击命中的是主角而非物品给可交互物品显式 zIndex = 20
拖拽丢东西飞到远处ScreenToWorld 把鼠标像素反解成世界坐标当落点,依赖 zoom/offset,transform 链不稳定丢弃落点不做反解,直接读玩家脚底空间坐标
horizontal linehorizontal line

一、拾取:scripts/页面/校园地图.luaCreateObjectDot

物品点是一个 UI.Panel,关键是 zIndexonClick 守卫。
lua
local function CreateObjectDot(object, context) local interactionText = PlayerInteractionService.GetObjectInteraction(object) -- 命中区 = 包裹 placement 与 visual 的最小外接矩形(visual 偏移常比 footprint 大) local placementWidth = object.placementWidth or object.w or 1 local placementHeight = object.placementHeight or object.h or 1 local visualWidth = object.visualWidth or object.w or placementWidth local visualHeight = object.visualHeight or object.h or placementHeight local visualOffsetX = (placementWidth - visualWidth) * 0.5 local visualOffsetY = (placementHeight - visualHeight) * 0.5 local minGx = math.min(0, visualOffsetX) local maxGx = math.max(placementWidth, visualOffsetX + visualWidth) local minGy = math.min(0, visualOffsetY) local maxGy = math.max(placementHeight, visualOffsetY + visualHeight) local effectiveW = maxGx - minGx local effectiveH = maxGy - minGy local visualWidget = object.image and UI.Panel { position = "absolute", left = (visualOffsetX - minGx) * 5, top = (visualOffsetY - minGy) * 5, width = math.max(5, visualWidth * 5), height = math.max(5, visualHeight * 5), backgroundImage = object.image, backgroundFit = "contain", backgroundColor = { 0, 0, 0, 0 }, pointerEvents = "none", } or nil return UI.Panel { position = "absolute", left = (object.x + minGx) * 5, top = (object.y + minGy) * 5, width = math.max(5, effectiveW * 5), height = math.max(5, effectiveH * 5), backgroundColor = object.image and { 0, 0, 0, 0 } or { 210, 198, 160, 220 }, borderWidth = interactionText and 1 or 0, borderColor = interactionText and { 235, 190, 90, 220 } or { 40, 36, 28, 255 }, tooltip = interactionText, visible = not object.pickedUp, -- ★ 关键修复:可交互物品必须浮在主角标记(无 zIndex)之上,否则被遮挡点不中 zIndex = interactionText and 20 or 0, children = visualWidget and { visualWidget } or nil, -- 只有可交互物品才有点击处理器(否则为 nil,不可点) onClick = interactionText and function(widget, event) -- ★ 右键守卫:只屏蔽右键(平移),不要写 event.button ~= MOUSEB_LEFT(MOUSEB_LEFT 未暴露为全局) if event and event.button and event.button == MOUSEB_RIGHT then return end if event then event:StopPropagation() end local ok, reason = PlayerInteractionService.PickupAny(context, object.id) if not ok and reason and context.store and context.store.AddEvent then context.store.AddEvent("无法拾取:" .. tostring(reason), "物品交互") end context.RebuildUI() end or nil, } end
配套的拾取判定scripts/系统/主角交互服务.lua:距离用空间服务实时坐标(不能读 player.x/y,那是初始值),阈值 8 格:
lua
local INTERACTION_DISTANCE = 8 local function DistanceTo(player, x, y) local px, py = ActorSpatialService.GetPosition(player.id) -- ★ 读实时坐标 px = px or player.x or 0; py = py or player.y or 0 local dx, dy = px - x, py - y return math.sqrt(dx * dx + dy * dy) end function PlayerInteractionService.PickupAny(context, objectId) local player = GetPlayer() if not player then return false, "主角不存在" end local object = FindObject(objectId) if not object then return false, "物件不存在" end if object.pickedUp or object.carriedBy then return false, "已拾取/被拿走" end if object.interactable ~= true then return false, "不可交互" end if object.carryable ~= true then return false, "不可携带" end if DistanceToObject(player, object) > INTERACTION_DISTANCE then return false, "请先走到物件附近" end object.carriedBy = player.id object.pickedUp = true -- 手持空且有 equipSlot → 装备;否则入背包 ... return true end
horizontal linehorizontal line

二、丢弃:scripts/界面组件/底部物品栏.lua → 拖拽 pointerup

抬起时两条分支:拖到小人→扔给他;否则→丢玩家脚底(不反解坐标)。
lua
slot:OnEvent("pointerup", function(event, widget) if not dragState_ then return end if not dragState_.active then -- 没拖够阈值 = 点击 ClearDrag(); if onClick then onClick() end; return end local gx, gy = event.x, event.y local CampusMap = require("页面.校园地图") local wx, wy = CampusMap.ScreenToWorld(gx, gy) -- 仅用于"是否拖到某人身上"的命中检测 local interactSvc = require("系统.主角交互服务") local success, action = false, "cancel" local target = nil if wx and wy then target = HitTestCharacter(wx, wy, context) end if target then -- 分支 A:拖到小人身上 → 扔给该人(不限距离) local ok, reason if dragState_.source == "held" then ok, reason = interactSvc.ThrowHeldItemTo(context, target.id) else ok, reason = interactSvc.ThrowInventoryItemTo(context, target.id, dragState_.source.fromInventory) end if ok then success = true; action = "throw" end else -- 分支 B:★ 丢到玩家脚底 —— 直接读玩家空间坐标,绝不用 ScreenToWorld 当落点 local ActorSpatialService = require("系统.小人空间服务") local StudentService = require("系统.学生服务") local pchar = StudentService.GetPlayerCharacter(context.store.GetState()) local fx, fy = nil, nil if pchar then fx, fy = ActorSpatialService.GetPosition(pchar.id) fx = fx or pchar.x; fy = fy or pchar.y end if fx and fy then local ok, reason = interactSvc.DropItemAt(context, dragState_.source, fx, fy) if ok then success = true; action = "drop" end end end ClearDrag() if context.RequestUIRebuild then context.RequestUIRebuild() else context.RebuildUI() end end)
horizontal linehorizontal line

三个可复用的「避雷口诀」

  1. 同父容器的命中层叠看 zIndex:需要被点的交互层(物品、按钮命中区)必须显式给 zIndex,否则会被数组中靠后、无 zIndex 的层(如主角标记)盖住。
  2. MOUSEB_LEFT 在本环境不是全局(为 nil)。右键守卫只能写 if event.button == MOUSEB_RIGHT then return end绝不能event.button ~= MOUSEB_LEFT(会让左键 0 ~= nil 恒真而被静默吞掉)。
  3. 丢弃落点别做屏幕反解event.x/y 在有/无 CSS transform 的祖先链下语义不同,ScreenToWorld 反复踩坑。要丢脚底就直接 ActorSpatialService.GetPosition(player.id)ScreenToWorld 只留给"拖到谁身上"的命中判断。
穿透层修复
经验一:CSS transform 穿透层修复模式 问题本质 当容器使用 CSS transform: translateX/Y/scale 平移/缩放子树时,即使父容器有 overflow = "hidden",引擎的命中检测可能不裁剪越界区域。如果 transform 后的子树有更高的 zIndex,它会在视觉范围外拦截其他兄弟节点的指针事件。 受影响的场景 地图编辑器(右键平移/滚轮缩放后)
官方
4 赞
地面样式配置归一化
1. 唯一真相源 scripts/数据/地图/地面样式配置.lua local FloorStyleConfig = {} -- ★ 所有地面样式集中定义在这里 FloorStyleConfig.STYLES = { ["木地板"] = { texture = "image/地面/木地板_无缝.png", tileSize = 40 }, ["草地"] = { texture = "imag
官方
4 赞
右键拖拽平移
一、问题本质 不是平移公式写错,而是 UrhoX UI 库指针事件派发的两个不对称陷阱叠加: 陷阱 现象 根因文件 OnPointerMove 不向父冒泡 右键按下 isPanning_=true,但 move 事件永远到不了 mapView urhox-libs/UI/Core/Widget.lua(OnPointerDown/Up 有 parent 冒泡,OnPointerMove 没有) H
官方
4 赞
以鼠标为锚点缩放
核心数学原理 缩放前后,鼠标指向的那个世界坐标点必须保持在同一屏幕位置。 已知:旧缩放 oldZoom、新缩放 newZoom、鼠标锚点屏幕坐标 (ax, ay)、当前偏移 (panX, panY) 步骤1:算出旧缩放下鼠标指向的世界坐标 wx = (ax - panX) / oldZoom wy = (ay - panY) / oldZoom 步骤2:新缩放后反解新偏移,让 wx/wy
官方
1 赞
小人的认知系统
这是一个很好的思路。RND 的核心——用"预测误差"度量新奇度,驱动探索——可以直接映射到校园小人的认知系统上,而且不需要真的跑神经网络。下面是方案讨论。 RND 思想到校园小人的映射 RND 原始结构 vs 校园简化版 RND 组件 原始含义 校园小人对应 目标网络(固定随机) 为每个状态生成固定特征 环境的客观快照:房间类型、物件数、人员密度、卫生度、当前时段 预测网络(可训练) 学习预测目标
官方
4 赞
图片加载三层优化设计
目标场景:地图编辑器"物品/墙体/地面/房间/人员"调色板弹窗的图片首帧显示 文档目的:沉淀"图片为什么慢、怎么治、发布后玩家体验如何、未来加资源时 preload_groups 怎么取舍"的完整结论,避免重复踩坑。 一句话结论 WASM 下图片"慢"不是单一问题,而是下载 / 解码 / 上传 GPU 三件事全堆在"玩家点弹窗那一刻"做导致的。优化的本质是把这三件事提前到启动期做(让玩家点的时候图
官方
1 赞
精灵平滑移动、墙碰撞体积
一、 渲染层用连续坐标(scripts/页面/校园地图.lua,三处统一): -- NPC 定位(第408-410行) local contLeft = (character.x or 0) * 5 - 18 - charLeft local contTop = (character.y or 0) * 5 - 36 - charTop -- 主角定位(第504-506行) local con
官方
1 赞
槽位变更只走原子方法
经验 下标 = 视觉格子,空槽是合法状态。定长 9 槽,inventory[i]=nil 即第 i 格空着,中间允许空。任何"把物品往前挪/压缩/重排"的操作都会让"你看到的格子"和"数组真实下标"脱节 → 物品消失。渲染直映 inventory[i],数据层绝不重排。 稀疏表禁用 #。带 nil 洞的表 #inv 是未定义的(可能=0 直接读成空包,或=洞前边界)。所有遍历上界一律 Player
官方
4 赞
修复"地图编辑器刷新数据丢失"
问题诊断过程 用户报告:编辑器建好房间 → 保存 → 刷新页面 → 进编辑器 → 数据全没了。 关键线索:用户发现"校园日"模式刷新后能看到房间,但一进编辑器就没了。 这直接指向了编辑器页面自身的初始化逻辑,而非持久化链路。 根因 地图编辑模式.lua 的 EnsureBlankEditorLayout() 设计意图是"首次打开编辑器提供空白画布",用模块级布尔 editorLayoutClear
官方
4 赞
自动寻路赠送
核心功能 携带模式(点击物品跟鼠标)下: ≤8格:点击小人直接赠送(金色边框) >8格:点击远距离小人 → 自动寻路走向目标 → 到达8格内自动赠送 取消:玩家按方向键/WASD/摇杆手动移动 → 立即取消,恢复物品 关键经验(3条踩坑教训) 经验1:类型标识不是中文,是英文枚举值 地图编辑器放置的角色 type/job 字段是英文标识(teacher_3、doctor、cleaner_2、tea
官方
1 赞