Skip to content

Commit

Permalink
feat: [#3283] Add tile enter/leave events to TileMap
Browse files Browse the repository at this point in the history
  • Loading branch information
eonarheim committed Nov 24, 2024
1 parent fb88f4b commit 1e5e9f1
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 208 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added `pointerenter` and `pointerleave` events to `ex.TileMap` tiles!
- Added new `ex.BezierCurve` type for drawing cubic bezier curves
- Added 2 new actions `actor.actions.curveTo(...)` and `actor.actions.curveBy(...)`
- Added new `ex.lerp(...)`, `ex.inverseLerp(...)`, and `ex.remap(...)` for numbers
Expand Down
16 changes: 13 additions & 3 deletions sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1105,9 +1105,19 @@ game.input.pointers.primary.on('down', (evt: ex.PointerEvent) => {
}
});

tileMap.tiles[0].events.on('pointerdown', (evt) => {
console.log('tile clicked', evt);
});
for (let i = 0; i < tileMap.tiles.length; i++) {
tileMap.tiles[i].events.on('pointerdown', (evt) => {
console.log('tile clicked', tileMap.tiles[i]);
});

tileMap.tiles[i].events.on('pointerenter', (evt) => {
console.log('pointer entered tile', tileMap.tiles[i].x, tileMap.tiles[i].y);
});

tileMap.tiles[i].events.on('pointerleave', (evt) => {
console.log('pointer left tile', tileMap.tiles[i].x, tileMap.tiles[i].y);
});
}

game.input.keyboard.on('up', (evt?: ex.KeyEvent) => {
if (evt.key == ex.Keys.F) {
Expand Down
283 changes: 283 additions & 0 deletions src/engine/Input/PointerEventsToObjectDispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import { EventEmitter } from '../EventEmitter';
import { PointerEvent } from './PointerEvent';
import { GlobalCoordinates } from '../Math';
import { PointerEventReceiver } from './PointerEventReceiver';
import { RentalPool } from '../Util/RentalPool';

/**
* Signals that an object has nested pointer events on nested objects that are not an Entity with
* a PointerComponent. For example TileMap Tiles
*/
export interface HasNestedPointerEvents {
_dispatchPointerEvents(receiver: PointerEventReceiver): void;
_processPointerToObject(receiver: PointerEventReceiver): void;
}

/**
*
*/
function hasNestedEvents(object: any): object is HasNestedPointerEvents {
return object && object._dispatchPointerEvents && object._processPointerToObject;
}

export class PointerTargetObjectProxy<TObject extends { events: EventEmitter }> {
public object!: TObject;
public contains!: (point: GlobalCoordinates) => boolean;
public active!: () => boolean;
public get events(): EventEmitter {
return this.object.events;
}
public init(object: TObject, contains: (point: GlobalCoordinates) => boolean, active: () => boolean): void {
this.object = object;
this.contains = contains;
this.active = active;
}
}

export class PointerEventsToObjectDispatcher<TObject extends { events: EventEmitter }> {
private _proxyPool = new RentalPool(
() => new PointerTargetObjectProxy<TObject>(),
(p) => p,
100
);
private _objectToProxy = new Map<TObject, PointerTargetObjectProxy<TObject>>();
private _proxies: PointerTargetObjectProxy<TObject>[] = [];

/**
* Tracks an object to associate with pointers and their events
* @param object
* @param contains
* @param active
*/
public addObject(object: TObject, contains: (point: GlobalCoordinates) => boolean, active: () => boolean): void {
const proxy = this._proxyPool.rent(false);
proxy.init(object, contains, active);
this._proxies.push(proxy);
this._objectToProxy.set(object, proxy);
}

private _getProxy(object: TObject): PointerTargetObjectProxy<TObject> {
const proxy = this._objectToProxy.get(object);
if (proxy) {
return proxy;
}
throw new Error('No PointerTargetProxy for object');
}

/**
* Untracks an object associated with pointers and their events
* @param object
*/
public removeObject(object: TObject): void {
const proxy = this._objectToProxy.get(object);
if (proxy) {
const index = this._proxies.indexOf(proxy);
if (index > -1) {
this._proxies.splice(index, 1);
}
this._proxyPool.return(proxy);
}
}
private _lastFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>();
private _currentFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>();
private _objectCurrentlyUnderPointer(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
return !!(this._currentFrameObjectToPointers.has(object) && this._currentFrameObjectToPointers.get(object)!.includes(pointerId));
}

private _objectWasUnderPointer(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
return !!(this._lastFrameObjectToPointers.has(object) && this._lastFrameObjectToPointers.get(object)!.includes(pointerId));
}

private _entered(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
return this._objectCurrentlyUnderPointer(object, pointerId) && !this._lastFrameObjectToPointers.has(object);
}

private _left(object: PointerTargetObjectProxy<any>, pointerId: number): boolean {
return !this._currentFrameObjectToPointers.has(object) && this._objectWasUnderPointer(object, pointerId);
}

/**
* Manually associate a pointer id with an object.
*
* This assumes you've checked that the pointer is indeed over the object.
*/
public addPointerToObject(object: TObject, pointerId: number): void {
const maybeProxy = this._objectToProxy.get(object);
if (maybeProxy) {
this._addPointerToProxy(maybeProxy, pointerId);
}
}

private _addPointerToProxy(object: PointerTargetObjectProxy<any>, pointerId: number): void {
if (!this._currentFrameObjectToPointers.has(object)) {
this._currentFrameObjectToPointers.set(object, [pointerId]);
return;
}
const pointers = this._currentFrameObjectToPointers.get(object)!;
this._currentFrameObjectToPointers.set(object, pointers.concat(pointerId));
}

/**
* Dispatches the appropriate pointer events in sortedObject order on tracked objects
* @param receiver
* @param sortedObjects
*/
public dispatchEvents(receiver: PointerEventReceiver, sortedObjects: TObject[]) {
const lastFrameEntities = new Set(this._lastFrameObjectToPointers.keys());
const currentFrameEntities = new Set(this._currentFrameObjectToPointers.keys());
// Filter preserves z order
let lastMovePerPointer: Map<number, PointerEvent>;
let lastUpPerPointer: Map<number, PointerEvent>;
let lastDownPerPointer: Map<number, PointerEvent>;
// 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);
}
if (lastFrameEntities.has(proxy) || currentFrameEntities.has(proxy)) {
lastDownPerPointer = this._processDownAndEmit(receiver, proxy);

lastUpPerPointer = this._processUpAndEmit(receiver, proxy);

lastMovePerPointer = this._processMoveAndEmit(receiver, proxy);

const lastUpDownMoveEvents = [...lastMovePerPointer.values(), ...lastDownPerPointer.values(), ...lastUpPerPointer.values()];
this._processEnterLeaveAndEmit(receiver, proxy, lastUpDownMoveEvents);

this._processCancelAndEmit(receiver, proxy);

this._processWheelAndEmit(receiver, proxy);
}
}
}

/**
* Given the tracked objects, update pointer containment given the provided contains()
* @param receiver
* @param objects
*/
public processPointerToObject(receiver: PointerEventReceiver, objects: TObject[]) {
// Pre-process find entities under pointers
for (let objectIndex = 0; objectIndex < objects.length; objectIndex++) {
const object = objects[objectIndex];
const proxy = this._getProxy(object);
if (hasNestedEvents(object)) {
object._processPointerToObject(receiver);
}
for (const [pointerId, pos] of receiver.currentFramePointerCoords.entries()) {
if (proxy.contains(pos)) {
this._addPointerToProxy(proxy, pointerId);
}
}
}
}

/**
* Clear current frames pointer-object associations and track last frame pointer-object associations
*/
public clear() {
this._lastFrameObjectToPointers.clear();
this._lastFrameObjectToPointers = new Map<PointerTargetObjectProxy<any>, number[]>(this._currentFrameObjectToPointers);
this._currentFrameObjectToPointers.clear();
}

private _processDownAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
const lastDownPerPointer = new Map<number, PointerEvent>();
// Loop through down and dispatch to entities
for (const event of receiver.currentFrameDown) {
if (event.active && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
object.events.emit('pointerdown', event as any);
if (receiver.isDragStart(event.pointerId)) {
object.events.emit('pointerdragstart', event as any);
}
}
lastDownPerPointer.set(event.pointerId, event);
}
return lastDownPerPointer;
}

private _processUpAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
const lastUpPerPointer = new Map<number, PointerEvent>();
// Loop through up and dispatch to entities
for (const event of receiver.currentFrameUp) {
if (event.active && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
object.events.emit('pointerup', event as any);
if (receiver.isDragEnd(event.pointerId)) {
object.events.emit('pointerdragend', event as any);
}
}
lastUpPerPointer.set(event.pointerId, event);
}
return lastUpPerPointer;
}

private _processMoveAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>): Map<number, PointerEvent> {
const lastMovePerPointer = new Map<number, PointerEvent>();
// Loop through move and dispatch to entities
for (const event of receiver.currentFrameMove) {
if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
// move
object.events.emit('pointermove', event as any);

if (receiver.isDragging(event.pointerId)) {
object.events.emit('pointerdragmove', event as any);
}
}
lastMovePerPointer.set(event.pointerId, event);
}
return lastMovePerPointer;
}

private _processEnterLeaveAndEmit(
receiver: PointerEventReceiver,
object: PointerTargetObjectProxy<any>,
lastUpDownMoveEvents: PointerEvent[]
) {
// up, down, and move are considered for enter and leave
for (const event of lastUpDownMoveEvents) {
// enter
if (event.active && object.active() && this._entered(object, event.pointerId)) {
object.events.emit('pointerenter', event as any);
if (receiver.isDragging(event.pointerId)) {
object.events.emit('pointerdragenter', event as any);
}
break;
}
if (
event.active &&
object.active() &&
// leave can happen on move
(this._left(object, event.pointerId) ||
// or leave can happen on pointer up
(this._objectCurrentlyUnderPointer(object, event.pointerId) && event.type === 'up'))
) {
object.events.emit('pointerleave', event as any);
if (receiver.isDragging(event.pointerId)) {
object.events.emit('pointerdragleave', event as any);
}
break;
}
}
}

private _processCancelAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>) {
// cancel
for (const event of receiver.currentFrameCancel) {
if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, event.pointerId)) {
object.events.emit('pointercancel', event as any);
}
}
}

private _processWheelAndEmit(receiver: PointerEventReceiver, object: PointerTargetObjectProxy<any>) {
// wheel
for (const event of receiver.currentFrameWheel) {
// Currently the wheel only fires under the primary pointer '0'
if (event.active && object.active() && this._objectCurrentlyUnderPointer(object, 0)) {
object.events.emit('pointerwheel', event as any);
}
}
}
}
Loading

0 comments on commit 1e5e9f1

Please sign in to comment.