Tutorial

This guide shows how adding bgio-effects to an example boardgame.io game can help solve subtle issues that may occur when rendering your game state on the client. It assumes a basic understanding of the boardgame.io framework. Check out the tutorial in their docs to learn more.

Our starting point

We will start with a single-player game. Each turn, the player rolls a die. If they roll a 6, they get a point. Once they’ve scored 5 points, they win!

const game = {
  // Game state includes the current die value & the current score.
  setup: () => ({ roll: 1, score: 0 }),

  moves: {
    roll: (G, ctx) => {
      const roll = ctx.random.D6();
      G.roll = roll;
      if (roll === 6) G.score++;
    },
  },

  // End the game when the player has scored 5 points.
  endIf: (G) => G.score >= 5,
};

A UI for playing this thrilling game of skill could look something like this.

Rolls: 0

snippet-1.tsx

Here’s an example of how we might update the die each time we receive a state update from boardgame.io. This code is buggy. We’ll see why in the next section.

let previousRoll;

client.subscribe(({ G }) => {
  // If G.roll has changed, trigger the die’s roll animation.
  if (G.roll !== previousRoll) animateDie(G.roll);
  previousRoll = G.roll;

  // more logic to render score, etc.
});

(The client variable in this example is a plain JS boardgame.io client.)

const Board = ({ G }) => {
  // Each time G.roll changes, trigger the die’s roll animation.
  useEffect(() => {
    animateDie(G.roll);
  }, [G.roll]);

  return <div>{/* render die, score etc. */}</div>;
};

Spot the difference

boardgame.io tells us the current state of the game each time it changes. But it doesn’t tell us what changed. A lot of the time, that’s OK. For example, the score in our UI increments each time a 6 is rolled. It doesn’t matter what the previous score was, we just need to display the current one.

However, if you’ve tried rolling the die in our example above, you may have noticed a problem. Sometimes the roll animation isn’t triggered! That’s because we’re only animating the die when G.roll changes. What happens if we roll the same value twice in a row? Nothing. The value of G.roll hasn’t changed, so the animation wasn’t triggered.

So now comes the problem: how do we tell if a 1 followed by a 1 means that the die wasn’t rolled or if it means that we rolled a 1 twice in a row?

bgio-effects is one answer to that question. It allows the game code to say explicitly, “The die was rolled,” so that the client doesn’t have to guess by trying to spot the difference between two states.

Adding effects to the game

To start using bgio-effects, we first have to add the NPM package to our project:

npm i bgio-effects

# or if you use Yarn
yarn add bgio-effects

bgio-effects provides a plugin for boardgame.io that adds new functionality for use in your game code. We import the plugin like this:

import { EffectsPlugin } from 'bgio-effects/plugin';

We now need to configure the plugin so it knows about the kinds of effects that exist in our game. Let’s configure a single roll effect that will take the new value of the rolled die. (See the configuration docs for more details.)

const configuredEffectsPlugin = EffectsPlugin({
  effects: {
    roll: {
      create: (value) => value,
    },
  },
});

Now we add our configuredEffectsPlugin to our game’s plugins and update our roll move to use the effects API we’ve just added.

const game = {
  // Add the plugin to the game.
  plugins: [configuredEffectsPlugin],

  setup: () => ({ roll: 1, score: 0 }),

  moves: {
    roll: (G, ctx) => {
      const roll = ctx.random.D6();
      // Call the newly added roll effect.
      ctx.effects.roll(roll);
      G.roll = roll;
      if (G.roll === 6) G.score++;
    },
  },

  endIf: (G) => G.score >= 5,
};

This won’t appear to have much effect yet. G still looks and behaves the same, but behind the scenes, the plugin is storing data that will let us receive effects on the client.

Listening for effects

We can update how our client code triggers the die animation. Instead of animating each time G.roll changes, we can animate each time our new roll effect is emitted.

import { EffectsEmitter } from 'bgio-effects/client';

const emitter = EffectsEmitter(client);

emitter.on('roll', (newValue) => {
  animateDie(newValue);
});

emitter.state.subscribe((state) => {
  // more logic to render score, etc.
});

See the plain JS client docs for more details about EffectsEmitter.

import { EffectsBoardWrapper, useEffectListener } from 'bgio-effects/react';

const Board = ({ G }) => {
  // Each time the roll effect fires, trigger the die’s roll animation.
  useEffectListener('roll', (newValue) => {
    animateDie(newValue)
  }, []);

  return <div>{/* render die, score etc. */}</div>;
};

// Wrap the board component to add the effects emitter.
// Pass this wrapped component to boardgame.io’s React client.
const BoardWithEffects = EffectsBoardWrapper(Board);

See the React client docs for more details about the EffectsBoardWrapper and the useEffectListener hook.

With these changes implemented, our die rolls every time it should! Even if we roll the same value twice in a row.

Rolls: 0

snippet-2.tsx

Wait a second…

History hangs in the balance. Will you roll a six and move one point closer to a hard-won victory? The die spins, starting to settle. You’re in suspense, on tenterhooks, waiting with baited breath… Or rather, you would be, but our UI ruins the tension. While the die spins, the score updates immediately and gives away what the result will be.

Timing is the second area where bgio-effects can help us out.

When configuring the effects plugin, we can set a default duration for each effect. The die animation takes 1 second, so we set the roll effect to have a duration of 1.

const configuredEffectsPlugin = EffectsPlugin({
  effects: {
    roll: {
      create: (value) => value,
      // Give our effect a default duration of 1 second.
      duration: 1,
    },
  },
});

On its own this change won’t do anything yet. We want to delay showing the new score until the effect has finished. To do that we need to make a change to our client.

We can opt into this behaviour when setting up our EffectsEmitter:

const emitter = EffectsEmitter(client, {
  // Wait until all effects have finished before updating state.
  updateStateAfterEffects: true,
});

We can opt into this behaviour when wrapping our board component:

const BoardWithEffects = EffectsBoardWrapper(Board, {
  // Wait until all effects have finished before updating state.
  updateStateAfterEffects: true,
});

By enabling the updateStateAfterEffects option, we can continue to use the declarative state provided by boardgame.io wherever it makes sense to, but only show it once we’ve finished rendering effects.

After adding these two changes to our example UI, you can see that the score only updates once the die stops rolling.

Rolls: 0

snippet-3.tsx

Wrapping up

We’ve successfully used bgio-effects to solve a couple of problems that would have been tricky to fix using boardgame.io on its own. Using a roll effect gave us an imperative hook to trigger animations with instead of trying to compare state updates on the client. Adding an effect duration and turning on updateStateAfterEffects allowed us to delay state updates until our roll effect had completed.

Next, you may want to read about how to sequence multiple effects or look at the in-depth client documentation for plain JS and React clients.