Files
esengine/packages/tilemap/src/loaders/TiledConverter.ts
YHH 3fb6f919f8 Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
2025-11-25 22:23:19 +08:00

333 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tiled Map Editor format converter
* Tiled 地图编辑器格式转换器
*
* Converts Tiled JSON export format to our internal tilemap/tileset format.
* 将 Tiled JSON 导出格式转换为内部 tilemap/tileset 格式。
*/
import { ITilemapAsset } from './TilemapLoader';
import { ITilesetAsset } from './TilesetLoader';
/**
* Tiled map JSON format (exported from Tiled)
* Tiled 地图 JSON 格式
*/
export interface ITiledMap {
/** Map width in tiles */
width: number;
/** Map height in tiles */
height: number;
/** Tile width in pixels */
tilewidth: number;
/** Tile height in pixels */
tileheight: number;
/** Map orientation (orthogonal, isometric, etc.) */
orientation: string;
/** Render order */
renderorder: string;
/** Layers array */
layers: ITiledLayer[];
/** Tilesets array */
tilesets: ITiledTileset[];
/** Custom properties */
properties?: ITiledProperty[];
/** Tiled version */
tiledversion?: string;
/** Map version */
version?: string | number;
}
/**
* Tiled layer format
* Tiled 图层格式
*/
export interface ITiledLayer {
/** Layer name */
name: string;
/** Layer type (tilelayer, objectgroup, imagelayer, group) */
type: string;
/** Layer ID */
id: number;
/** Layer width in tiles */
width?: number;
/** Layer height in tiles */
height?: number;
/** Tile data (for tilelayer) */
data?: number[];
/** Layer visibility */
visible: boolean;
/** Layer opacity (0-1) */
opacity: number;
/** Layer X offset */
x: number;
/** Layer Y offset */
y: number;
/** Objects (for objectgroup) */
objects?: ITiledObject[];
/** Custom properties */
properties?: ITiledProperty[];
}
/**
* Tiled object format
* Tiled 对象格式
*/
export interface ITiledObject {
id: number;
name: string;
type: string;
x: number;
y: number;
width: number;
height: number;
rotation: number;
visible: boolean;
properties?: ITiledProperty[];
}
/**
* Tiled tileset format
* Tiled 瓦片集格式
*/
export interface ITiledTileset {
/** First GID (global tile ID) */
firstgid: number;
/** Tileset name */
name: string;
/** Image path */
image: string;
/** Image width */
imagewidth: number;
/** Image height */
imageheight: number;
/** Tile width */
tilewidth: number;
/** Tile height */
tileheight: number;
/** Tile count */
tilecount: number;
/** Columns */
columns: number;
/** Margin */
margin: number;
/** Spacing */
spacing: number;
/** Tile properties/metadata */
tiles?: ITiledTile[];
/** External tileset source (if embedded) */
source?: string;
}
/**
* Tiled tile metadata
* Tiled 瓦片元数据
*/
export interface ITiledTile {
id: number;
type?: string;
properties?: ITiledProperty[];
}
/**
* Tiled property format
* Tiled 属性格式
*/
export interface ITiledProperty {
name: string;
type: string;
value: unknown;
}
/**
* Conversion result
* 转换结果
*/
export interface ITiledConversionResult {
/** Converted tilemap */
tilemap: ITilemapAsset;
/** Converted tilesets */
tilesets: ITilesetAsset[];
/** Object layers (if any) */
objects: Array<{
name: string;
objects: ITiledObject[];
}>;
}
/**
* Tiled format converter
* Tiled 格式转换器
*/
export class TiledConverter {
/**
* Convert Tiled JSON map to internal format
* 将 Tiled JSON 地图转换为内部格式
*/
static convert(tiledMap: ITiledMap, mapName: string = 'tilemap'): ITiledConversionResult {
// Convert tilesets
const tilesets = tiledMap.tilesets.map(ts => this.convertTileset(ts));
// Find the first tile layer for main data
const tileLayer = tiledMap.layers.find(l => l.type === 'tilelayer');
// Convert tile data (Tiled uses global IDs, we need to adjust)
let data: number[] = [];
if (tileLayer && tileLayer.data) {
data = this.convertTileData(tileLayer.data, tiledMap.tilesets);
}
// Extract collision layer if exists
const collisionLayer = tiledMap.layers.find(
l => l.type === 'tilelayer' &&
(l.name.toLowerCase().includes('collision') || l.name.toLowerCase().includes('solid'))
);
const collisionData = collisionLayer?.data
? this.convertTileData(collisionLayer.data, tiledMap.tilesets)
: undefined;
// Collect all layers
const layers = tiledMap.layers
.filter(l => l.type === 'tilelayer')
.map(l => ({
name: l.name,
visible: l.visible,
opacity: l.opacity,
data: l.data ? this.convertTileData(l.data, tiledMap.tilesets) : undefined
}));
// Collect object layers
const objects = tiledMap.layers
.filter(l => l.type === 'objectgroup')
.map(l => ({
name: l.name,
objects: l.objects || []
}));
// Convert properties
const properties: Record<string, unknown> = {};
if (tiledMap.properties) {
for (const prop of tiledMap.properties) {
properties[prop.name] = prop.value;
}
}
const tilemap: ITilemapAsset = {
name: mapName,
version: 1,
width: tiledMap.width,
height: tiledMap.height,
tileWidth: tiledMap.tilewidth,
tileHeight: tiledMap.tileheight,
tileset: tilesets.length > 0 ? tilesets[0].name : '',
data,
layers: layers.length > 1 ? layers : undefined,
collisionData,
properties
};
return {
tilemap,
tilesets,
objects
};
}
/**
* Convert Tiled tileset to internal format
* 将 Tiled 瓦片集转换为内部格式
*/
private static convertTileset(tiledTileset: ITiledTileset): ITilesetAsset {
const tiles = tiledTileset.tiles?.map(t => ({
id: t.id,
type: t.type,
properties: t.properties?.reduce((acc, p) => {
acc[p.name] = p.value;
return acc;
}, {} as Record<string, unknown>)
}));
return {
name: tiledTileset.name,
version: 1,
image: tiledTileset.image,
imageWidth: tiledTileset.imagewidth,
imageHeight: tiledTileset.imageheight,
tileWidth: tiledTileset.tilewidth,
tileHeight: tiledTileset.tileheight,
tileCount: tiledTileset.tilecount,
columns: tiledTileset.columns,
rows: Math.ceil(tiledTileset.tilecount / tiledTileset.columns),
margin: tiledTileset.margin || 0,
spacing: tiledTileset.spacing || 0,
tiles
};
}
/**
* Convert Tiled tile data (global IDs to local IDs)
* 转换 Tiled 瓦片数据全局ID到本地ID
*
* Tiled uses global tile IDs where each tileset has a firstgid.
* We convert to local IDs starting from 1 (0 = empty).
*/
private static convertTileData(data: number[], tilesets: ITiledTileset[]): number[] {
if (tilesets.length === 0) return data;
// For single tileset, simple conversion
if (tilesets.length === 1) {
const firstgid = tilesets[0].firstgid;
return data.map(gid => {
if (gid === 0) return 0;
// Clear flip flags (high bits in Tiled)
const tileId = gid & 0x1FFFFFFF;
return tileId - firstgid + 1;
});
}
// For multiple tilesets, find which tileset each tile belongs to
return data.map(gid => {
if (gid === 0) return 0;
const tileId = gid & 0x1FFFFFFF;
// Find the tileset this tile belongs to
let tileset: ITiledTileset | null = null;
for (let i = tilesets.length - 1; i >= 0; i--) {
if (tileId >= tilesets[i].firstgid) {
tileset = tilesets[i];
break;
}
}
if (!tileset) return 0;
return tileId - tileset.firstgid + 1;
});
}
/**
* Parse Tiled JSON string
* 解析 Tiled JSON 字符串
*/
static parse(jsonString: string): ITiledMap {
return JSON.parse(jsonString) as ITiledMap;
}
/**
* Convert and stringify to internal format
* 转换并序列化为内部格式
*/
static convertToJson(tiledMap: ITiledMap, mapName?: string): {
tilemapJson: string;
tilesetJsons: string[];
} {
const result = this.convert(tiledMap, mapName);
return {
tilemapJson: JSON.stringify(result.tilemap, null, 2),
tilesetJsons: result.tilesets.map(ts => JSON.stringify(ts, null, 2))
};
}
}