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


一、拾取:scripts/页面/校园地图.lua → CreateObjectDot
物品点是一个 UI.Panel,关键是 zIndex 和 onClick 守卫。
lualocal 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 格:
lualocal 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


二、丢弃:scripts/界面组件/底部物品栏.lua → 拖拽 pointerup
抬起时两条分支:拖到小人→扔给他;否则→丢玩家脚底(不反解坐标)。
luaslot: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)


三个可复用的「避雷口诀」
- 同父容器的命中层叠看 zIndex:需要被点的交互层(物品、按钮命中区)必须显式给 zIndex,否则会被数组中靠后、无 zIndex 的层(如主角标记)盖住。
- MOUSEB_LEFT 在本环境不是全局(为 nil)。右键守卫只能写 if event.button == MOUSEB_RIGHT then return end,绝不能写 event.button ~= MOUSEB_LEFT(会让左键 0 ~= nil 恒真而被静默吞掉)。
- 丢弃落点别做屏幕反解。event.x/y 在有/无 CSS transform 的祖先链下语义不同,ScreenToWorld 反复踩坑。要丢脚底就直接 ActorSpatialService.GetPosition(player.id);ScreenToWorld 只留给"拖到谁身上"的命中判断。

