引言
2D 平台游戏、俯视角 RPG、Metroidvania——它们的关卡底层都由 Tilemap(瓦片地图) 构建。Defold 的 Tilemap 不仅是一个可视化关卡编辑器,更内置了物理碰撞支持,让地砖、墙壁、坑洞天然具备碰撞属性。本文系统讲解 Defold Tilemap:编辑器怎么画 tile、碰撞组怎么设(组与掩码)、碰撞形状怎么配(AABB/圆形)、传感器 tile 怎么用;进阶部分讲动态 runtime 修改 tile,以及用 Perlin noise 等算法程序化生成大地图。
前置:/defold-level-editor/(关卡设计与编辑器基础)。物理碰撞原理见 /defold-physics-collision/。
目录
- 1. Tilemap 编辑器:从空白画布到关卡
- 2. 碰撞组设置与掩码
- 3. Tilemap 碰撞形状:AABB 与圆形
- 4. 传感器 Tile 与触发区域
- 5. 动态 Tile 修改:运行时编辑地图
- 6. 程序化生成关卡:Perlin Noise 等算法
- 7. 性能优化与最佳实践
- 8. 速查表
- 相关阅读
- 延伸阅读
1. Tilemap 编辑器:从空白画布到关卡
1.1 创建 Tilemap
1. 准备 Tile Source
→ 右键 Assets → New → Tile Source
→ 选择 Spritesheet 或单张 tile 图
→ 设置 Tile Width / Tile Height(如 32x32)
→ 设置 Margin / Spacing(如果图集中 tile 有间距)
2. 创建 Tilemap
→ 右键 Assets → New → Tilemap
→ 将 Tile Source 拖到属性面板
→ 在编辑器中像「涂油漆」一样刷 tile
1.2 Tile Source vs Tilemap
| 概念 | 说明 | 文件 |
|---|---|---|
| Tile Source | Tile 素材集(每张图切成多个 tile) | .tilesource |
| Tilemap | 使用 Tile Source 绘制的关卡 | .tilemap |
| Tile Layer | Tilemap 内的分层(前景/背景/碰撞) | 内嵌 |
场景树结构:
[Collection]
└── ["level" Game Object]
└── [tilemap Component] ← .tilemap 文件引用
└── Layer 0: background
└── Layer 1: ground ← 可能有碰撞
└── Layer 2: foreground
1.3 编辑器操作
左键 → 放置当前选中的 tile
右键 → 擦除 tile
数字键 1-9 → 快速选择 tile 编号
鼠标滚轮 → 缩放视图
Shift + 拖动 → 批量填充区域
2. 碰撞组设置与掩码
2.1 碰撞组(Collision Groups)
在 game.project → Physics 中,可以定义碰撞组名称:
[physics]
gravity_y = -10
max_collisions = 64
; 定义碰撞组
collisiongroup0 = player
collisiongroup1 = ground
collisiongroup2 = enemy
collisiongroup3 = trigger
组决定「我是谁」,掩码(Mask)决定「我碰谁」。
2.2 Tilemap 碰撞体设置
- 打开 Tile Source(.tilesource)
- 选中一个 tile
- 在右侧 Collision 面板勾选 Collision 复选框
- 选择碰撞形状(Box/Shrink/Circle)
- 选择该 tile 所属的碰撞组
tile 0 (草地):
[✓] Collision
Shape: Box
Group: ground
tile 1 (尖刺):
[✓] Collision
Shape: Box
Group: ground (或 enemy)
2.3 碰撞掩码在 Game Object 中配置
1. 给玩家 Game Object 添加 Collision Object 组件
2. 设置 Collision Object → Group = "player"
3. 设置 Collision Object → Mask → 勾选 "ground" 和 "enemy"
→ 表示玩家会与 ground 组和 enemy 组发生碰撞
2.4 碰撞掩码配置解释
Group: player
Mask: [✓] ground → 玩家碰到地面 → on_message("collision_response")
[ ] wall → 不勾选 → 穿透 walls
[✓] enemy → 碰到敌人 → 受伤
[✓] trigger → 碰到触发器 → 触发事件
记忆:碰撞发生的条件 = A 的 Mask 包含 B 的 Group,且 B 的 Mask 包含 A 的 Group(双向检查)——不勾选就不会碰。
3. Tilemap 碰撞形状:AABB 与圆形
3.1 三种碰撞形状
| 形状 | 说明 | 适用场景 |
|---|---|---|
| Box | 占满整个 tile 的矩形 | 普通墙壁、地面 |
| Shrink Box | 比 tile 稍小的矩形 | 需要微小间隙的 tile |
| Circle | 外接圆 | 球体、圆形陷阱 |
3.2 设置 AABB 碰撞
在 Tile Source 编辑器中:
→ 选中 tile
→ Collision → Shape: Box
→ 自动占满 32x32(tile size)
3.3 设置圆形碰撞
在 Tile Source 编辑器中:
→ 选中 tile
→ Collision → Shape: Circle
→ 半径自动为 tile 宽度的一半(16px)
碰撞回调:
-- player.script — 处理碰撞事件
function on_message(self, message_id, message, sender)
if message_id == hash("collision_response") then
-- message.other_group = 碰撞对方的组名 hash
if message.other_group == hash("ground") then
self.on_ground = true
elseif message.other_group == hash("enemy") then
take_damage(self, 1)
end
elseif message_id == hash("trigger_response") then
-- trigger 类型的碰撞(传感器,不产生反弹)
if message.other_group == hash("checkpoint") then
save_checkpoint(self, go.get_position())
end
end
end
说明:
collision_response带物理反应(弹性/摩擦),trigger_response只通知不施加物理力。
4. 传感器 Tile 与触发区域
4.1 什么场景用传感器
- 检查点(checkpoint):站到上面存档
- 传送门:进入后切换场景
- 陷阱:踩到后扣血但不反弹
- 收集品:金币、道具,接触即收集
- 对话 NPC 范围:靠近自动弹出对话
4.2 配置传感器 Tile
在 Tile Source 编辑器中:
→ 选中 tile (如 金币 tile)
→ [✓] Collision
→ Group: trigger
→ [✓] Is Trigger ← 关键!勾选后只触发消息,无物理反应
4.3 传感器事件处理
-- player.script
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
-- message.enter = true (进入) / false (离开)
if message.enter then
if message.other_group == hash("coin") then
collect_coin(self, sender)
-- 传感器 tile 需要自行移除(传感器不自动销毁)
msg.post(sender, "disable")
elseif message.other_group == hash("checkpoint") then
save_game(self)
print("Checkpoint saved!")
end
end
end
end
5. 动态 Tile 修改:运行时编辑地图
5.1 tilemap 运行时 API
local tilemap = require("tilemap")
-- 获取/设置 tile
local t = tilemap.get_tile("/level#tilemap", "ground", 5, 3)
-- 返回 tile 的索引编号(在 Tile Source 中的序号)
-- 设置 tile: 将 (5, 3) 位置设为 tile 编号 2(如变成空白/泥土)
tilemap.set_tile("/level#tilemap", "ground", 5, 3, 2)
5.2 挖掘/破坏效果
-- 链式爆炸:删除 3x3 区域内的 tile
function explode_area(self, center_x, center_y, radius)
for dx = -radius, radius do
for dy = -radius, radius do
local x, y = center_x + dx, center_y + dy
-- 设为空白 tile(假设 tile 0 是透明/空白)
tilemap.set_tile("/level#tilemap", "ground", x, y, 0)
end
end
-- 可选:添加粒子效果
factory.create("#explosion_particle",
tilemap_to_world(center_x, center_y))
end
-- tile 坐标转世界坐标
function tilemap_to_world(tx, ty)
local tile_size = 32
return vmath.vector3(tx * tile_size, ty * tile_size, 0)
end
5.3 动态添加碰撞
-- 创建新的碰撞体(非 tilemap)做动态障碍物
function spawn_dynamic_block(self, tx, ty)
local pos = tilemap_to_world(tx, ty)
local block = factory.create("#dynamic_block_factory", pos)
self.dynamic_blocks[block] = { tx = tx, ty = ty }
end
注意:tilemap.set_tile 只修改视觉 tile,不自动更新碰撞——因为 tilemap 的碰撞是通过 Tile Source 的碰撞预设决定的。如果 tile 0 没有 collision,则设成 0 后该区域变为可通行。
6. 程序化生成关卡:Perlin Noise 等算法
6.1 程序化生成流程
1. 生成高度图/噪声图(Perlin/Simplex)
2. 根据阈值映射到 tile 编号
3. 用 tilemap.set_tile 填充地图
4. 选取出生点、终点、敌人生成点
6.2 Perlin Noise 基础实现
-- perlin.lua(简化版,Defold 内可直接使用)
local M = {}
-- 简单的插值噪声(真实 Perlin 更复杂,这里用值噪声演示)
function M.noise(x, y)
local ix = math.floor(x)
local iy = math.floor(y)
local fx = x - ix
local fy = y - iy
-- 伪随机哈希(简化)
local function hash(n)
n = math.sin(n * 12.9898 + 78.233) * 43758.5453
return n - math.floor(n)
end
local a = hash(ix + iy * 374761)
local b = hash(ix + 1 + iy * 374761)
local c = hash(ix + (iy + 1) * 374761)
local d = hash(ix + 1 + (iy + 1) * 374761)
-- 双线性插值
local u = fx * fx * (3 - 2 * fx) -- smoothstep
local v = fy * fy * (3 - 2 * fy)
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v
end
-- 分形Brown运动(FBM)叠加多层噪声
function M.fbm(x, y, octaves)
local value = 0
local amplitude = 1
local frequency = 1
for i = 1, octaves do
value = value + M.noise(x * frequency, y * frequency) * amplitude
amplitude = amplitude * 0.5
frequency = frequency * 2
end
return value
end
return M
6.3 噪声转 Tilemap
-- level_generator.script
local perlin = require("perlin")
local MAP_W, MAP_H = 80, 45
local TILE_SIZE = 32
function generate_level(self)
local seed = os.time() % 10000
print("Generating level with seed:", seed)
for x = 0, MAP_W - 1 do
for y = 0, MAP_H - 1 do
-- 缩放坐标,让噪声频率合适
local nx = x / 20 + seed
local ny = y / 20 + seed
local h = perlin.fbm(nx, ny, 4)
-- 阈值映射
local tile = 0 -- 空白/天空
if h > 0.6 then
tile = 3 -- 石头
elseif h > 0.4 then
tile = 2 -- 泥土
elseif h > 0.2 then
tile = 1 -- 草地
end
tilemap.set_tile("/level#tilemap", "ground", x, y, tile)
end
end
-- 放置起点与终点
tilemap.set_tile("/level#tilemap", "objects", 2, 10, 10) -- 起点
tilemap.set_tile("/level#tilemap", "objects", MAP_W - 3, 10, 11) -- 终点
end
function init(self)
generate_level(self)
end
映射规则示例:
噪声值 range → tile
==============
< 0.2 → 0 (空白/空气)
0.2 - 0.4 → 1 (草地)
0.4 - 0.6 → 2 (泥土/平台)
> 0.6 → 3 (岩石/墙壁)
6.4 洞穴生成(细胞自动机)
-- 细胞自动机生成洞穴
function generate_cave(self)
local map = {}
local w, h = 60, 40
-- 随机初始化
for x = 1, w do
map[x] = {}
for y = 1, h do
map[x][y] = math.random() < 0.45 and 1 or 0
end
end
-- 平滑迭代(4-5 次)
for iter = 1, 4 do
local new_map = {}
for x = 1, w do
new_map[x] = {}
for y = 1, h do
local neighbors = count_neighbors(map, x, y, w, h)
if neighbors > 4 then
new_map[x][y] = 1 -- 墙壁
elseif neighbors < 4 then
new_map[x][y] = 0 -- 空地
else
new_map[x][y] = map[x][y]
end
end
end
map = new_map
end
-- 写入 tilemap
for x = 1, w do
for y = 1, h do
tilemap.set_tile("/level#tilemap", "ground", x - 1, y - 1, map[x][y])
end
end
end
function count_neighbors(map, x, y, w, h)
local count = 0
for dx = -1, 1 do
for dy = -1, 1 do
local nx, ny = x + dx, y + dy
if nx >= 1 and nx <= w and ny >= 1 and ny <= h then
if not (dx == 0 and dy == 0) and map[nx][ny] == 1 then
count = count + 1
end
else
count = count + 1 -- 边界外视为墙壁
end
end
end
return count
end
7. 性能优化与最佳实践
| 问题 | 优化方案 | 效果 |
|---|---|---|
| 大地图 tile 量大 | 只渲染视口内 tile(视锥剔除) | 显著减 Draw Call |
| tilemap set_tile 每帧调用 | 批量修改后一次性重建 | 减少 CPU 开销 |
| 过多碰撞体 | 动态障碍物用 factory 创建 | 物理引擎只处理附近区域 |
| 程序化生成卡顿 | 分帧生成(coroutine 或 timer) | 不阻塞主线程 |
| 多层重复 | 前景/背景用不同图集 | 合并同类 Draw Call |
分帧生成示例:
function generate_chunked(self)
local cx, cy = 0, 0
local total_w, total_h = 80, 45
local chunk_size = 10
timer.delay(0, true, function()
for x = cx, math.min(cx + chunk_size - 1, total_w - 1) do
for y = 0, total_h - 1 do
-- 生成并写入 tile
local tile = compute_tile(x, y)
tilemap.set_tile("/level#tilemap", "ground", x, y, tile)
end
end
cx = cx + chunk_size
if cx >= total_w then
return false -- 停止 timer
end
end)
end
8. 速查表
| 需求 | 方法 | 说明 |
|---|---|---|
| 创建 Tilemap | New → Tile Source → New → Tilemap | Tile Source 是素材,Tilemap 是关卡 |
| 给 tile 加碰撞 | Tile Source 编辑器 → 选 tile → Collision | 勾选并选形状/组 |
| 碰撞组定义 | game.project → Physics | 最多 16 组 |
| 碰撞发生条件 | A.mask ∩ B.group ≠ ∅ 且反之 | 双向检查 |
| 传感器 | Collision → Is Trigger | 只发消息无物理 |
| 获取 tile | tilemap.get_tile(url, layer, x, y) | 返回 tile 编号 |
| 设置 tile | tilemap.set_tile(url, layer, x, y, tile) | 实时修改地图 |
| 噪声生成 | Perlin FBM 叠加多层噪声 | 控制频率与振幅 |
| 洞穴生成 | 细胞自动机 + 平滑迭代 | 随机初始化后迭代 |
| 分帧生成 | timer.delay 分批写入 | 防卡顿 |
一句话记忆:Defold Tilemap = Tile Source(素材集)+ Tilemap(关卡)+ 组/掩码(碰撞)+ set_tile(动态编辑)+ 噪声/细胞自动机(程序化生成);碰撞形状选 Box/Shrink/Circle,传感器用 trigger 做检查点/收集;大地图分帧生成、视锥剔除——tile 不只是静态地砖,更是活的关卡骨架。
相关阅读
- /defold-level-editor/ — 关卡编辑器与场景设计基础
- /defold-physics-collision/ — 碰撞系统原理与 Collision Object 详解
- /defold-performance-optimization/ — 对象数量与 Draw Call 优化
延伸阅读
- /defold-collections-factories/ — 动态对象创建与对象池
- /defold-script-system-lua/ — Lua 脚本与消息系统
- /defold-save-serialization/ — 关卡状态保存与加载
- [[game]] — 程序生成关卡(PCG)理论
- [[graphics]] — 2D 渲染管线与视锥剔除
-- ======================================
-- 完整可运行示例:Tilemap + 碰撞 + 程序化生成
-- 放入 level_manager.script
-- ======================================
local perlin = require("perlin")
go.property("map_width", 40)
go.property("map_height", 24)
go.property("tile_size", 32)
go.property("use_noise", true)
function init(self)
self.map = {}
if self.use_noise then
generate_perlin_map(self)
else
generate_test_map(self)
end
print("[init] 地图生成完成:", self.map_width, "x", self.map_height)
end
function generate_perlin_map(self)
local seed = os.time() % 10000
for x = 0, self.map_width - 1 do
for y = 0, self.map_height - 1 do
local nx = x / 15 + seed * 0.1
local ny = y / 15 + seed * 0.1
local h = perlin.fbm(nx, ny, 3)
local tile = 0 -- 空白
if h > 0.55 then
tile = 3 -- 石头墙
elseif h > 0.35 then
tile = 2 -- 泥土
elseif h > 0.15 then
tile = 1 -- 草地
end
tilemap.set_tile("/level#tilemap", "ground", x, y, tile)
end
end
end
function generate_test_map(self)
-- 生成一个简单的平台关卡
for x = 0, self.map_width - 1 do
for y = 0, self.map_height - 1 do
local tile = 0
-- 地面层
if y == 5 then
tile = 1 -- 草地平台
elseif y < 5 then
tile = 0 -- 空白
else
tile = 2 -- 泥土背景
end
tilemap.set_tile("/level#tilemap", "ground", x, y, tile)
end
end
end
function explode_tile(self, tx, ty, radius)
for dx = -radius, radius do
for dy = -radius, radius do
local x, y = tx + dx, ty + dy
if x >= 0 and x < self.map_width and y >= 0 and y < self.map_height then
-- 设为空白(tile 0 无碰撞)
tilemap.set_tile("/level#tilemap", "ground", x, y, 0)
end
end
end
print("[explode] 爆炸中心:", tx, ty, "半径:", radius)
end
function on_message(self, message_id, message, sender)
if message_id == hash("destroy_tile") then
-- 世界坐标转 tile 坐标
local tx = math.floor(message.x / self.tile_size)
local ty = math.floor(message.y / self.tile_size)
explode_tile(self, tx, ty, message.radius or 1)
elseif message_id == hash("regenerate") then
generate_perlin_map(self)
end
end
function final(self)
print("[final] Level Manager 退出")
end
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。