资讯动态

Unity平铺网格地图(Tilemap)从零搭建与性能优化全攻略

发布时间:2026/10/21 22:07:12 来源:尧图企业网站定制
做2D游戏搞地图最怕什么我见过太多项目策划改一版地形程序就得手调一整天的坐标美术画好的素材还得在引擎里一张张摆。平铺网格地图Tilemap要解决的就是这个痛点把地图切成规则的格子用瓦片批量拼装碰撞、美术、后续修改全都在同一套网格上完成。这篇文章我会从零开始用一个实际可跑的流程把Unity里平铺网格地图怎么做、为什么这么做、踩坑怎么排查一次讲透。适合正在做2D关卡、想提升地图制作效率的开发者。1. 从需求谈起为什么你的2D游戏需要一个Tilemap系统1.1 直接手画地图的痛点我见过太多新手第一次做横版或者俯视角地图都会选择最原始的办法把美术素材一张张拖进场景然后对着Inspector手动改坐标。这样做小Demo没问题但稍微复杂一点就崩溃了。我举个具体例子假设你要做一张20×15的草地地图素材是1×1的瓦片纯手动摆300个Sprite到场景里还要保证每块严丝合缝光是眼睛对网格就能对半小时。更麻烦的是改图策划说要加一条河所有相关坐标全部要重调图层顺序稍微一乱角色就被草挡住了。还有一个很容易被忽视的问题手摆Sprite做地图时碰撞体是逐块生成的。一个300×300的格子场景就会产生几百个独立的碰撞体物理系统要实时处理这些互相接壤的碰撞边界性能损耗非常不划算。玩家可能感觉不出来但一旦场景变大、动态物体变多帧率波动就开始明显了。所以我觉得地图制作这件事从第一天就应该用对工具而不是等项目中期再返工。1.2 Tilemap 与手写网格的根本差异Tilemap的核心思路是“网格 瓦片”两层结构。网格负责定义空间规则瓦片负责记录每个格子长的什么样、有什么属性。你不再直接操作一个个Sprite的Transform而是操作一个二维数组每个数组元素是一块瓦片。渲染和碰撞交给引擎统一合成。这就是它和“手写网格”本质的区别字典式的数据结构取代了离散对象堆叠。使用Tilemap有几大立竿见影的好处。第一是批量刷图地形、河流、墙壁可以用笔刷直接画出修改成本极低。第二是统一合批同一图集的瓦片会被自动合并批次渲染比几百个Sprite快得多。第三是碰撞合并Tilemap Collider 2D 可以把相邻格子生成一个合并多边形物理边界数量大大减少。第四是数据友好地图本质是一个数据模型运行时改造、存档、程序化生成都非常自然。有人会问那我自己写一个二维数组再加一套网格渲染逻辑是不是也一样理论上可以但你要自己处理网格坐标换算、纹理图集采样、碰撞合并、编辑器可视化、规则瓦片自动过渡这些功能加起来工作量非常大而且很难做得比现成系统更稳定。所以除非你有非常特殊的地形需求比如完全不规则的六边形变体、自定义编辑工具链否则基于Tilemap扩展性价比远高于从零造轮子。2. 动手前的准备工作2.1 项目创建与包管理器检查先从创建项目开始。打开Unity Hub新建一个2D项目。很多人会问我做的是俯视角3D或者2.5D是不是该选3D模板这里我的建议是只要渲染方式还是基于Sprite和平面坐标就选2D模板。2D模板会自动更改编辑器布局、摄像机投影、默认导入器的纹理类型省掉一堆设置。如果实在要混着做后面也可以手动改成正交摄像机。新建项目后第一件事是检查包管理器里有没有Tilemap相关模块。在较新的Unity版本里2D项目默认自带2D Tilemap Editor。打开Window Package Manager在Unity Registry搜索“2D Tilemap Editor”。看不到或者被误删了就点Install装回来。顺带检查2D Sprite、2D PSD Importer这些常用包地图做多了都会用到。另外强烈建议把Edit Project Settings Editor里的Sprite Packer模式设为Enabled For Build这个和后面的合批优化有关我放到性能那一节详细说。很多人一开始不重视这个开关等地图刷大了才发现Draw Call爆炸再回去改图集配置反而麻烦。2.2 弄懂 Grid 和 Tilemap 这对核心组件Unity的Tilemap体系里Grid和Tilemap是父子关系职责完全不同。Grid定义网格的排列规则。Cell Layout决定格子是矩形Rectangle、六边形Hexagon还是等轴IsometricCell Size决定格子的物理大小Cell Gap决定格子之间的间隙。Grid本身不渲染任何东西它只是一个空间参考系。Tilemap依附在Grid下真正存储和渲染瓦片的组件。一个Grid下可以挂多个Tilemap比如“地形层”“装饰层”“碰撞层”各挂一个叠加显示。为什么拆成两个组件而不是合成一个因为实际项目里经常需要“同一个网格多层地图”。地形、血迹、阴影都要对准同一套格子坐标如果每个层都各自定义网格坐标换算会非常痛苦。Grid作为唯一的地图参考系N个Tilemap共用同一把尺子这是很优雅的设计。还有一点要知道Tilemap的坐标不是世界坐标而是格子坐标。比如(0,0)是中心格(1,0)是它的右边一格。网格坐标和世界坐标之间的换算是开发时绕不开的基础操作后面我会给具体代码。3. 第一张地图的完整搭建流程3.1 创建Grid与Tilemap层级结构在Hierarchy窗口里右键选择2D Object Tilemap Rectangular场景里会自动生成一个Grid对象下面挂一个叫Tilemap的子级。这一步实际上做了一个很有价值的事在Grid上自动加了Grid组件在Tilemap上自动加了Tilemap和Tilemap Renderer组件。建完之后我给不同用途的层命名规范防止后面图层多到认不出。我的习惯是Terrain地形层放草地、土地这类底层。Props道具层放树木、石头、箱子。Collider碰撞层单独用不渲染的瓦片做隐藏碰撞。命名先不说层级顺序很重要。Tilemap越靠上渲染越靠前。地形层应该放在最底下装饰层在上面。这个顺序对应的是“地图最终绘制顺序”。这里我要提醒一个新手常犯的错不要在一个Tilemap里既放可视瓦片又放碰撞瓦片。想法很简单我希望某个格子既有草地的外观又能挡人。但如果把碰撞挂在可见瓦片上Tilemap Collider 2D会对所有瓦片生成碰撞体包括那些只是想装饰的小草、小石子结果角色被一颗草卡住非常影响体验。正确的姿势是视觉层只管画碰撞层用专门的无Sprite空瓦片或者另一个Tilemap单独做。这样做还有一个好处视觉层可以做循环动画比如水面闪烁碰撞逻辑完全不受影响。3.2 把美术切片变成可用瓦片地图素材通常是一张整图要先切成小瓦片。Unity切图不一定要用外部工具Sprite Editor就够。选中导入的图片Inspector里把Texture Type设为Sprite (2D and UI)Sprite Mode设为Multiple然后点Sprite Editor。在Sprite Editor窗口里用Slice菜单按Grid By Cell Size切分。比如瓦片是16×16就在Pixel Size填16、16Apply之后图片就被切成了N个独立Sprite。这里有一个决定成败的细节像素单位Pixels Per Unit必须和格子大小匹配。如果瓦片是16×16像素而PPU设成了默认的100那这个瓦片在场景里占0.16个单位和Grid默认1个单位的Cell Size根本对不上。我习惯把PPU统一设置成瓦片边长16就设1632就设32这样一块瓦片正好占据1个Grid单元格。当然也可以在Grid组件的Cell Size里写0.16去适配但那样所有美术素材都得很一致比改组件设置麻烦得多。切好的Sprite会显示在Project窗口展开图片就能看到。补充一个实战技巧切图时建议把Sprite Editor的“Keep Padding”勾上并设置Padding值为4或8。这样每个瓦片四周会多一圈透明像素运行渲染时能避免边缘采样出血。这个问题在连续的地形图里尤其明显我现在切图直接默认勾上省得后面出现细缝再回来处理。3.3 用调色板刷出地形瓦片备好了打开Window 2D Tile Palette。第一次打开点Create New Palette命名为MapCell Layout选Rectangle。创建后调色板窗口左边是空画布把Project里的Sprite拖进画布Unity会自动生成对应的Tile资产Asset。这些Tile资产才是瓦片绘制的最小单位而不是Sprite本身。两者区别要搞清楚Sprite是纯图像数据Tile是位于特定网格中的“带有交互逻辑的实例描述”它可以额外携带碰撞、动画、规则等数据。然后就可以画图了。选中调色板里的瓦片用画笔工具在Scene视图刷图或者选中一块区域用矩形填充工具。刷的时候按住Shift可以连续填充矩形这个快捷方式我强烈建议记下来大面积铺地形效率能高一倍以上。画完第一张图你会看到Tilemap Renderer把瓦片组合成一个Mesh进行渲染。这时可以做个小测试选择角色放到地图上跑一跑看看有没有穿帮或者缝隙。缝隙问题通常来自图集中瓦片边缘的出血处理办法我在性能与排查那一节详细讲。4. 细节打磨让地图不再是“积木块”刷完第一版地形整体看起来会有一种“格子感”——草地是一格一格完全相同的块相邻地形类型之间没有任何过渡像网格纸上填色。这一节主要解决这个问题。4.1 规则瓦片让地形自动过渡所谓“规则瓦片”英文是Rule Tile它的核心思想是你不用手动选每一块瓦片只要定义“这个瓦片在什么邻居组合下显示什么Sprite”系统就会根据周围8个格子的状态自动选择最合适的贴图。这很像自动铺路算法当你把一块规则瓦片刷到一个位置它的外观会根据邻居的情况自动变化。拿草地与泥土交界举例。美术通常会提供一组过渡瓦片中间的泥土、中间偏上的泥草边界、右侧泥草边界、右上角的草角等等。在Unity里创建一个Rule Tile很简单Project窗口右键Create Tiles Rule Tile。选中该资产把美术素材按规则棋盘的九宫格拖入。左下角那个小窗口代表当前瓦片自身周围8个格子设定每种邻居的判定状态。状态有三类Required必须是某瓦片、Not Required必须不是某瓦片、Dont Care无所谓。配好之后把这个Rule Tile刷到地上你会看到边界自动衔接了。重点是如果后期要改地形走向只需要用橡皮擦擦掉几个格子周边瓦片会自动重新计算并替换外观。这极大减少手工微调的工作量。这里我想多说两句配置细节。同类地形最好用同一个Tile Base作为邻居判断标准。如果草地来自图集A、泥土来自图集B规则判断时可以选择限定“同Tile基础”不要用“任意Tile”否则跨Tilemap的层也会参与判断容易出现意外结果。另外大量Rule Tile同时刷新时会有一定的计算开销建议只在刷完地图后执行一次批量刷新不要在Update里每帧刷新所有Tilemap。关于Rule Tile的进阶用法还有一个叫Animated Tile的变体。它允许一个瓦片连续替换多张Sprite适合做水面、火焰这类循环动画。配合Tilemap的刷新机制做动态场景非常出色。我曾在一个小项目里用Animated Tile做河流结果整个水面都在循环流动编辑器里直接能看到效果效率非常高。4.2 排序层角色与地图的遮挡关系瓦片是2D Sprite要正确处理角色和地图的遮挡需要看渲染排序。2D排序核心按两个因素Sorting Layer和Order in Layer。不同Tilemap层之间Sorting Layer越靠后的先渲染、越靠前的后渲染后渲染的覆盖先渲染的。我的习惯是地形层Sorting Layer设为Default的“0”其Order in Layer设为-10装饰层设为Default的“5”角色动态物体单独给最高层。如果你做俯视角且地图有Y轴深度例如人物要绕到大树后面只有Sorting Layer还远远不够得开启Sprite Renderer的Custom Axis排序。在Project Settings Graphics Sprite Renderer里可以设置Transparency Sort Mode为Custom Axis并把Transparency Sort Axis设为(0, 1, 0)这样引擎按Y坐标从低到高排序站在树下方Y更小的物体先渲染树后方的物体自然被树遮挡实现“绕到背后”的效果。注意这个设置是全局的会同时影响所有Sprite Renderer调试时要统一观察。Tilemap Renderer 2D本身也可以参与这种Y轴排序。在Render Mode里选择Individual它会按每个瓦片的Y坐标动态调整渲染顺序。代价是批次合并减少因为每个瓦片的Sort Order都不一样GPU必须单独绘制。所以我通常只在装饰层用Individual模式大面积地形层保持Chunk模式用空间换取视觉效果。这个选择没有绝对标准关键看你项目的镜头是固定还是跟随、场景是开放还是封闭。4.3 碰撞体的配置与合并给Tilemap加物理选中Tilemap对象添加Tilemap Collider 2D。默认情况下引擎会为每个有碰撞体的瓦片生成一个独立的碰撞多边形然后同Tilemap内的相邻瓦片自动合并所以通常不需要自己拼碰撞。加上之后建议再勾选Used By Composite、并在同物体上添加Composite Collider 2D。Composite Collider会把多个碰撞多边形进一步合并成一个凸多边形/凹多边形组合物理模拟时边界数量大幅减少也规避了“角色卡在格子缝里”的抖动问题。不过要注意Composite Collider要求碰撞体必须是凸多边形瓦片生成的多边形需要做标记确保瓦片资产里的Collider Type设置为Sprite而不是Grid或None且对应的Sprite形状简单。硬要复杂碰撞形建议用专门的Physics Shape编辑否则Composite会报错或忽略部分形状。在碰撞层上使用的瓦片我个人的做法是新建一个空白的Tile叫CollisionTile不需要关联Sprite。然后给这个Tile资产设置一个矩形碰撞体Collider Type选Sprite。把CollisionTile刷到需要阻挡的位置渲染上完全不可见但碰撞生效。这种做法让地图的“视觉”和“物理”彻底分离修改碰撞时完全不影响画面表现。尤其在做复杂关卡时策划自己就能在编辑器里添加看不见的墙非常直观。5. 运行时动态操作与代码控制编辑器里刷好的地图只是“结果”真实游戏里往往还需要在运行时创建、替换、删除瓦片甚至程序化生成整张地图。这就要用代码操作了。5.1 用代码动态创建与替换瓦片所有Tilemap的代码操作都围绕几个核心API// 获取某个格子的瓦片 TileBase tile tilemap.GetTile(new Vector3Int(0, 0, 0)); // 设置某个格子的瓦片 tilemap.SetTile(new Vector3Int(0, 0, 0), myTile); // 清空某些格子 tilemap.SetTile(new Vector3Int(0, 0, 0), null);SetTile是底层API无论你用的是规则瓦片、动画瓦片还是普通Tile格子类型统一叫TileBase。写代码前一定要明白这个抽象概念你操作的是接口不是某个具体瓦片类。网格坐标向世界坐标转换需要经过Grid// 格子坐标转世界坐标 Vector3 worldPos grid.CellToWorld(new Vector3Int(5, 5, 0)); // 世界坐标转格子坐标 Vector3Int cell grid.WorldToCell(transform.position);在写放瓦片逻辑时最常见的需求是“单位走到哪就把脚下的某块瓦片替换掉”。比如踩过的草地变成泥土。流程是在Update里对玩家的世界坐标做WorldToCell得到格子坐标然后用GetTile判断当前格子的类型如果是草地就SetTile替换成泥土。注意WorldToCell返回的Vector3Int已经是整数格子坐标Z层的值一般取0。这里我提醒一个细节不同Tilemap层是独立的修改前先确认你操作的是哪个层的Tilemap组件。我见过有同学GetTile取的是装饰层SetTile却设置到地形层结果地图上出现两个重叠的物件排查半天才发现是层引用搞混了。5.2 地图数据的序列化与保存刷好的地图是场景数据的一部分随场景保存。但如果做了运行时动态修改比如地面被破坏存档时就需要把修改后的瓦片状态记录下来。最简单的方案遍历Tilemap的所有格子把每个非空格子的坐标加TileBase名称存进一个列表。加载存档时再反过来SetTile。这个小方案有几个坑第一遍历要用tilemap.cellBounds它是一个BoundsInt记录被占用的最小包围盒第二TileBase没有公开的ID拿TileBase.name做标识要注意重名问题第三如果是规则瓦片存档时直接保存的是规则瓦片本身重新SetTile后规则逻辑会自动重新计算外观所以不需要额外保存邻居状态这点很关键。如果地图特别大比如1000×1000遍历全部格子做存档就不太现实。我建议维护一个“脏格子”列表只在格子状态改变的时候记录该格子的坐标和瓦片名。运行结束后合并保存。这也是很多体素游戏和roguelike通用的做法。实际项目中我习惯把存档数据封装成一个自定义的DataContainer格子列表序列化成JSON或二进制加载时再分发到对应Tilemap后续扩展也比较方便。5.3 运行时地图刷新性能注意点动态改Tilemap时引擎会自动对改动区域做Mesh重建所以不需要手动刷新。但如果一帧内改了成千上万个格子Mesh重建会拖垮帧率。应对策略是把改动缓存在一个队列里每帧最多处理一定数量比如每帧500格这样大量刷地的功能不会带来卡顿。另一个实用的API是tilemap.ResizeBounds()当你批量扩展地图边界时调它可以让引擎一次性更新包围盒避免每设置一格就计算一次边界。我自己写程序化生成算法时都是先算好整个地图范围直接ResizeBounds然后在一个循环里SetTile结束后一次刷新效率高很多。程序化生成方面网上能找到很多Perlin噪声生成地形的例子原理就是遍历格子、采样噪声值、根据阈值SetTile搭配规则瓦片就能快速生成一片自然过渡的地形区域。6. 性能优化与常见问题排查写到这里平铺网格地图的基础操作已经能完整跑通了。但如果要放进真实项目还必须面对性能问题。2D地图看起来简单性能瓶颈藏得很深。6.1 图集、平台设置与合批最重要的一件事把地图素材打进图集。选中所有瓦片Sprite把它们放入同一个Sprite Atlas右键Create 2D Sprite Atlas并设置Atlas的Type为Master。运行后Tilemap Renderer会把这些瓦片绘制合并为一个Draw Call。没有图集的话Shader需要逐个纹理去绑定Draw Call数量会线性上升移动端尤其明显。影响合批还有一个容易被忽略的变量瓦片的颜色。如果瓦片Sprite引用了不同图集或者Tilemap上启用了Alpha裁剪合批都可能被打断。在Tilemap Renderer的Detect Chunk Culling Bounds被修改时同样会临时中断渲染合并所以别一直动态改Render Bounds。再说说瓦片Unity包里的Texture Compression。地图这类大图集在PC上可以用RGBA32在移动端建议压缩成ASTC。压缩太高近距离会出现画面模糊太低则内存爆炸。我通常把图集的Max Size设置成2048或者4096具体看地图最大尺寸来定。最后记得在Inspector里点一下Atlas资产的Generate按钮否则图集不会真正生成。6.2 常见问题速查表整理几个我实际工作中高频遇到的问题按“现象-原因-解法”列出来方便大家按图索骥现象原因解法瓦片之间有细缝图集无Padding或者PPU设置不一致切图时勾选Keep Padding统一PPU角色卡在瓦片边缘瓦片单格碰撞体过多或者碰撞合并缺失用Composite Collider合并或单独碰撞层背景和前景重叠看不清Sorting Layer设置错误设置正确层级俯视角开启Y轴排序动态刷地图卡顿一帧内SetTile太多分帧处理先ResizeBounds瓦片显示变色或模糊图集压缩太高或Sprite Atlas未生成调压缩格式检查Atlas是否已Generate规则瓦片边缘不衔接邻居判定条件写错检查Rule Tile九宫格配置注意Required/Not Required状态Tilemap没有碰撞忘了加Collider或空Tile资产上未设置碰撞确保地形或碰撞层挂Tilemap Collider 2D这里我要单独拎出来讲一个排查很坑的事Tilemap Collider 2D在设置了Used By Composite后如果Tilemap对象上还挂了Rigidbody 2D刚体类型必须是Static或者Kinematic否则组件之间会发生冲突碰撞体完全失效但编辑器不报错。我遇到过两次都是排查了半天才发现是Rigidbody 2D类型设错。正常情况下静态地图不要给Tilemap对象挂Dynamic刚体用静态碰撞就够了。另一个常被忽略的问题像素完美。如果项目用了Pixel Perfect Camera瓦片上的Sprite PPU、Camera的Pixel Ratio、目标分辨率三者要匹配。否则即使Tilemap本身没错屏幕上也会出现发虚或闪烁。这个和前面缝隙问题往往同时出现排查时优先确认PPU。在手机上不同屏幕分辨率带来的像素缩放差异尤其明显建议打包前用多个分辨率测试一遍。7. 写在最后我的一些工作习惯最后分享几个我自己在多个项目里沉淀下来的习惯不一定适合所有人但确实让我省了很多事。第一个习惯是给瓦片命名时带上位置和状态信息。比如grass_center、grass_top_0、grass_top_1规则瓦片多了之后在Project窗口按名字排序非常重要。Unity资源窗口不支持多级Tag管理命名规范基本就是唯一的管理手段。第二个习惯是项目一开始就建立碰撞层哪怕地图还没画。这样后续刷图时可以顺手把碰撞画上去而不是等美术全部画完再回头补物理。我吃过几次亏地图已经定稿策划开始调关卡结果发现还有个地方忘了加碰撞再回去改动就会影响已验证的流程非常麻烦。第三个习惯是尽量用ScriptableObject保存瓦片的扩展信息。瓦片本身只负责长什么样但一张地图往往还需要“这块地能不能走”“踩上去会不会扣血”“要不要触发事件”这类逻辑数据。把这些配置放在和Tile对应的ScriptableObject里策划改数据不需要动代码程序这边也更容易维护。平铺网格地图这个领域入门不难难的是把细节做到位。网格坐标、规则瓦片、碰撞合并、图集合批每一环都会影响最终手感。你按照这篇文章的路径从零搭一遍再对照排查表调几轮基本上就能踩平90%的坑。剩下的就交给实际项目去磨了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