图块地图
用一张图块集拼出关卡:地面、墙、平台。可以在代码里摆格子,也可以直接读 Tiled 做的关卡。
TileMapLayer
ts
import { Scene, TileMapLayer, tileset, v } from 'sapling2d'
// 图块集:图集里每格 16px,编号从 1 开始(左上角是 1,从左到右、从上到下)
const TILES = tileset('tiles.png', {
tileSize: 16,
tiles: {
1: { collision: 'solid' }, // 地面
2: { collision: 'solid', data: { breakable: true } }, // 砖块
3: { collision: 'oneWay' }, // 单向平台
},
})
// 关卡用字符画写在代码里;以后可以从 Tiled 导入
const LEVEL = [
'................',
'.....222........',
'..........333...',
'................',
'1111111111111111',
]
const IDS: Record<string, number> = { '.': 0, '1': 1, '2': 2, '3': 3 }
export class Level extends Scene {
static override assets = { tiles: TILES }
ground!: TileMapLayer
override ready() {
this.ground = this.add(
new TileMapLayer({
name: 'Ground',
tileSet: TILES,
width: LEVEL[0]!.length,
height: LEVEL.length,
cells: LEVEL.join('').split('').map((c) => IDS[c]!),
position: v(0, 200), // 格子 (0, 0) 的左上角在这里
}),
)
}
/** 顶砖块:point 是图层的局部坐标(像素)。 */
bump(point: { x: number; y: number }) {
const cell = this.ground.localToMap(v(point.x, point.y))
if (this.ground.getCellTileData(cell.x, cell.y)?.data.breakable) this.ground.eraseCell(cell.x, cell.y)
}
}图块集
tileset(path, { tileSize, tiles, columns?, margin?, spacing? }),放进 static assets 预加载。
- 图块编号从 1 开始:图集左上角是 1,从左到右、从上到下。0 表示空格子。
tiles给图块定义属性:collision('solid'整格实心 /'oneWay'单向平台)和data(游戏自己的字段)。tileSet.tile(id)读取。
图层
TileMapLayer 是一层格子,大小(width × height 格)创建后固定,格子左上角在节点原点。多层地图就是多个节点,叠放顺序看场景树。
| 方法 | 作用 |
|---|---|
setCell(cx, cy, id) / eraseCell / getCell | 改、删、读格子(地图外返回 0) |
getCellTileData | 格子上图块的属性,空格子为 null |
localToMap / mapToLocal | 像素 → 格子;格子 → 格子中心的像素 |
getUsedRect / usedCellCount | 用到的范围(单位是格)、非空格子数 |
- 只画屏幕内的区块(16 × 16 格),改格子很便宜(顶碎砖块直接
eraseCell)。几万格的关卡也只是一个节点,不要用Sprite2D一格一格地拼。 - 图层可以平移,不要旋转、缩放(碰撞按格子查)。
限制
- 一个图块集只能用一张图;图块不能翻转、旋转,没有动画。
- 碰撞只有整格,没有斜坡。
- 图块地图不生成物理刚体:
RigidBody2D不会被它挡住。和图块碰撞的角色用CharacterBody2D。
Tiled 关卡
ts
import { Camera2D, Node2D, Scene, TileMapLayer, tiledMap, v } from 'sapling2d'
// 关卡文件放在资源目录里:public/assets/levels/1-1.json(图块集和图片按 Tiled 里的相对路径放)
const LEVEL = tiledMap('levels/1-1.json')
class Coin extends Node2D {
value = 1
}
export class Level extends Scene {
static override assets = { level: LEVEL } // 进入场景前加载关卡文件、图块集和图片
coins: Coin[] = []
player!: Node2D
override ready() {
// 每个图块层一个 TileMapLayer,按 Tiled 里从下到上的顺序 add,叠放顺序和 Tiled 一样
for (const layer of TileMapLayer.fromTiled(LEVEL)) this.add(layer)
// 对象层只是数据:游戏按 type(Tiled 里的 Class)自己创建节点
for (const o of LEVEL.objects('Entities')) {
if (o.type === 'Spawn') this.player = this.add(new Node2D({ name: 'Player', position: v(o.x, o.y) }))
if (o.type === 'Coin') {
const coin = this.add(new Coin({ position: v(o.x + o.width / 2, o.y + o.height / 2) })) // 矩形对象的 (x, y) 是左上角
coin.value = Number(o.properties.value ?? 1)
this.coins.push(coin)
}
}
this.player.add(new Camera2D({ limitLeft: 0, limitTop: 0, limitRight: LEVEL.pixelWidth, limitBottom: LEVEL.pixelHeight }))
}
}加载
tiledMap(path)读 Tiled 的 JSON 格式关卡,放进static assets。图块集(嵌入的或外部 JSON)和图片按 Tiled 里的相对路径放,都要在资源目录里。- 扩展名用
.json(Tiled 保存时可以选):微信小游戏的代码包可能不收.tmj/.tsj。XML 格式(.tmx/.tsx)不支持。 - vite 插件会检查关卡文件和它引用的图块集、图片。切换场景时图片按张判断是否卸载,两个关卡共用的图不会被卸载。
读取
TileMapLayer.fromTiled(map):每个图块层建一个TileMapLayer(名字、偏移、可见性、不透明度来自 Tiled),从下到上排列;fromTiled(map, name)只建那一层。- 地图数据:
tileLayers、tileLayer(name)、layerNames、width/height(格)、tileSize、pixelWidth/pixelHeight(设相机 limit 用)、tileSets、properties。 objects(layerName?):对象层的数据(id、name、type、x、y、width、height、properties……)。矩形和点的 (x, y) 是左上角 / 点本身,图块对象是左下角(Tiled 的约定)。
在 Tiled 里设置碰撞
- 在图块集编辑器里选中图块,加字符串属性
collision,值为solid(实心)或oneWay(单向平台)。其他属性进getCellTileData(...).data。 - Tiled 碰撞编辑器里画的形状不起作用(会警告)。
- 图块层加整数属性
collisionLayer设置碰撞层(默认 1)。
限制
加载时遇到这些情况会报错,并指出是哪个图层或图块集:
- 只支持正交、非无限的地图;
- 图块是正方形、和地图格子一样大;每个图块集一张图片;一个图块层只用一个图块集;
- 图块不能翻转、旋转;
- 图块层格式用 CSV(Map Properties → Tile Layer Format);
- 不支持图层组、图片图层。动画图块只画第一帧(会警告)。
无头测试读真实文件:createTestGame({ ..., assetsDir }),默认 public/assets。