diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ffab09..dd0c7016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed Tilemap/Isometric map pointer performance on moderate to large maps, we changed the strategy to only consider tiles under the pointer instead of try to do sorted dispatch on NxM tiles. - Fixed issue that caused coroutines to not automatically discover the engine scheduler when inside an async lifecycle sometimes. This is because of the stack replacement issue of async/await the context reverts too soon. ### Updates diff --git a/sandbox/tests/tilemap/tilemap.ts b/sandbox/tests/tilemap/tilemap.ts index 82f3bdda..01e00d5c 100644 --- a/sandbox/tests/tilemap/tilemap.ts +++ b/sandbox/tests/tilemap/tilemap.ts @@ -27,8 +27,8 @@ var tm = new ex.TileMap({ pos: ex.vec(-100, -100), tileWidth: 16 * 20, tileHeight: 16 * 20, - columns: 40, - rows: 40 + columns: 400, + rows: 400 }); //tm.addComponent(new ex.ParallaxComponent(ex.vec(.4, .4))); @@ -40,7 +40,9 @@ tileSprite.destSize.width = 320; tileSprite.destSize.height = 320; for (var i = 0; i < tm.columns * tm.rows; i++) { - tm.getTileByIndex(i).addGraphic(tileSprite); + const tile = tm.getTileByIndex(i); + tile.addGraphic(tileSprite); + tile.on('pointermove', () => console.log(tile.x, tile.y)); } game.add(tm); diff --git a/src/engine/Input/PointerEventsToObjectDispatcher.ts b/src/engine/Input/PointerEventsToObjectDispatcher.ts index de182697..1dd9c2c8 100644 --- a/src/engine/Input/PointerEventsToObjectDispatcher.ts +++ b/src/engine/Input/PointerEventsToObjectDispatcher.ts @@ -56,7 +56,7 @@ export class PointerEventsToObjectDispatcher { + public _getProxy(object: TObject): PointerTargetObjectProxy { const proxy = this._objectToProxy.get(object); if (proxy) { return proxy; @@ -123,19 +123,38 @@ export class PointerEventsToObjectDispatcher; let lastUpPerPointer: Map; let lastDownPerPointer: Map; + + let sortedObjectsProxies: PointerTargetObjectProxy[] = []; + if (sortedObjects) { + for (let i = 0; i < sortedObjects.length; i++) { + const object = sortedObjects[i]; + const proxy = this._getProxy(object); + sortedObjectsProxies.push(proxy); + // TODO it might be faster to "just" maintain a sorted list from last/current frame + // this will require some more engineering to accurately track z index changes over time + } + } else { + // eschew sorted order just use last/current frame entities in any order + // TODO use Set.union when it's more supported by the last 5 Safari versions + const unsortedObjectProxies = new Set(lastFrameEntities); + for (const other of currentFrameEntities) { + unsortedObjectProxies.add(other); + } + sortedObjectsProxies = Array.from(unsortedObjectProxies); + } + // Dispatch events in proxy z order - for (let i = 0; i < sortedObjects.length; i++) { - const object = sortedObjects[i]; - const proxy = this._getProxy(object); - if (hasNestedEvents(object)) { - object._dispatchPointerEvents(receiver); + for (let i = 0; i < sortedObjectsProxies.length; i++) { + const proxy = sortedObjectsProxies[i]; + if (hasNestedEvents(proxy.object)) { + proxy.object._dispatchPointerEvents(receiver); } if (lastFrameEntities.has(proxy) || currentFrameEntities.has(proxy)) { lastDownPerPointer = this._processDownAndEmit(receiver, proxy); diff --git a/src/engine/TileMap/IsometricMap.ts b/src/engine/TileMap/IsometricMap.ts index c4e6e036..514c572d 100644 --- a/src/engine/TileMap/IsometricMap.ts +++ b/src/engine/TileMap/IsometricMap.ts @@ -18,6 +18,8 @@ import { EventEmitter } from '../EventEmitter'; import type { HasNestedPointerEvents } from '../Input/PointerEventsToObjectDispatcher'; import { PointerEventsToObjectDispatcher } from '../Input/PointerEventsToObjectDispatcher'; import type { PointerEventReceiver } from '../Input/PointerEventReceiver'; +import type { GlobalCoordinates } from '../Math'; +import { CoordPlane } from '../Math'; export type IsometricTilePointerEvents = { pointerup: PointerEvent; @@ -403,7 +405,17 @@ export class IsometricMap extends Entity implements HasNestedPointerEvents { * @internal */ public _processPointerToObject(receiver: PointerEventReceiver) { - this._pointerEventDispatcher.processPointerToObject(receiver, this.tiles); + // 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); + } + } } /** diff --git a/src/engine/TileMap/TileMap.ts b/src/engine/TileMap/TileMap.ts index 71d12e16..522bfcc9 100644 --- a/src/engine/TileMap/TileMap.ts +++ b/src/engine/TileMap/TileMap.ts @@ -565,14 +565,25 @@ export class TileMap extends Entity implements HasNestedPointerEvents { * @internal */ public _processPointerToObject(receiver: PointerEventReceiver) { - this._pointerEventDispatcher.processPointerToObject(receiver, this.tiles); + // 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) { - this._pointerEventDispatcher.dispatchEvents(receiver, this.tiles); + // DO NOT PASS this.tiles, this signals to the dispatcher that order is not important + this._pointerEventDispatcher.dispatchEvents(receiver); } public update(engine: Engine, elapsed: number) {