拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity 2D岛屿地图碰撞与阻挡全攻略:从Tilemap到复合碰撞体

做2D游戏只要地图范围一大“碰撞与阻挡”这四个字就绕不开了。尤其是岛屿地图陆地、沙滩、草丛、岩石、树林、水面互相穿插玩家可以往哪走、不可以往哪走、踩到水是不是要减速这些规则全都要靠碰撞系统一条一条落实。我最早做这类地图时给每一块地形单独拖Collider结果角色在拼接缝里卡得怀疑人生后面把Tilemap Collider 2D和Composite Collider 2D用对了一块几百格的地图最终只生成一个复合碰撞体性能又稳、编辑又省事。这篇内容就以Unity 2D游戏的岛屿地图为背景把碰撞检测、阻挡体配置、边缘封锁、区域触发器这些东西从设计思路一直讲到代码落地。适合正在做2D开放地图、关卡类游戏或者想系统理解Unity 2D碰撞体系的初中级开发者参考。里面所有操作都基于Unity 2021及以上的版本2D项目用默认URP或内置渲染管线都行差别不大。1. 岛屿地图阻挡需求分析与选型1.1 一张岛屿地图里到底有哪些阻挡很多人一说“阻挡”第一反应就是给障碍物加个碰撞体让玩家不能穿过去。实际上在一个岛屿地图里阻挡至少分成四个维度少考虑一个后面都会返工。第一类是硬阻挡即物理层面走不过去的东西。岛屿上的岩石、树干、房屋、栅栏、山体都属于这一类。这类阻挡要用真实参与碰撞的Collider玩家碰到之后会被物理引擎推开或者直接挡住不能前进。硬阻挡的核心要求是“边缘贴合”你画了一块石头碰撞体最好不要比石头大一圈也不要缩进去一截否则玩家会被空气墙挡住或者卡进石头里。第二类是软阻挡也就是可以进入但有负面效果的区域。比如浅滩玩家可以踩进去但移动速度要降下来毒沼进去之后每秒掉血流沙进去之后会缓慢下陷。这类区域通常用Trigger触发器来做不产生物理碰撞只负责检测“玩家进没进这块区域”。第三类是边界阻挡目的是不让玩家走出地图边界。岛屿四周是海如果玩家一路划出地图外就会掉进无限深渊。边界阻挡可以用Edge Collider 2D沿岛屿外围绕一圈也可以在场景周围放一圈边界墙二选一即可。第四类是逻辑阻挡。比如某扇门需要钥匙才能打开某段藤蔓在任务完成前不允许玩家通过这类阻挡本质上是一块“可开关的碰撞体”在条件满足之后关闭碰撞体或者移动位置。很多人前期规划时忽略逻辑阻挡等到做任务系统时才发现以前当美术背景画的铁门根本没有碰撞体角色可以直接穿过去非常出戏。把阻挡分清楚之后岛屿地图的碰撞设计才有章可循。接下来的内容我会按照硬阻挡、边界阻挡、软阻挡、逻辑阻挡的顺序逐个说明怎么在Unity里落地。1.2 碰撞方案选型与层级规划Unity 2D碰撞方案选型核心是看你的地图是怎么组织的。如果你的岛是用Tilemap一格一格拼出来的那就不要手动给每一格加Box Collider 2D那样碰撞体数量会爆炸而且格子之间的接缝会造成各种抖动和卡顿。优先方案是Tilemap Collider 2D加Composite Collider 2D。原理很简单Tilemap Collider 2D会读取瓦片地图上所有带碰撞的瓦片生成对应的碰撞形状Composite Collider 2D负责把这一堆碰撞形状合并成一个复合体最终只保留一个Rigidbody 2D和一个Collider。这样整个岛屿的陆地碰撞从几十上百个碰撞体变成一个连续的多边形轮廓开销小得多。选好方案之后还要规划物理层级。Unity的Project Settings里有个Layer Collision Matrix也就是碰撞矩阵它决定了哪些层级之间能发生碰撞。我习惯在2D游戏里至少分四层层级用途参与碰撞矩阵Default默认层场景装饰、粒子、无碰撞物体只与Player和Environment做必要检测Player玩家角色与Environment、Trigger碰撞Environment地面、岩石、建筑等硬阻挡与Player、Enemy碰撞Trigger水域、毒沼、任务区域等触发器与Player、Enemy触发检测层级规划的目的是让碰撞矩阵尽可能“关小口子”。比如Environment层完全不需要与Trigger层碰撞那就在矩阵里取消这两层的勾选减少不必要的物理计算。还有个容易忽略的点如果玩家脚下所有的地面都是Environment层玩家跳跃落地的一瞬间会触发大量碰撞回调矩阵设置合理之后回调数量能明显下降。物理层级做好后续无论是加敌人、加NPC还是加交互区域都不会出现“这个物体撞了一下不该撞的东西”这种诡异问题。2. 基于Tilemap的岛屿地形碰撞实现2.1 从Tilemap绘制到碰撞体合并的完整步骤先用Tilemap把岛屿地形搭出来。在Hierarchy面板右键选择2D Object - Tilemap - RectangularUnity会自动创建一个Grid对象和其下的Tilemap子对象。如果你的素材是菱形或六边形格子就选Isometric或Hexagonal但这篇内容以最常用的矩形Tilemap为例。打开Window - 2D - Tile Palette把岛屿相关的瓦片导入然后像画画一样在Tilemap上铺地面。铺的时候注意陆地瓦片和海洋瓦片最好放在不同的Tilemap上比如一个叫GroundLayer一个叫WaterLayer。分开的好处是你只给陆地Tilemap加碰撞水面Tilemap作为纯视觉层碰撞体就不会多算一整片海域。地面铺完之后选中陆地Tilemap在Inspector里点Add Component加一个Tilemap Collider 2D。你会看到每一个有瓦片的格子都被单独生成了碰撞体密密麻麻。这时候先别急着运行直接在同一个Tilemap上再添加一个Composite Collider 2D系统会弹提示说需要Rigidbody 2D点是Unity会自动加一个Body Type为Static的Rigidbody 2D。然后回到Tilemap Collider 2D勾选Used By Composite。这一步是关键。勾选之后所有单个瓦片碰撞体会被Composite Collider 2D吃进去合并成一个大轮廓。运行游戏用Scene视图观察你会看到地面碰撞从一堆小方块变成了一条贴合岛屿形状的连续多边形。这就是岛屿陆地碰撞的基座。这里有几点必须注意。Composite Collider 2D的Geometry Type建议选Polygons。选Polygons会生成贴合地形的多边形角色踩上去比较自然选Outlines只生成边缘线段适合做墙面不适合做地面。另外给Tilemap加的Rigidbody 2DBody Type保持Static重力Scale为0其他参数不要乱动。只要你勾选了Used By Composite就别再去手动改Tilemap Collider 2D里的参数因为运行时它会把碰撞数据统一交给Composite处理。2.2 Collider参数细节与几个关键坑点Tilemap Collider 2D加Composite Collider 2D组合在实际使用中有几个非常容易踩的坑。第一个坑是Tilemap的格子尺寸与Physics2D单位不一致。Unity默认一个Tilemap格子是1乘1单位如果你的素材导入之后Pixels Per Unit是100而每张瓦片是100乘100像素那刚好一个格子等于一个单位没问题。但如果你用64乘64的瓦片却忘了把Pixels Per Unit改成64导入后Tilemap会把瓦片拉伸到一个格子内视觉上没大问题碰撞体却会跟着瓦片被拉伸导致角色碰撞点位和画面不一致。所以导入瓦片素材的时候一定要检查Pixels Per Unit是否和瓦片像素值匹配。第二个坑是Composite Collider 2D对Tilemap动态修改的支持。如果你在游戏里要动态挖坑、炸地形直接修改Tilemap的瓦片Composite Collider 2D会重新生成碰撞体这个过程有性能开销而且偶尔会出现碰撞体比画面慢一拍的情况比如玩家已经看到坑了但碰撞体还在原位。解决方法是需要动态改地形时把Dynamic Tilemap那个选项打开让碰撞体跟随Tilemap实时刷新如果地形不会变保持默认关闭就行性能更好。第三个坑是“Collider边缘太薄”导致的颠簸。岛屿地面瓦片之间如果画了很细的装饰层比如一层草边这类装饰瓦片不要在同一块Tilemap上开启碰撞。细长瓦片碰撞体与玩家胶囊体碰撞时很容易产生抖动。正确做法是把装饰瓦片放在一个单独的Tilemap不给它加Collider或者把装饰瓦片的碰撞类型在Tilemap Palette里设为None。第四个坑是Tilemap的GameObject不要有任何Scale缩放。如果父级Grid或Tilemap的Scale不是1Composite Collider 2D的碰撞数据会出现偏移看起来很正常的岛屿玩家会在某个位置莫明其妙被透明墙挡住。所以做2D Tilemap世界所有Grid和Tilemap对象的Transform.Scale都保持默认值1缩放场景时缩放父物体或者调整摄像机不要缩放Tilemap本身。2.3 岛屿上的道具与装饰物阻挡岛屿地图不可能只有一块平坦地面岩石、树木、房屋这种装饰物才是阻挡的主角。每一个有实际阻挡功能的装饰物都要单独处理。树建议用Circle Collider 2D或者Box Collider 2D都行。树干是圆的用Circle更贴合但要考虑玩家从树旁边经过的体验如果树冠很大、树干很细碰撞体只包住树干玩家就会从树冠下穿过视觉上很怪。这种我建议用Box Collider 2D稍微比树干宽一圈模拟“树冠遮挡区域”碰撞体大小参考树冠投影不要参考树干。大型岩石用Polygon Collider 2D最合适。选中岩石Sprite添加Polygon Collider 2DUnity会自动根据Sprite轮廓生成顶点。生成之后一定要手动检查顶点位置自动生成的轮廓往往包含一些噪点导致碰撞体边缘锯齿感很强玩家贴在岩石上移动时会被这些锯齿“绊”住。手动拖动顶点让碰撞轮廓圆滑一点能极大提升角色贴墙移动的顺滑度。房屋和围墙这类建筑建议不要做单一的复杂碰撞体。一个房子画成Box Collider 2D虽然简单但门的位置没法通过玩家会被空气墙挡住。正确的做法是把一栋建筑拆成几块区域墙壁用Box Collider 2D门口留空柱子用Circle Collider 2D。这样玩家既能绕着房子走也能从门口进去碰撞表现才符合直觉。加装饰物阻挡时还有个美术层面的事2D游戏的遮挡关系。角色走到树后面应该被树冠遮住。这个不归碰撞系统管但要配合Sorting Layer和Order in Layer来做。地面、树根、树干、树冠、角色、屋顶这些要分层排序。我习惯的做法是地面整个Sorting Layer设为0树根和树干设为10角色设为20树冠设为30角色走到树冠下方时树冠的Sprite会在角色上方自然遮挡。这一套做顺了岛屿地图的层次感会非常舒服玩家也会觉得“这游戏有空间感”而不是所有东西摊在一张纸上。3. 岛屿边缘封锁与玩家移动的碰撞基础3.1 用Edge Collider 2D把整座岛圈起来岛屿四周是海玩家的移动范围应该被限制在岛内。这里有人会在Grid外面摆一圈Box Collider 2D当围墙简单粗暴但不好看。更贴合岛屿手感的方式是用Edge Collider 2D沿着岛屿边缘画一圈封闭线段。操作步骤是在场景中创建一个空GameObject命名Boundary给它添加Edge Collider 2D。在Inspector里点Edit Collider然后在Scene视图里沿着岛屿的海岸线边缘点出一圈顶点。顶点数量不用太多每隔一段距离一个点就行Unity会自动在顶点之间连成线段。画边界的时候最核心的技巧是让边界点稍微往海的方向偏移一点而不是正好压在海陆分界线上。因为陆地Tilemap的碰撞体轮廓本身已经有半格子的边缘如果边界再压上去玩家走到海岸线时会被两个碰撞体“卡”一下。让Edge Collider往外偏移0.2到0.3个单位玩家的移动就能从陆地碰撞体自然过渡到边界碰撞体手感顺滑。Edge Collider 2D是单向的这个要注意。它只有单面碰撞所以在保证玩家从内侧朝外走会被挡住的前提下边界线法线方向必须朝向岛内。如果方向反了玩家会直接穿过去。验证方法很简单运行游戏控制玩家往海边走如果撞到空气墙说明方向正确如果直接走出去把Edge Collider 2D的顶点顺序反一下或者把整个边界旋转180度再重新对位。3.2 角色掉出地图后的兜底恢复逻辑边界碰撞做得再严密也有出Bug的时候。Tilemap动态修改、物理引擎瞬间穿透、玩家使用瞬移技能都可能把角色送到边界外。这时候如果没有兜底逻辑角色就会在黑色海洋里无限下落。兜底逻辑最简单的做法是利用一个不可见的Trigger区域。在岛屿中心区域底部放一个覆盖全图范围的大型Box Collider 2D勾选IsTrigger放到Trigger层。当玩家进入这个触发器说明已经掉出有效地图范围立刻执行复位。复位代码可以这样写public class FallRecovery : MonoBehaviour { public Transform player; public Vector2 respawnPoint; private void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(Player)) { player.position respawnPoint; } } }这里respawnPoint可以是岛屿中心的出生点也可以是玩家最近经过的安全存档点。如果是开放世界建议用玩家最后踩到的地面位置来更新安全点避免掉坑里被传回出生点导致进度丢失。除了触发器还可以用位置判断做双保险。在Update里检查玩家Transform.position的y值如果小于某个阈值比如-50就强制复位。触发器可能因为物理延迟错判位置判断则完全由逻辑层控制两个同时存在基本能杜绝“角色丢失”的问题。3.3 Player刚体参数与摄像机跟随的配合碰撞和阻挡最终要落实到角色移动上。玩家角色一般用Rigidbody2D加Collider2D注意几个关键参数。Rigidbody2D的Interpolate选项一定不要设为None建议选Interpolate。尤其在地形碰撞体很多、物体移动速度变化频繁的情况下Interpolate可以让角色位置渲染更平滑不会出现碰撞后高频抖动。Collision Detection选Continuous代价是稍微多一点性能开销但对高速移动场景非常有效能够明显减少物理穿透。Linear Drag和Angular Drag保持默认或者调成0。很多新手为了让角色“不滑”会把Linear Drag调得很大但那是纯模拟力的做法并不适合大多数2D动作游戏。实际项目里移动更推荐用Rigidbody2D.velocity直接赋值或者用MovePosition做插值控制而不是AddForce否则角色碰撞后很难停下来。摄像机跟随这部分如果你想做得简单直接用Cinemachine 2D Package创建Virtual Camera把Target设为玩家关掉碰撞体对相机的干涉就行。Cinemachine的好处是玩家在岛屿上移动时镜头会有平滑的阻尼感而且能设置Dead Zone防止镜头因为玩家微小抖动而晃动。有了稳定的镜头碰撞体的小误差不容易被玩家注意到相反镜头一直在跳哪怕碰撞完全正确玩家也会觉得手感很烂。个人经验是物理碰撞和摄像机跟随是两个系统但最终体验是连在一起的。做岛屿地图这种大面积开放场景摄像机的跟随边界也要提前设置好不要让镜头拉到海面上留下大半个屏幕的空白否则玩家会感觉岛屿很小海岛氛围直接归零。4. 触发器级别的软阻挡与区域逻辑4.1 浅滩减速用IsTrigger做水域区域岛屿地图的经典软阻挡就是水域。玩家直接冲进海里如果放着不管就会出现角色在水面上走路的诡异画面。更合理的设计是浅滩可以进去但移动速度大幅下降深水区域直接禁止前进或者强制掉血并传送回浅滩。实现方式是在海洋区域放一个Polygon Collider 2D覆盖整个可进入的海水区域勾选IsTrigger。然后在角色身上挂一个移动控制脚本通过OnTriggerEnter2D和OnTriggerExit2D来切换移动参数。public class PlayerMovement : MonoBehaviour { public float normalSpeed 5f; public float waterSpeed 2f; private Rigidbody2D rb; private float currentSpeed; private void Start() { rb GetComponentRigidbody2D(); currentSpeed normalSpeed; } private void Update() { float moveX Input.GetAxisRaw(Horizontal); float moveY Input.GetAxisRaw(Vertical); rb.velocity new Vector2(moveX, moveY).normalized * currentSpeed; } private void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(Water)) { currentSpeed waterSpeed; } } private void OnTriggerExit2D(Collider2D other) { if (other.CompareTag(Water)) { currentSpeed normalSpeed; } } }这段代码关键点有两个。一是移动用normalized方向向量乘速度避免斜向移动时速度变快二是在OnTriggerEnter里用Tag判断确保只有碰到Water区域才减速碰到其他Trigger不误伤。浅滩减速的手感调节需要反复试。waterSpeed设成normalSpeed的40%到60%左右比较合适太慢玩家会觉得烦躁太快又体会不到“踩水”的感觉。同时可以顺手改一下角色动画的播放速度踩水时动画放慢视觉反馈会非常明显。4.2 带有解锁条件的动态阻挡逻辑阻挡也就是那些“暂时过不去但条件满足后能过去”的区域实现思路是在阻挡物上挂Collider和脚本由脚本控制Collider的开关或位置。常见的例子是岛屿上的石门、闸门、断桥。以石门为例它平时是一块带Box Collider 2D的石头门玩家推不动。拿到钥匙后触发一段开门动画同时把碰撞体关闭。代码可以这样写public class GateController : MonoBehaviour { public GameObject gateObject; public Collider2D gateCollider; public void OpenGate() { gateCollider.enabled false; // 可选播放开门动画 } public void CloseGate() { gateCollider.enabled true; } }这里我特别建议把“开关碰撞体”和“移动物体”分成两步。如果门是滑动的直接对gateObject做Transform平移平移结束之后再关闭碰撞体避免门还在移动时玩家就已经穿过去。如果门是旋转的也是一样动画播放完再禁用碰撞体这样逻辑最稳。动态阻挡还有一个容易忽略的问题碰撞体禁用后如果有物体卡在原来的碰撞体位置上会直接穿透掉落。比如玩家站在门边你刚把门的碰撞体禁用玩家如果正在向门方向移动就会顺着惯性滑进原来门的位置看起来像穿模。处理方法是禁用碰撞体的同时检查门周围有没有玩家或其他可移动物体有的话先做一个推力或者瞬移校正再关掉碰撞体。这块逻辑虽然简单但在关卡设计里很关键。岛屿地图往往会用一条主线把这些门串联起来玩家每到一处都需要钥匙或任务进度来解锁。把Collider的开关做好整个地图的探索节奏就顺了。5. 碰撞与阻挡常见问题排查实录5.1 角色在地形上高频抖动抖动问题是2D碰撞里出现频率最高的也是最容易让人崩溃的。表现是角色站在地上或者贴墙走时身体会不停地轻微上下震动尤其在Tilemap拼成的平坦地面上格外明显。排查抖动先看Rigidbody2D的Interpolate。如果Interpolate是None物理刷新和渲染刷新不同步角色就会出现画面上的高频抖动这种直接改成Interpolate能解决一大半。再看碰撞体的形状和精度。Tilemap单格子的碰撞体如果与相邻格子碰撞体之间存在微小的缝隙玩家的Box Collider 2D在跨越缝隙时会轻微下沉。所以这就是为什么前面强烈建议用Composite Collider 2D合并地形碰撞——合并后的碰撞体是一整块多边形没有接缝抖动自然消失。还有个隐蔽原因是角色自身的Collider太薄。如果你给玩家加了Box Collider 2D高度只有0.1单位落在地形上时物理引擎会因为接触面积太小产生很多不稳定的微型碰撞解算表现为“颤”。把玩家的碰撞体加高加厚一点比如改成0.4乘0.8的胶囊体抖动现象会明显好转。5.2 高速移动下的碰撞穿透玩家开启疾跑、冲刺或者是子弹、飞行道具这类高速物体很容易直接穿过障碍物。这个问题根源在于Unity的物理检测是离散的物体单帧移动距离超过碰撞体厚度时物理引擎就可能错过碰撞点位。解决穿透问题有以下几个组合拳。第一把高速物体的Rigidbody2D Collision Detection改成Continuous。这个选项专门用于处理快速移动物体对静态碰撞体的穿透开销可以接受。第二适当加厚高速物体的碰撞体。第三如果速度实在太快比如每帧移动超过地图半个格子宽度光是Continuous也不够要么用射线检测做自定义阻挡要么把物体切成两帧移动。对于持续高速移动的敌人或飞刀我建议在FixedUpdate里移动物体并且设置较小的Max Speed而不是在Update里直接改Transform。物理系统所有计算都在FixedUpdate时间步里进行保持移动与物理时钟同步能减少大量穿透类Bug。5.3 碰撞体与画面错位碰撞体比看到的画面大一圈或者小一圈是很破坏沉浸感的问题。排查思路看以下几点。先看物体本身的Pivot位置。Sprite的Pivot如果不在底部、而在中心为它添加Collider后碰撞体会跟着Pivot定位容易和画面偏离。岛屿地图里的树和岩石Pivot习惯设在底部中心这样放地上时树根正好压在地面碰撞体也自然贴合。再看GameObject的Scale。任何非1的Scale都会让Collider变形尤其是负Scale还会导致碰撞体方向反转。所以Collider相关物体尽量用原始Scale要放大缩小调整Sprite的大小而不动Transform。最后看Tilemap的Composite Collider 2D和Tilemap数据是否同步。如果运行中动态改过瓦片复合碰撞体刷新延迟就会出现“画面变了碰撞体还是老样子”的错位。把Tilemap Collider 2D的Dynamic Tilemap勾上或者改完瓦片后手动调用TilemapCollider2D.enabled false再true强制刷新一次。5.4 碰撞体数量过多导致的性能下降岛屿地图范围大如果碰撞体数量控制不好即使在PC上也会卡。最直观的指标是Profiler里Physics2D的耗时。降低碰撞体数量的核心思路是合并和分层。Tilemap用Composite Collider 2D合并成整体是最大的优化岛屿上散落的小石头、小草如果不需要阻挡功能就不要加Collider只做视觉需要阻挡的小物数量控制在合理范围内理想情况整个岛屿的场景碰撞体不超过30个。另外Layer Collision Matrix要好好检查。比如玩家层的Projectile子弹就不需要和环境层的静态装饰物产生碰撞敌人的攻击范围Trigger也不需要和环境碰撞。把不必要的碰撞排除掉物理引擎每帧的检测次数会大幅下降。如果有做微信小游戏或者WebGL版本物理性能会更敏感。WebGL平台下2D物理引擎虽然能用但碰撞体的数量和质量都比原生平台要求更高发布之前最好用Profile分析一下Physics2D模块Collider数量超过一定阈值时优先考虑用触发器做简单交互而不是堆物理碰撞体。5.5 复现问题的小技巧碰撞问题最大的难点是“有时候出现有时候不出现”。我个人的经验是不要只靠运行游戏去试要活用Scene视图和物理调试。在Collider所在对象上确保Inspector里的Gizmos显示正常运行后随时观察碰撞体轮廓是否与画面匹配。还可以用Unity的Physics Debugger在Window - Analysis - Physics Debugger里打开它能显示所有Collider的激活状态、休眠状态、接触点信息。碰撞位置不对、碰撞体没激活、物体卡在碰撞体内都能在这个窗口里直观看到。排查2D碰撞问题这个窗口比盲改代码高效太多。还有做2D碰撞相关修改时记住改完一个变量就跑一下。物理碰撞的问题往往和多个参数叠加有关一次改多个参数出了问题很难定位是哪个参数引起的。这种“一次只改一个变量”的排查习惯看起来慢实际省下的时间是以小时计的。在我做过的一个岛屿地图项目里最折腾的不是复杂逻辑而是一块平平无奇的海边石头。它比地图地面多出0.3单位的高度角色跳过它时经常被弹飞。后来把石头的Polygon Collider 2D手动修圆滑又把Player的碰撞体加厚了一点弹飞问题彻底消失。后来我总结出一句话2D碰撞的问题90%出在碰撞体形状没有贴合视觉、参数设置不够保守这两件事上。只要把Collider轮廓调顺、把Rigidbody2D关键参数设置好剩下的逻辑基本都是水到渠成。如果你正在做的也是岛屿地图建议把本文提到的硬阻挡、软阻挡、边界检测、动态开关这几类需求在地图设计阶段就列成一份清单画格子的时候同步标注。等到代码阶段你会发现所有碰撞问题都有了明确归属改起来不慌不忙。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门