Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Animation improvements fromSpriteSheetCoordinates + EventEmitter #2666

Merged
merged 3 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,57 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Deprecated

- [[ex.Input.Gamepad]] `isButtonPressed` has been renamed to `isButtonHeld`
- `ex.EventDispatcher` is marked deprecated, will eventually be removed in v0.29.0


### Added

- Added new helper called `ex.Animation.fromSpriteSheetCoordinates` to help build animations more tersely from SpriteSheets
```typescript
const spriteSheet = SpriteSheet.fromImageSource({...});
const anim = Animation.fromSpriteSheetCoordinates({
spriteSheet,
frameCoordinates: [
{x: 0, y: 5, duration: 100},
{x: 1, y: 5, duration: 200},
{x: 2, y: 5, duration: 100},
{x: 3, y: 5, duration: 500}
],
strategy: AnimationStrategy.PingPong
});
```

- Added new `FrameEvent` to `ex.Animation` which includes the frame index of the current frame!
```typescript
const anim = new Animation();

// TS autocompletes the handler
anim.on('frame', (frame: FrameEvent) => {
// Do stuff on frame
});
```

- Added new typed `ex.EventEmitter` which will eventually replace the old `ex.EventDispatcher`, this gives users a way of strongly typing the possible events that can be emitted using a type map. This is loosely typed you can still emit any event you want, you only get type completion suggestions for the type map.
```typescript
export type AnimationEvents = {
frame: FrameEvent;
loop: Animation;
ended: Animation;
};

export class Animation {
public events = new EventEmitter<AnimationEvents>();
...
}

const anim = new Animation();

// TS autocompletes the handler
anim.on('frame', (frame: FrameEvent) => {
// Do stuff on frame
});
```

- Added ability to perform arbitrary ray casts into `ex.Scene`, the `ex.PhysicsWorld` can be passed a variety of options to influence the types of ray cast hits that
are returned
```typescript
Expand Down
3 changes: 3 additions & 0 deletions src/engine/EventDispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { GameEvent } from './Events';
import { Eventable } from './Interfaces/Evented';

/**
* @deprecated Use [[EventEmitter]] will be removed in v0.29.0
*/
export class EventDispatcher<T = any> implements Eventable {
private _handlers: { [key: string]: { (event: GameEvent<T>): void }[] } = {};
private _wiredEventDispatchers: Eventable[] = [];
Expand Down
105 changes: 105 additions & 0 deletions src/engine/EventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
export type EventMap = Record<string, any>;
export type EventKey<T extends EventMap> = string & keyof T;
export type Handler<EventType> = (event: EventType) => void;

/**
* Interface that represents a handle to a subscription that can be closed
*/
export interface Subscription {
close(): void;
}

/**
* Excalibur's typed event emitter, this allows events to be sent with any string to Type mapping
*/
export class EventEmitter<T extends EventMap> {
private _paused = false;
private _listeners: Record<string, Handler<any>[]> = {};
private _listenersOnce: Record<string, Handler<any>[]> = {};
private _pipes: EventEmitter<any>[] = [];

on<K extends EventKey<T>>(eventName: K, fn: Handler<T[K]>): Subscription;
on(eventName: string, fn: Handler<unknown>): Subscription;
on<K extends EventKey<T> | string>(eventName: K, fn: Handler<T[K]>): Subscription {
this._listeners[eventName] = this._listeners[eventName] ?? [];
this._listeners[eventName].push(fn);
return {
close: () => this.off(eventName, fn)
};
}

once<K extends EventKey<T>>(eventName: K, fn: Handler<T[K]>): Subscription;
once(eventName: string, fn: Handler<unknown>): Subscription;
once<K extends EventKey<T> | string>(eventName: K, fn: Handler<T[K]>): Subscription {
this._listenersOnce[eventName] = this._listenersOnce[eventName] ?? [];
this._listenersOnce[eventName].push(fn);
return {
close: () => this.off(eventName, fn)
};
}

off<K extends EventKey<T>>(eventName: K, fn: Handler<T[K]>): void;
off(eventName: string, fn: Handler<unknown>): void;
off(eventName: string): void;
off<K extends EventKey<T> | string>(eventName: K, fn?: Handler<T[K]>): void {
if (fn) {
const listenerIndex = this._listeners[eventName]?.indexOf(fn);
if (listenerIndex > -1) {
this._listeners[eventName]?.splice(listenerIndex, 1);
}
const onceIndex = this._listenersOnce[eventName]?.indexOf(fn);
if (onceIndex > -1) {
this._listenersOnce[eventName]?.splice(onceIndex, 1);
}
} else {
delete this._listeners[eventName];
}
}

emit<K extends EventKey<T>>(eventName: K, event: T[K]): void;
emit(eventName: string, event?: any): void;
emit<K extends EventKey<T> | string>(eventName: K, event?: T[K]): void {
if (this._paused) {
return;
}
this._listeners[eventName]?.forEach((fn) => fn(event));
const onces = this._listenersOnce[eventName];
this._listenersOnce[eventName] = [];
if (onces) {
onces.forEach((fn) => fn(event));
}
this._pipes.forEach((pipe) => {
pipe.emit(eventName, event);
});
}

pipe(emitter: EventEmitter<any>): Subscription {
if (this === emitter) {
throw Error('Cannot pipe to self');
}
this._pipes.push(emitter);
return {
close: () => {
let i = -1;
if ((i = this._pipes.indexOf(emitter)) > -1) {
this._pipes.splice(i, 1);
}
Comment on lines +83 to +86
Copy link
Contributor

@tonivj5 tonivj5 Jul 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @eonarheim! I saw this code some days ago... And I was a bit curious of why this way 😅

could it be rewritten in this way or there's something I don't see?

const i = this._pipes.indexOf(emitter);
if (i > -1) {
   this._pipes.splice(i, 1);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonivj5 good catch, I like yours better! (I'd support a PR 😄)

I'm not sure why I did that, normally I'd do what you suggested (I originally wrote this code a while ago and was holding on to the snippet)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we go #2707 😄

}
};
}

unpipe(emitter: EventEmitter<any>): void {
let i = -1;
if ((i = this._pipes.indexOf(emitter)) > -1) {
this._pipes.splice(i, 1);
}
}

pause(): void {
this._paused = true;
}

unpause(): void {
this._paused = false;
}
}
Loading