
做独立游戏最折磨人的环节之一就是画地形瓦片。我第一款横版像素demo做到一半就被卡住了草地区域要加一圈泥土过渡边美术朋友给我拉了一张表——上、下、左、右四条边四个角加上内角外角各种邻接组合最后精确到47张。当时我没觉得这个数字离谱毕竟素材网上的地形包都是这么卖的。直到一个做引擎的朋友随口说了句“有种双网格方案47张能压到9张”我才发现被传统工作流按在地上摩擦了这么多年。这篇就把这套方法完整拆开双网格Dual Grid Autotiling到底怎么回事、开源免费的工具链怎么选、在Tiled里怎么配置、Godot里怎么实现以及我实际项目里踩过的一堆坑。如果你也在做像素风独立游戏尤其是地形探索、建造类玩法这篇应该能帮你省下大量重复劳动。1. 47张地形瓦片是怎么变成行业痛点的1.1 自动瓦片不画满全图就必须面对组合爆炸要理解双网格的价值得先知道47张这个数字怎么来的。最暴力的做法是压根不搞自动瓦片直接在地图编辑器里一格一格铺满带边缘的手绘贴图。小场景勉强能扛地图超过几百格时美术直接崩溃而且后期改地形等于重画。于是业界搞出了自动瓦片AutoTile地图引擎根据某个格子周围邻居的状态自动挑选对应的瓦片贴上去。程序员只要告诉引擎“这格是草地、那格是水”引擎自己判断哪里该出现过渡边缘。听起来很美好但代价从底层就开始了——判断逻辑通常以中心格为基准看它周围8个邻居各自是实心还是空地。这个判断的规模是2的8次方总共256种组合。去掉旋转对称和镜像翻转的重复项剩下的唯一边缘形态大约在45到48种之间。所以你看素材网站上出售的地形瓦片包一套完整的就是47或48张中间那张基础填充周围一圈全是各种过渡。很多引擎的地形集设计包括一些成熟引擎里的自动瓦片规则至今还是按这个标准来的。47张不是谁拍脑袋定的是8邻接组合爆炸之后的自然产物。1.2 手绘47张的实际情况一个地形吃一整天如果你只有一张32x32的过渡瓦片画起来大概5到10分钟。这个速度已经算乐观了像素画讲究边缘处理、颜色过渡、明暗变化要画出自然纹理远比填充一个色块麻烦。47张逐张手绘最顺利也要四五个小时中间画烦了返工一下一个工作日就没了。更难受的是现在的游戏不会只有一个地形。草地旁边有泥土泥土旁边有水域沙漠和岩石也接壤每多一种地形类型就要再来一套过渡瓦片。场景里有五六种地形的时候光边缘素材就攒了两百多张。而且这些瓦片之间风格必须统一否则草地的泥土边和沙地的泥土边看起来像两个游戏。我现在一看到素材包里那一长排地形预览图还会条件反射地头疼。1.3 现有替代思路为什么总是顾此失彼面对47张的负担大家不是没想办法。首先想到的是把8邻接降成4邻接只判断上下左右四个正交方向。这样组合数从256降到了16瓦片数确实锐减。但牺牲也很直接对角方向的过渡没法表达草地边缘在45度方向会出现断裂、台阶感一眼看过去像马赛克拼错位了。像素游戏本来就在吃像素红利这种瑕疵特别扎眼。另一条路是程序化生成边缘写算法根据地形数据实时画过渡带。这种方法胜在素材少、修改方便但程序生成的边缘很难碰撞上人工手绘的质感尤其是自然生态类游戏的石头、草丛、水面边缘程序画出来总有一种“标准答案”的生硬感。美术想要的是手绘曲线算法给的是直线和固定噪声两者经常打架。还有一类思路偏管理层面把47张瓦片打包成模板团队里所有人共用一套靠规范和复用摊薄成本。这确实能缓解协作问题但解决不了本质——只要你的地形判定是“中心格看8个邻居”瓦片量的瓶颈就一直在那里。2. 双网格思路把地形判定和贴图画法拆开2.1 核心机制每块视觉瓦片只裁决2x2的逻辑格双网格方案绕开了“中心格看8个邻居”这个前提。它的做法是把一套网格拆成两套来考虑一套是逻辑地形网格solid grid它记录“这格有没有地形”另一套是视觉渲染网格render grid它和逻辑网格错开半格瓦片不再铺在逻辑格中心而是铺在逻辑格的顶点位置。听起来抽象说具体一点。传统方案里一张32x32的瓦片代表“一个逻辑格及其周围一圈邻居的状态”双网格里一张32x32的瓦片只负责“逻辑格四个顶点交汇处”那一小片区域。这个顶点周围只涉及四个逻辑格左上、右上、左下、右下。这样一来每个视觉瓦片需要判断的邻接数量从8个降到了4个。判断范围缩小组合规模也跟着缩小——2的4次方是16种组合比256种少了一个数量级。而对玩家来说画面上的边缘依然完整因为每个瓦片都卡在逻辑格的顶点它天然覆盖了四种地形状态的交界点。边缘过渡不是被取消了是被拆分到了顶点上。2.2 从47到9这份减负账是怎么算出来的16种组合听起来还是不少但旋转和翻转能帮你消灭大量重复。四个逻辑格全实心需要1张内部填充瓦片四格全空什么都不用画两格实心根据实心方向需要横向边、纵向边各1张三格实心剩下一个空角需要4张内角缺口对角两格实心需要外角或者说角点瓦片也通过旋转覆盖。实操下来一套基础地形通常只需要9张左右瓦片1张全实心内部填充、4张边过渡、4张角过渡。有些实现会再加1张纯角点素材总共10张。这和47张对比降幅接近80%。更划算的是后续每增加一种新地形只需要再为这种地形补一小组顶点瓦片而不是再背一遍47张。这里要插一句双网格渲染出来的画面并不是“模糊版”传统方案。单个顶点瓦片的分辨率和原先完全一样只是它描述的空间范围从“一个逻辑格一圈邻居”变成了“一个顶点的四种状态”。换句话说双网格的瓦片数量少不代表单张瓦片信息量下降只是组合逻辑挪到了顶点层面。画质不打折工作量打折。2.3 常见的误解双网格不是砍分辨率有朋友第一次接触双网格时问是不是等于把瓦片做成透明拼图然后用更小的格子贴出来其实不是。瓦片尺寸没变内部纹理精度没变变的只是“一张瓦片放在哪里”和“它代替哪些格子的边缘”。你可以把它理解成砌墙传统方案是把整面墙的花纹都做成预制板每种花纹都要单独开模具双网格是砖块和勾缝分开处理砖摆成什么样勾缝材料自动填进去而勾缝材料只需要有限的几种规格。另一个常见误解是双网格只适合方方正正的格子地形遇到圆角、斜角就废了。我的经验是双网格主要解决的是“大量地形过渡组合”斜角、圆角属于额外表现层完全可以在顶点瓦片里增加少量素材来覆盖。后面专门聊扩展时会细说这里先有个概念双网格是一个把基础组合爆炸问题消解掉的工作框架不是一套锁死美术风格的模板。3. 开源免费工具链落地Tiled Godot LibreSprite3.1 选型清单与理由原理讲清楚之后问题就剩一个用什么工具落地。如果你打开搜索引擎找“双网格瓦片地图绘制工具”会发现现成的、开箱即用的单软件其实不多。我现在的做法是用三个开源组件拼成一条完整工具链每个环节都免费且跨平台长期维护也有社区保障。环节工具选型理由逻辑地形编辑Tiled老牌开源地图编辑器对象层、自定义属性、多格式导出齐全操作手感经过十年以上验证运行时渲染Godot开源引擎GDScript写顶点瓦片生成逻辑非常直接内置TileMapLayer性能足够像素素材绘制LibreSprite / Pixelorama开源免费的像素画工具操作方式和Aseprite系一致适合画顶点瓦片当然这组不是唯一答案。你完全可以用Unity替换Godot或者用LDTK替换Tiled但本文后面的步骤以这套组合为准原因是它们组合起来成本为零而且每一环都便宜好上手。对个人开发者或两三人小团队来说这已经是很舒服的状态了。3.2 第一步画好9张顶点瓦片在LibreSprite里新建一个32x32的图集横向排开9个格子分别画内部填充1张、上边缘、下边缘、左边缘、右边缘各1张、左上内角、右上内角、左下内角、右下内角各1张。图集总尺寸变成288x32导出PNG备用。画的时候有个小技巧不要试图在一张瓦片里画出“完整地形边缘”只要表达清楚“这个顶点附近的地形状态”就行。比如上边缘瓦片实际内容就是上半部分是草地材质、下半部分是泥土过渡带因为这张瓦片会被放在逻辑格的顶点上旁边逻辑格的状态已经决定它该当哪条边的过渡。下意识用传统地形瓦片的画法反而会重复画好几遍渐变。素材风格上建议先画好一张内部填充作为基准然后把它的边缘裁出来复制给过渡瓦片这样能保证所有过渡纹路在视觉上连续。我一开始每张都独立画结果相邻瓦片之间的草叶纹理根本对不上接缝处像补丁后来改成先搭基准纹理再裁剪叠加就顺畅多了。3.3 第二步Tiled里用对象层定义逻辑地形打开Tiled新建地图瓦片大小设置成逻辑格尺寸32x32。注意这里的地图网格尺寸和最终视觉瓦片尺寸一致但地图本身只是“逻辑地形”的容器并不直接承载视觉瓦片。关键一步来了新建一个对象层命名“terrain”用矩形工具把地图上的地形区块框出来。矩形尺寸必须是32的整数倍保证导出后能对齐逻辑格。为什么推荐对象层而不是瓦片层因为绝大多数地形的形状是连续区块用矩形框选比一格一格刷瓦片快几个量级而且这些矩形后续还能直接转成碰撞体一份数据两处使用。导出时选JSON格式引擎端解析矩形数组再按数组里的坐标把对应逻辑格子标记为实心。我的习惯是地图里每个矩形就代表一块实心地形矩形之外的区域默认空地。这样Tiled这边不需要准备任何美术素材它在这条链路里的角色纯粹是一个“逻辑地形编辑器”这反而让关卡策划用起来很轻。3.4 第三步Godot里把逻辑地形变成渲染瓦片运行时生成瓦片的思路非常简单核心就是遍历逻辑顶点、读取周围四个格子、按16种组合选择图集区域。先用一个二维数组表示逻辑地形var solid_map: Array [ [0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 0], [0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0], ]生成脚本遍历每个顶点取左上、右上、左下、右下四个格子的值然后按状态映射到图集矩形func _region_for_vertex(tl: int, tr: int, bl: int, br: int) - Rect2: # 1 表示实心0 表示空地 if tl 1 and tr 1 and bl 1 and br 1: return Rect2(0, 0, 32, 32) # 内部填充 if tr 1 and br 1 and tl 0 and bl 0: return Rect2(32, 0, 32, 32) # 左侧过渡 if tl 1 and bl 1 and tr 0 and br 0: return Rect2(64, 0, 32, 32) # 右侧过渡 if bl 1 and br 1 and tl 0 and tr 0: return Rect2(96, 0, 32, 32) # 上侧过渡 if tl 1 and tr 1 and bl 0 and br 0: return Rect2(128, 0, 32, 32) # 下侧过渡 # 三格实心、一格空对应内角缺口 if tl 0: return Rect2(160, 0, 32, 32) if tr 0: return Rect2(192, 0, 32, 32) if bl 0: return Rect2(224, 0, 32, 32) if br 0: return Rect2(256, 0, 32, 32) return Rect2() # 四格全空透明区域不会调用实现的时候记得做一步坐标换算顶点位置要从逻辑格坐标转换出来。我的写法是对于逻辑地图宽w高h遍历时x取0到wy取0到h也就是在网格交点位置生成瓦片。这里有个重要细节顶点瓦片数量是(w1)(h1)而逻辑格数量是wh。换句话说假如你在Tiled里画了一张100x100的逻辑地图最终生成的瓦片网格是101x101其中外圈有一圈纯粹的边界瓦片。这个特性给了一个钩子只要逻辑地图四周再包一圈空地视觉上就能自然生成完整的地形边缘。为了性能生成结果直接塞进TileMapLayer用TileSetAtlasSource关联图集通过set_cell批量填充不要用循环创建Sprite2D节点。一个500x500的地图顶点瓦片大约25万个TileMapLayer完全扛得住启动时生成一次后续不再变动的话帧率没有影响。4. 实测工程中的细节坑和应对方式4.1 地图边缘缺半圈忘了padding虚拟格这是我第一次接入双网格时最头疼的Bug。逻辑地图明明画满了地形但地图右侧和下侧边缘的过渡带不完整草皮像被裁掉一条边。排查半天才想明白顶点瓦片数量比逻辑格多一圈生成时如果只遍历到w-1和h-1最外侧一行的顶点根本没被处理自然缺了样貌。正确做法是在生成瓦片时把遍历范围扩成(w1)*(h1)然后对逻辑格数组做padding。规则是访问越界位置时按“地图边界之外的格子都当作空地”处理。这样最外圈顶点会生成一圈完整的边缘过渡让你的地形看起来有厚度而不像悬浮贴图。我把这步写进了生成函数之后无论怎么改地形都不会再漏。4.2 纹理滤波和贴图出血像素风的第一隐形杀手画好的瓦片接缝处出现一圈半透明的杂色几乎每个像素游戏项目都会遇到双网格方案也一样。根源通常是纹理过滤图集在运行时被线性插值采样瓦片边缘混入了相邻瓦片的颜色看起来就像水彩晕染。在Godot里把TileSetAtlasSource关联的纹理filter设为Nearest同时把图集每个瓦片之间留出1到2像素的间隔或者在图集源里配置separator就能彻底解决。LibreSprite里导出PNG前也建议给每张瓦片的四周留一点透明边距渲染时空边距不会出画面但能防止邻近瓦片颜色被采样进来。这个坑很蠢但几乎所有第一次用图集做地形的人都会踩一遍。4.3 碰撞体跟着哪套网格走别让视觉和物理打架双网格把视觉和逻辑拆成了两套网格碰撞体就必须有明确归属。我的经验是碰撞体直接从Tiled导出的矩形对象生成而不是从视觉瓦片生成。逻辑地形是规则格子转碰撞体最简单、最稳定轨迹也容易预测。但这里有一个视觉偏差要注意视觉瓦片是铺在逻辑格顶点上的它表现的地形边缘比逻辑实心格略微向外扩张了半个格子的视觉范围。表现在游戏里玩家站在河边时脚尖可能会踩进“水”里一点点。解决方案是生成碰撞体时把每个矩形向外扩半格或者在Tiled画矩形时直接把地形边界画大一些。两种方案选一种就行关键是别让碰撞体和视觉层来回对不齐否则玩家会觉得地面忽高忽低。4.4 多地形、排序与光照双网格不是只能单地形有的朋友会问这套方案是不是只能处理一种地形草地和水的边界怎么办答案是可以叠加。在Tiled里为每种地形单独建一个对象层每个层的矩形渲染成一套顶点瓦片然后把这些瓦片放进不同的RenderLayer通过它们的Y坐标参与排序。比如水边地形水的顶点瓦片画在底层草地画在上一层玩家站在草地后面时Y排序自然把他遮住站到前面时显示正常。光照阴影也有现成路径直接用逻辑地形数据生成静态阴影而不是拿视觉瓦片做轮廓提取。因为逻辑地形是干净的布尔数组生成碰撞和阴影都是先天的方便。经验是不要试图让视觉瓦片、碰撞体、光照三套数据都从不同渠道生成那样改一次地形要改三处很容易不同步。5. 省下的时间花在哪工作流效率与扩展空间5.1 直接量化一套地形从画瓦片到跑通需要多久我最近做一个洞穴场景按传统方案先列瓦片清单画到第30张左右的时候果断切到双网格。算了一下实际耗时画9张顶点瓦片加微调大约一小时四十分钟写Godot生成脚本因为之前模板现成半小时Tiled里框地形区块整个洞穴两百多个矩形对象半小时搞定。对比之前做草地场景走传统自动瓦片流程美术画47张地形花了整整一天程序配自动瓦片规则又花了一个晚上。两套工作流做出来的边缘效果直观对比差别不大但投入时间差了四倍多。对独立开发者来说这种压缩是直接把项目周期从“月”往“周”里推的级别。后来我又意识到一个隐藏收益修改地形的成本。传统方案要改地形边缘得去素材库里翻对应瓦片或者重新画一组过渡双网格方案直接改Tiled里矩形对象的位置引擎重启自动重新生成。关卡策划自己都能操作不需要每次改版都拉着美术和程序一起排队。5.2 斜面和圆角双网格能不能继续扩展能而且扩展成本可以很平滑。双网格的瓦片映射本质上是一个“状态到图集矩形”的查表过程你完全可以往表里加自己想要的状态。比如给顶点瓦片增加斜坡标记就是为“上侧实心、下侧实心、左侧空、右侧空”这种特殊状态分配一张斜坡素材同时在Tiled对象层里标记“此处是斜坡”。需要注意斜坡通常不只是视觉问题它还牵扯角色能否斜向站立、能否走上坡所以碰撞体要单独处理。圆角同理在四个角的位置各画一张带圆弧的顶点瓦片替换默认直角角块。对纯像素风格来说圆角并非刚需但一旦需要双网格的扩展性比传统方案好得多——传统方案做一个圆角要同时画8张不同朝向的过渡瓦片双网格只需要在现有基础上补2到3张。5.3 哪些项目适合哪些项目没必要折腾如果你做的是地形密集的像素游戏尤其是建造、探索、平台跳跃类双网格几乎是必选项。地形过渡越频繁看得见的信息越多省下的工程量越大。反过来如果你的游戏地形非常简单比如就是一条平台或者纯背景板或者你用程序生成的地形本身就不需要手绘质感那确实没必要专门搭一套双网格工作流传统方案或者干脆手绘图块就够了。还有一个适用范围容易被忽略换皮和多主题场景。独立游戏经常做主题皮肤系统换一套草地换一套雪地。传统方案每换一次主题就要换47张素材双网格每换一次只要画9张左右。这个数字差异在规模化以后非常可观我现在每逢新主题都会庆幸当初没有坚持传统自动瓦片路线。最后分享一个实际项目里养成的习惯把Tiled里的对象层当作整个场景的唯一数据源。双网格瓦片生成、碰撞体生成、光照阴影、小地图标记全部从这一层矩形派生。一开始你可能觉得绕等地图改过三轮之后就会发现单源数据的收益远远大于理解成本。这套方法我已经在持续维护的项目里跑了小半年每次做新地形都是同一套流程画9张新瓦片框矩形重启看效果。没有惊喜也没有惊吓稳定本身就是独立游戏开发里最值钱的东西。