Skip to content

Commit

Permalink
feat: [#382] Implement collection of image tilesets (#383)
Browse files Browse the repository at this point in the history
This PR implements the logic to parse collection of images based Tiled tilesets.

<img width="270" alt="image" src="https://user-images.githubusercontent.com/612071/177015856-d32091fa-7450-4856-afa9-76283a51c65c.png">

<img width="659" alt="image" src="https://user-images.githubusercontent.com/612071/175797163-cfd4e6f6-1552-445b-9660-3426353fb551.png">

TODOs
* [x] Tests
* [x] JSON collection of image tileset
* [x] External JSON collection of image tileset
  • Loading branch information
eonarheim authored Jul 2, 2022
1 parent bd1aeab commit d2a4fea
Show file tree
Hide file tree
Showing 13 changed files with 419 additions and 38 deletions.
120 changes: 119 additions & 1 deletion example/example-city.tmx

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions example/external-collection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.9" tiledversion="1.9.0" name="external-collection" tilewidth="111" tileheight="128" tilecount="5" columns="0">
<grid orientation="orthogonal" width="1" height="1"/>
<tile id="0">
<image width="111" height="128" source="assets/isometric-blocks/PNG/Abstract tiles/abstractTile_08.png"/>
</tile>
<tile id="1">
<image width="111" height="128" source="assets/isometric-blocks/PNG/Abstract tiles/abstractTile_16.png"/>
</tile>
<tile id="2">
<image width="111" height="128" source="assets/isometric-blocks/PNG/Abstract tiles/abstractTile_17.png"/>
</tile>
<tile id="3">
<image width="111" height="128" source="assets/isometric-blocks/PNG/Abstract tiles/abstractTile_28.png"/>
</tile>
<tile id="4">
<image width="111" height="128" source="assets/isometric-blocks/PNG/Abstract tiles/abstractTile_29.png"/>
</tile>
</tileset>
36 changes: 36 additions & 0 deletions example/external-collsion-json.tsj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{ "columns":0,
"grid":
{
"height":1,
"orientation":"orthogonal",
"width":1
},
"margin":0,
"name":"external-collection-json",
"spacing":0,
"tilecount":3,
"tiledversion":"1.9.0",
"tileheight":128,
"tiles":[
{
"id":0,
"image":"assets\/isometric-blocks\/PNG\/Abstract tiles\/abstractTile_13.png",
"imageheight":128,
"imagewidth":111
},
{
"id":1,
"image":"assets\/isometric-blocks\/PNG\/Abstract tiles\/abstractTile_14.png",
"imageheight":128,
"imagewidth":111
},
{
"id":2,
"image":"assets\/isometric-blocks\/PNG\/Abstract tiles\/abstractTile_15.png",
"imageheight":128,
"imagewidth":111
}],
"tilewidth":111,
"type":"tileset",
"version":"1.8"
}
13 changes: 9 additions & 4 deletions src/tiled-map-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ZSTDDecoder } from 'zstddec';

import { RawTiledMap } from "./raw-tiled-map";
import { TiledLayer } from "./tiled-layer";
import { TiledObject, TiledObjectGroup } from "./tiled-object";
import { TiledObjectGroup } from "./tiled-object";
import { TiledTileset } from './tiled-tileset';

/**
Expand Down Expand Up @@ -174,12 +174,17 @@ export class TiledMap {
for(let tileset of rawMap.tilesets) {
// Map non-embedded tilesets
if (!tileset.source) {
tileset.imagewidth = tileset.image.width;
tileset.imageheight = tileset.image.height;
if (tileset.image) {
tileset.imagewidth = tileset.image.width;
tileset.imageheight = tileset.image.height;
tileset.image = tileset.image.source;
}
tileset.objectalignment = tileset.objectalignment ?? 'unspecified';
tileset.image = tileset.image.source;
_convertToArray(tileset, 'tile', true);
tileset.tiles.forEach((t: any) => {
if (t.image?.source) {
t.image = t.image.source;
}
if (t.objectgroup){
t.objectgroup.type = 'objectgroup';
_convertToArray(t.objectgroup, 'object', true);
Expand Down
89 changes: 62 additions & 27 deletions src/tiled-map-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { getCanonicalGid, isFlippedDiagonally, isFlippedHorizontally, isFlippedV
import { getProperty, TiledEntity } from './tiled-entity';
import { TiledObjectComponent } from './tiled-object-component';
import { TiledLayerComponent } from './tiled-layer-component';
import { TiledLayer, TiledObjectGroup } from '.';
import { RawTilesetTile, TiledLayer, TiledObjectGroup } from '.';

export enum TiledMapFormat {

Expand Down Expand Up @@ -71,6 +71,7 @@ export class TiledMapResource implements Loadable<TiledMap> {
readonly mapFormat: TiledMapFormat;
public ex: ExcaliburData;
public imageMap: Record<string, ImageSource>;
public tileImageMap: Record<string, [tile: RawTilesetTile, image: ImageSource][]>;
public sheetMap: Record<string, SpriteSheet>;
public layers?: TileMap[] = [];
public isoLayers: IsometricMap[] = [];
Expand Down Expand Up @@ -106,6 +107,7 @@ export class TiledMapResource implements Loadable<TiledMap> {
this.ex = {};
this.imageMap = {};
this.sheetMap = {};
this.tileImageMap = {};
this.convertPath = (originPath: string, relativePath: string) => {
// Use absolute path if specified
if (relativePath.indexOf('/') === 0) {
Expand Down Expand Up @@ -370,19 +372,40 @@ export class TiledMapResource implements Loadable<TiledMap> {

// retrieve images from tilesets and create textures
tiledMap.rawMap.tilesets.forEach(ts => {
let tileSetImage = ts.image;
if (ts.source) {
// if external tileset "source" is specified and images are relative to external tileset
tileSetImage = this.convertPath(ts.source, ts.image)
let tileSetImages: string[] = [];
// if image is specified it's a single image tileset
if (ts.image) {
if (ts.source) {
// if external tileset "source" is specified and images are relative to external tileset
tileSetImages = [this.convertPath(ts.source, ts.image)];
} else {
// otherwise for embedded tilesets, images are relative to the tmx (this.path)
tileSetImages = [this.convertPath(this.path, ts.image)];
}
for (let image of tileSetImages) {
const tx = new ImageSource(image);
this.imageMap[ts.firstgid] = tx;
externalImages.push(tx.load());
Logger.getInstance().debug("[Tiled] Loading associated tileset image: " + ts.image);
}
} else {
// otherwise for embedded tilesets, images are relative to the tmx (this.path)
tileSetImage = this.convertPath(this.path, ts.image)
// otherwise it's a collection of images tileset
for (let tile of ts.tiles) {
let tileImage: string;
if (ts.source) {
tileImage = this.convertPath(ts.source, tile.image);
} else {
tileImage = this.convertPath(this.path, tile.image);
}
const tx = new ImageSource(tileImage);
externalImages.push(tx.load());
if (!this.tileImageMap[ts.firstgid]) {
this.tileImageMap[ts.firstgid] = [];
}
this.tileImageMap[ts.firstgid].push([tile, tx]);
Logger.getInstance().debug("[Tiled] Loading associated tileset image: " + tileImage);
}
}
const tx = new ImageSource(tileSetImage);
this.imageMap[ts.firstgid] = tx;
externalImages.push(tx.load());

Logger.getInstance().debug("[Tiled] Loading associated tileset: " + ts.image);
});

return Promise.all(externalImages).then(() => {
Expand Down Expand Up @@ -548,22 +571,34 @@ export class TiledMapResource implements Loadable<TiledMap> {
const spacing = tileset.spacing ?? 0;
const cols = Math.floor((tileset.imagewidth + spacing) / (tileset.tilewidth + spacing));
const rows = Math.floor((tileset.imageheight + spacing) / (tileset.tileheight + spacing));
const ss = SpriteSheet.fromImageSource({
image: this.imageMap[tileset.firstgid],
grid: {
columns: cols,
rows: rows,
spriteWidth: tileset.tilewidth,
spriteHeight: tileset.tileheight
},
spacing: {
margin: {
x: tileset.spacing ?? 0,
y: tileset.spacing ?? 0,
// Single image tilesets
if (this.imageMap[tileset.firstgid]) {
const ss = SpriteSheet.fromImageSource({
image: this.imageMap[tileset.firstgid],
grid: {
columns: cols,
rows: rows,
spriteWidth: tileset.tilewidth,
spriteHeight: tileset.tileheight
},
spacing: {
margin: {
x: tileset.spacing ?? 0,
y: tileset.spacing ?? 0,
}
}
}
});
this.sheetMap[tileset.firstgid.toString()] = ss;
});
this.sheetMap[tileset.firstgid.toString()] = ss;
// Image collection tilesets
} else {
const tiles = this.tileImageMap[tileset.firstgid];
const sprites = tiles.map(([tile, imageSource]) => {
const sprite = imageSource.toSprite();
return sprite;
})
const ss = new SpriteSheet({sprites});
this.sheetMap[tileset.firstgid.toString()] = ss;
}
}

// Create Excalibur sprites for each cell
Expand Down
19 changes: 13 additions & 6 deletions src/tiled-tileset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ export class TiledTileset {

/**
* Path to the image used for tiles in this set
*
* If no image is specified this is a collection of images tileset and individual tiles have images
*/
image!: string;
image?: string;

/**
* Height of source image in pixels
Expand Down Expand Up @@ -147,6 +149,7 @@ export class TiledTileset {
export class TiledTilesetTile {
id!: number;
tileset!: TiledTileset;
image?: string;
objectgroup?: TiledObjectGroup;
terrain?: number[];
animation?: TiledFrame[];
Expand Down Expand Up @@ -174,9 +177,10 @@ export class TiledTilesetTile {
return null;
}

public static parse(rawTilesetTile: RawTilesetTile, tileset: TiledTileset) {
public static parse(rawTilesetTile: RawTilesetTile, tileset: TiledTileset): TiledTilesetTile {
const tile = new TiledTilesetTile();
tile.id = +rawTilesetTile.id;
tile.image = rawTilesetTile.image;
tile.tileset = tileset;
tile.properties = Array.isArray(rawTilesetTile.properties) ? rawTilesetTile.properties : (rawTilesetTile.properties as any)?.property ?? [];
if (rawTilesetTile.objectgroup) {
Expand Down Expand Up @@ -243,13 +247,16 @@ export const parseExternalTsx = (tsxData: string, firstGid: number, source: stri

rawTileset.firstgid = firstGid;
rawTileset.source = source;
rawTileset.imagewidth = rawTsx.image.width;
rawTileset.imageheight = rawTsx.image.height;
rawTileset.imagewidth = rawTsx.image?.width;
rawTileset.imageheight = rawTsx.image?.height;
rawTileset.objectalignment = rawTsx.objectalignment ?? 'unspecified';
rawTileset.image = rawTsx.image.source;
rawTileset.image = rawTsx.image?.source;
rawTileset.spacing = isNaN(rawTsx.spacing) ? 0 : rawTsx.spacing;
_convertToArray(rawTsx, "tile", true);
rawTsx.tiles.forEach((t: any) => {
if (t.image?.source) {
t.image = t.image.source;
}
if (t.objectgroup){
t.objectgroup.type = 'objectgroup';
_convertToArray(t.objectgroup, 'object', true);
Expand All @@ -272,7 +279,7 @@ export const parseExternalTsx = (tsxData: string, firstGid: number, source: stri
imageWidth: rawTileset.imagewidth,
imageHeight: rawTileset.imageheight,
objectAlignment: rawTileset.objectalignment ?? 'unspecified',
image: rawTileset.image,
image: rawTileset.image,
spacing: isNaN(rawTileset.spacing) ? 0 : rawTileset.spacing,
horizontalFlipTransform: Matrix.identity().translate(rawTileset.tilewidth, 0).scale(-1, 1),
verticalFlipTransform: Matrix.identity().translate(0, rawTileset.tileheight).scale(1, -1),
Expand Down
Binary file added test/unit/assets/platformerTile_40.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/unit/assets/platformerTile_48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions test/unit/collection-external.tsj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{ "columns":0,
"grid":
{
"height":1,
"orientation":"orthogonal",
"width":1
},
"margin":0,
"name":"collection-external",
"spacing":0,
"tilecount":2,
"tiledversion":"1.9.0",
"tileheight":128,
"tiles":[
{
"id":0,
"image":"test\/unit\/assets\/platformerTile_40.png"
},
{
"id":1,
"image":"test\/unit\/assets\/platformerTile_48.png"
}],
"tilewidth":111,
"type":"tileset",
"version":"1.8"
}
10 changes: 10 additions & 0 deletions test/unit/collection-external.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.9" tiledversion="1.9.0" name="collection-external" tilewidth="111" tileheight="128" tilecount="2" columns="0">
<grid orientation="orthogonal" width="1" height="1"/>
<tile id="0">
<image width="111" height="128" source="test/unit/assets/platformerTile_40.png"/>
</tile>
<tile id="1">
<image width="111" height="128" source="test/unit/assets/platformerTile_48.png"/>
</tile>
</tileset>
38 changes: 38 additions & 0 deletions test/unit/tiled-map-resource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,42 @@ describe('A Tiled Map Excalibur Resource', () => {
expect(parallax?.parallaxFactor.x).toBe(1.5);
expect(parallax?.parallaxFactor.y).toBe(1.6);
})

it('can load collection tilesets (tmx)', async () => {
const tiled = new TiledMapResource('test/unit/tileset-with-collection.tmx');
try {
await tiled.load();

expect(tiled.isLoaded()).toBe(true);

expect(tiled.data.tileSets.length).toBe(2);
expect(tiled.data.tileSets[0].tiles.length).toBe(2);
expect(tiled.data.tileSets[0].tiles[0].image).toBe('assets/platformerTile_40.png');
expect(tiled.data.tileSets[0].tiles[1].image).toBe('assets/platformerTile_48.png');
expect(tiled.data.tileSets[1].tiles.length).toBe(2);
expect(tiled.data.tileSets[1].tiles[0].image).toBe('test/unit/assets/platformerTile_40.png');
expect(tiled.data.tileSets[1].tiles[1].image).toBe('test/unit/assets/platformerTile_48.png');
} catch {
fail();
}
});

it('can load collection tilesets (tmj)', async () => {
const tiled = new TiledMapResource('test/unit/tileset-with-collection.tmj');
try {
await tiled.load();

expect(tiled.isLoaded()).toBe(true);

expect(tiled.data.tileSets.length).toBe(2);
expect(tiled.data.tileSets[0].tiles.length).toBe(2);
expect(tiled.data.tileSets[0].tiles[0].image).toBe('assets/platformerTile_40.png');
expect(tiled.data.tileSets[0].tiles[1].image).toBe('assets/platformerTile_48.png');
expect(tiled.data.tileSets[1].tiles.length).toBe(2);
expect(tiled.data.tileSets[1].tiles[0].image).toBe('test/unit/assets/platformerTile_40.png');
expect(tiled.data.tileSets[1].tiles[1].image).toBe('test/unit/assets/platformerTile_48.png');
} catch {
fail();
}
});
});
Loading

0 comments on commit d2a4fea

Please sign in to comment.