下载 App

游戏中墙与门的碰撞判断:两阶段检测架构

05/0630 浏览开发交流
在魔塔类网格游戏中,"墙"和"门"看起来都是挡路的障碍物,但它们的交互逻辑完全不同。本文拆解一套实际运行在生产项目中的两阶段检测架构——第一阶段做通行性判断,第二阶段做交互触发——解决"同样是走不通,墙要弹回来,门要尝试打开"的设计问题。
horizontal linehorizontal line
问题:同一个"不可通行",两种处理结果在网格移动的游戏中,玩家每次移动的目标是相邻的一格。最朴素的做法是判断目标格能不能走:能走 → 移过去
不能走 → 不动
这对纯墙壁够用了。但一旦引入门,问题就出现了:
  • :不可通行,碰到就弹回,没有任何交互
  • :不可通行,但碰到要检查钥匙、消耗资源、打开变成空地
  • NPC:不可通行,但碰到要打开对话/商店
复制-- ❌ 反模式:判断和交互混在一起
if target == WALL then
    -- 弹回
elseif target == DOOR then
    -- 检查钥匙 → 开门
elseif target == DOOR_BLUE then
    -- 检查蓝钥匙 → 开门
elseif target == NPC then
    -- 打开商店
elseif ...
每加一种障碍就多一个分支,移动逻辑和交互逻辑耦合在一起,改一个容易破坏另一个。
解决方案:两阶段检测把「能不能通行」和「碰到了做什么」拆成两个独立阶段:阶段 1:移动判断(PlayerController)
  → 目标格可通行?移过去
  → 目标格不可通行?弹回 + 标记"检测到障碍"
阶段 2:交互触发(主循环)
  → 消费标记 → 读取面前是什么 → 分发到对应处理器
两阶段之间通过一个布尔标记 doorDetected_ 解耦。为什么要跨帧?阶段 1 发生在玩家输入处理时(PlayerController.Update),此时玩家可能还没对齐到格子中心。阶段 2 发生在下一帧的主循环中(main.lua Update),此时玩家已经对齐,面朝方向确定,读到的"面前一格"坐标是准确的。
阶段 1:通行性判断瓦片分类首先定义哪些瓦片是"实体"(不可穿越):lua
复制-- TileTypes.lua
local SOLID_TILES = {
    [TILE.WALL]       = true,   -- 石墙
    [TILE.DOOR]       = true,   -- 黄门
    [TILE.DOOR_BLUE]  = true,   -- 蓝门
    [TILE.DOOR_RED]   = true,   -- 红门
    [TILE.DOOR_PURPLE]= true,   -- 紫门
    [TILE.HOUSE]      = true,   -- NPC(蘑菇屋)
    [TILE.CAREER_NPC] = true,   -- 职业NPC
}
注意:门和墙在这个表里是平等的——它们都是实体,都不可通行。这一层不关心"为什么不能走"。通行性查询lua
复制-- MapManager.lua
function MapManager.IsTilePassable(tx, ty)
    -- 边界检查
    if tx < 1 or tx > mapWidth or ty < 1 or ty > mapHeight then
        return false
    end
    local tile = currentMapData_[ty][tx] or TILE.GRASS
    return not SOLID_TILES[tile]
end
返回值只有 true / false,不区分原因。移动判断 + 标记玩家按下方向键时:lua
复制-- PlayerController.lua
local doorDetected_ = false  -- 检测标记
-- 键盘按下方向键
local nx, ny = cx + dx, cy + dy  -- 目标格坐标
if MapManager.IsTilePassable(nx, ny) then
    -- ✅ 可通行 → 移动到目标格
    waypoint = { x = TileCenterX(nx), y = TileCenterY(ny) }
else
    -- ❌ 不可通行 → 对齐回当前格中心
    waypoint = { x = TileCenterX(cx), y = TileCenterY(cy) }
    -- 关键:如果是地图内的障碍(不是边界外),标记为"检测到"
    if inBounds then
        doorDetected_ = true
    end
end
这里的核心设计:被挡住时不立即处理交互,只设标记。为什么要区分"地图内的障碍"和"边界外"?因为撞到地图边界没有任何交互意义,不需要触发阶段 2。标记消费接口lua
复制function PlayerController.ConsumeDetection()
    if doorDetected_ then
        doorDetected_ = false
        return true
    end
    return false
end
一次性消费:读取后立即清除,防止重复触发。面前一格查询lua
复制function PlayerController.GetFacingTile()
    local px, py = currentTile()
    -- 根据玩家面朝方向偏移一格
    if direction == Dir.UP    then py = py - 1
    elseif direction == Dir.DOWN  then py = py + 1
    elseif direction == Dir.LEFT  then px = px - 1
    elseif direction == Dir.RIGHT then px = px + 1
    end
    return px, py
