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

Improving custom event DX #3082

Open
Autsider666 opened this issue May 30, 2024 · 2 comments
Open

Improving custom event DX #3082

Autsider666 opened this issue May 30, 2024 · 2 comments
Labels
stale This issue or PR has not had any activity recently

Comments

@Autsider666
Copy link
Contributor

Autsider666 commented May 30, 2024

Context

Excalibur has a solid typesafe/autocompleted Event/Listener setup, but it's really hard to add your own custom events without creating wrappers or fooling TS.

Proposal

I propose using generic types on EX classes like Actor, Entity and Engine to handle event types, making it a lot easier to have autocompletion and type safety with custom events as well.

The following code example shows my suggested code changes to handle this with minimal code changes and max backwards compatibility:

import {
    Actor as ExActor,
    EventEmitter,
    EventKey,
    InitializeEvent,
    Engine, Handler,
    type EntityEvents,
    CollisionEndEvent, EventMap,
} from "excalibur";
// Not importable through "excalibur", while EntityEvents is, so could possibly be a bug.
// import type {ActorEvents} from "excalibur/build/dist/Actor";

// Imported ActorEvents act wonky in Jetbrains IDEs, probably because of weird input
type ActorEvents = {
    initialize: InitializeEvent,
    collisionend: CollisionEndEvent,
};

// I had a hard time figuring out the correct names for everything, so suggestions are welcome for this type and its variables.
type AppendedEventMap<TOriginal extends EventMap, TExtra extends EventMap | undefined> = TExtra extends EventMap ? TOriginal & TExtra : TOriginal;

// You can also add this to Engine and other classes using EventEmitter
class Entity<
    /** TKnownComponents extends Component = unknown, **/
    TCustomEvents extends EventMap | undefined = undefined,
    // TEvents isn't the nicest way to solve this, but using `AppendedEventMap<EntityEvents, TCustomEvents>`
    // everywhere makes the rest of the code harder to read.
    TEvents extends EventMap = AppendedEventMap<EntityEvents, TCustomEvents>
> /** implements OnInitialize, OnPreUpdate, OnPostUpdate **/ {
    public events = new EventEmitter<TEvents>();

    // No longer 3 different ways to handle events to confuse IDEs
    public on<TEventName extends EventKey<TEvents>>(eventName: TEventName, handler: Handler<TEvents[TEventName]>) {
        this.events.on(eventName, handler);
    }

    public off<TEventName extends EventKey<TEvents>>(eventName: TEventName, handler: Handler<TEvents[TEventName]>) {
        this.events.off(eventName, handler);
    }

    public emit<TEventName extends EventKey<TEvents>>(eventName: TEventName, event: TEvents[TEventName]): void {
        this.events.emit(eventName, event);
    }
}

class Actor<
    TCustomEvents extends EventMap | undefined = undefined,
    TEvents extends EventMap = AppendedEventMap<ActorEvents, TCustomEvents>
> extends Entity<TEvents> /** implements Eventable, PointerEvents, CanInitialize, CanUpdate, CanBeKilled **/ {
    // No need to override this.events anymore
}

// Custom event type
interface DamageTakenEvent {
    amount: number
}

type CustomEvents = {
    "damage-taken": DamageTakenEvent,
    "random-number": number,
}

class Player extends Actor<CustomEvents> {}

// `new Actor<CustomEvents>()` is also valid
const actorWithCustomEvents = new Player();

// Valid custom events
actorWithCustomEvents.on('damage-taken', (event) => console.log('Damage taken!', event));
actorWithCustomEvents.emit('damage-taken', {amount: 1});
actorWithCustomEvents.emit('random-number', Math.random());

// @ts-expect-error Example of invalid code because of missing data
actorWithCustomEvents.on('damage-taken');
// @ts-expect-error Example of invalid code because of invalid data
actorWithCustomEvents.on('damage-taken', 1);


// Valid system event
actorWithCustomEvents.on('initialize', (event: InitializeEvent) => console.log('initialize:', event));
actorWithCustomEvents.emit('initialize', new InitializeEvent(new Engine(), new ExActor()));

// @ts-expect-error Example of invalid System Event code
actorWithCustomEvents.on('collisionend', (event: InitializeEvent) => console.log('initialize:', event));

// This change will also make it easy to hook into the already existing system events because of GameEvent.
// (This is the only not 100% valid code example, because of the Actor/Entity class created for this example)
actorWithCustomEvents.on('collisionend', (event: CollisionEndEvent<Player>) => {
    event.other.emit('damage-taken', (event: DamageTakenEvent) => {
        console.log(event);
    });
});
@eonarheim
Copy link
Member

Hi @Autsider666 Awesome! Totally agree its difficult and clunky to extend types for the events.

I'll noodle but this seems very reasonable.

There was some good conversation on the discord around this, let's keep chatting https://discord.com/channels/1195771303215513671/1240674832421752842/1240674832421752842

Copy link

github-actions bot commented Aug 1, 2024

This issue hasn't had any recent activity lately and is being marked as stale automatically.

@github-actions github-actions bot added the stale This issue or PR has not had any activity recently label Aug 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale This issue or PR has not had any activity recently
Projects
None yet
Development

No branches or pull requests

2 participants