đ¤ Helpers for managing state effects in boardgame.io.
This package provides a structured approach to triggering ephemeral âeffectsâ in game code that can be consumed from state on the client. It provides a game plugin and a React board wrapper that emits client-side events for your effects.
npm i bgio-effects
Call effects from your moves or other game code:
function move(G, ctx) {
ctx.effects.explode();
}
Listen for effects from your board component:
useEffectListener('explode', () => {
// render explosion/play sound/etc.
});
The bgio-effects
plugin needs a configuration object. This object configures
the effects that will be available to your game logic.
// effects-config.js
export const config = {
// Declare the effect types you need.
effects: {
// Each effect is named by its key.
// This creates a zero-config endTurn effect:
endTurn: {},
rollDie: {
// Effects can declare a `create` function.
// If defined, the return value of create will be
// available as the payload for an effect.
create: (value) => ({ value }),
// Effects can declare a default duration in seconds
// (see âSequencing effectsâ below).
duration: 2,
},
},
};
To use the plugin, include it in your game definitionâs plugins
array,
passing it your configuration object:
// game.js
import { EffectsPlugin } from 'bgio-effects/plugin';
import { config } from './effects-config';
const game = {
name: 'my-game',
plugins: [EffectsPlugin(config)],
// Each effect type declared in your config will
// be available in your moves as ctx.effects[effectType]
moves: {
roll: (G, ctx) => {
G.roll = ctx.random.D6();
ctx.effects.rollDie(G.roll);
if (G.roll > 4) ctx.effects.explode();
},
end: (G, ctx) => {
ctx.events.endTurn();
ctx.effects.endTurn();
},
},
};
You can add timing information to your effects to sequence them on the client.
By default, effects have a duration of 0
and are added to the end of the
timeline in the order they are called, which means they will all trigger
together as soon as the game state updates.
You can set an alternative default duration for each effect in its config object:
{
effects: {
longEffect: {
duration: 5,
},
},
}
Now an effect called after longEffect
will be added to the timeline
5 seconds after longEffect
by default. For example:
0 . . . . 5 . . . . 10
â â
longEffect nextEffect
You can also specify where an effect is placed on the timeline and override its default duration when calling it, by passing position and duration parameters:
effect(position, duration);
effectWithCreateFn(createArg, position, duration);
-
- type:
string
|number
- default:
'>'
(end of the timeline)
Specifies the placement of this effect on the timeline.
A number places the effect at an absolute time, e.g.
3
would place the effect at 3 seconds along the timeline.A string is parsed according to a terse syntax for expressing different placements along the timeline:
-
'>'
: Relative to the end of the timeline, for example:-
'>+1'
: 1 second after the end of the timeline -
'>-1'
: 1 second before the end of the timeline
-
-
'<'
: Relative to the start of the last effect on the timeline, for example:-
'<'
: Aligned with the start of the last effect on the timeline -
'<+0.1'
: 0.1 seconds after the start of the last effect on the timeline
-
-
'^'
: Insert at an absolute time and shift all subsequent effects in time, for example:-
'^3'
: Insert at 3 seconds and shift subsequent effects by this effectâs duration -
'^3->0.5'
: Insert at 3 seconds and shift subsequent effects by 0.5 seconds
-
- type:
-
- type:
number
- default:
0
orduration
in the effectâs config if set
A time in seconds to override the effectâs default duration.
- type:
The following effects create the following timeline.
A(0, 4); // add A at 0s, with a duration of 4s
B('>-1', 1);// add B 1s before the end of the timeline, i.e. at 3s
C('^2->1'); // add C at 2s, shift later effects by 1s
D('^0', 5); // add D at 0s, shift later effects by its duration (5s)
E('<'); // add E, aligning it with start of last effect
0 . . . . 5 . â . â 10
â â â â
D A C B+E
The provided React component wrapper and hooks allow you to consume your effects as events, emitting them over time if you used the effect sequencing features.
To include the core effects engine in your app, wrap your board component with
the EffectsBoardWrapper
before passing it to the boardgame.io client factory:
import { Client } from 'boardgame.io/react';
import { EffectsBoardWrapper } from 'bgio-effects/react';
import { BoardComponent } from './Board';
const board = EffectsBoardWrapper(BoardComponent);
const BGIOClient = Client({ board, /* game, etc. */ });
In addition to passing EffectsBoardWrapper
your board component, you can also
pass an options object to configure the effects behaviour.
const board = EffectsBoardWrapper(BoardComponent, {
// Delay passing the updated boardgame.io state to your board
// until after the last effect has been triggered.
// Default: false
updateStateAfterEffects: true,
// Global control of the speed of effect playback.
// Default: 1
speed: 1,
});
-
Effect Type (
string
) â the effect you want to listen for. -
Callback (
function
) â the function to run when the effect is fired. -
Dependencies (
array
) â an array of variables your callback depends upon (similar to ReactâsuseCallback
hook). -
(optional) On-End Callback (
function
) â a function to run when the effect ends (as defined by the effectâsduration
). -
(optional) On-End Dependencies (
array
) â an array of variables your on-end callback depends on.
Within your board component or child components, use the useEffectListener
hook to listen for effect events:
import { useEffectListener } from 'bgio-effects/react';
function Component() {
useEffectListener('effectName', (effectPayload, boardProps) => {}, []);
return <div/>;
}
effectPayload
will be the data returned by your create
function or
undefined
for effects without a create
function.
boardProps
will be the latest props passed by boardgame.io. This is particularly useful when using the updateStateAfterEffects
option to get early access to the new global state.
Your callback can return a clean-up function, which will be run the next time the effect is fired, if the variables in the dependency array change, or if the component unmounts. This is similar to cleaning up in Reactâs useEffect
hook.
You can listen for all effects using the special '*'
wildcard. In this case,
your callback receives both the effect name and payload:
useEffectListener('*', (effectName, effectPayload, boardProps) => {}, []);
Two other special events will also always be fired:
-
'effects:start'
will fire before any other effects. -
'effects:end'
will fire after all the effects in the queue.
import React, { useState } from 'react';
import { useEffectListener } from 'bgio-effects/react';
function DiceComponent() {
const [animate, setAnimate] = useState(false);
// Subscribe to the ârollDieâ effect type:
useEffectListener(
// Name of the effect to listen for.
'rollDie',
// Function to call when the effect fires.
() => {
setAnimate(true);
const timeout = window.setTimeout(() => setAnimate(false), 1000);
// Return a clean-up function to cancel the timeout.
return () => window.clearTimeout(timeout);
},
// Dependency array of variables the callback uses.
[setAnimate]
);
return <div className={animate ? 'animated' : 'static'} />;
}
The useEffectState
hook provides an abstraction around useEffectListener
for cases where you donât need to call other imperative code
when an effect fires.
-
Effect Type (
string
) â the effect you want to observe state for. -
(optional) Initial State (
any
) â a value to use forstate
before an effect is received.
A [state, isActive]
tuple.
state
: The latest value for this effect type. This will beundefined
until the effect fires.isActive
: A boolean indicating whether the effect is currently active.
import { useEffectState } from 'bgio-effects/react';
function Component() {
const [roll, isRolling] = useEffectState('rollDie', 1);
const className = isRolling ? 'animated' : 'static';
return <div className={className}>{roll}</div>;
}
When using the updateStateAfterEffects
option, you may run into situations
where you have components that need the latest boardgame.io props before the
global props get updated. This hook allows you to get the latest props early
when the specified effects fire.
One or more effect types that should cause the props to update.
import { useLatestPropsOnEffect } from 'bgio-effects/react';
function Component() {
const { G, ctx } = useLatestPropsOnEffect('rollDie', 'endTurn');
return <div>{G.roll}</div>;
}
This also works with the special events. For example, in a component that needs the latest props as soon as they are available:
const { G, ctx } = useLatestPropsOnEffect('effects:start');
The useEffectQueue
hook lets child components control the effect queue if necessary:
import { useEffectQueue } from 'bgio-effects/react';
function Component() {
const { clear, flush, size } = useEffectQueue();
return (
<div>
<p>Queue Size: {size}</p>
<button onClick={clear}>Clear</button>
<button onClick={flush}>Flush</button>
</div>
);
}
useEffectQueue
returns the following methods and properties:
clear()
: Cancel any currently queued effects from being fired.flush()
: Immediately trigger any currently queued effects.size
: The number of effects currently queued.
This library is not designed with highly precise timing and animation
synchronisation in mind. Effects are emitted from a requestAnimationFrame
callback and the general implementation aims to be as performant and simple as
possible. Exact timing will depend on the frame rate of a userâs browser and
the accuracy of Date.now()
(which may be limited for security reasons).
This is an experimental project and feedback is welcome. Please open an issue if you run into any problems, have a question, or want to suggest features/improvements. PRs are welcome too đ.
Please also note the code of conduct and be kind to each other.
The code in this repository is provided under the terms of an Anti-Fascist MIT License.