1. 先搞清楚 TileMapLayer 到底是什么
很多刚接触 Godot 4 的朋友,第一次听说 TileMapLayer 时的反应和我当初一样:这名字和以前 Godot 3 里的 TileMap 有什么关系?我是不是学了一个异端版本?
其实没那么复杂。Godot 4 把原来那个一把抓的 TileMap 节点,拆成了一个更干净的 TileMapLayer。它的定位很明确:只负责管理一层瓦片地图。你一个场景里要是有地面、墙壁、装饰、碰撞区域,以前全塞进一个 TileMap 里,现在你直接新建多个 TileMapLayer,各管各的层,逻辑清楚得多,修改哪一层也不会误伤其他内容。
那"在 TileMapLayer 中显示一个瓦块"这个需求,听起来像是个幼儿园级的操作,但实际上它是你整个 Godot 瓦片地图学习路线的第一块基石。我见过太多人卡在"新建完节点之后,底部面板怎么画都画不上东西"这一步,问题十有八九出在 TileSet 资源没配好。你可以把 TileMapLayer 想成一个画框,TileSet 才是真正装着颜料和图样的调色盘,画框本身不存储任何图案,没有调色盘,你在画框上再怎么比划也是白费功夫。
所以当我们讨论"显示一个瓦块"时,其实是在做三件事:先创建 TileSet 瓦片集资源,再把 TileMapLayer 和 TileSet 绑定,最后通过画笔或代码把瓦块放进指定格子。这篇文章我就按这条主线,把这个过程里所有容易踩的坑、容易懵的概念,一次讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么新版改用 TileMapLayer,它到底好在哪
2.1 TileMap 到 TileMapLayer 的拆分之争
在 Godot 3.x 时代,TileMap 节点是典型的"我都行"选手。地形画完了,碰撞多边形也画在同一个 TileMap 里,装饰物也放在同一层。一开始做小游戏确实方便,但项目一旦大起来,麻烦就来了:你想单独调试哪一块碰撞区域,或者想让某个装饰层做轻微滚动视差,就得把一层分出去,代码里各种操作自己绕来绕去,费劲且容易出错。
Godot 4 干脆把图层概念从"一个节点内部存储多个图层"改成"一个节点就是一个图层"。你是地面就单独建一个 TileMapLayer,你是墙壁就再建一个,你是道具层就继续叠加。节点的职责单一了,编辑器里也直观了:哪个层有什么东西,点开节点树一目了然,不需要再进属性面板来回切图层索引。
从我做小项目的感觉来说,这个改动最直接的收益是避免数据混乱。以前一个 TileMap 里可能混着不同 TileSet 的图块,至少要管理 layer 之间的依赖关系。现在每个 TileMapLayer 里只有一个 TileSet,独立编辑、独立隐藏、独立做程序化逻辑,心智负担小了很多。
2.2 TileSet、TileSetSource、TileSetAtlasSource 的角色分配
很多人一打开 TileSet 资源编辑器就头晕,因为里面层级比想象中多。我们从头捋一遍:
- TileSet:是整个瓦片资源的总入口,里面可以塞多个瓦片源(Source)。
- TileSetSource:瓦片源的抽象基类,最常用的是从图片图集创建的 TileSetAtlasSource。
- TileSetAtlasSource:负责把一张大图(图集)按网格切成一个个小瓦片,并暴露出具体的瓦片 ID 和坐标。
用一个生活化类比:TileSet 是你的衣柜,TileSetSource 是柜子里不同的收纳盒,TileSetAtlasSource 就是其中一个装了透明小格子的收纳盒,每格里放一种图案。
实际操作中,你通常不会直接和 TileSetSource 打交道,而是直接在 TileSet 里添加一个 Atlas Source,选择一张 PNG 图片,设置好每个瓦片的像素宽高。Godot 会自动把图集按网格切好,你在编辑器底部会看到一排排切好的瓦块。这一步是"显示一个瓦块"的前提:如果这里没切好,后面怎么放都会错位。
2.3 单个瓦块和多个 TileSet 层如何搭配
这里我想额外说一句:虽然 TileMapLayer 建议一层一个 TileSet,但同一个场景里完全可以有多个 TileMapLayer 共用同一个 TileSet 资源。比如你有一个"地面与墙壁"的 TileSet,可以建一个 TileMapLayer 画地面,再建一个 TileMapLayer 画墙壁,两个节点共用同一个调色盘。这在场景里非常常见,既保持了图层分离,又不会每个层都要重新配一遍瓦片集。
如果你的项目里还有寻路、碰撞等功能需求,TileSet 内部还支持添加物理层、导航层、替代瓦片(Alternative Tile)。但这一切都得从"先把第一个瓦块显示出来"起步,所以这篇文章先不展开那些进阶功能,等到基础跑通了,再逐层加也不迟。
3. 从零到一:在 TileMapLayer 里显示一个瓦块的完整流程
3.1 准备瓦片素材
别急着写代码,先把素材准备好。你可以自己画一张格子图,比如 64x64 像素的方块,或者从 Kenney.nl 这类免费素材站下载一套图集。第一次做我建议用整张图集,因为这样才能体会 TileSetAtlasSource 的切图逻辑。
我这里演示用的是自己随便画的一张 256x128 的 PNG,里面横向 4 格、竖向 2 格,每格 64x64。图片尺寸不重要,重要的是网格尺寸和图片像素对齐。
能力强的朋友可以直接用代码创建 TileSet,但第一次学的时候,我建议用编辑器点一遍,因为编辑器里能直观看到切出来的每一块瓦片,理解会更扎实。
3.2 创建场景和 TileMapLayer 节点
打开 Godot 4,新建一个 2D 场景,在根节点上右键添加子节点,选择 TileMapLayer。此时选中这个新节点,你会看到属性面板里有一个 TileSet 属性,目前是空值。
很多新手卡在第一关:"我明明选了 TileMapLayer,怎么底部没有瓦片编辑器?"原因很简单——节点虽然有了,但 TileSet 还没创建,编辑器不知道该显示什么。所以下一步就是解决 TileSet 这个空缺。
在属性面板的 TileSet 下拉框里,点击 "New TileSet" 可以直接在节点上内建一份资源,但这种方式创建的资源是随场景保存的,很难在别的场景复用。我更推荐先在外面创建一个独立的 TileSet 资源文件,再用文件拖动方式赋给节点,这样以后别的场景也能引用同一份资源。
操作步骤:
- 在文件系统底部区域新建一个文件夹,比如
tileset。 - 在文件夹里右键选择 创建资源 -> TileSet,命名为
ground.tres。 - 双击打开这个
.tres文件,进入 TileSet 编辑器面板。 - 在 TileSet 编辑器里找到 添加图集源,选择下拉菜单里的 TileSetAtlasSource。
- 在下方出现的 "纹理" 槽位里拖入你的 PNG 图集文件。
此时你会发现编辑器自动按某种尺寸把瓦片切开了。这个尺寸来自图集源的属性,默认可能是 16x16。你需要把它改成你素材的实际瓦片大小,比如 64x64。
3.3 配置 TileSet 图集源的关键参数
这里有几个属性值得逐个解释,因为它们是后续所有问题的高发区:
- 纹理尺寸(Texture Region Size):每个瓦片的像素尺寸。如果你素材是 64x64 一格,这里就填 64 和 64。如果填错,切出来的瓦片要么残缺,要么多块重叠。
- 纹理边距(Texture Margin):从图集边缘开始预留的空白像素。如果你的素材四周有透明描边或出血,就可以用这个参数把边缘区域排除掉。
- 瓦片间距(Texture Separation):图片里每个瓦片之间相隔的像素数。有些图集为了便于美术绘制,瓦片之间会留 1 像素或 2 像素空隙,这里就填对应值。
- 备选瓦片(Alternative Tile):同一个格子可以创建多个变体,比如同一个地板图案的旋转版本。这个后面学自动瓦片时会用上,刚入门可以先忽略。
设置好之后,TileSet 编辑器中间会出现可点击的瓦块网格。你可以单击选中某一个瓦块,右侧会高亮显示。这一步就说明图集源已经切好了,接下来可以启动 TileMapLayer 了。
3.4 把 TileMapLayer 绑定到 TileSet 并开始绘制
回到 2D 场景,选中刚才创建的 TileMapLayer 节点。在属性面板的 TileSet 一栏里,拖入刚才的 ground.tres 文件,或者在文件系统面板里直接拖拽到 TileMapLayer 节点上。
绑定成功后,神奇的事情发生了:编辑器底部会出现 TileMap 面板,里面展示了你在 TileSet 中配置好的所有瓦块。这就是我们所谓的"调色盘"就位了。
接下来:
- 在底部面板里点选一个瓦块。
- 顶部的 TileMapLayer 工具栏会切换出 绘制工具(快捷键 D)。
- 在 2D 视口里网格画布上单击鼠标左键,一个瓦块就显示出来了。
如果你没有看到网格对齐,可以在 2D 视口顶部的智能吸附栏里打开"吸附到网格",把网格尺寸设成瓦片尺寸。这样鼠标点击的时候,瓦块会自动对齐到格子,绘制手感会顺畅很多。
这一步做完整,你已经在 TileMapLayer 中成功显示了一个瓦块。剩下的问题就是如何用代码控制、如何调整显示效果、以及画不出来或显示异常时怎么排查。
3.5 用代码动态放置瓦块
编辑器里能点出来,只是第一步。做游戏迟早要用代码动态改地图,比如人物踩到某个机关后地板裂开、弹幕游戏里随机生成障碍物。这些操作都依赖 TileMapLayer 的代码接口。
在 Godot 4 中,TileMapLayer 提供了一套很简洁的 API。下面是最常用的几个方法:
gdscript复制# 设置一个格子上的瓦块
$TileMapLayer.set_cell(local_coords, source_id, atlas_coords, alternative_tile)
# 读取一个格子的 source_id,如果返回 -1,说明这个格子是空的
var source := $TileMapLayer.get_cell_source_id(local_coords)
# 读取一个格子上的瓦片坐标
var atlas_coords := $TileMapLayer.get_cell_atlas_coords(local_coords)
# 清理所有格子
$TileMapLayer.clear()
# 把局部坐标转换成格子坐标
var cell := $TileMapLayer.local_to_map(local_pos)
# 把格子坐标转换成该格子中心的局部坐标
var cell_center := $TileMapLayer.map_to_local(cell)
这里需要注意 local_coords 指的是 瓦片地图自身的局部坐标系,不是全局坐标。如果你要根据鼠标点击位置动态放置瓦块,通常要做两层换算:
gdscript复制func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
var local_pos := $TileMapLayer.to_local(get_global_mouse_position())
var clicked_cell := $TileMapLayer.local_to_map(local_pos)
$TileMapLayer.set_cell(clicked_cell, 0, Vector2i(0, 0), 0)
这段代码把全局鼠标位置转换到 TileMapLayer 的局部空间,再换算成格子坐标,最后写入瓦块。这里的 0, Vector2i(0, 0), 0 分别代表 source_id、atlas_coords、alternative_tile。在只有一个图集源的情况下,source_id 通常就是 0。atlas_coords 是你在图集源中的瓦块行列位置,比如第一行第一列就是 Vector2i(0, 0)。
如果你是做弹幕游戏或者 Roguelike 地图生成的,这套 API 会频繁用到。现在把它弄明白,后面写自动生成地图时就可以减少很多试错。
4. 把显示效果调到舒服:模糊、锯齿、瓦片边缘
4.1 图片模糊怎么办:纹理过滤与 Nearest 模式
有个热词叫"godot 中 2d 人物走路模糊",其实瓦片地图也一样容易糊。Godot 默认对纹理启用线性过滤,放大缩小时做平滑插值。这对照片类贴图是好消息,但对像素风瓦片就是灾难——本来锐利的像素边缘会被模糊成一片灰色过渡,看起来脏兮兮的。
解决方案很简单,在项目设置里把纹理过滤改成最邻近(Nearest):
- 打开 项目设置 -> 渲染 -> 纹理。
- 找到 默认纹理过滤(Default Texture Filter)。
- 从 Inherit 改成 Nearest。
改完之后,场景里的瓦片边缘会重新变得干净利落。注意这个设置影响整个项目所有纹理,如果你有些图片希望继续保持平滑过渡,可以单独修改那个 Sprite2D 或 TextureRect 的 Texture Filter 属性,给它单独指定 Linear。这种"项目级默认 + 节点级覆盖"的思路,在引擎里很常见,既能保证整体风格统一,又给个别节点留了差异化空间。
4.2 锯齿严重:抗锯齿和像素对齐
热词里还有一个"godot 锯齿严重"。这个问题的根源通常不只是纹理过滤,而是相机缩放和像素渲染没有对齐。2D 游戏里,相机位置如果落在半个像素上,所有瓦片边缘就会产生锯齿抖动感。
常规做法是:
- 在项目设置 -> 渲染 -> 抗锯齿里打开 MSAA 2D,选择 2x 或 4x。
- 如果做像素风游戏,把 项目设置 -> 显示 -> 窗口 -> 拉伸模式(Stretch Mode) 设为
canvas_items,拉伸方法设为viewport,这样游戏分辨率可以有效放大的同时保持像素风格清晰。 - 在代码里让相机坐标取整:
camera.position = camera.position.floor(),或者用平滑后对齐到整数像素,能明显减轻边缘抖动。
这里要特别提醒:像素风游戏和普通高清 2D 游戏的处理方向刚好相反。高清游戏希望越平滑越好,所以抗锯齿尽量开高;像素风游戏则希望像素边缘锐利,所以纹理过滤用 Nearest,抗锯齿反而不能乱开。你先确认自己做的是什么风格,再决定调哪边。
4.3 瓦片边缘出现白线或黑色细缝
这是瓦片地图最经典的坑,几乎每个美术素材都会遇到一次。你辛辛苦苦画好瓦片,贴到场景里,结果相邻瓦片之间冒出一道细细的白线或黑线,放大看又没有。原因是图集纹理采样的边缘溢色。
素材在保存时,瓦片边缘的颜色可能和相邻瓦片边缘的颜色不同。当 GPU 采样时,线性过滤下会取到边缘之外的像素,于是那条缝就出现了。
解决办法有几个:
- 在图集源的 纹理边距(Texture Margin) 里给图片四周留 1 到 2 像素的透明边距,这样采样不会跨到其他瓦片。
- 如果素材本身没有边距,可以在图片处理软件里把所有瓦片向外扩展 1 像素(俗称"出血/羽化边")。
- 纹理过滤设置为 Nearest 后,这个现象通常会减轻,因为不再发生线性插值采样了。
从实际经验看,最稳妥的组合是:素材画布预留 1 像素透明间距 + 纹理过滤设 Nearest。只要这两点做好,瓷砖之间的细缝会几乎消失。
5. 常见问题与排查技巧实录
5.1 我选中了瓦块,但怎么画都画不上
这个问题排在所有瓦片地图问题里的第一名。原因通常有三个:
一是 TileMapLayer 节点没有选中。Godot 的绘制操作是作用在当前选中节点上的,如果你不小心选中了根节点,底部面板可能还是在的,但画到 2D 视口上就没反应。先点击属性面板里的 TileMapLayer 节点,然后再画。
二是 TileSet 没有成功绑定。去看属性面板 TileSet 一栏是不是空的,如果为空,绘画面板是空的,你什么都选不了。
三是 你选的是移动工具,不是绘制工具。TileMapLayer 节点上有一排工具栏,包括移动、绘制、擦除、选择等。如果当前处于移动工具模式,左键点击会用来拖动物体而不是画瓦片。点一下绘制工具(快捷键 D)再试试。
5.2 画完以后瓦片位置不对,偏移了半格
如果瓦片显示的位置和鼠标点击的位置差半格,多半是吸附网格没有勾选,或者吸附网格尺寸与瓦片尺寸不一致。
在 2D 视口顶部,有一个磁性吸附按钮,点开后旁边可以设置网格步长。你要把它设置成和图集源里瓦片尺寸一致,比如 64 像素。另外检查 TileMapLayer 节点设置里有没有调整过 Y 排序或偏移属性,如果之前为了做层叠效果改过 Y Sort,绘制位置看起来就可能和鼠标脱节。
还有一种情况:场景根节点是 Node2D 而不是 TileMapLayer 本身,并且你在代码里给 TileMapLayer 设置了非零的 position,此时使用全局坐标换算是容易出错的。统一用 to_local + local_to_map 可以避免很多这类型的偏移问题。
5.3 代码删除节点和清空地图的快捷方式
热词里有一条"godot 中代码删除节点",这虽然和瓦片地图不是直接强相关,但做动态地图时很常用。如果你想在运行时删掉一个 TileMapLayer 节点,正常写法:
gdscript复制$TileMapLayer.queue_free()
但如果你只是想清空所有瓦块,让层还留在场景里,则用:
gdscript复制$TileMapLayer.clear()
我建议优先用 clear() 而不是循环遍历 set_cell(..., -1, ...),因为 clear() 是引擎内部的一次性清理,性能更好,代码也简洁。等你后面做地图随机生成,需要先清空再重建时,这两行会让你少踩很多坑。
另外提醒一句:queue_free() 是在当前帧结束后删除节点,如果你紧接着还要访问这个节点,可能会报"node is not inside the scene tree"之类的错误。需要立即删除且不允许访问时,可以考虑 free(),但它不安全,强烈不建议乱用,queue_free() 才是常规操作。
5.4 瓦片显示出来,但运行时看不到
很多人在编辑器里画得漂亮,按下 F5 运行时屏幕却空空如也。排查思路就三条:
- 检查 TileMapLayer 节点是否被隐藏。
- 检查相机是否对准了地图区域。2D 场景里没有 Camera2D,或者相机停在原点,而你的地图画在几千像素之外,自然看不到。
- 检查场景树里有没有脚本不小心把层移到了别的地方。
如果以上都没问题,在编辑器里按 V 键可以 zoom to fit,确认瓦块的实际位置。运行时在 Godot 的 Remote 场景树里点开 TileMapLayer,看它的 position 是否正常。
5.5 关于导出 APK 和加载 PCK 的杂谈
热词里出现"godot apk 加载 pck 下载",新手很容易在某次导出 Android 包后,发现瓦片显示都正常,但换成 PCK 方式加载外部资源后地图素材全丢了。这是因为 PCK 模式下资源加载路径和打包时不同,特别是你用了类似 load("res://...") 路径做热更新的项目,TileSet 里的纹理引用如果写的是绝对路径,解包后就找不到。
这个问题的标准做法是:所有动态加载资源统一走 ProjectSettings.globalize_path() 或 ResourceLoader.load(),并且尽量把瓦片素材放在常规 res:// 路径里,不要依赖外部 PCK 去加载引擎核心资源。真需要热更新,也建议把动态资源限定在图片、音频、配置文件层面,瓦片图集这类核心渲染资源最好还是打进主包里。
不过这个话题对刚学瓦片地图的人来说有点远,先记录在这里,等你真做了多平台导出再回来翻。
6. 更进一步:从单瓦块到完整地图的扩展方向
单瓦块显示成功之后,你其实已经解锁了一大片玩法空间。我建议按下面几个方向循序渐进去扩展:
- 多瓦块绘制:按住 Shift 拖拽可以一次性画一排,这在铺地板时非常高效。
- 随机瓦片:TileSet 面板里选中一个格子,点右键可以添加多个替代瓦片并设定权重,然后配合"随机瓦片"工具,能快速生成高低错落的地面纹理。
- 碰撞与导航:在 TileSet 资源里新建一个物理层,然后给瓦块手动画碰撞多边形。这样玩家角色走上去就不会掉穿。要做敌人寻路,再添加一个导航层,Godot 4 的导航区域可以直接从 TileMapLayer 生成。
- 自动瓦片(Terrains):这是瓦片地图最好用的进阶功能之一,它能让不同地形块根据相邻块自动匹配边缘,画草地、洞穴时能省掉 90% 的手工操作。刚上手时先别急,等单瓦块逻辑完全吃透再看也不迟。
我做小项目时最喜欢的一套工作流是:一个 TileSet 资源配好几个 TileMapLayer,层与层之间用不同 Z 轴或 Y 轴偏移形成视觉层次。地面层放土块,装饰层放花草,碰撞层单独放墙壁。这样代码里的逻辑判断非常清晰:角色撞到哪一层,我直接在 body_entered 回调里查 get_cell_source_id 对应的格子类型就行。
7. 自己动手时的一些体会
最后说点个人经验。刚开始学 TileMapLayer 时,我总想一口气把自动瓦片、碰撞、导航全都学会,结果就是不停报错、不停改,最后烦躁到差点弃坑。后来我强迫自己"先只画一个瓦片",把这个过程里的节点、资源、坐标、代码全部跑通,之后再叠加功能明显顺手了很多。
所以如果你现在就是一个刚打开 Godot 的新手,我强烈建议你跟着这篇文章做完一遍:新建场景,新建 TileMapLayer,创建 TileSet,拖图集,设尺寸,选瓦块,画上去。哪怕只显示一个瓦片,也已经是里程碑了。
之后每加一个功能之前,都先问自己一句:这个新功能会不会影响我已经跑通的瓦块显示?如果会,就先备份一份场景,或者用 Git 打个版本。时间久了你会感激这个好习惯,至少能救回许多被改崩的项目。
好了,这次的分享就先讲到这,希望你在瓦片地图这条路上走得顺手。有拿不准的细节,最好的办法还是直接开个新场景亲手试几次,比看文档十遍都管用。
