Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
TypeScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012import { BoundingBox } from '../collision/bounding-box';import type { Engine } from '../engine';import { Vector, vec } from '../math/vector';import { Logger } from '../util/log';import type { EntityEvents } from '../entity-component-system/entity';import { Entity } from '../entity-component-system/entity';import { TransformComponent } from '../entity-component-system/components/transform-component';import { BodyComponent } from '../collision/body-component';import { CollisionType } from '../collision/collision-type';import { Colliders } from '../collision/colliders/colliders';import type { ExcaliburGraphicsContext, Graphic } from '../graphics';import { GraphicsComponent, hasGraphicsTick, ParallaxComponent } from '../graphics';import { MotionComponent } from '../entity-component-system/components/motion-component';import { ColliderComponent } from '../collision/collider-component';import type { CompositeCollider, CompositeStrategy } from '../collision/colliders/composite-collider';import { DebugGraphicsComponent } from '../graphics/debug-graphics-component';import type { Collider } from '../collision/colliders/collider';import { PostDrawEvent, PostUpdateEvent, PreDrawEvent, PreUpdateEvent } from '../events';import type { EventKey, Handler, Subscription } from '../event-emitter';import { EventEmitter } from '../event-emitter';import { CoordPlane } from '../math/coord-plane';import type { DebugConfig } from '../debug';import { clamp } from '../math/util';import { PointerComponent } from '../input/pointer-component';import type { PointerEvent } from '../input/pointer-event';import type { PointerEventReceiver } from '../input/pointer-event-receiver';import type { HasNestedPointerEvents } from '../input/pointer-events-to-object-dispatcher';import { PointerEventsToObjectDispatcher } from '../input/pointer-events-to-object-dispatcher';import type { GlobalCoordinates } from '../math';
export interface TileMapOptions { /** * Optionally name the tile map */ name?: string; /** * Optionally specify the position of the tile map */ pos?: Vector; /** * Width of an individual tile in pixels */ tileWidth: number; /** * Height of an individual tile in pixels */ tileHeight: number; /** * The number of tile columns, or the number of tiles wide */ columns: number; /** * The number of tile rows, or the number of tiles high */ rows: number;
/** * Optionally render from the top of the graphic, by default tiles are rendered from the bottom */ renderFromTopOfGraphic?: boolean;
/** * Optionally configure the meshing lookbehind for Tilemap, Tilemaps combine solid tiles into optimal * geometry and the lookbehind configures how far back the Tilemap to look for geometry when combining. Meshing * is an expensive operation, so when the Tilemap geometry is invalidated it must be recalculated. * * Default is 10 slots, but if your Tilemap does not change positions or solid tiles often you can increase this to * Infinity. */ meshingLookBehind?: number;
/** * Optionally set the composite strategy for the internal tilemap collider. * * Default is 'separate' for tilemaps which assumes this tilemap is being used to build disconnect geometry representing a level. Normally composites have 'together'. */ compositeStrategy?: CompositeStrategy;}
export interface TilePointerEvents { pointerup: PointerEvent; pointerdown: PointerEvent; pointermove: PointerEvent; pointercancel: PointerEvent; pointerenter: PointerEvent; pointerleave: PointerEvent;}
export interface TileMapEvents extends EntityEvents, TilePointerEvents { preupdate: PreUpdateEvent<TileMap>; postupdate: PostUpdateEvent<TileMap>; predraw: PreDrawEvent; postdraw: PostDrawEvent;}
export const TileMapEvents = { PreUpdate: 'preupdate', PostUpdate: 'postupdate', PreDraw: 'predraw', PostDraw: 'postdraw', PointerEnter: 'pointerenter', PointerLeave: 'pointerleave', PointerUp: 'pointerup', PointerDown: 'pointerdown', PointerMove: 'pointermove', PointerCancel: 'pointercancel'};
/** * The TileMap provides a mechanism for doing flat 2D tiles rendered in a grid. * * TileMaps are useful for top down or side scrolling grid oriented games. */export class TileMap extends Entity implements HasNestedPointerEvents { public override events = new EventEmitter<TileMapEvents>(); private _token = 0; private _engine!: Engine;
public logger: Logger = Logger.getInstance(); public readonly tiles: Tile[] = []; private _rows: Tile[][] = []; private _cols: Tile[][] = [];
public readonly tileWidth: number; public readonly tileHeight: number; public readonly rows: number; public readonly columns: number;
public renderFromTopOfGraphic = false; public meshingLookBehind = 10;
private _collidersDirty = true; private _pointerEventDispatcher: PointerEventsToObjectDispatcher<Tile>; public flagCollidersDirty() { this._collidersDirty = true; }
public flagTilesDirty(): void { for (let i = 0; i < this.tiles.length; i++) { if (this.tiles[i]) { this.tiles[i].flagDirty(); } } }
public pointer: PointerComponent; public transform: TransformComponent; private _motion: MotionComponent; private _graphics: GraphicsComponent; public collider: ColliderComponent; private _composite: CompositeCollider;
public get x(): number { return this.transform.pos.x ?? 0; }
public set x(val: number) { if (this.transform?.pos) { this.get(TransformComponent).pos = vec(val, this.y); } }
public get y(): number { return this.transform?.pos.y ?? 0; }
public set y(val: number) { if (this.transform?.pos) { this.transform.pos = vec(this.x, val); } }
public get z(): number { return this.transform.z ?? 0; }
public set z(val: number) { if (this.transform) { this.transform.z = val; } }
private _oldRotation: number = 0; public get rotation(): number { return this.transform?.rotation ?? 0; }
public set rotation(val: number) { if (this.transform) { if (val !== this.transform.rotation) { this.flagCollidersDirty(); this.flagTilesDirty(); } this.transform.rotation = val; } }
private _oldScale: Vector; public get scale(): Vector { return this.transform?.scale ?? Vector.One; }
public set scale(val: Vector) { if (this.transform) { if (!val?.equals(this.transform.scale)) { this.flagCollidersDirty(); this.flagTilesDirty(); } this.transform.scale = val; } }
private _oldPos: Vector; public get pos(): Vector { return this.transform.pos; }
public set pos(val: Vector) { if (this.transform) { if (!val?.equals(this.transform.pos)) { this.flagCollidersDirty(); this.flagTilesDirty(); } this.transform.pos = val; } }
public get vel(): Vector { return this._motion.vel; }
public set vel(val: Vector) { this._motion.vel = val; }
/** * Width of the whole tile map in pixels */ public get width(): number { return this.tileWidth * this.columns * this.scale.x; }
/** * Height of the whole tilemap in pixels */ public get height(): number { return this.tileHeight * this.rows * this.scale.y; }
public override emit<TEventName extends EventKey<TileMapEvents>>(eventName: TEventName, event: TileMapEvents[TEventName]): void; public override emit(eventName: string, event?: any): void; public override emit<TEventName extends EventKey<TileMapEvents> | string>(eventName: TEventName, event?: any): void { this.events.emit(eventName, event); }
public override on<TEventName extends EventKey<TileMapEvents>>( eventName: TEventName, handler: Handler<TileMapEvents[TEventName]> ): Subscription; public override on(eventName: string, handler: Handler<unknown>): Subscription; public override on<TEventName extends EventKey<TileMapEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.on(eventName, handler); }
public override once<TEventName extends EventKey<TileMapEvents>>( eventName: TEventName, handler: Handler<TileMapEvents[TEventName]> ): Subscription; public override once(eventName: string, handler: Handler<unknown>): Subscription; public override once<TEventName extends EventKey<TileMapEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.once(eventName, handler); }
public override off<TEventName extends EventKey<TileMapEvents>>(eventName: TEventName, handler: Handler<TileMapEvents[TEventName]>): void; public override off(eventName: string, handler: Handler<unknown>): void; public override off(eventName: string): void; public override off<TEventName extends EventKey<TileMapEvents> | string>(eventName: TEventName, handler?: Handler<any>): void { this.events.off(eventName, handler as any); }
/** * @param options */ constructor(options: TileMapOptions) { super([], options.name); this.meshingLookBehind = options.meshingLookBehind ?? this.meshingLookBehind; this.addComponent(new TransformComponent()); this.addComponent(new MotionComponent()); this.addComponent( new BodyComponent({ type: CollisionType.Fixed }) ); this.addComponent( new GraphicsComponent({ onPostDraw: (ctx, elapsed) => this.draw(ctx, elapsed) }) ); this.addComponent(new DebugGraphicsComponent((ctx, debugFlags) => this.debug(ctx, debugFlags), false)); this.addComponent(new ColliderComponent()); this.addComponent(new PointerComponent()); this.pointer = this.get(PointerComponent); this._graphics = this.get(GraphicsComponent); this.transform = this.get(TransformComponent); this._motion = this.get(MotionComponent); this.collider = this.get(ColliderComponent); this._composite = this.collider.useCompositeCollider([]); this._composite.compositeStrategy = options.compositeStrategy ?? 'separate';
this.transform.pos = options.pos ?? Vector.Zero; this._oldPos = this.transform.pos.clone(); this._oldScale = this.transform.scale.clone(); this.renderFromTopOfGraphic = options.renderFromTopOfGraphic ?? this.renderFromTopOfGraphic; this.tileWidth = options.tileWidth; this.tileHeight = options.tileHeight; this.rows = options.rows; this.columns = options.columns;
this._pointerEventDispatcher = new PointerEventsToObjectDispatcher();
this.tiles = new Array<Tile>(this.rows * this.columns); this._rows = new Array(this.rows); this._cols = new Array(this.columns); let currentCol: Tile[] = []; for (let i = 0; i < this.columns; i++) { for (let j = 0; j < this.rows; j++) { const tile = new Tile({ x: i, y: j, map: this }); tile.map = this; this.tiles[i + j * this.columns] = tile; this._pointerEventDispatcher.addObject( tile, (vec: GlobalCoordinates) => { // TODO handle geometry/graphics return tile.bounds.contains(vec.worldPos); }, () => true ); currentCol.push(tile); if (!this._rows[j]) { this._rows[j] = []; } this._rows[j].push(tile); } this._cols[i] = currentCol; currentCol = []; }
this._graphics.localBounds = new BoundingBox({ left: 0, top: 0, right: this.columns * this.tileWidth * this.scale.x, bottom: this.rows * this.tileHeight * this.scale.y }); }
public override _initialize(engine: Engine) { super._initialize(engine); this._engine = engine; }
private _originalOffsets = new WeakMap<Collider, Vector>(); private _getOrSetColliderOriginalOffset(collider: Collider): Vector { if (!this._originalOffsets.has(collider)) { const originalOffset = collider.offset; this._originalOffsets.set(collider, originalOffset); return originalOffset; } else { return this._originalOffsets.get(collider) ?? Vector.Zero; } }
/** * Tiles colliders based on the solid tiles in the tilemap. */ private _updateColliders(): void { this.collider.$colliderRemoved.notifyAll(this._composite); this._composite.clearColliders(); const colliders: BoundingBox[] = []; this._composite = this.collider.useCompositeCollider([]); let current: BoundingBox | null = null;
/** * Returns wether or not the 2 boxes share an edge and are the same height * @param prev * @param next * @returns true if they share and edge, false if not */ const shareEdges = (prev: BoundingBox, next: BoundingBox) => { if (prev && next) { // same top/bottom return ( prev.top === next.top && prev.bottom === next.bottom && // Shared right/left edge prev.right === next.left ); } return false; };
/** * Potentially merges the current collider into a list of previous ones, mutating the list * If checkAndCombine returns true, the collider was successfully merged and should be thrown away * @param current current collider to test * @param colliders List of colliders to consider merging with * @param maxLookBack The amount of colliders to look back for combination * @returns false when no combination found, true when successfully combined */ const checkAndCombine = (current: BoundingBox, colliders: BoundingBox[], maxLookBack = this.meshingLookBehind) => { if (!current) { return false; } // walk backwards through the list of colliders and combine with the first that shares an edge for (let i = colliders.length - 1; i >= 0; i--) { if (maxLookBack-- < 0) { // blunt the O(n^2) algorithm a bit return false; } const prev = colliders[i]; if (shareEdges(prev, current)) { colliders[i] = prev.combine(current); return true; } } return false; };
// ? configurable bias perhaps, horizontal strips vs. vertical ones // Bad tile collider packing algorithm for (let i = 0; i < this.columns; i++) { // Scan column for colliders for (let j = 0; j < this.rows; j++) { const tile = this.tiles[i + j * this.columns]; // Current tile in column is solid build up current collider if (tile.solid) { // Use custom collider otherwise bounding box if (tile.getColliders().length > 0) { // tile with custom collider interrupting the current run for (const collider of tile.getColliders()) { const originalOffset = this._getOrSetColliderOriginalOffset(collider); collider.offset = vec(tile.x * this.tileWidth * this.scale.x, tile.y * this.tileHeight * this.scale.y).add(originalOffset); collider.owner = this; this._composite.addCollider(collider); } //we push any current collider before nulling the current run if (current && !checkAndCombine(current, colliders)) { colliders.push(current); } current = null; // Use the bounding box } else { if (!current) { // no current run, start one current = tile.defaultGeometry; } else { // combine with current run current = current.combine(tile.defaultGeometry); } } } else { // Not solid skip and cut off the current collider // End of run check and combine if (current && !checkAndCombine(current, colliders)) { colliders.push(current); } current = null; } } // After a column is complete check to see if it can be merged into the last one // Eno of run check and combine if (current && !checkAndCombine(current, colliders)) { // else new collider if no combination colliders.push(current); } current = null; }
for (const c of colliders) { const collider = Colliders.Box(c.width, c.height, Vector.Zero, vec(c.left - this.pos.x, c.top - this.pos.y)); collider.owner = this; this._composite.addCollider(collider); } this.collider.update(); // Notify that colliders have been updated this.collider.$colliderAdded.notifyAll(this._composite); }
/** * Returns the {@apilink CompositeCollider} */ public getCompositeCollider(): CompositeCollider { return this._composite; }
/** * Returns the {@apilink Tile} by index (row major order) * * Returns null if out of bounds */ public getTileByIndex(index: number): Tile | null { return this.tiles[index] ?? null; } /** * Returns the {@apilink Tile} by its x and y integer coordinates * * Returns null if out of bounds * * For example, if I want the tile in fifth column (x), and second row (y): * `getTile(4, 1)` 0 based, so 0 is the first in row/column */ public getTile(x: number, y: number): Tile | null { if (x < 0 || y < 0 || x >= this.columns || y >= this.rows) { return null; } return this.tiles[x + y * this.columns]; } /** * Returns the {@apilink Tile} by testing a point in world coordinates, * returns `null` if no Tile was found. */ public getTileByPoint(point: Vector): Tile | null { const { x, y } = this._getTileCoordinates(point); const tile = this.getTile(x, y); if (x >= 0 && y >= 0 && x < this.columns && y < this.rows && tile) { return tile; } return null; }
private _getTileCoordinates(point: Vector): { x: number; y: number } { // Convert to Tile Space point point = this.transform.applyInverse(point);
const x = Math.floor(point.x / this.tileWidth); const y = Math.floor(point.y / this.tileHeight); return { x, y }; }
public getRows(): readonly Tile[][] { return this._rows; }
public getColumns(): readonly Tile[][] { return this._cols; }
/** * Returns the on screen tiles for a tilemap, this will overshoot by a small amount because of the internal quad tree data structure. * * Useful if you need to perform specific logic on onscreen tiles */ public getOnScreenTiles(): readonly Tile[] { const worldBounds = this._engine.screen.getWorldBounds(); let parallaxOffset = vec(0, 0); let bounds = this.transform.coordPlane === CoordPlane.Screen ? this._engine.screen.getScreenBounds() : worldBounds; const maybeParallax = this.get(ParallaxComponent); if (maybeParallax && this.isInitialized) { const oneMinusFactor = Vector.One.sub(maybeParallax.parallaxFactor); parallaxOffset = this._engine.currentScene.camera.pos.scale(oneMinusFactor); bounds = bounds.translate(parallaxOffset.negate()); }
const topLeft = this._getTileCoordinates(bounds.topLeft); const topRight = this._getTileCoordinates(bounds.topRight); const bottomRight = this._getTileCoordinates(bounds.bottomRight); const bottomLeft = this._getTileCoordinates(bounds.bottomLeft);
const tileStartX = Math.min(clamp(topLeft.x, 0, this.columns - 1), clamp(topRight.x, 0, this.columns - 1)); const tileStartY = Math.min(clamp(topLeft.y, 0, this.rows - 1), clamp(topRight.y, 0, this.rows - 1)); const tileEndX = Math.max(clamp(bottomRight.x, 0, this.columns - 1), clamp(bottomLeft.x, 0, this.columns - 1)); const tileEndY = Math.max(clamp(bottomRight.y, 0, this.rows - 1), clamp(bottomLeft.y, 0, this.rows - 1));
const tiles: Tile[] = []; for (let x = tileStartX; x <= tileEndX; x++) { for (let y = tileStartY; y <= tileEndY; y++) { tiles.push(this.getTile(x, y)!); } } return tiles; }
/** * @internal */ public _processPointerToObject(receiver: PointerEventReceiver) { // custom processor for tilmaps because it can be done VERY efficiently // DO NOT CALL this._pointerEventDispatcher.processPointerToObject const pointers: [pointerId: number, pos: GlobalCoordinates][] = Array.from(receiver.currentFramePointerCoords.entries());
// find specific tiles tile for pointer for (const [pointerId, pos] of pointers) { const tile = this.getTileByPoint(this.transform.coordPlane === CoordPlane.World ? pos.worldPos : pos.screenPos); if (tile) { this._pointerEventDispatcher.addPointerToObject(tile, pointerId); } } }
/** * @internal */ public _dispatchPointerEvents(receiver: PointerEventReceiver) { // DO NOT PASS this.tiles, this signals to the dispatcher that order is not important this._pointerEventDispatcher.dispatchEvents(receiver); }
public override update(engine: Engine, elapsed: number) { this._initialize(engine); this.onPreUpdate(engine, elapsed); this.emit('preupdate', new PreUpdateEvent(engine, elapsed, this));
// Update colliders if (!this._oldPos.equals(this.pos) || this._oldRotation !== this.rotation || !this._oldScale.equals(this.scale)) { this.flagCollidersDirty(); this.flagTilesDirty(); } if (this._collidersDirty) { this._collidersDirty = false; this._updateColliders(); }
// Clear last frame's events this._pointerEventDispatcher.clear();
this._token++;
this.pos.clone(this._oldPos); this._oldRotation = this.rotation; this.scale.clone(this._oldScale); this.transform.pos = this.pos; this.onPostUpdate(engine, elapsed); this.emit('postupdate', new PostUpdateEvent(engine, elapsed, this)); }
/** * Draws the tile map to the screen. Called by the {@apilink Scene}. * @param ctx ExcaliburGraphicsContext * @param elapsed The number of milliseconds since the last draw */ public draw(ctx: ExcaliburGraphicsContext, elapsed: number): void { if (!this.isInitialized) { return; } this.emit('predraw', new PreDrawEvent(ctx as any, elapsed, this)); // TODO fix event
let graphics: readonly Graphic[], graphicsIndex: number, graphicsLen: number;
const tiles = this.getOnScreenTiles(); for (let i = 0; i < tiles.length; i++) { const tile = tiles[i]; // get non-negative tile sprites const offsets = tile.getGraphicsOffsets(); graphics = tile.getGraphics();
for (graphicsIndex = 0, graphicsLen = graphics.length; graphicsIndex < graphicsLen; graphicsIndex++) { // draw sprite, warning if sprite doesn't exist const graphic = graphics[graphicsIndex]; const offset = offsets[graphicsIndex]; if (graphic) { if (hasGraphicsTick(graphic)) { graphic?.tick(elapsed, this._token); } const offsetY = this.renderFromTopOfGraphic ? 0 : graphic.height - this.tileHeight; graphic.draw(ctx, tile.x * this.tileWidth + offset.x, tile.y * this.tileHeight - offsetY + offset.y); } } } this.emit('postdraw', new PostDrawEvent(ctx as any, elapsed, this)); }
public debug(gfx: ExcaliburGraphicsContext, debugFlags: DebugConfig) { const { showAll, showGrid, gridColor, gridWidth, showSolidBounds: showColliderBounds, solidBoundsColor: colliderBoundsColor, showColliderGeometry } = debugFlags.tilemap; const { geometryColor, geometryLineWidth, geometryPointSize } = debugFlags.collider; const width = this.tileWidth * this.columns * this.scale.x; const height = this.tileHeight * this.rows * this.scale.y; const pos = this.pos; if (showGrid || showAll) { for (let r = 0; r < this.rows + 1; r++) { const yOffset = vec(0, r * this.tileHeight * this.scale.y); gfx.debug.drawLine(pos.add(yOffset), pos.add(vec(width, yOffset.y)), { color: gridColor, lineWidth: gridWidth }); }
for (let c = 0; c < this.columns + 1; c++) { const xOffset = vec(c * this.tileWidth * this.scale.x, 0); gfx.debug.drawLine(pos.add(xOffset), pos.add(vec(xOffset.x, height)), { color: gridColor, lineWidth: gridWidth }); } }
if (showAll || showColliderBounds || showColliderGeometry) { const colliders = this._composite.getColliders(); gfx.save(); gfx.translate(this.pos.x, this.pos.y); gfx.scale(this.scale.x, this.scale.y); for (const collider of colliders) { const bounds = collider.localBounds; const pos = collider.worldPos.sub(this.pos); if (showColliderBounds) { gfx.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, { color: colliderBoundsColor }); } } gfx.restore(); if (showColliderGeometry) { for (const collider of colliders) { collider.debug(gfx, geometryColor, { lineWidth: geometryLineWidth, pointSize: geometryPointSize }); } } }
if (showAll || showColliderBounds) { gfx.save(); gfx.z = 999; if (showColliderBounds) { for (let i = 0; i < this.tiles.length; i++) { this.tiles[i].bounds.debug(gfx); } } gfx.restore(); } }}
export interface TileOptions { /** * Integer tile x coordinate */ x: number; /** * Integer tile y coordinate */ y: number; map: TileMap; solid?: boolean; graphics?: Graphic[];}
/** * TileMap Tile * * A light-weight object that occupies a space in a collision map. Generally * created by a {@apilink TileMap}. * * Tiles can draw multiple sprites. Note that the order of drawing is the order * of the sprites in the array so the last one will be drawn on top. You can * use transparency to create layers this way. */export class Tile { private _bounds!: BoundingBox; private _geometry!: BoundingBox; private _pos!: Vector; private _posDirty = false;
public events = new EventEmitter<TilePointerEvents>();
/** * Return the world position of the top left corner of the tile */ public get pos() { if (this._posDirty) { this._recalculate(); this._posDirty = false; } return this._pos; }
/** * Integer x coordinate of the tile */ public readonly x: number;
/** * Integer y coordinate of the tile */ public readonly y: number;
private _width: number; /** * Width of the tile in pixels */ public get width(): number { return this._width; }
private _height: number; /** * Height of the tile in pixels */ public get height(): number { return this._height; }
/** * Reference to the TileMap this tile is associated with */ public map: TileMap;
private _solid = false; /** * Wether this tile should be treated as solid by the tilemap */ public get solid(): boolean { return this._solid; } /** * Wether this tile should be treated as solid by the tilemap */ public set solid(val: boolean) { this.map?.flagCollidersDirty(); this._solid = val; }
private _graphics: Graphic[] = []; private _offsets: Vector[] = [];
/** * Current list of graphics for this tile */ public getGraphics(): readonly Graphic[] { return this._graphics; }
/** * Current list of offsets for this tile's graphics */ public getGraphicsOffsets(): readonly Vector[] { return this._offsets; }
/** * Add another {@apilink Graphic} to this TileMap tile * @param graphic */ public addGraphic(graphic: Graphic, options?: { offset?: Vector }) { this._graphics.push(graphic); if (options?.offset) { this._offsets.push(options.offset); } else { this._offsets.push(Vector.Zero); } }
/** * Remove an instance of a {@apilink Graphic} from this tile */ public removeGraphic(graphic: Graphic) { const index = this._graphics.indexOf(graphic); if (index > -1) { this._graphics.splice(index, 1); this._offsets.splice(index, 1); } }
/** * Clear all graphics from this tile */ public clearGraphics() { this._graphics.length = 0; this._offsets.length = 0; }
/** * Current list of colliders for this tile */ private _colliders: Collider[] = [];
/** * Returns the list of colliders */ public getColliders(): readonly Collider[] { return this._colliders; }
/** * Adds a custom collider to the {@apilink Tile} to use instead of it's bounds * * If no collider is set but {@apilink Tile.solid} is set, the tile bounds are used as a collider. * * **Note!** the {@apilink Tile.solid} must be set to true for it to act as a "fixed" collider * @param collider */ public addCollider(collider: Collider) { this._colliders.push(collider); this.map.flagCollidersDirty(); }
/** * Removes a collider from the {@apilink Tile} * @param collider */ public removeCollider(collider: Collider) { const index = this._colliders.indexOf(collider); if (index > -1) { this._colliders.splice(index, 1); } this.map.flagCollidersDirty(); }
/** * Clears all colliders from the {@apilink Tile} */ public clearColliders() { this._colliders.length = 0; this.map.flagCollidersDirty(); }
/** * Arbitrary data storage per tile, useful for any game specific data */ public data = new Map<string, any>();
constructor(options: TileOptions) { this.x = options.x; this.y = options.y; this.map = options.map; this._width = options.map.tileWidth * this.map.scale.x; this._height = options.map.tileHeight * this.map.scale.y; this.solid = options.solid ?? this.solid; this._graphics = options.graphics ?? []; this._recalculate(); }
public flagDirty(): void { this._posDirty = true; }
private _recalculate(): void { const geometryPos = this.map.pos.add(vec(this.x * this.map.tileWidth, this.y * this.map.tileHeight)); this._geometry = new BoundingBox(geometryPos.x, geometryPos.y, geometryPos.x + this.map.tileWidth, geometryPos.y + this.map.tileHeight);
this._width = this.map.tileWidth * this.map.scale.x; this._height = this.map.tileHeight * this.map.scale.y;
this._pos = this.map.pos.add(vec(this.x * this._width, this.y * this._height)); this._bounds = new BoundingBox(this._pos.x, this._pos.y, this._pos.x + this._width, this._pos.y + this._height);
if (this.map.rotation) { this._bounds = this._bounds.rotate(this.map.rotation, this.map.pos); } this._posDirty = false; }
/** * Tile bounds in world space */ public get bounds(): BoundingBox { if (this._posDirty) { this._recalculate(); } return this._bounds; }
public get defaultGeometry(): BoundingBox { return this._geometry; }
/** * Tile position in world space */ public get center(): Vector { if (this._posDirty) { this._recalculate(); } return new Vector(this._pos.x + this._width / 2, this._pos.y + this._height / 2); }
public emit<TEventName extends EventKey<TilePointerEvents>>(eventName: TEventName, event: TilePointerEvents[TEventName]): void; public emit(eventName: string, event?: any): void; public emit<TEventName extends EventKey<TilePointerEvents> | string>(eventName: TEventName, event?: any): void { this.events.emit(eventName, event); }
public on<TEventName extends EventKey<TilePointerEvents>>( eventName: TEventName, handler: Handler<TilePointerEvents[TEventName]> ): Subscription; public on(eventName: string, handler: Handler<unknown>): Subscription; public on<TEventName extends EventKey<TilePointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.on(eventName, handler); }
public once<TEventName extends EventKey<TilePointerEvents>>( eventName: TEventName, handler: Handler<TilePointerEvents[TEventName]> ): Subscription; public once(eventName: string, handler: Handler<unknown>): Subscription; public once<TEventName extends EventKey<TilePointerEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.once(eventName, handler); }
public off<TEventName extends EventKey<TilePointerEvents>>(eventName: TEventName, handler: Handler<TilePointerEvents[TEventName]>): void; public off(eventName: string, handler: Handler<unknown>): void; public off(eventName: string): void; public off<TEventName extends EventKey<TilePointerEvents> | string>(eventName: TEventName, handler?: Handler<any>): void { if (handler) { this.events.off(eventName, handler); } else { this.events.off(eventName); } }}