westy

Advanced interaction event handling

Recommended Posts

Let's say I click on a sprite, which might be nested in other containers and so on, each with their own onclick event listeners. Each of those objects will fire an onclick event, with the most nested object firing first.

What's the best way to go about passing information about the inner nested click events to the outer objects? To put it more specifically, how can I let my outer objects know if their click event originated as a direct click to them, or as a click to another object that bubbled through?

I attempted to attach a property to the nested onclick event like so:

var button = new PIXI.Sprite();
button.interactive = true;
button.on("mousedown", function(e) {
	e.originatedFromButton = true;
});

This works to an extent (the outermost handler can read for the property) however it appears that the interaction manager uses a common object for event data, which means that this property hangs around on all future events. I could delete the property at the top level event handler but that is a very inelegant solution which I'd like to avoid if possible.

Anyone have any suggestions as to a better way to accomplish what I need?

Share this post


Link to post
Share on other sites

Does pixi implement the bubbling/capture phase of DOM interaction events?

However, a deeper answer is that you never have a hierarchy that requires this knowledge internally, its a big architectural smell. The solution is to decouple your actions from the events the trigger them, then the problem becomes easier, and when you're talking about a chaotic system like user interaction simplicity is your best friend.

Share this post


Link to post
Share on other sites

@mattstyles thanks for your reply!

I think PIXI implements some level of event bubbling/capturing, at least through my experiment.

Could you give an example of the decoupling to which you are referring?  The example I provided is a simple one would never be done like that in a real use case. I'm simply interested in knowing a not terrible way to accomplish what I need to accomplish.

To give some context, the reason I want to do this is to have some UI that will go away when the mouse is clicked anywhere except on the UI itself.

Share this post


Link to post
Share on other sites

A simple way to achieve this is using the pub/sub pattern, sometimes called observer (or other stuff, its a well used pattern) using event emitters or dispatchers or signals or something.

A nice clean architecture is to have triggers and actions. Triggers simply explain that something has happened in the world, this could be a simple event or it could contain some data with it depending on how complicated you want to get. Actions perform your mutations, so they listen for triggers and perform the mutations to the game state required progress the game to the next state i.e. in our case, they are listening for a user interaction and are responsible making stuff happen. There is a clear delineation here between consumers and producers, and it is this split which forms the tenet for all of your architecture. 

In MVVM this pattern is trivial, views are dumb and reactive and the models contain all the logic. MVC complicates this a little by adding a controller, if you adopt MVC you have more options, generally the view is still pretty dumb but the model might also be dumb, the controller handles changes but also handles changing the model data into a view (the complexity of MVC is treated by many as a red herring, despite its popularity).

So how does all this application architecture refer to your game? Well, they're all architectural patterns directed around user interaction and games tend to have a lot of that.


// Pub/sub
var events = new EventEmitter()

// Model
class Entity {
  constructor () {
    this.count = 0
    addEventListener('click', this.onClick.bind(this)
  }

  onClick (event) {
    events.emit('CLICK_EVENT', this)
  }
}

// Active game models
var entities = []

for (let i = 0; i < 10; i++) {
  entities.push(new Entity())
}

// Add our actions
events.on('CLICK_EVENT', entity => {
  entities.forEach(e => e.count++)
})

In this case we have set up a simple architecture, its not quite MVC, you can see the model clearly, I've omitted any view code (which is probably mangled on to that poor old entity object too) but there is no controller, the logic does not live in the model or a viewModel so its not MVVM, its closest to a Model-View-Update architecture.

So, the crux of the problem here is that a click on any entity affects every other entity. We could have done this explicitly within any entity, particularly as our entity array is global, but, then, conceptually we have linked together each of our entities. This is bad. This is tight coupling and can be a killer as you codebase grows.

Instead of linking objects together we have created an abstraction whereby the models become producers and it is the job of the update function to change the state of the world from one state to another, in this case it manually manipulates each entities count variable, it could easily have called entity.increment if you felt that logic only pertained to an entity and would best be modelled there (which it does, the key here is that any entity methods so pertain only to themselves, this keeps them decoupled).

The idea is that we are going pretty much top-down with this. In our scene graph, or architectural model graph, the entities are child nodes, they're out there in the world presenting themselves and dealing with users, they produce events that go back in at a top level, the update function operates at a high level and makes changes to the world. Because of this it is easier to reason about data flow through your application as well, making things more predictable, hiding unexpected consequences and managing side effects.

This top-down data flow is wonderful for UI code, UI events are, by nature, chaotic, and this is a simple way to help mitigate that chaos. You can get more complex than this, adding tokens (i.e. one action only happens after another action, or used for timed events), multiple event emitters, privileged updaters, adding event hierarchies, passing data with events, using streams etc etc, but, this simple pub/sub is often more than adequate for the job and with few moving parts it tends to be fast and reliable.

In your example, I'd suggest that your close button, a definite child node, emits a close event, the updater (could be a controller in MVC) then closes the modal/box/menu. Another advantage here is that the logic happens in one place, want to make the 'escape' key close the box, no problem, let it fire the same event and you get the same result. Wunderbar!

Share this post


Link to post
Share on other sites

@mattstyles Thank you for your post! Although your thoughtful statements on architecture have given me a lot to think about, I'm still looking for an answer to my question. You seem to assume that I have some sort of 'close button', which would certainly be a technically trivial problem (although, as you note, setting up a good architecure isn't trivial).

I'm simply looking for a way to have a panel or something disappear when the user clicks anywhere other than the panel. The architecture is one matter however I already have certain systems in place for that aspect.

Share this post


Link to post
Share on other sites
50 minutes ago, themoonrat said:

PIXI v4 has the ability to listen to interaction events globally as well as through display objects.

For example:


renderer.plugins.interaction.on( 'mousedown', function() { console.log('mousedown') } );

 

All right, but how does that help? It's easy enough to listen for a click event anywhere on the stage (just make the stage interactive), what I need to do is essentially filter out the events that passed through certain specific display objects.

Share this post


Link to post
Share on other sites

Just add a container the size of the screen underneath the modal and listen for clicks on that, same as if you were doing it in DOM. Any click on the screen-sized element closes the popup, same as a click on some sort of close control (close button, done button, escape key etc) on the modal.

Share this post


Link to post
Share on other sites
2 hours ago, mattstyles said:

Just add a container the size of the screen underneath the modal and listen for clicks on that, same as if you were doing it in DOM. Any click on the screen-sized element closes the popup, same as a click on some sort of close control (close button, done button, escape key etc) on the modal.

This is what I've been doing, however event bubbling means that the modal will close when the modal itself is clicked, which is something I don't want.

Share this post


Link to post
Share on other sites

I'm not super clear on how z-indexing works in Pixi but as a hack you could add a click handler to the modal that consumes the event, that way it won't hit the backdrop behind the modal and any controls on the modal should consume their events before it hits the modal, bit of a hack but a trusted old-school web method. I'm making a bit of an assumption here that event handling works similarly in Pixi to the DOM, please disregard if my assumptions are wrong.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.