Skip to content

Commit

Permalink
feat: Add pointer events to TileMap/IsometricMap and tiles (#2968)
Browse files Browse the repository at this point in the history
This PR adds:
- Pointer event support to `ex.TileMap`'s and individual `ex.Tile`'s
- Pointer event support to `ex.IsometricMap`'s and individual `ex.IsometricTile`'s


```typescript
// Tilemap
const tileMap = new TileMap({...});
for (const tile of tileMap.tiles) {
  tile.on('pointerdown', (evt: ex.PointerEvent) => {
    console.log(tile.x, tile.y);
  });
}

// Isometric
const isoMap = new IsometricMap({...});
for (const tile of isoMap.tiles) {
  tile.on('pointerdown', (evt: ex.PointerEvent) => {
    console.log(tile.x, tile.y);
  });
}
```
  • Loading branch information
eonarheim authored Mar 29, 2024
1 parent 99c7a88 commit 48ddac9
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added pointer event support to `ex.TileMap`'s and individual `ex.Tile`'s
- Added pointer event support to `ex.IsometricMap`'s and individual `ex.IsometricTile`'s
- Added `useAnchor` parameter to `ex.GraphicsGroup` to allow users to opt out of anchor based positioning, if set to false all graphics members
will be positioned with the top left of the graphic at the actor's position.
```typescript
Expand Down
7 changes: 7 additions & 0 deletions sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ tileMap.tiles.forEach(function(cell: ex.Tile) {
cell.solid = true;
cell.addGraphic(spriteTiles.sprites[0]);
});

for (const tile of tileMap.tiles) {
tile.on('pointerdown', (evt: ex.PointerEvent) => {
console.log(tile.x, tile.y);
});
}

game.add(tileMap);

// Create spriteFont
Expand Down
5 changes: 5 additions & 0 deletions sandbox/tests/isometric/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ var isoMap2 = new ex.IsometricMap({
});
isoMap2.tiles.forEach(t => t.addGraphic(isoTileSprite));
game.currentScene.add(isoMap2);
for (const tile of isoMap2.tiles) {
tile.on('pointerdown', (evt: ex.PointerEvent) => {
console.log(tile.x, tile.y);
});
}

var tileCoord = ex.vec(0, 0);
game.input.pointers.on('move', evt => {
Expand Down
19 changes: 19 additions & 0 deletions src/engine/Input/PointerComponent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Component } from '../EntityComponentSystem/Component';
import { BoundingBox } from '../excalibur';

export interface PointerComponentOptions {
useColliderShape?: boolean;
useGraphicsBounds?: boolean;
localBounds?: BoundingBox;
}

/**
* Add this component to optionally configure how the pointer
Expand All @@ -21,4 +28,16 @@ export class PointerComponent extends Component {
* bounds around the graphic to trigger pointer events. Default is `true`.
*/
public useGraphicsBounds = true;

/**
* Optionally use other bounds for pointer testing
*/
public localBounds?: BoundingBox;

constructor(options?: PointerComponentOptions) {
super();
this.useColliderShape = options?.useColliderShape ?? this.useColliderShape;
this.useGraphicsBounds = options?.useGraphicsBounds ?? this.useGraphicsBounds;
this.localBounds = options?.localBounds;
}
}
10 changes: 10 additions & 0 deletions src/engine/Input/PointerSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ export class PointerSystem extends System {
for (const entity of entities) {
transform = entity.get(TransformComponent);
pointer = entity.get(PointerComponent) ?? new PointerComponent;
// If pointer bounds defined
if (pointer.localBounds) {
const pointerBounds = pointer.localBounds.transform(transform.get().matrix);
for (const [pointerId, pos] of receiver.currentFramePointerCoords.entries()) {
if (pointerBounds.contains(transform.coordPlane === CoordPlane.World ? pos.worldPos : pos.screenPos)) {
this.addPointerToEntity(entity, pointerId);
}
}
}

// Check collider contains pointer
collider = entity.get(ColliderComponent);
if (collider && (pointer.useColliderShape || this.overrideUseColliderShape)) {
Expand Down
42 changes: 41 additions & 1 deletion src/engine/TileMap/IsometricMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@ import { CollisionType } from '../Collision/CollisionType';
import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
import { vec, Vector } from '../Math/vector';
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
import { Entity } from '../EntityComponentSystem/Entity';
import { Entity, EntityEvents } from '../EntityComponentSystem/Entity';
import { DebugGraphicsComponent, ExcaliburGraphicsContext, Graphic, GraphicsComponent } from '../Graphics';
import { IsometricEntityComponent } from './IsometricEntityComponent';
import { DebugConfig } from '../Debug';
import { PointerComponent } from '../Input/PointerComponent';
import { PointerEvent } from '../Input/PointerEvent';
import { EventEmitter } from '../EventEmitter';

export type IsometricTilePointerEvents = {
pointerup: PointerEvent;
pointerdown: PointerEvent;
pointermove: PointerEvent;
pointercancel: PointerEvent;
}

export class IsometricTile extends Entity {
/**
* Indicates whether this tile is solid
*/
public solid: boolean = false;

public events = new EventEmitter<EntityEvents & IsometricTilePointerEvents>();

private _gfx: GraphicsComponent;
private _tileBounds = new BoundingBox();
private _graphics: Graphic[] = [];
Expand Down Expand Up @@ -295,6 +308,8 @@ export class IsometricMap extends Entity {
*/
public collider: ColliderComponent;

public pointer: PointerComponent;

private _composite: CompositeCollider;

constructor(options: IsometricMapOptions) {
Expand All @@ -304,6 +319,7 @@ export class IsometricMap extends Entity {
type: CollisionType.Fixed
}),
new ColliderComponent(),
new PointerComponent(),
new DebugGraphicsComponent((ctx, debugFlags) => this.debug(ctx, debugFlags), false)
], options.name);
const { pos, tileWidth, tileHeight, columns: width, rows: height, renderFromTopOfGraphic, graphicsOffset, elevation } = options;
Expand All @@ -318,6 +334,7 @@ export class IsometricMap extends Entity {
this.collider.set(this._composite = new CompositeCollider([]));
}

this.pointer = this.get(PointerComponent);

this.renderFromTopOfGraphic = renderFromTopOfGraphic ?? this.renderFromTopOfGraphic;
this.graphicsOffset = graphicsOffset ?? this.graphicsOffset;
Expand All @@ -338,6 +355,28 @@ export class IsometricMap extends Entity {
this.addChild(tile);
}
}

this.pointer.localBounds = BoundingBox.fromDimension(
tileWidth * width * this.transform.scale.x,
tileHeight * height * this.transform.scale.y,
vec(.5, 0)
);

this._setupPointerToTile();
}

private _forwardPointerEventToTile = (eventType: string) => (evt: PointerEvent) => {
const tile = this.getTileByPoint(evt.worldPos);
if (tile) {
tile.events.emit(eventType, evt);
}
};

private _setupPointerToTile() {
this.events.on('pointerup', this._forwardPointerEventToTile('pointerup'));
this.events.on('pointerdown', this._forwardPointerEventToTile('pointerdown'));
this.events.on('pointermove', this._forwardPointerEventToTile('pointermove'));
this.events.on('pointercancel', this._forwardPointerEventToTile('pointercancel'));
}

public update(): void {
Expand Down Expand Up @@ -386,6 +425,7 @@ export class IsometricMap extends Entity {
* @param worldCoordinate
*/
public worldToTile(worldCoordinate: Vector): Vector {
// TODO I don't think this handles parent transform see TileMap
worldCoordinate = worldCoordinate.sub(this.transform.globalPos);

const halfTileWidth = this.tileWidth / 2;
Expand Down
75 changes: 70 additions & 5 deletions src/engine/TileMap/TileMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
import { CoordPlane } from '../Math/coord-plane';
import { DebugConfig } from '../Debug';
import { clamp } from '../Math/util';
import { PointerComponent } from '../Input/PointerComponent';
import { PointerEvent } from '../Input/PointerEvent';

export interface TileMapOptions {
/**
Expand Down Expand Up @@ -61,18 +63,29 @@ export interface TileMapOptions {
meshingLookBehind?: number;
}

export type TileMapEvents = EntityEvents & {
export type TilePointerEvents = {
pointerup: PointerEvent;
pointerdown: PointerEvent;
pointermove: PointerEvent;
pointercancel: PointerEvent;
}

export type TileMapEvents = EntityEvents & TilePointerEvents & {
preupdate: PreUpdateEvent<TileMap>;
postupdate: PostUpdateEvent<TileMap>;
predraw: PreDrawEvent;
postdraw: PostDrawEvent
postdraw: PostDrawEvent;
}

export const TileMapEvents = {
PreUpdate: 'preupdate',
PostUpdate: 'postupdate',
PreDraw: 'predraw',
PostDraw: 'postdraw'
PostDraw: 'postdraw',
PointerUp: 'pointerup',
PointerDown: 'pointerdown',
PointerMove: 'pointermove',
PointerCancel: 'pointercancel'
};

/**
Expand Down Expand Up @@ -111,6 +124,7 @@ export class TileMap extends Entity {
}
}

public pointer: PointerComponent;
public transform: TransformComponent;
private _motion: MotionComponent;
private _graphics: GraphicsComponent;
Expand Down Expand Up @@ -232,6 +246,8 @@ export class TileMap extends Entity {
);
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);
Expand Down Expand Up @@ -270,6 +286,8 @@ export class TileMap extends Entity {
currentCol = [];
}

this._setupPointerToTile();

this._graphics.localBounds = new BoundingBox({
left: 0,
top: 0,
Expand All @@ -283,6 +301,19 @@ export class TileMap extends Entity {
this._engine = engine;
}

private _forwardPointerEventToTile = (eventType: string) => (evt: PointerEvent) => {
const tile = this.getTileByPoint(evt.worldPos);
if (tile) {
tile.events.emit(eventType, evt);
}
};

private _setupPointerToTile() {
this.events.on('pointerup', this._forwardPointerEventToTile('pointerup'));
this.events.on('pointerdown', this._forwardPointerEventToTile('pointerdown'));
this.events.on('pointermove', this._forwardPointerEventToTile('pointermove'));
this.events.on('pointercancel', this._forwardPointerEventToTile('pointercancel'));
}

private _originalOffsets = new WeakMap<Collider, Vector>();
private _getOrSetColliderOriginalOffset(collider: Collider): Vector {
Expand Down Expand Up @@ -657,12 +688,14 @@ export interface TileOptions {
* 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 extends Entity {
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
*/
Expand Down Expand Up @@ -820,7 +853,6 @@ export class Tile extends Entity {
public data = new Map<string, any>();

constructor(options: TileOptions) {
super();
this.x = options.x;
this.y = options.y;
this.map = options.map;
Expand Down Expand Up @@ -877,4 +909,37 @@ export class Tile extends Entity {
}
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);
}
}
}
35 changes: 35 additions & 0 deletions src/spec/IsometricMapSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,39 @@ describe('A IsometricMap', () => {
expect(sut.getTileByPoint(ex.vec(250, 234)).y).toBe(14);
});

it('can respond to pointer events', async () => {
const engine = TestUtils.engine({width: 100, height: 100});
await TestUtils.runToReady(engine);
const sut = new ex.IsometricMap({
pos: ex.vec(100, 100),
tileWidth: 64,
tileHeight: 48,
rows: 20,
columns: 20
});
engine.add(sut);

const tile = sut.getTile(0, 0);

const pointerdown = jasmine.createSpy('pointerdown');
const pointerup = jasmine.createSpy('pointerup');
const pointermove = jasmine.createSpy('pointermove');
const pointercancel = jasmine.createSpy('pointercancel');
tile.on('pointerdown', pointerdown);
tile.on('pointerup', pointerup);
tile.on('pointermove', pointermove);
tile.on('pointercancel', pointercancel);

engine.input.pointers.triggerEvent('down', ex.vec(110, 110));
engine.input.pointers.triggerEvent('up', ex.vec(110, 110));
engine.input.pointers.triggerEvent('move', ex.vec(110, 110));
engine.input.pointers.triggerEvent('cancel', ex.vec(110, 110));
expect(pointerdown).toHaveBeenCalledTimes(1);
expect(pointerup).toHaveBeenCalledTimes(1);
expect(pointermove).toHaveBeenCalledTimes(1);
expect(pointercancel).toHaveBeenCalledTimes(1);

engine.dispose();
});

});
Loading

0 comments on commit 48ddac9

Please sign in to comment.