end
阶段 2:交互分发
主循环每帧检查标记,消费后查面前是什么,分发到对应处理器:lua
复制-- main.lua Update()
if PlayerController.ConsumeDetection() then
    local fx, fy = PlayerController.GetFacingTile()
    local facingTile = MapManager.GetTile(fx, fy)
    if ALL_DOORS[facingTile] then
        -- 面前是门 → 尝试开门
        if CombatSystem.TryOpenDoor(player, facingTile) then
            MapManager.SetTile(fx, fy, TILE.GRASS)  -- 门变空地
        end
    elseif facingTile == TILE.HOUSE then
        -- 面前是 NPC → 打开商店
        ShopPanel.Show()
    end
    -- 面前是墙 → 什么都不做(静默弹回已在阶段 1 完成)
end
墙不需要显式处理——阶段 1 已经把玩家弹回去了,阶段 2 里 WALL 不匹配任何分支,自然跳过。新增障碍类型只需要在这里加一个 elseif,不影响移动逻辑。开门逻辑lua
复制-- CombatSystem.lua
function CombatSystem.TryOpenDoor(player, doorTileType)
    local color = DOOR_COLOR[doorTileType]  -- 门瓦片 → 钥匙颜色
    if player.keys[color] and player.keys[color] > 0 then
        player.keys[color] = player.keys[color] - 1
        return true   -- 开门成功
    end
    return false      -- 没有钥匙
end
门的颜色和钥匙颜色通过一张映射表对应:lua
复制local DOOR_COLOR = {
    [TILE.DOOR]        = "yellow",
    [TILE.DOOR_BLUE]   = "blue",
    [TILE.DOOR_RED]    = "red",
    [TILE.DOOR_PURPLE] = "purple",
}
新增门颜色只需要:SOLID_TILES 加一行 + ALL_DOORS 加一行 + DOOR_COLOR 加一行。开门逻辑本身不需要改。
寻路的特殊处理:门是"半透明"的上面说的是键盘逐格移动。但触屏点击移动用的是 BFS 寻路,这里有一个有趣的设计差异:寻路时门被视为可通行。lua
复制-- BFS 寻路
if MapManager.IsTilePassable(nx, ny) then  -- 门不在这里被挡
    -- 加入队列
end
等等,IsTilePassable 不是会把门判为不可通行吗?实际上寻路用了一个略微不同的检查——它走的路径可以穿过门的位置,只是在实际移动到门旁时,触发阶段 2 的开门逻辑。如果开门成功,门变成空地,角色继续沿路径走下一格;如果没有钥匙,角色停在门前。这意味着玩家在触屏模式下点击门后面的格子,寻路算法会规划一条经过门的路径,走到门前自动尝试开门,而不是报"无法到达"。这比"门在寻路中被视为墙"的体验好得多。障碍类型寻路阶段实际移动碰到后的交互墙不可通过不可通过无门可通过不可通过检查钥匙 → 开门NPC不可通过不可通过打开对话/商店空地可通过可通过无
概率门:编译期消失的第五种瓦片地图模板中还有一种特殊符号 i——概率门。但它和上面四种瓦片不在同一层:lua
复制local SYMBOL_TO_TILE = {
    ["#"] = TILE.WALL,
    ["."] = nil,           -- 空地
    ["i"] = "RANDOM_DOOR", -- 概率门
}
概率门在地图加载时就被掷骰替换了:掷骰结果概率替换为消失50%空地黄门43%TILE.DOOR蓝门6.5%TILE.DOOR_BLUE红门0.5%TILE.DOOR_RED运行时不存在"概率门"这种瓦片——它在加载阶段就变成了空地或具体颜色的门。后续的两阶段检测完全不知道这个门是"概率门变来的"还是"固定放置的",处理逻辑完全一致。这个分层很干净:地图生成层负责决定"这里有没有门",碰撞检测层负责处理"碰到门了怎么办",互不干涉。
架构总结┌─────────────────────────────────────────────────┐
│                   地图生成层                      │
│  TerrainTemplates → 概率门掷骰 → 确定瓦片类型     │
└──────────────────────┬──────────────────────────┘
                       ↓
┌──────────────────────┴──────────────────────────┐
│                 数据定义层                        │
│  TileTypes.lua                                   │
│  ├─ SOLID_TILES  → 通行性表(墙、门、NPC 都在)   │
│  ├─ ALL_DOORS    → 门集合(黄蓝红紫)             │
│  └─ DOOR_COLOR   → 门→钥匙颜色映射               │
└──────────────────────┬──────────────────────────┘
                       ↓
┌──────────────────────┴──────────────────────────┐
│              阶段 1:移动判断                      │
│  PlayerController.lua                            │
│  ├─ IsTilePassable()  → 能走就走,不能走就弹回    │
│  ├─ doorDetected_     → 被挡住时设标记            │
│  └─ GetFacingTile()   → 提供面前坐标              │
└──────────────────────┬──────────────────────────┘
                       ↓  (跨帧,通过布尔标记传递)
┌──────────────────────┴──────────────────────────┐
│              阶段 2:交互分发                      │
│  main.lua Update()                               │
│  ├─ ConsumeDetection()  → 一次性消费标记          │
│  ├─ ALL_DOORS[tile]?    → CombatSystem.TryOpenDoor│
│  ├─ HOUSE?              → ShopPanel.Show()        │
│  └─ WALL?               → 静默跳过(已弹回)      │
└─────────────────────────────────────────────────┘
核心思路就一句话:通行性判断不关心"为什么不能走",交互触发不关心"怎么走过来的"。两阶段各管各的,通过一个布尔标记连接,新增障碍类型只需要在数据表加一行、在分发逻辑加一个分支。
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
68 赞
21 回复
01:27
《大二暑假,我用AI做了一款属于自己的游戏》截图
《大二暑假,我用AI做了一款属于自己的游戏》
本人自制游戏现已加入taptap封闭测试(仅适配安卓机型)欢迎大家前来尝试(八月十号可以正式开始游玩) 如果有建议或者bug反馈可以加入qq群928501443 #发现好游戏 #游戏日常 #浅评一下 #今天游戏圈发生了啥
3 赞
03:06
为了不被斩杀,我只能做到这些了截图
为了不被斩杀,我只能做到这些了
之前一直申请不到计划3,之前说太花哨,后面说体量完成度不足,在我一番小作文下,编辑终于给了我解答,什么原生质感,玩家开局体验啥的。 本来计划2也够用,准备以后再优化了,结果计划2赠送的积分要砍半了,学生党实在消费不起,于是只能再冲一波计划3了 从周六中午到现在一天半时间,优化了一遍角色卡,战斗卡,还有抽卡界面,优化了新手教程,该说不说确实比之前要舒服些。 前半部分为新版,后半部分为旧版。 明天再次
32 赞
6 回复
嗯,真的,随便了(并不是什么下台阶)截图
嗯,真的,随便了(并不是什么下台阶)
只是觉得,为以前这样让朋友推荐这个平台的我,感到。。。无所谓了。你们喜欢吧。
1 赞
3 回复
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
122 赞
44 回复
开发心得-界面布局心得截图
开发心得-界面布局心得
大家好,我正在用 TapTap 制造制作一款2D 治愈林间徒步旅行游戏《徒步荒野》。 我分享一下我的开发心得,希望能有用,下面是一些界面布局的心得。 第一步:定版式、操作 先确定游戏是横版还是竖版、需要哪些操作按钮,敲定整体玩法思路。 我的玩法灵感参考《边境之旅》,主打散步看风景、沿途触发随机事件。 原版是 3D,考虑到自己是新手,选择2D 横版街机式来做,制作更简单、好上手。 第二步:敲定全局美
6 赞
3 回复
游戏自荐《少女终焉契约》截图
游戏自荐《少女终焉契约》
之前玩放置类型的rpg游戏的时候我就在想,现在游戏的操作有的太过于繁琐了,有没有一款休闲一点的,想上线就上线,离线的时候角色能自动行动的游戏呢?感谢TapTap制造,我按照我的想法开发出了现在这款游戏。 一、故事背景和UI 游戏的故事背景是传统的异世界穿越故事,玩家被女神召唤到异世界,随后组建工会,招募美少女,打怪升级探索世界。所以我选择了更加二次元萌系画风的UI风格,主页为各个主要的功能,点开来
8 赞
3 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
00:54
等距场景编辑器——自行编辑2D瓦片场景地图截图
等距场景编辑器——自行编辑2D瓦片场景地图
一个可嵌入任何 UrhoX 项目的完整等距瓦片地图编辑器,适用于 RPG、策略、模拟经营等类型游戏的关卡设计。 SKILL审核已通过,各位创作者可以根据自己的需要对工具进行调整修改 声明:该工具仅作为场景编辑以及序列化,交互等功能需要用户自行在项目中定义 后续会根据实际情况补充rule tile相关逻辑,实现tile map编辑自由 目录 1. 界面总览 2. 六种编辑工具 3. 双视图模式 4.
精华
21 赞
11 回复
「游戏上新」秦王的游戏截图
「游戏上新」秦王的游戏
你是秦王嬴政身边的廷臣。 方士徐福献上一匣【帝王卡牌】,四张凶牌封死命运: 屠戮|淫欲|奢华|征伐 七日死限,必须一一折断卡牌对应的欲望任务。 办不成? 秦王殿上长剑出鞘,君臣性命,顷刻倾覆。 ⚔游戏核心玩法 ▪回合卡牌策略,抉择牵动秦廷朝堂势力 ▪朝堂派系博弈:王权、法家、军功贵族、方士暗流角力 ▪多重结局:功成辅秦王一统,或是任务失败血染咸阳宫 ▪随机事件触发,每一局嬴政的猜忌程度截然
4 赞
2 回复