-
Notifications
You must be signed in to change notification settings - Fork 30
Description
In discussions about integrating with Patch (#195), it seems to be clear that we can improve the design of Leopard to make it more extensible and valuable for users if we extract out what we currently call the "trigger" system into a separate, cleaner, more general-purpose control flow library/package/module/something so that users of the Leopard library can choose whether or not they want to use it. (Or, one might want to mimic Scratch's control flow without using everything else that Leopard provides.)
Additionally, as I've spent more time editing Leopard projects by hand, I have found myself annoyed with the triggers system. The way we currently set up a triggers array in the sprite constructor...
this.triggers = [
new Trigger(Trigger.GREEN_FLAG, this.whenGreenFlagClicked),
new Trigger(Trigger.KEY_PRESSED, { key: "space" }, this.whenSpaceKeyPressed), // I am trying to write this from memory and I can't even recall if this is actually the correct syntax. The fact that I can't remember it is an indicator of the seemingly-random specific-to-us API that I'd like to replace.
]...is very functional and reasonably concise (although we could definitely make it more concise). But it is hyper-specific to Leopard and is not really similar to the way that other JavaScript code generally handles events.
I think a more similar-to-js API would be to have some kind of "event listener" system analogous to the DOM element.addEventListener("click", handler) API. I see a few main distinctions that would set our API apart:
- Control flow based on generator functions. The most important feature would be to support generators so that we can support Scratch-style yield functionality. If we wanted to, we could choose to support regular and async (and async-generator) functions as well.
- Customizable interruption behavior. If an event fires again while a script is in progress, do we interrupt the current script? Run it twice simultaneously? Ignore the new event? This behavior should be customizable, either at the call site where the event is being fired or in the
addEventListenersetup. - Special handling of
this. The current triggers system is very careful to make sure that whatever method you pass is bound to the sprite so thatthis.whateverworks as expected from within the called method. The DOMaddEventListenerAPI does this differently, usingevent.targetinstead, but I definitely think I prefer thethisbinding for Leopard's purposes.
If we're extracting this event/control flow logic into its own module (or something like that), I think it makes sense to have a generic API that can be used outside of the context of OOP (where events are attached to a particular sprite instance and so on), but that can be easily plugged into the Sprite/Stage/Project class system as needed.
Here's a proposal for the generic API:
import { EventHandler } from "@leopard/control-flow"; // or whatever
const myHandler = new EventHandler(mySprite); // Optionally, when setting up an event handler, pass an object that will be bound as `this`. In Leopard use, this would often be a sprite, but you could attach anything you want.
myHandler.addEventListener("click", function() {
this.move(10); // this === mySprite because we chose that above
});
myHandler.dispatchEvent("click"); // There's nothing special or magical about the name "click". We could have used "asdf" here and above and gotten the same behavior. When we dispatch an event, it just looks for all the listeners that match.
myHandler.addEventListener("keyUp", function*(event) {
if (event.key === "a") {
for (let i = 0; i < 10; i++) {
yield* this.sayForSecs(i, 1);
}
}
});
myHandler.dispatchEvent("keyUp", { key: "a" }, { duplicate: "ignore" }); // If there is already a "keyUp" event running, ignore this newly-dispatched event.
// ^ This isn't quite good enough. In Scratch, key press events for the same key will be ignored but key press events for two different keys can run simultaneously. Is there a nice-but-still-generic way to represent that behavior? I don't have a good answer.
// Note that because the "keyUp" event listener is a generator function, it can't run independently on its own. There needs to be
// some kind of frame loop that is repeatedly asking it to step.
function loop() {
requestAnimationFrame(loop);
myHandler.step();
}
loop();
// ^ For use in a regular Leopard project, the Project object would be responsible for calling step on each sprite's event handlerThis generic API could then be integrated into a Leopard project like so:
class MySprite extends Sprite {
constructor() {
// The base `Sprite` class creates an internal `EventEmitter` object for each sprite instance and exposes `addEventListener()`
// It also fires events with well-known names (like "click") for you as you would expect.
// (This would also enable us to provide more events like "keydown", "keyup", and "keypress" instead of just one.)
this.addEventListener("greenFlag", this.whenGreenFlagClicked);
this.addEventListener("keyup", this.whenKeyUp);
this.sprites.moveButton.addEventListener("click", this.whenClicked); // Listen to other sprite's events. This would remove some of the need for broadcasts.
}
// Maybe you like doing async things. Have fun!
async whenGreenFlagClicked() {
const res = await fetch("https://example.com/api/get-a-number")
.then(res => res.json());
this.move(res.num);
}
*whenKeyUp(event) {
if (event.key === "space") {
yield* this.jump(); // or whatever you want to do
}
}
// Doesn't need to be a generator function if you don't want to be interruptible
whenClicked() {
this.x += 50;
}
}There are a couple of problems and open questions with this approach:
- The big one: I don't have a good way lf handling duplicate events that may or may not be considered duplicates depending on the event data being sent. I'm mostly thinking about
- I haven't worked out the details of what this would look like with clones. I'm hoping that because each
EventEmittercan be given a sprite instance to bind to, it won't be too bad. - There isn't a good way to listen to all of the events in a Leopard project. For example, it might be nice to be able to have one sprite listen to every single click in the entire project. Maybe there's some kind of event bubbling that makes sense, where events are fired on a particular sprite but bubble up to the
Projectand you can listen to events on the whole project if you want? - I don't have a clear picture of what broadcasts should look like. Do we encourage users to create their own custom event names? (So Leopard fires "click" and "greenFlag" and stuff like that for you, but you can also define "myCoolEvent" and fire it?) If so, how do you communicate across sprites? Maybe inside each receiving sprite you do something like
this.project.addEventListener("myCoolEvent", mySpriteMethod)and you send out a broadcast by doingthis.project.dispatchEvent("myCoolEvent"), but then you have to be careful to make sure that the listener method is somehow being bound to the correct sprite–because by default it would be bound to the project. Or, alternatively, do we have a dedicated "broadcast" method that just provides the broadcast name to the listener?