From e829fbe53de902f805c2166c7f8819b2a1e9f2ef Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Tue, 19 Dec 2023 23:16:50 -0600 Subject: [PATCH] Add entity factories, remove lots of missing implementations --- example/example-city.tmx | 4 +- example/game.ts | 33 ++++++- src/parser/tiled-parser.ts | 2 +- src/resource/layer.ts | 133 ++++++++++++++++++--------- src/resource/tiled-data-component.ts | 17 ++++ src/resource/tiled-resource.ts | 105 +++++++++++++++++---- 6 files changed, 228 insertions(+), 66 deletions(-) create mode 100644 src/resource/tiled-data-component.ts diff --git a/example/example-city.tmx b/example/example-city.tmx index 74d081f1..873bc03e 100644 --- a/example/example-city.tmx +++ b/example/example-city.tmx @@ -1,5 +1,5 @@ - + @@ -1282,7 +1282,7 @@ - + diff --git a/example/game.ts b/example/game.ts index 34431b43..46ff5671 100644 --- a/example/game.ts +++ b/example/game.ts @@ -3,6 +3,25 @@ import { TiledMapResource } from '@excalibur-tiled'; import { ImageFiltering, ImageSource, Input, IsometricEntityComponent, Shape } from 'excalibur'; import { TiledResource } from '../src/resource/tiled-resource'; +class Player extends ex.Actor { + override onPostUpdate(engine: ex.Engine) { + this.vel = ex.vec(0, 0) + const speed = 64; + if (engine.input.keyboard.isHeld(ex.Keys.Right)) { + this.vel.x = speed; + } + if (engine.input.keyboard.isHeld(ex.Keys.Left)) { + this.vel.x = -speed; + } + if (game.input.keyboard.isHeld(ex.Input.Keys.Up)) { + this.vel.y = -speed; + } + if (game.input.keyboard.isHeld(ex.Input.Keys.Down)) { + this.vel.y = speed; + } + } +} + const game = new ex.Engine({ width: 800, height: 600, @@ -11,7 +30,19 @@ const game = new ex.Engine({ antialiasing: false }); -const newResource = new TiledResource('./example-city.tmx'); +const newResource = new TiledResource('./example-city.tmx', { + entityClassNameFactories: { + 'player-start': (props) => { + return new Player({ + pos: props.worldPos, + width: 16, + height: 16, + color: ex.Color.Blue, + collisionType: ex.CollisionType.Active + }); + } + } +}); const loader = new ex.Loader([newResource]); let currentPointer!: ex.Vector; diff --git a/src/parser/tiled-parser.ts b/src/parser/tiled-parser.ts index 1d5af3d6..d0128afd 100644 --- a/src/parser/tiled-parser.ts +++ b/src/parser/tiled-parser.ts @@ -143,7 +143,7 @@ export const TiledText = z.object({ const TiledObject = z.object({ id: z.number().optional(), // Template files might not have an id for some reason name: z.string().optional(), - type: z.string(), + type: z.string().optional(), x: z.number(), y: z.number(), rotation: z.number().optional(), diff --git a/src/resource/layer.ts b/src/resource/layer.ts index dd8caa43..b6720a85 100644 --- a/src/resource/layer.ts +++ b/src/resource/layer.ts @@ -1,11 +1,13 @@ -import { Actor, Color, ParallaxComponent, Polygon as ExPolygon, Shape, TileMap, Tile as ExTile, Vector, toRadians, vec, GraphicsComponent, CompositeCollider } from "excalibur"; +import { Actor, Color, ParallaxComponent, Polygon as ExPolygon, Shape, TileMap, Tile as ExTile, Vector, toRadians, vec, GraphicsComponent, CompositeCollider, Entity } from "excalibur"; import { Properties, mapProps } from "./properties"; import { TiledMap, TiledObjectGroup, TiledObjectLayer, TiledTileLayer, isCSV, needsDecoding } from "../parser/tiled-parser"; import { Decoder } from "./decoder"; -import { TiledResource } from "./tiled-resource"; +import { FactoryProps, TiledResource } from "./tiled-resource"; import { Ellipse, InsertedTile, PluginObject, Point, Polygon, Polyline, Rectangle, Text, parseObjects } from "./objects"; import { getCanonicalGid } from "./gid-util"; import { Tile } from "./tileset"; +import { TiledDataComponent } from "./tiled-data-component"; +import { satisfies } from "compare-versions"; export type LayerTypes = ObjectLayer | TileLayer; @@ -19,9 +21,10 @@ export interface Layer extends Properties { export class ObjectLayer implements Layer { public readonly name: string; properties = new Map(); - objects: Object[] = []; - actors: Actor[] = []; - objectToActor = new Map(); + objects: PluginObject[] = []; + entities: Entity[] = []; + private _objectToActor = new Map(); + private _actorToObject = new Map(); constructor(public tiledObjectLayer: TiledObjectLayer, public resource: TiledResource) { this.name = tiledObjectLayer.name; @@ -29,50 +32,94 @@ export class ObjectLayer implements Layer { } _hasWidthHeight(object: PluginObject) { - return object instanceof Rectangle || object instanceof InsertedTile + return object instanceof Rectangle || object instanceof InsertedTile; } - getObjectByName(): PluginObject[] { - // TODO - return []; + getObjectByName(name: string): PluginObject[] { + return this.objects.filter(o => o.tiledObject.name === name); } - getActorByName(): PluginObject[] { - // TODO - return []; + getEntityByName(name: string): Entity[] { + return this.entities.filter(a => a.name === name); } - getObjectByProperty(): PluginObject[] { - // TODO - return []; - + getEntityByObject(object: PluginObject): Entity | undefined { + return this._objectToActor.get(object); } - getActorByProperty(): PluginObject[] { - // TODO - return []; + + getObjectByEntity(actor: Entity): PluginObject | undefined { + return this._actorToObject.get(actor); + } + + /** + * Search for a tiled object that has a property name, and optionally specify a value + * @param propertyName + * @param value + * @returns + */ + getObjectsByProperty(propertyName: string, value?: any): PluginObject[] { + if (value !== undefined) { + return this.objects.filter(o => o.properties.get(propertyName) === value); + } else { + return this.objects.filter(o => o.properties.has(propertyName)); + } + } + /** + * Search for actors that were created from tiled objects + * @returns + */ + getActorsByProperty(propertyName: string, value?: any): Actor[] { + return this.getObjectsByProperty(propertyName, value).map(o => this._objectToActor.get(o)).filter(a => !!a) as Actor[]; } - getObjectByClassName(): PluginObject[] { - return []; + /** + * Search for an Tiled object by it's Tiled class name + * @returns + */ + getObjectsByClassName(className: string): PluginObject[] { + return this.objects.filter(o => o.tiledObject.name === className); } - getActorByClassName(): PluginObject[] { - return []; + /** + * Search for an Actor created by the plugin by it's Tiled object + * @param className + * @returns + */ + getActorByClassName(className: string): Actor[] { + return this.getObjectsByClassName(className).map(o => this._objectToActor.get(o)).filter(a => !!a) as Actor[]; } async load() { + // TODO object alignment specified in tileset! https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile const opacity = this.tiledObjectLayer.opacity; const hasTint = !!this.tiledObjectLayer.tintcolor; const tint = this.tiledObjectLayer.tintcolor ? Color.fromHex(this.tiledObjectLayer.tintcolor) : Color.White; const offset = vec(this.tiledObjectLayer.offsetx ?? 0, this.tiledObjectLayer.offsety ?? 0); - // TODO object alignment specified in tileset! https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile - - - // TODO factory instantiation! const objects = parseObjects(this.tiledObjectLayer); + for (let object of objects) { + let worldPos = vec((object.x ?? 0) + offset.x, (object.y ?? 0) + offset.y); + + if (object.tiledObject.type) { + // TODO we should also use factories on templates + const factory = this.resource.factories.get(object.tiledObject.type); + if (factory) { + const entity = factory({ + worldPos, + name: object.tiledObject.name, + class: object.tiledObject.type, + layer: this, + object, + properties: object.properties + } satisfies FactoryProps); + this._recordObjectEntityMapping(object, entity); + continue; // If we do a factor method we skip any default processing + } + } + // TODO excalibur smarts for solid/collision type/factory map + // TODO collision type const newActor = new Actor({ name: object.tiledObject.name, x: (object.x ?? 0) + offset.x, @@ -84,7 +131,6 @@ export class ObjectLayer implements Layer { if (graphics) { graphics.opacity = opacity; } - if (object instanceof Text) { newActor.graphics.use(object.text); @@ -144,12 +190,11 @@ export class ObjectLayer implements Layer { } if (object instanceof Polyline) { - console.log('polyline', object); - // TODO should we do any actor stuff here + // ? should we do any excalibur things here } if (object instanceof Point) { - // TODO should we do any actor stuff here + // ? should we do any excalibur things here } if (object instanceof Rectangle) { @@ -164,16 +209,23 @@ export class ObjectLayer implements Layer { console.log(object); } - this.objects.push(object); - this.actors.push(newActor); - // TODO do we need this? - this.objectToActor.set(object, newActor); + this._recordObjectEntityMapping(object, newActor); } } + + private _recordObjectEntityMapping(object: PluginObject, entity: Entity){ + entity.addComponent(new TiledDataComponent({ + tiledObject: object + })); + this.objects.push(object); + this.entities.push(entity); + this._objectToActor.set(object, entity); + this._actorToObject.set(entity, object); + } } /** - * Tile information for both excalibur and tiled tile represenations + * Tile information for both excalibur and tiled tile representations */ export interface TileInfo { /** @@ -208,6 +260,7 @@ export class TileLayer implements Layer { */ tilemap!: TileMap; + getTileByPoint(worldPos: Vector): TileInfo | null { // TODO IF the resource is not loaded & decoded THIS WONT WORK // log a warning @@ -248,14 +301,6 @@ export class TileLayer implements Layer { return null; } - getTileByClass() { - // TODO implement getTileByClass - } - - getTileByProperty() { - // TODO implement getTileByProperty - } - constructor(public tiledTileLayer: TiledTileLayer, public resource: TiledResource) { this.name = tiledTileLayer.name; mapProps(this, tiledTileLayer.properties); diff --git a/src/resource/tiled-data-component.ts b/src/resource/tiled-data-component.ts new file mode 100644 index 00000000..73fc1780 --- /dev/null +++ b/src/resource/tiled-data-component.ts @@ -0,0 +1,17 @@ +import { Component } from "excalibur"; +import { PluginObject } from "./objects"; +import { Properties } from "./properties"; + +export interface TiledDataComponentOptions { + tiledObject: PluginObject; + // tiledProperties: Properties; +} +export class TiledDataComponent extends Component<'ex.tiled-data'> { + public readonly type = 'ex.tiled-data'; + public tiledObject: PluginObject; + constructor(options: TiledDataComponentOptions){ + super(); + const {tiledObject} = options; + this.tiledObject = tiledObject; + } +} \ No newline at end of file diff --git a/src/resource/tiled-resource.ts b/src/resource/tiled-resource.ts index fe420c36..2d880f3d 100644 --- a/src/resource/tiled-resource.ts +++ b/src/resource/tiled-resource.ts @@ -1,6 +1,6 @@ import { Entity, ImageSource, Loadable, Resource, Scene, SpriteSheet, Vector } from "excalibur"; import { TiledMap, TiledParser, TiledTile, TiledTileset, TiledTilesetEmbedded, TiledTilesetExternal, TiledTilesetFile, isTiledTilesetCollectionOfImages, isTiledTilesetEmbedded, isTiledTilesetExternal, isTiledTilesetSingleImage } from "../parser/tiled-parser"; -import { Tileset } from "./tileset"; +import { Tile, Tileset } from "./tileset"; import { Layer, ObjectLayer, TileLayer } from "./layer"; import { Template } from "./template"; import { compare } from "compare-versions"; @@ -8,32 +8,47 @@ import { getCanonicalGid } from "./gid-util"; import { pathRelativeToBase } from "./path-util"; import { PluginObject } from "./objects"; +export interface TiledAddToSceneOptions { + pos: Vector; +} + export interface TiledResourceOptions { /** * Plugin will operate in headless mode and skip all graphics related * excalibur items. */ - headless: boolean; + headless?: boolean; // TODO implement /** * Default true. If false, only tilemap will be parsed and displayed, it's up to you to wire up any excalibur behavior. * Automatically wires excalibur to the following * * Wire up current scene camera - * * Make Actors/Tiles with colliders on Tiled tiles & Tild objects + * * Make Actors/Tiles with colliders on Tiled tiles & Tiled objects * * Support solid layers */ useExcaliburWiring?: boolean; // TODO implement + + + useTilemapCameraStrategy?: boolean // TODO implements + /** * Plugin detects the map type based on extension, if you know better you can force an override. */ mapFormatOverride?: 'TMX' | 'TMJ'; + /** * The pathMap helps work around odd things bundlers do with static files. * * When the Tiled resource comes across something that matches `path`, it will use the output string instead. */ - pathMap?: { path: string | RegExp, output: string }[]; + pathMap?: { path: string | RegExp, output: string }[]; // TODO implement + + /** + * Optionally provide a custom file loader implementation instead of using the built in Excalibur resource ajax loader + * that takes a path and returns file data + */ + fileLoader?: (path: string) => Promise; // TODO implement /** * By default `true`, means Tiled files must pass the plugins Typed parse pass. @@ -48,7 +63,13 @@ export interface TiledResourceOptions { * * By default it's 4 for 4x scaled bitmap */ - textQuality?: number; // TODO implement + textQuality?: number; + + /** + * Configure custom Actor/Entity factory functions to construct Actors/Entities + * given a Tiled class name. + */ + entityClassNameFactories?: Record Entity>; // TODO implement } export interface FactoryProps { @@ -59,7 +80,7 @@ export interface FactoryProps { /** * Tiled name in UI */ - name: string; + name?: string; /** * Tiled class in UI (internally in Tiled is represented as the string 'type') */ @@ -97,15 +118,21 @@ export class TiledResource implements Loadable { layers: Layer[] = []; public readonly mapFormat: 'TMX' | 'TMJ' = 'TMX'; - private _factories = new Map Entity>(); + // ? should this be publish? + public factories = new Map Entity>(); private _resource: Resource; private _parser = new TiledParser(); public firstGidToImage = new Map(); private tileToImage = new Map(); + public readonly textQuality: number = 4; constructor(public path: string, options?: TiledResourceOptions) { - const { mapFormatOverride } = { ...options }; + const { mapFormatOverride, textQuality, entityClassNameFactories } = { ...options }; + this.textQuality = textQuality ?? this.textQuality; + for (const key in entityClassNameFactories) { + this.registerEntityFactory(key, entityClassNameFactories[key]); + } const detectedType = mapFormatOverride ?? (path.includes('.tmx') ? 'TMX' : 'TMJ'); switch (detectedType) { case 'TMX': @@ -122,17 +149,17 @@ export class TiledResource implements Loadable { } registerEntityFactory(className: string, factory: (props: FactoryProps) => Entity): void { - if (this._factories.has(className)) { + if (this.factories.has(className)) { console.warn(`Another factory has already been registered for tiled class/type "${className}", this is probably a bug.`); } - this._factories.set(className, factory); + this.factories.set(className, factory); } unregisterEntityFactory(className: string) { - if (!this._factories.has(className)) { + if (!this.factories.has(className)) { console.warn(`No factory has been registered for tiled class/type "${className}", cannot unregister!`); } - this._factories.delete(className); + this.factories.delete(className); } getTilesetForTile(gid: number): Tileset { @@ -147,20 +174,61 @@ export class TiledResource implements Loadable { throw Error(`No tileset exists for tiled gid [${gid}] normalized [${normalizedGid}]!`); } - getLayersByName() { + getTilesByClassName(className: string): Tile[] { + let results: Tile[] = []; + for (let tileset of this.tilesets) { + results = results.concat(tileset.tiles.filter(t => t.class === className)); + } + + return results; + } + + getTilesByProperty(name: string, value?: any): Tile[] { + let results: Tile[] = []; + for (let tileset of this.tilesets) { + if (value !== undefined) { + results = results.concat(tileset.tiles.filter(t => t.properties.get(name) === value)); + } else { + results = results.concat(tileset.tiles.filter(t => t.properties.has(name))); + } + } + return results; } - - getLayersByClass() { + getImageLayers(): Layer[] { + // TODO implement + return []; } - getLayersByProperty() { + getTileLayers(): TileLayer[] { + // TODO implement + return []; + } + + getObjectLayers(): ObjectLayer[] { + // TODO implement + return []; + } + + getLayersByName(name: string): Layer[] { + // TODO implement + return []; + } + + getLayersByClassName(className: string): Layer[] { + // TODO implement + return []; + } + getLayersByProperty(name: string, value?: any): Layer[] { + // TODO implement + return []; } async load(): Promise { + // TODO refactor this method is too BIG const data = await this._resource.load(); // Parse initial Tiled map structure @@ -332,17 +400,18 @@ export class TiledResource implements Loadable { console.log(this); } - addToScene(scene: Scene) { + addToScene(scene: Scene, options?: TiledAddToSceneOptions) { // TODO implement // TODO pick a position to insert into the scene? for (const layer of this.layers) { if (layer instanceof TileLayer) { scene.add(layer.tilemap); } if (layer instanceof ObjectLayer) { - for (const actor of layer.actors) { + for (const actor of layer.entities) { scene.add(actor); } } + // TODO image layers } }