Defold Tilemap 碰撞与关卡设计

系统讲解 Defold Tilemap 碰撞系统:Tilemap 编辑器使用、碰撞组与掩码配置、tilemap 碰撞形状、AABB 与圆形碰撞检测、传感器 tile、动态 tile 修改,以及程序化生成关卡(Perlin noise 等算法)。

引言

2D 平台游戏、俯视角 RPG、Metroidvania——它们的关卡底层都由 Tilemap(瓦片地图) 构建。Defold 的 Tilemap 不仅是一个可视化关卡编辑器,更内置了物理碰撞支持,让地砖、墙壁、坑洞天然具备碰撞属性。本文系统讲解 Defold Tilemap:编辑器怎么画 tile、碰撞组怎么设(组与掩码)、碰撞形状怎么配(AABB/圆形)、传感器 tile 怎么用;进阶部分讲动态 runtime 修改 tile,以及用 Perlin noise 等算法程序化生成大地图。

前置:/defold-level-editor/(关卡设计与编辑器基础)。物理碰撞原理见 /defold-physics-collision/。


目录


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 SourceTile 素材集(每张图切成多个 tile).tilesource
Tilemap使用 Tile Source 绘制的关卡.tilemap
Tile LayerTilemap 内的分层(前景/背景/碰撞)内嵌
场景树结构:
  [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 碰撞体设置

  1. 打开 Tile Source(.tilesource)
  2. 选中一个 tile
  3. 在右侧 Collision 面板勾选 Collision 复选框
  4. 选择碰撞形状(Box/Shrink/Circle)
  5. 选择该 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. 速查表

需求方法说明
创建 TilemapNew → Tile Source → New → TilemapTile Source 是素材,Tilemap 是关卡
给 tile 加碰撞Tile Source 编辑器 → 选 tile → Collision勾选并选形状/组
碰撞组定义game.project → Physics最多 16 组
碰撞发生条件A.mask ∩ B.group ≠ ∅ 且反之双向检查
传感器Collision → Is Trigger只发消息无物理
获取 tiletilemap.get_tile(url, layer, x, y)返回 tile 编号
设置 tiletilemap.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

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold 性能分析与调试工具链
  2. Defold Collections、Factories 与动态实例化
  3. Defold IAP 与广告接入:内购流程、Store 验证与广告变现