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
API to make registering events in scriptType easier #4910
Comments
If event methods such as on/once would return event handler instead of |
So it would be more like?
|
Maybe, something like this: this.handlers.mouseDown = this.app.mouse.on('mousedown', this.onMouseDown, this); And execute the next code on the side of the engine while destroying a component instance: for (const handlerName of Object.keys(this.handlers)) {
const handler = this.handlers[handlerName];
if (typeof handler.off === "function") {
handler.off();
}
} So, a user don't destroy anything manually and just do the first line (this.handlers.mouseDown = this.app.mouse.on). |
To be even more pedantic, a "good" script should look at least somewhat like this: class DebugGamepadFlyCamera extends pc.ScriptType {
// ...
initialize() {
this.hookEvents();
this.on('destroy', this.onDestroy , this);
this.on("enable" , this.hookEvents , this);
this.on("disable", this.unhookEvents, this);
}
onDestroy() {
this.unhookEvents();
// whatever else needs to be done...
this.app.timeScale = this._defaultTimeScale;
}
hookEvents() {
console.log("hookEvents");
this.app.mouse.on('mousemove', this._onMouseMove, this);
this.app.on('framerender', this._update, this);
}
unhookEvents() {
console.log("unhookEvents");
this.app.off('framerender', this._update, this);
this.app.mouse.off('mousemove', this._onMouseMove, this);
}
// ...
} A script has no business reacting to mouse/keyboard/touch as long it is disabled. The shipped OrbitCamera also reacts to input when it's disabled, so I guess it's fair to say that everyone is forgetting these things 😅 engine/scripts/camera/orbit-camera.js Lines 382 to 398 in 4c16f9a
But yes, I think you are on the right track with your suggestion (just that it should also automatically handle enabling/disabling) |
I guess the problem here is flexibility as there will be cases that even disabled, some users will want to listen to events 🤔 The way I've been doing it more recently so far is: // initialize code called once per entity
UiManager.prototype.initialize = function () {
this.setEvents('on');
this.on('destroy', function () {
this.setEvents('off');
});
};
UiManager.prototype.setEvents = function (offOn) {
this.app.graphicsDevice[offOn]('resizecanvas', this.onResize, this);
}; And |
It looks like emitting this.on('destroy', function handleDestroy() {
this.setEvents('off');
}, this); we have handleDestroy connected to the "destroy" event (until garbage collection). It is required to use once or turn it off manually. Or I miss something. this.once('destroy', function handleDestroy() {
this.setEvents('off');
}, this)
// OR
this.on('destroy', function handleDestroy() {
this.setEvents('off');
this.off('destroy', handleDestroy, this);
}, this); My point is that it's easy to forget to turn some handlers off. Maybe add removing handlers into EventEmmiter? Since we move callback ownership to EventEmmiter when we call on/once class EventHandler {
// . . .
constructor() {
// . . .
this.handlers = { };
this.once("destroy", handleDestroy() {
this._callbacks = {}
this._callbackActive = {}
for (const handlerName of Object.keys(this.handlers)) {
const handler = this.handlers[handlerName];
if (typeof handler.off === "function") {
handler.off();
}
}
}, this);
}
// . . .
} So, a user just need to assign a new handler to this.handlers.mouseDown = this.app.mouse.on('mousedown', this.onMouseDown, this);
// OR
this.addHandler(this.app.mouse.on('mousedown', this.onMouseDown, this)); |
As a note, I would vote for disabled scripts to not be listening for events. If there is an action that needs to be called on event, that action should be on an active script. A disabled script should have no business with the running app. Use cases, when a developer enables a disabled entity (via its script listening to it) is a bad pattern. It often introduces hard to debug / unexpected bugs, rather than gives any particular benefit. Just like disabled models should not affect the rendering pipeline, a disabled script should not affect the application. |
@LeXXik I guess you mean s/allow/forbid? Otherwise the rest doesn't make sense to me. We are discussing overcomplicating the "easy" interface for the sake of a few users who may want to listen to events while disabled. IMO until some user comes up with a convincing/compelling use-case, there shouldn't be a method like And if some use-case is really so compelling, the user can still just use this.on('destroy', () => this.setEvents('off'));` |
You don't need to remove the destroy listener as the object we are listening to (itself) is being destroyed so no new events will be emitted from it and there's no external reference to the function or scriptType to be kept in memory
This is interesting 🤔 . Wondering if this should be on ScriptType or be wrapped in an API |
@kungfooman right, English is hard 😅 |
Yep, those handlers can be stored in an array. To take dynamic subscription into an account, special event handler manager class can be created, which basically stores a list of handlers, and when they are off'ed, will forget about them. And when needed one call to this.events = pc.EventHandlers(this);
this.events.add(entity.on('state', this.onEntityState, this)); When this entity is destroyed, handler will call off to all its events if any left. |
Ah, I see. Sounds similar to @querielo suggestion |
I have seen this style in Angular, paraphrased: class EventSink {
events = [];
set sink(value) {
this.events.push(value);
}
}
const eventSink = new EventSink(); And then it could be used like: self.sink = pc.app.mouse.on("mousemove", (e) => console.log('mousemove', e.x, e.y));
self.sink = pc.app.mouse.on("mousedown", (e) => console.log('mousedown', e.x, e.y)); This prevents
@Maksims You can't forget about the events, since you need to reenable every event between enable/disable toggling. |
I tried to take in all ideas, but I cannot find a solution that looks nicer than @yaustar's initial ScriptTypeListen.js import * as pc from 'playcanvas';
export class ScriptTypeListen extends pc.ScriptType {
_sunk = [];
constructor(args) {
super(args);
this.on("enable" , () => this._listenSetEvents('on' ), this);
this.on("disable", () => this._listenSetEvents('off'), this);
this.on('destroy', () => this._listenSetEvents('off'), this);
}
/**
* @param {pc.EventHandler } eventHandler - Whatever extends pc.EventHandler,
* like `app` or `app.mouse`.
* @param {string } name - Name of the event, like `mousemove`.
* @param {pc.callbacks.HandleEvent} callback - The callback.
* @param {any } scope - The scope.
*/
listen(eventHandler, name, callback, scope = this) {
this._sunk.push({eventHandler, name, callback, scope});
//console.log("sink", eventHandler, name, callback, scope);
eventHandler.on(name, callback, scope);
}
/**
* @param {'on'|'off'} onOrOff - On or off.
*/
_listenSetEvents(onOrOff) {
//console.log('setEvents', onOrOff);
for (const {eventHandler, name, callback, scope} of this._sunk) {
eventHandler[onOrOff](name, callback, scope);
}
}
} Usage is just like described in the first post. The only issue I ran into is that PlayCanvas doesn't listen on all "interesting" events that developers use/need, so I think Or you end up with code like this: this.listen(this.app.mouse, pc.EVENT_MOUSEDOWN , this.onMouseDown );
this.listen(this.app.mouse, pc.EVENT_MOUSEUP , this.onMouseUp );
this.listen(this.app.mouse, pc.EVENT_MOUSEMOVE , this.onMouseMove );
this.listen(this.app.mouse, pc.EVENT_MOUSEWHEEL, this.onMouseWheel);
// Listen to when the mouse travels out of the window
window.addEventListener('mouseout', onMouseOut, false);
// Remove the listeners so if this entity is destroyed
this.on('destroy', function() {
window.removeEventListener('mouseout', onMouseOut, false);
}); And then of course... |
That's a thought 🤔 Maybe it should fire an event for that, would make it nicer |
Been thinking on this and actually, if someone does want a disabled script to be listening for events, they can manage it themselves and not use this API |
Been thinking more on this and I'm personally leaning towards a Would love to get feedback from other PlayCanvas team members on this when they are free :) |
Off'ed event, destroys its handle, so it should be removed from the list. Every subscription - creates new handle. |
+1 to support in a base class for this. I've ended up doing the same in my project. Paraphrased below for brevity as the actual base class(es!) in the project have a bunch of other stuff in there. My problem with this though, is that even in this simple example, I've had to make a few design decisions that happen to work for my project. For example, I assume that the reference to a target must exist, so calls to All of this to say, adding this functionality to the Script Type directly feels wrong to me. Lots of game engines have experienced base class bloat and paid the price, and while this seems super convenient to the out of box experience, it feels like keeping Possible alternatives:
|
Not sure how bad the the bloat here would be here. I was intending to only listen to the enable/disable/etc events if Good shout on swap though, not thought about that 🤔 |
@csubagio Don't get too lost in thinking about SLA or whatever is the current buzzword for "better", it can achieve the opposite aswell. Once you start turning every loop body into a method, you simply get lost in otherwise useless extra methods e.g. or you introduce for example more functional programming and then many developers are also not comfortable with that. I extended Things being lame is often a good thing, because you can deal with it without overstressing systems (be it your own brain or of colleagues). Usually every attempt of doing something "smart" has a trail of issues and bugs all over GitHub (e.g. transpiling mixins into ES5 etc.) It probably also adds more strain on the decision making process for every dev at the start of writing every new method: "Should I use pc.ScriptType? Wasn't there something about ScriptTypeListen#listen? Or ScriptTypeAutoEvent#listen? Or ScriptTypeExtended#listen? Huh getting lost"
|
We've had an internal meeting about this and reached the following conclusion. We will create separate PRs for the following:
|
A common issue with developers new to PlayCanvas is ensuring that event listeners to objects like the mouse are cleaned up properly when the scriptType is destroyed (eg destroying the Entity or changing scenes)
Users either don't know and/or assume it's done automatically. And to do it correctly requires some boilerplate. eg:
Could we introduce some API to make this easier/automatic? Such as a function that automatically removes the listener when the scriptType is destroyed? It would turn the above code into:
And would also work with anonymous functions too
The text was updated successfully, but these errors were encountered: