4kbar

Managing interactions with streams?

Recommended Posts

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!

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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 :D

Share this post


Link to post
Share on other sites

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 :D

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....

Share this post


Link to post
Share on other sites

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 */);

 

Share this post


Link to post
Share on other sites

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?

Share this post


Link to post
Share on other sites

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;

 

Share this post


Link to post
Share on other sites

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?

Share this post


Link to post
Share on other sites
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?

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now


  • Recently Browsing   0 members

    No registered users viewing this page.