Skip to content

Instantly share code, notes, and snippets.

@raphaeltraviss
Last active November 7, 2020 20:20
Show Gist options
  • Save raphaeltraviss/0426b6cff83d3fa51cd26e612a369cd4 to your computer and use it in GitHub Desktop.
Save raphaeltraviss/0426b6cff83d3fa51cd26e612a369cd4 to your computer and use it in GitHub Desktop.
Basic XState model for moving/striking with "looking at" vector in PhaserJS
import { assign, interpret, Machine, Interpreter, EventObject } from 'xstate';
// Phaser implicitly available
interface PawnContext {
moveTowards?: Phaser.Math.Vector2,
}
interface PawnStateSchema {
states: {
idle: {};
moving: {};
striking: {};
};
}
type PawnEvent =
| { type: 'MOVE', vector: Phaser.Math.Vector2 }
| { type: 'STRIKE_START' }
| { type: 'STRIKE_END' }
const rememberVector = assign<PawnContext, { type: 'MOVE', vector: Phaser.Math.Vector2} >({
moveTowards: (_, ev) => ev.vector
});
const pawnState = Machine<PawnContext, PawnStateSchema, PawnEvent>({
initial: 'idle',
context: {
moveTowards: new Phaser.Math.Vector2(0, 0),
},
states: {
'idle': {
entry: ['halt'],
on: {
'MOVE': {
target: 'moving',
cond: (_, ev) => ev.vector.length() > 0,
actions: [rememberVector, 'move'],
},
'STRIKE_START': {
target: 'striking',
actions: ['strike'],
},
}
},
'moving': {
on: {
'MOVE': [
{
target: 'idle',
cond: (_, ev) => ev.vector.length() === 0,
},
{
target: 'moving',
cond: (ctx, ev) => !ctx.moveTowards.equals(ev.vector),
actions: [rememberVector, 'move'],
}
],
'STRIKE_START': {
target: 'striking',
actions: ['strike'],
},
}
},
'striking': {
on: {
'MOVE': [
{
target: 'striking',
actions: [rememberVector],
}
],
'STRIKE_END': [
{
target: 'moving',
cond: (ctx, _) => ctx.moveTowards && ctx.moveTowards.length() > 0,
actions: ['move'],
},
{
target: 'idle',
}
]
}
},
},
},
{
actions: {
move: () => { console.log('moving!'); },
strike: () => { console.log('striking!'); },
halt: () => { console.log('halting!'); },
},
});
export class InputSampler {
output_events?: Phaser.Events.EventEmitter;
output_target: string;
service?: Interpreter<PawnContext, PawnStateSchema, EventObject>
constructor() {
const state = pawnState.withConfig({
actions: {
move: this.move,
strike: this.strike,
halt: this.halt
},
});
this.service = interpret(state);
this.service.start();
}
move(ctx, ev) {
if (!this.output_events) return;
// Use the last-remembered vector, if there is none requested.
const vector = ev.vector || ctx.moveTowards;
const moveTowards = `${this.output_target}/${VampireTunnelsEvent.req_vector}`;
this.output_events.emit(moveTowards, vector);
}
strike(ctx, ev) {
if (!this.output_events) return;
// Probably not needed, but just to provide a fallback.
const vector = ev.vector || ctx.moveTowards;
const strikeTowards = `${this.output_target}/${VampireTunnelsEvent.req_strike}`;
this.output_events.emit(strikeTowards, vector);
}
halt(_, ev) {
if (!this.output_events) return;
const moveTowards = `${this.output_target}/${VampireTunnelsEvent.req_vector}`;
this.output_events.emit(moveTowards, new Phaser.Math.Vector2(0, 0));
}
// @TODO: these conditionals are probably no longer necessary.
// Called externally by 'preupdate' event loop
sample() {
const nextMove = this.get_input_vector();
const will_strike = this.strike_key.isDown || this.get_pad_strike();
if (will_strike) {
this.service.send('STRIKE_START');
}
else if (this.service.state.context.moveTowards !== nextMove) {
this.service.send('MOVE', { vector: nextMove || new Phaser.Math.Vector2(0, 0) });
}
else {
this.service.send('MOVE', { vector: new Phaser.Math.Vector2(0,0) });
}
}
// Called via event listener from Animator, when strike animation finishes
private handle_struck() {
this.service.send('STRIKE_END');
}
private get_input_vector() {
// reduce controller+keyboard direction to a single vector
}
}
@raphaeltraviss
Copy link
Author

game components listen for async events, immediately update their local state chart, and queue up a list of mutations (not shown) to perform during the next render frame.

Some components, such as this one, immediately capture and transform raw PhaserJS events or input samples into game events that are sent to the other components, e.g. successful transition to 'moving' sends a { MOVE, vector } game event, which another component such as Animator might be listening for

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment