Pretty much all my gamedev is hobby Phaser stuff and I've settled on a very specific state-machine-based pattern for logic in my games. I'm curious what game logic (e.g. player movement) looks like in other systems, esp since I've never had more than a cursory look at engines like Godot where behavior is more split between raw code and systems configured from the UI. Phaser is entirely a code library so I'm used to not having a GUI of any kind.
Anyone wanna share their own or know of any open source games that I could look at?
class CrouchingState extends PlayerState {
handleEntered() {
this.player.stop().setFrame(5).setVelocityX(0).setCollisionBox(CROUCHING_COLLISION_BOX);
}
update() {
const { scene, player } = this;
// Falling
if (!player.body.blocked.down) {
return this.transition('falling');
}
// Stay crouched as long as down is held
if (!scene.controller.down.isDown) {
return this.transition('idle');
}
// Dropping from a droppable floor
if (scene.controller.button1.isDown && player.canDrop) {
player.body.y += DROP_GAP;
return this.transition('falling');
}
}
handleExited() {
this.player.setCollisionBox(STANDING_COLLISION_BOX);
}
}Fairly straightforward, has handlers to run when you enter and exit a state, update that runs once a frame, etc. Once nice thing is that this.transition can accept arguments that get passed to handleEntered for parameterized states:
class WalkingState extends PlayerState {
facing!: Facing;
handleEntered(facing: Facing) {
this.facing = facing;
this.player
.setFacing(facing)
.setVelocityX(directionalVelocity(WALKING_VELOCITY_X, facing))
.play(this.player.holdingObject ? 'playerHoldingWalking' : 'playerWalking');
}This also shows why I favor classes for each state; they often end up needing to store some data that otherwise isn't useful to the rest of the player class. Inheritance and customizing a state via constructor arguments have also proven useful; most of my states of the player midair inherit from a base MidairState.
My favorite bit, though, is that the enter/exit handlers can be async:
class HurtState extends PlayerState {
async handleEntered() {
const { player, scene } = this;
// Invulnerability duration
scene.time.delayedCall(1000, () => {
player.invulnerable = false;
});
// Shove player back
player
.stop()
.setFrame(14)
.setInvulnerable(true)
.setVelocity(directionalVelocity(HURT_VELOCITY_X, oppositeDirection(player.facing)), HURT_VELOCITY_Y);
scene.soundHurt.play();
await wait(scene, 300);
return this.transition('idle');
}
}Crucially, the state machine will not run the update handler until the promise returned by handleEntered resolves, which ends up being really handy for states that involve running a bunch of animations / sounds / delays / etc. in sequence before transitioning. In this case we chuck the player backwards, wait 300ms for them to start falling, and then handle control back to the player.
At this point mostly I just wanna tighten up the syntax a bit and add useful typing to it so you can do things like type check state parameters.
