Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Emitting events from Redux reducers

I'm using redux as a state container in a simple shooter game. State is completely deterministic, the only input the system receives is user input (eg. a weapon was fired, etc). My problem is that I have to track (and process) certain events, that happen during the game (eg. something was destroyed, etc), and I'm not quite sure how to do that. My current solution is that the reducer maintains an events array in the current state, and every reducer just appends events to it.

FIRE_WEAPON+-+          FIRE_WEAPON+-+
             |                       |
             |                       |
           +-v--------+--------------v------------->
                      |
                      |
                      +->PLAYER_DESTROYED

Here the reduces receives two FIRE_WEAPON action, and should "emit" a PLAYER_DESTROYED event (right now, it is used to render an explosion there).

The project is open source, the reducer looks something like this (it's just pseudocode, but here is the relevant game logic):

// combine is just (f, g) => ((s, a) => g(f(s, a), a))
const reducer = combine(

  // simulate world
  (state, action) => {
    while (state.time < action.time) {
      state = evolve(state, delta); // evolve appends the happened in-game events to the state
    }
    return state;
  },

  // handle actual user input
  (state, action) => {
    return handleUserInput(state, action);
  }
);

const evolve = (state, delta) => {
  const events = [];

  // some game logic that does some `events.push(...)`

  return {
    ...state,
    time: state.time + delta,
    events: state.events.concat(events),
  };
}

We can assume, that handleUserInput is a simple x => x identity function (it doesn't touch the events array). During evolve, I'd like to "emit" events, but since that would make evolve impure, I cannot do that. As I said, right now I'm doing this by storing the happened events in the state, but there might be a better way. Any suggestions?

These events are used during rendering, which looks likes this:

let sprites = [];

// `subscribe`d to store
function onStateChange() {
  // `obsolete` removes old sprites that should not be displayed anymore
  // `toSprite` converts events to sprites, you can assume that they are just simple objects
  sprites = sprites.filter(obsolete).concat(store.getState().events.map(toSprite));
}

function render(state) {
  renderState(state);
  renderSprites(sprites);
}

But later on I'd like use events on the server (the reducer described above runs on the server too), for calculating various stats (eg. enemies destroyed, etc.).

Ps.: these "emitted" events have no influence on the state (they are totally unrelated), so I'm sure that they shouldn't be actions (because they would leave the state unchanged). They are processed after the reducer has finished, after that they can be dropped (the reducer always receives an empty events array).

like image 559
lennoff Avatar asked Dec 07 '15 16:12

lennoff


1 Answers

I'm pretty sure that you can divide it in three parts:

-Actions:

  const fireWeapon = ()=>({
   type: FIRE_WEAPON
   })

You can launch actions like fireWeapon okey, as you said reducers are pure functions so you can store in the state how much times you have launched that action.

-Reducer Fires

 initialState: { fireWeapon: 0, fireShotgun:0}

 CASE FIRE_WEAPON: 
   return {...state, fireWeapon: state.fireWeapon+1}

And finally, the key part, a libary called redux-observable, it's based on rxjs, reactive programming. You can suscribe to stream of actions and emit a new ones.

A really easy example is :

export const clearDeletedSiteEpic = (action$,state$) =>
  action$.pipe(
    ofType(FIRE_WEAPON),
    map(() => {
     if (state$.value.fires.fireWeapon % 2 === 0){
       playerDestroyed() // action is launched 
     }
   }
  );
like image 180
Samuel Martinez Toledano Avatar answered Sep 22 '22 14:09

Samuel Martinez Toledano