4kbar Posted February 4, 2016 Share Posted February 4, 2016 First of all, let me say that I love the apparent philosophy of PixiJS: do rendering well, and let userland be userland. Awesome. I'm playing with different ways of handling interactions that originate within the scene graph. I'm not a fan of declaring interaction logic on sprites themselves (e.g., using the '.on' method and callbacks). Instead, I'd prefer something that functions more like event delegation: a single listener on my scene that pipes out all relevant events for me to slice and dice later. Currently, I'm experimenting with using the Sprite prototype to feed all events into an RxJS observable. More concretely: // Create a new observable var pixiEventStream = new Rx.Subject(); // Hack into Sprite's prototype, redirecting all 'mousedown' and 'mouseup' events into the observable var toStream = (e) => pixiEventStream.onNext(e); PIXI.Sprite.prototype = Object.assign(PIXI.Sprite.prototype, { mousedown: toStream, mouseup: toStream, // etc for all events PIXI detects }); // ... // Later, I create a bunch of sprites, some with {interactive: true} // But I *don't* specify any .on callbacks // ... // Later, I can deal with the events // Here, I'm filtering out only the mousedown events for further processing var mouseDownEvents = pixiEventStream.filter(e => e.type === "mousedown"); mouseDownEvents.subscribe((e) => { var sprite = e.target; // do something with a sprite on mousedown }); Is there a less hacky way of doing this? Has anyone tried something similar? Cheers! ivan.popelyshev 1 Quote Link to comment Share on other sites More sharing options...
ivan.popelyshev Posted February 4, 2016 Share Posted February 4, 2016 That's good idea. I use similar approach in some projects. We can add it in v4 Quote Link to comment Share on other sites More sharing options...
4kbar Posted February 4, 2016 Author Share Posted February 4, 2016 Cool! Just wanted a sanity check. This method seems to be working great. I'll report back if I get around to doing a more proper integration with the InteractionManager. P.S.- If you're building an app with lots of interactions, I highly suggest watching this egghead.io series on Cycle.js. I don't use this framework, but these videos helped change how I think about building applications. Very cool patterns. Quote Link to comment Share on other sites More sharing options...
PixelPicoSean Posted February 5, 2016 Share Posted February 5, 2016 That can be done by calling the "fromEvent" method to create streams from any EventEmitter sub-classes. Not only Rx.js but Bacon.js and Kefir.js have similar method. And you will never want to merge all those input events into a single stream (but it can still be done by calling "merge"). BTW. glad to see more people start to try Reactive Programming in games 4kbar and mattstyles 2 Quote Link to comment Share on other sites More sharing options...
4kbar Posted February 5, 2016 Author Share Posted February 5, 2016 Thanks for the pointer on fromEvent. That seems a lot more idiomatic. 2 hours ago, PixelPicoSean said: And you will never want to merge all those input events into a single stream (but it can still be done by calling "merge"). Can you elaborate? Personally, I find myself attracted to the idea single stream that I can fork later on. Is there a downside to starting with a single stream, and mapping out from there? 2 hours ago, PixelPicoSean said: BTW. glad to see more people start to try Reactive Programming in games I've really enjoyed the learning process. It seems that game development is often a very imperative and object oriented world (and perhaps for good reasons in some cases!). But it has been interesting to explore the Reactive style. It makes a lot more sense to my brain sometimes.... Quote Link to comment Share on other sites More sharing options...
PixelPicoSean Posted February 6, 2016 Share Posted February 6, 2016 By merging all the input events into a single stream, you are forced to use filters and mappings to create a stream from any specific target, which may cause performance issues when lots of objects are interactive. Maybe you just don't care about the continuance of events, then RP will be something overkill, create a event handler with callbacks instead and it will also work. IMO the best benefit of using RP is that we can combine streams to get some meaningful "declarative" ones. Let me place some examples (using Kefir, but there will not be much difference between Kefir and other FRP frameworks) Classic Drag'n Drop // Any interactive objects let draggable = new PIXI.Sprite(); draggable.interactive = true; // Declare input events as "named" streams const inputdown$ = Kefir.merge([ Kefir.fromEvents(draggable, 'mousedown'), Kefir.fromEvents(draggable, 'touchstart'), ]); const inputmove$ = Kefir.merge([ Kefir.fromEvents(draggable, 'mousemove'), Kefir.fromEvents(draggable, 'touchmove'), ]); const inputup$ = Kefir.merge([ Kefir.fromEvents(draggable, 'mouseup'), Kefir.fromEvents(draggable, 'touchend'), ]); // Returns the position diff between 2 input events function posDiff(prevEvt, nextEvt) { return { x: nextEvt.global.x - prevEvt.global.x, y: nextEvt.global.y - prevEvt.global.y, }; } // Let's create the drag stream const when2Drag$ = inputdown$.flatMap((downEvent) => { return inputmove$.takeUntilBy(inputup$) .diff(posDiff, downEvent); }); // I call this a "Reactive Property" const position$ = when2Drag$ // Calculate new position after the movement .scan((currPos, move) => { x: currPos.x + move.x, y: currPos.y + move.y }, { x: draggable.x, y: draggable.y }); // Now let's apply the "reactive position property" to our draggable object position$.onValue(pos => { draggable.position.set(pos.x, pos.y); }); // Logic clear, bugfree and you can reuse all these streams later Advance timers // Repeat 1s timer for 10 times const countTimer$ = Kefir.interval(1000, 0).take(10); // Create a "reactive property" that shows countdown numbers const countNum$ = countTimer$.scan(currNum => currNum - 1, 10); // Now let's show the countdown number let numberText = new PIXI.Text('10'); countNum$.onValue(num => numberText.text = num.toString()); UI // RP is awesome when you use it for the UIs let button = new PIXI.Sprite(); button.interactive = true; const press$ = Kefir.fromEvents(button, 'mousedown'); const release$ = Kefir.fromEvents(button, 'mouseup'); const mouseout$ = Kefir.fromEvents(button, 'mouseleave'); // Change button states, not fancy though press$.onValue(() => { // Change button texture to pressed }); Kefir.merge([release$, mouseout$]).onValue(() => { // Change button texture to released }); // Only paid players can upload their score (WTF!) const paid$ = Kefir.fromCallback(callback => { someAsyncAPI(player_uid, (paid) => { callback(paid); }); }) // Before validation, all people are equal .toProperty(() => false); const try2Conn$ = press$.filterBy(paid$); // You don't want the upload to server button makes a request each time the button is down, right? // Let's add a throttle, so that no matter how many times the player has pressed, it only request // once per 10 seconds O_o let when2Connect$ = try2Conn$.throttle(10000); when2Connect$.onValue(() => /* lets connect now */); 4kbar 1 Quote Link to comment Share on other sites More sharing options...
4kbar Posted February 6, 2016 Author Share Posted February 6, 2016 Awesome; thanks for all the examples! I may check out Kefir as well. Quote Link to comment Share on other sites More sharing options...
4kbar Posted March 24, 2016 Author Share Posted March 24, 2016 Hi @PixelPicoSean! Quick question for you: Has Pixi's mutation of interaction events ever given you trouble? Last night, I was trying to track the distance that a user's cursor moved over a full-screen view after the mouse button had been pressed and held. Here's some code to explain: var dragSurface = new PIXI.Container(); dragSurface.interactive = true; dragSurface.hitArea = new PIXI.Rectangle(0, 0, 999999, 999999); const inputdown$ = Kefir.fromEvents(dragSurface, 'mousedown'); const inputup$ = Kefir.fromEvents(dragSurface, 'mouseup'); const inputmove$ = Kefir.fromEvents(dragSurface, 'mousemove'); // This *doesn't* work as expected, // because downEvent is mutated by future events. const drag$ = inputdown$.flatMap((downEvent) => { return inputmove$.takeUntilBy(inputup$).map((moveEvent) => { x: moveEvent.data.global.x - downEvent.data.global.x, y: moveEvent.data.global.y - downEvent.data.global.y }); }); // This *does* work as expected, // because I'm retaining reference to downEvent's original values. const drag$ = inputdown$.flatMap((downEvent) => { var ox = downEvent.data.global.x; var oy = downEvent.data.global.y; return inputmove$.takeUntilBy(inputup$).map((moveEvent) => { x: moveEvent.data.global.x - ox, y: moveEvent.data.global.y - oy }); }); This feels ... wrong ... to me. I think this mutation would also affect your examples above. Any thoughts? Quote Link to comment Share on other sites More sharing options...
PixelPicoSean Posted March 24, 2016 Share Posted March 24, 2016 Hi @4kbar, I just notice that the example is not correct since PIXI only has one input event and reuse it each time. And your code should work since the coordinate of downEvent is cached and assigned to a new object. One more thing, you can replace the dragSurface.hitArea = new PIXI.Rectangle(0, 0, 99999, 99999); with dragSurface.containsPoint = () => true; Quote Link to comment Share on other sites More sharing options...
4kbar Posted March 24, 2016 Author Share Posted March 24, 2016 Thanks; just checking! The second example does work, it just feels odd to have to cache. (And nice hack on the containsPoint!) Quote Link to comment Share on other sites More sharing options...
dmko Posted June 12, 2017 Share Posted June 12, 2017 Oh wow... I'm just coming off of watching the Dr Boolean and funfunfunction stuff... and scratching my head on how to apply this to PIXI The examples here are great! Any more like this? Next up I gotta learn about Observables/streams.... seems "most" is a popular framework for that? ivan.popelyshev 1 Quote Link to comment Share on other sites More sharing options...
ivan.popelyshev Posted June 12, 2017 Share Posted June 12, 2017 dragSurface.hitArea = new PIXI.Rectangle(0, 0, 99999, 99999); //that's better dragSurface.hitArea = new PIXI.Rectangle(-100000, -100000, 200000, 200000); Quote Link to comment Share on other sites More sharing options...
dmko Posted June 19, 2017 Share Posted June 19, 2017 On 2/4/2016 at 3:43 PM, ivan.popelyshev said: That's good idea. I use similar approach in some projects. We can add it in v4 @ivan.popelyshev - is this something that's been added? i.e. to automatically wire certain PIXI primitives (e.g. Sprite) to call a top-level function (such as is done at the top of this thread, by changing the prototype? Quote Link to comment Share on other sites More sharing options...
ivan.popelyshev Posted June 19, 2017 Share Posted June 19, 2017 No, and I dont think it will be, because that means dependency on some lib. v5 is already depending on Promises in some places (loaders), but for everything else its using mini-signals and runner (pixi-specific thing), and there's no chaining in them. Quote Link to comment Share on other sites More sharing options...
dmko Posted June 19, 2017 Share Posted June 19, 2017 v5?! woo! OK... so if making these changes it needs to be done manually via changing the prototype somehow? Quote Link to comment Share on other sites More sharing options...
ivan.popelyshev Posted June 19, 2017 Share Posted June 19, 2017 In v4 all events are managed through EventEmitter, and in v5 there'll be signals. You dont have to override prototype. dmko 1 Quote Link to comment Share on other sites More sharing options...
dmko Posted June 19, 2017 Share Posted June 19, 2017 oh, it seems touch events can be intercepted globally - https://github.com/pixijs/pixi.js/pull/2658 ? Quote Link to comment Share on other sites More sharing options...
dmko Posted June 19, 2017 Share Posted June 19, 2017 and that looking at my old code I already knew that? Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.