Last active
November 7, 2020 20:20
-
-
Save raphaeltraviss/0426b6cff83d3fa51cd26e612a369cd4 to your computer and use it in GitHub Desktop.
Basic XState model for moving/striking with "looking at" vector in PhaserJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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