excaliburjs/excalibur-tiled

Add separate loading support for external Tilesets

Closed this issue · 1 comments

Context

In Tiled, tilesets can be external, which means they can be shared across multiple maps. By allowing the separate loading of these external tilesets, we can optimize asset loading by ensuring that the same tileset isn't loaded multiple times. Furthermore, for certain use-cases, there's a need to load tilesets without associating them with any map.

Proposal

Introduce a TiledTilesetResource class that would allow for the separate loading of tilesets, independent of the map they may be associated with. This would not only cater to external tilesets in Tiled but also provide more flexibility for developers who may want to load tilesets without a map.

Here's a possible implementation of the TiledTilesetResource class:

import { Resource, ImageSource, SpriteSheet } from 'excalibur';
import { TiledTileset, RawTiledTileset, parseExternalTsx, parseExternalJson } from '@excaliburjs/plugin-tiled';
import type { Loadable } from 'excalibur';

export class TiledTilesetResource implements Loadable<TiledTileset | null> {

    protected _resource: Resource<RawTiledTileset>

    /**
     * Data associated with a loadable
     */
    public data: TiledTileset | null = null;

    public image: ImageSource | null = null;

    public spriteSheet: SpriteSheet | null = null;

    get raw() {
        return this._resource.data;
    }

    constructor(public path: string, public isJson: boolean = false) {
        this._resource = new Resource<RawTiledTileset>(path, isJson ? 'json' : 'text');
    }

    /**
     * Begins loading the resource and returns a promise to be resolved on completion
     */
    public async load(): Promise<TiledTileset> {
        let rawTileSet = await this._resource.load();

        if (this.isJson) {
            this.data = parseExternalJson(rawTileSet, 0, this.path);
        } else {
            this.data = parseExternalTsx(rawTileSet as unknown as string, 0, this.path);
        }

        if(this.data.image) {
            this.image = new ImageSource(this.convertPath(this.path, this.data.image));
            await this.image.load();

            const rows = this.data.tileCount / this.data.columns;

            this.spriteSheet = SpriteSheet.fromImageSource({
                image: this.image,
                grid: {
                    rows: rows,
                    columns: this.data.columns,
                    spriteWidth: this.data.tileWidth,
                    spriteHeight: this.data.tileHeight
                },
            });
        }

        return this.data;
    }

    /**
     * Returns true if the loadable is loaded
     */
    public isLoaded(): boolean {
        return this._resource.isLoaded();
    }

    protected convertPath(originPath: string, relativePath: string) {
        // Use absolute path if specified
        if (relativePath.indexOf('/') === 0) {
           return relativePath;
        }

        const originSplit = originPath.split('/');
        const relativeSplit = relativePath.split('/');
        // if origin path is a file, remove it so it's a directory
        if (originSplit[originSplit.length - 1].includes('.')) {
           originSplit.pop();
        }
        return originSplit.concat(relativeSplit).join('/');
    }

}

With this class in place, developers can have better control over the loading of tilesets, thus enhancing the efficiency and flexibility of asset management.

@JumpLink I like this idea! Let's do it!

I agree that being able to control loading external tilesets is super valuable!