From 8daf5d27aaebfafb429291d9a37ef4ee59c42cbd Mon Sep 17 00:00:00 2001 From: 1165565692-lang <1165565692@qq.com> Date: Wed, 10 Jun 2026 01:30:06 +0800 Subject: [PATCH] Fix layered collision selection --- game/docs/modules/map_editor.md | 4 +-- tools/map_editor/README.md | 8 ++--- tools/map_editor/app.js | 57 +++++++++++++++++++++++++-------- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/game/docs/modules/map_editor.md b/game/docs/modules/map_editor.md index 102e6fe..d1b560b 100644 --- a/game/docs/modules/map_editor.md +++ b/game/docs/modules/map_editor.md @@ -94,7 +94,7 @@ http://127.0.0.1:8787/tools/map_editor/ - Canvas 可视化 world、camera、player spawn、collision、rect、tile_rect、battle zone、spawn。 - 对象列表选择和画布点击选择。 - 网格吸附拖动对象。 -- 通过“编辑目标”过滤画布命中目标,默认“智能选择”优先选中 `tile_rect` 和图形,避免被重叠 `collision` 抢选。 +- 通过“编辑目标”过滤画布命中目标,默认“智能选择”把 `collision` 当作上层覆盖;重叠位置第一次点选碰撞块,再点一次切到下层 `tile_rect` 或图形。 - 矩形对象支持右下角手柄缩放。 - 属性面板编辑选中对象的数值或文本。 - 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。 @@ -137,7 +137,7 @@ assets/stage/stage_01/props/<图片文件名> 2. 已完成:能看到地面、平台、battle zone、spawn point。 3. 已完成:能新增/移动/删除一个 `collision`。 4. 已完成:能新增/移动/删除一个 `tile_rect`。 -5. 已完成:`tile_rect` 与 `collision` 重叠时,默认智能选择可直接选中并移动铺砖区。 +5. 已完成:`tile_rect` 与 `collision` 重叠时,默认智能选择先选中并移动碰撞块,再次点击同一区域可切到铺砖区。 6. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。 7. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。 8. 已完成:能撤回最近编辑操作。 diff --git a/tools/map_editor/README.md b/tools/map_editor/README.md index 70da690..1eeba6b 100644 --- a/tools/map_editor/README.md +++ b/tools/map_editor/README.md @@ -24,7 +24,7 @@ http://127.0.0.1:8787/tools/map_editor/ - 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。 - 从画布或对象列表选择对象。 - 在画布上拖动对象,支持网格吸附。 -- 通过“编辑目标”过滤画布选择对象,默认“智能选择”会优先选中铺砖区和图形,避免被重叠碰撞块挡住。 +- 通过“编辑目标”过滤画布选择对象,默认“智能选择”把碰撞块当作上层覆盖;同位置对象第一次点选碰撞块,再点一次切到下层铺砖区或图形。 - 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。 - 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`。 - 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。 @@ -43,8 +43,8 @@ http://127.0.0.1:8787/tools/map_editor/ ## 调整碰撞块 -1. 在左侧“编辑目标”里选择“碰撞块”。 -2. 在画布上点击红色碰撞框,或从对象列表选择对应碰撞块。 +1. 在左侧“编辑目标”里选择“智能选择”或“碰撞块”。 +2. 在画布上点击红色碰撞框,或从对象列表选择对应碰撞块;智能选择下碰撞块会优先命中。 3. 拖动碰撞块本体可以移动位置。 4. 拖动右下角蓝色手柄可以调整宽高。 5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。 @@ -52,7 +52,7 @@ http://127.0.0.1:8787/tools/map_editor/ ## 调整铺砖区 1. 在左侧“编辑目标”里选择“智能选择”或“铺砖区”。 -2. 在画布上点击铺砖区,或从对象列表选择对应铺砖区。 +2. 如果铺砖区和碰撞块重叠,智能选择下先点到碰撞块,再在同一区域点一次会切到铺砖区;也可以直接切到“铺砖区”。 3. 拖动铺砖区本体可以移动位置。 4. 拖动右下角蓝色手柄可以调整列数和行数。 5. 如果要编辑同位置碰撞体,再把“编辑目标”切到“碰撞块”。 diff --git a/tools/map_editor/app.js b/tools/map_editor/app.js index 93f452d..e105c59 100644 --- a/tools/map_editor/app.js +++ b/tools/map_editor/app.js @@ -731,21 +731,22 @@ function getSelectedObject() { function onCanvasMouseDown(event) { const world = screenToWorld(event.offsetX, event.offsetY); const selected = getSelectedObject(); - const object = hitTest(world.x, world.y); + const stack = hitStackAt(world.x, world.y); + const object = stack[0] || null; if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { state.dragging = { resize: true, last: snapPoint(world) }; setCanvasCursor("nwse-resize"); return; } - if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) { - state.dragging = { last: snapPoint(world) }; + if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object, stack)) { + state.dragging = dragStateFor(selected, stack, world, true); setCanvasCursor("move"); return; } if (object) { state.selected = object.key; - state.dragging = { last: snapPoint(world) }; + state.dragging = dragStateFor(object, stack, world, selected && sameSelection(selected.key, object.key)); setCanvasCursor("move"); refresh(); } else { @@ -775,6 +776,7 @@ function onCanvasMouseMove(event) { pushUndo(state.dragging.resize ? "调整对象尺寸" : "移动对象"); state.dragging.undoPushed = true; } + state.dragging.moved = true; if (state.dragging.resize && object.resize) { object.resize(dx, dy); } else if (object.move) { @@ -786,7 +788,11 @@ function onCanvasMouseMove(event) { } function onCanvasMouseUp() { + const dragging = state.dragging; state.dragging = null; + if (dragging && dragging.cycleOnClick && !dragging.moved && dragging.cycleKeys && dragging.cycleKeys.length > 1) { + cycleSelection(dragging.cycleKeys, dragging.cycleFrom); + } setCanvasCursor("crosshair"); } @@ -802,19 +808,23 @@ function onCanvasWheel(event) { } function hitTest(x, y) { - const objects = hitCandidates(); - for (const object of objects) { + return hitStackAt(x, y)[0] || null; +} + +function hitStackAt(x, y) { + const stack = []; + for (const object of hitCandidates()) { const bounds = object.bounds && object.bounds(); if (!bounds) continue; - if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) return object; + if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) stack.push(object); } - return null; + return stack; } function hitCandidates() { const objects = collectObjects().filter(isHitFilterEnabled); if (state.hitFilter !== "all") return objects.reverse(); - const priority = { tile: 0, rect: 1, spawn: 2, collision: 3, zone: 4, player: 5, camera: 6 }; + const priority = { collision: 0, rect: 1, tile: 2, spawn: 3, zone: 4, player: 5, camera: 6 }; return objects .map((object, index) => ({ object, index })) .sort((a, b) => { @@ -832,9 +842,29 @@ function isInsideBounds(object, x, y) { return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height; } -function canDragSelectedInPlace(selected, topHit) { +function canDragSelectedInPlace(selected, topHit, stack) { if (state.hitFilter !== "all") return true; - return topHit && sameSelection(selected.key, topHit.key); + return Boolean(topHit) && stack.some((object) => sameSelection(object.key, selected.key)); +} + +function dragStateFor(object, stack, world, cycleOnClick) { + return { + last: snapPoint(world), + cycleKeys: stack.map((item) => item.key), + cycleFrom: object.key, + cycleOnClick, + moved: false, + }; +} + +function cycleSelection(cycleKeys, currentKey) { + const index = cycleKeys.findIndex((key) => sameSelection(key, currentKey)); + if (index < 0) return; + const nextKey = cycleKeys[(index + 1) % cycleKeys.length]; + if (!sameSelection(nextKey, state.selected)) { + state.selected = nextKey; + refresh(); + } } function isHitFilterEnabled(object) { @@ -846,12 +876,13 @@ function isHitFilterEnabled(object) { function updateCanvasCursor(event) { const world = screenToWorld(event.offsetX, event.offsetY); const selected = getSelectedObject(); - const object = hitTest(world.x, world.y); + const stack = hitStackAt(world.x, world.y); + const object = stack[0] || null; if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { setCanvasCursor("nwse-resize"); return; } - if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) { + if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object, stack)) { setCanvasCursor("move"); return; }