Pixi.js 游戏开发教程 / Pixi.js Tilemap 地图(Tiled 集成)
14. Tilemap 地图(Tiled 集成)
概述
Tilemap 是 2D 游戏构建世界的标准方式。Tiled Map Editor 是最流行的瓦片地图编辑器,本章讲解如何将 Tiled 地图集成到 Pixi.js 中,实现高效的地图渲染。
Tiled 地图编辑器基础
Tiled 是免费开源的瓦片地图编辑器,支持正交、等轴测、六角网格地图。
核心概念
| 概念 | 说明 |
|---|---|
| Tilemap | 由瓦片组成的地图 |
| Tileset | 瓦片图集(一张大图包含多个小瓦片) |
| Tile Layer | 瓦片层,由瓦片 ID 组成的网格 |
| Object Layer | 对象层,用于放置怪物/NPC/触发器 |
| Image Layer | 图像层,用于背景等 |
推荐配置
地图设置:
- 地图方向: 正交
- 瓦片大小: 16x16 或 32x32(像素)
- 地图大小: 根据需求设定(如 100x100 瓦片)
- 渲染顺序: 右下
Tileset 设置:
- 间距: 0px(无间隙)
- 边距: 0px
- 导出格式: JSON
TMX/JSON 地图格式解析
Tiled 导出的 JSON 格式结构如下:
{
"width": 20,
"height": 15,
"tilewidth": 32,
"tileheight": 32,
"layers": [
{
"type": "tilelayer",
"name": "ground",
"data": [1, 1, 2, 3, 1, 1, 2, 2, 3, 1],
"width": 20,
"height": 15,
"visible": true
},
{
"type": "objectgroup",
"name": "entities",
"objects": [
{ "id": 1, "x": 128, "y": 256, "width": 32, "height": 32,
"type": "enemy", "properties": [{"name": "hp", "value": 100}] }
]
}
],
"tilesets": [
{ "firstgid": 1, "source": "terrain.tsx",
"imagewidth": 256, "imageheight": 256,
"tilewidth": 32, "tileheight": 32,
"columns": 8, "tilecount": 64 }
]
}
手动解析 JSON 地图
async function loadTiledMap(jsonPath) {
const response = await fetch(jsonPath);
const map = await response.json();
const result = {
width: map.width,
height: map.height,
tileWidth: map.tilewidth,
tileHeight: map.tileheight,
layers: {},
objects: [],
};
for (const layer of map.layers) {
if (layer.type === 'tilelayer') {
result.layers[layer.name] = {
data: layer.data,
width: layer.width,
height: layer.height,
visible: layer.visible,
};
} else if (layer.type === 'objectgroup') {
result.objects.push(...layer.objects.map(obj => ({
type: obj.type,
x: obj.x,
y: obj.y,
width: obj.width,
height: obj.height,
properties: Object.fromEntries(
(obj.properties || []).map(p => [p.name, p.value])
),
})));
}
}
return result;
}
@pixi/tilemap 库
@pixi/tilemap 是 Pixi.js 官方推荐的高性能瓦片地图渲染库:
npm install @pixi/tilemap
import { Application } from 'pixi.js';
import { CompositeTilemap } from '@pixi/tilemap';
const app = new Application();
await app.init({ width: 800, height: 600 });
// 加载地图纹理
const baseTexture = await Assets.load('/image/terrain.png');
// 创建 Tilemap
const tilemap = new CompositeTilemap();
// 设置瓦片
// tileSetFrame 是瓦片在图集中的矩形区域
const TILE_SIZE = 32;
const tilesPerRow = 8; // 图集中每行瓦片数
// 添加瓦片
function getTileRect(gid) {
const index = gid - 1; // GID 从 1 开始
const col = index % tilesPerRow;
const row = Math.floor(index / tilesPerRow);
return [col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE];
}
// 渲染地图
function renderTilemap(mapData) {
const layer = mapData.layers.ground;
for (let y = 0; y < layer.height; y++) {
for (let x = 0; x < layer.width; x++) {
const gid = layer.data[y * layer.width + x];
if (gid === 0) continue; // 空瓦片
const rect = getTileRect(gid);
tilemap.tile(
baseTexture, // 纹理
x * TILE_SIZE, // 目标 X
y * TILE_SIZE, // 目标 Y
{ u: rect[0], v: rect[1], tileWidth: TILE_SIZE, tileHeight: TILE_SIZE }
);
}
}
}
app.stage.addChild(tilemap);
CompositeTilemap vs Tilemap
| 类型 | 特点 | 适用场景 |
|---|---|---|
Tilemap | 单个 tileset,更高性能 | 简单地图,单一图集 |
CompositeTilemap | 多个 tileset,灵活 | 复杂地图,多图集 |
瓦片层/对象层/图像层
瓦片层渲染
function renderLayers(mapData, baseTextures) {
for (const [layerName, layer] of Object.entries(mapData.layers)) {
if (!layer.visible) continue;
const tilemap = new CompositeTilemap();
for (let y = 0; y < layer.height; y++) {
for (let x = 0; x < layer.width; x++) {
const gid = layer.data[y * layer.width + x];
if (gid === 0) continue;
const tex = resolveTexture(gid, baseTextures);
tilemap.tile(tex, x * TILE_SIZE, y * TILE_SIZE);
}
}
tilemap.layerName = layerName;
app.stage.addChild(tilemap);
}
}
对象层解析
function spawnEntities(mapData) {
for (const obj of mapData.objects) {
switch (obj.type) {
case 'spawn':
player.x = obj.x;
player.y = obj.y;
break;
case 'enemy':
spawnEnemy(obj.x, obj.y, obj.properties);
break;
case 'chest':
spawnChest(obj.x, obj.y, obj.properties.loot);
break;
case 'trigger':
createTrigger(obj.x, obj.y, obj.width, obj.height,
obj.properties.event);
break;
case 'portal':
createPortal(obj.x, obj.y, obj.properties.targetMap,
obj.properties.targetX, obj.properties.targetY);
break;
}
}
}
Tileset 配置
外部 TSX 文件解析
// Tiled 的 TSX 本质上是 XML,也可以导出为 JSON
async function loadTileset(tsxPath) {
const response = await fetch(tsxPath);
const text = await response.text();
// 简单解析 XML
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/xml');
const tileset = doc.querySelector('tileset');
return {
name: tileset.getAttribute('name'),
tileWidth: parseInt(tileset.getAttribute('tilewidth')),
tileHeight: parseInt(tileset.getAttribute('tileheight')),
tileCount: parseInt(tileset.getAttribute('tilecount')),
columns: parseInt(tileset.getAttribute('columns')),
image: tileset.querySelector('image').getAttribute('source'),
imageWidth: parseInt(tileset.querySelector('image').getAttribute('width')),
imageHeight: parseInt(tileset.querySelector('image').getAttribute('height')),
};
}
地图滚动
地图滚动通常通过移动地图容器实现(配合相机系统):
// 地图容器作为世界层
const world = new Container();
const tilemap = new CompositeTilemap();
world.addChild(tilemap);
app.stage.addChild(world);
// 相机跟随玩家
app.ticker.add(() => {
const camX = player.x - app.screen.width / 2;
const camY = player.y - app.screen.height / 2;
// 边界限制
const maxCamX = mapWidth * TILE_SIZE - app.screen.width;
const maxCamY = mapHeight * TILE_SIZE - app.screen.height;
world.x = -Math.max(0, Math.min(maxCamX, camX));
world.y = -Math.max(0, Math.min(maxCamY, camY));
});
自动图块 AutoTile
AutoTile 根据相邻瓦片自动选择正确的边缘/角落瓦片:
// 简化的 AutoTile 逻辑(47 种瓦片变体规则)
const AUTOTILE_RULES = {
// [上, 右, 下, 左] → tileIndex
'0000': 0, // 孤立
'1111': 5, // 全包围
'0101': 10, // 左右连接
'1010': 15, // 上下连接
// ... 完整 47 种规则
};
function getAutoTileIndex(map, x, y, layerName) {
const layer = map.layers[layerName];
const gid = layer.data[y * layer.width + x];
if (gid === 0) return -1;
// 检查四方向邻居
const up = y > 0 && layer.data[(y-1) * layer.width + x] !== 0 ? 1 : 0;
const right = x < layer.width - 1 && layer.data[y * layer.width + x + 1] !== 0 ? 1 : 0;
const down = y < layer.height - 1 && layer.data[(y+1) * layer.width + x] !== 0 ? 1 : 0;
const left = x > 0 && layer.data[y * layer.width + x - 1] !== 0 ? 1 : 0;
const key = `${up}${right}${down}${left}`;
return AUTOTILE_RULES[key] ?? 0;
}
💡 提示:完整的 AutoTile 有 47 种变体,推荐使用 Tiled 内置的地形工具或第三方 AutoTile 素材包(如 RPG Maker 格式)。
碰撞图层标记
在 Tiled 中使用专用图层标记碰撞区域:
function buildCollisionMap(mapData) {
const collisionLayer = mapData.layers.collision;
if (!collisionLayer) return null;
// 创建二维碰撞数组
const collision = [];
for (let y = 0; y < collisionLayer.height; y++) {
collision[y] = [];
for (let x = 0; x < collisionLayer.width; x++) {
collision[y][x] = collisionLayer.data[y * collisionLayer.width + x] !== 0;
}
}
return {
data: collision,
isBlocked(worldX, worldY) {
const tileX = Math.floor(worldX / TILE_SIZE);
const tileY = Math.floor(worldY / TILE_SIZE);
if (tileX < 0 || tileY < 0 ||
tileX >= collisionLayer.width || tileY >= collisionLayer.height) {
return true; // 地图外视为阻挡
}
return collision[tileY][tileX];
},
};
}
// 移动检测
function canMoveTo(x, y) {
return !collisionMap.isBlocked(x, y);
}
地图优化:仅渲染可见区域
对于大地图,只渲染摄像机视野内的瓦片可以大幅提升性能:
class VisibleTilemapRenderer {
constructor(tilemap, mapData, baseTexture) {
this.tilemap = tilemap;
this.mapData = mapData;
this.baseTexture = baseTexture;
this.lastVisibleRange = null;
}
updateVisibleTiles(cameraX, cameraY, screenW, screenH) {
const padding = 2; // 额外渲染 2 瓦片的边界
const startX = Math.max(0, Math.floor(cameraX / TILE_SIZE) - padding);
const startY = Math.max(0, Math.floor(cameraY / TILE_SIZE) - padding);
const endX = Math.min(this.mapData.width - 1,
Math.ceil((cameraX + screenW) / TILE_SIZE) + padding);
const endY = Math.min(this.mapData.height - 1,
Math.ceil((cameraY + screenH) / TILE_SIZE) + padding);
// 检查是否需要重新渲染
const range = `${startX},${startY},${endX},${endY}`;
if (range === this.lastVisibleRange) return;
this.lastVisibleRange = range;
// 重新构建可见瓦片
this.tilemap.clear();
const layer = this.mapData.layers.ground;
for (let y = startY; y <= endY; y++) {
for (let x = startX; x <= endX; x++) {
const gid = layer.data[y * layer.width + x];
if (gid === 0) continue;
const rect = getTileRect(gid);
this.tilemap.tile(this.baseTexture,
x * TILE_SIZE, y * TILE_SIZE,
{ u: rect[0], v: rect[1] });
}
}
}
}
⚠️ 注意:
CompositeTilemap.clear()有开销。建议只在相机移出缓冲区时才重新渲染,而非每帧调用。
无缝地图切换
class MapManager {
constructor(worldContainer) {
this.world = worldContainer;
this.currentMap = null;
this.collisionMap = null;
}
async loadMap(mapPath) {
// 清除旧地图
this.world.removeChildren();
// 加载新地图
const mapData = await loadTiledMap(mapPath);
const baseTexture = await Assets.load(mapData.tilesetImage);
const tilemap = new CompositeTilemap();
renderTilemap(mapData, baseTexture, tilemap);
this.world.addChild(tilemap);
// 构建碰撞
this.collisionMap = buildCollisionMap(mapData);
// 生成实体
spawnEntities(mapData);
this.currentMap = mapData;
}
async teleport(mapPath, x, y) {
// 淡出
await fadeOut(0.3);
await this.loadMap(mapPath);
player.x = x;
player.y = y;
// 淡入
await fadeIn(0.3);
}
}