Umbra: Development Notes 1
Last year, I quit my job and worked on some new game concepts for about six months. My initial idea didn’t quite turn out, but the second one stuck: a combat-focused runner game in pixel art for Android and iOS.
Introduction
The game is called Umbra. As my previous game, Gothic Survival, Umbra is also built using Flutter and Flame. I’d say the game has deep influences from runners like Postknight, Mario Run, Dark Lands, and finally a wee bit of Dark Souls.
Here’s what it looks like today:
Learning from the first game, I’ve emphasized creating tools for building game content, especially player animations and new monsters. Specifically, there are two aspects of the architecture that stand out and really make it possible to easily define complex behavior: the state machines and the hooks that latch onto them.
Yet Another (Dart) State Machine
The game’s various enemies have hundreds of states, each with their own animation(s) and effects.
I knew I needed a strong, flexible, and most of all legible state machine library in Dart, but I didn’t like my choices. I ended up writing yafsm
to satisfy my requirements.
Here’s what a yafsm
state machine definition looks like for a rat:
class RatState extends MachineComponent {
RatState() : super('Rat') {
root.initialize(isAlive);
alive.initialize(isIdle);
}
late final isAlive = root.state('alive');
late final isDead = root.state('dead');
late final onDeath = root.transition('onDeath', {isAlive}, isDead);
late final alive = isAlive.nest('alive');
late final isIdle = alive.state('idle');
late final isHit = alive.pstate<({Damage damage})>('hit');
late final isWalking = alive.state('walking');
late final isRunning = alive.state('running');
late final isTurning = alive.state('turning');
late final isBiting = alive.state('biting');
late final isPouncing = alive.state('pouncing');
late final onStop = alive.transition('stop', {isWalking, isRunning}, isIdle);
late final onWalk = alive.transition('walk', {isIdle, isRunning}, isWalking);
late final onRun = alive.transition('run', {isIdle, isWalking}, isRunning);
late final onTurn = alive.transition('turn', {isIdle, isWalking, isRunning}, isTurning);
late final onTurnFinished = alive.transition('turn finished', {isTurning}, isIdle);
late final onBite = alive.transition('bite', {isIdle, isWalking, isRunning}, isBiting);
late final onBiteFinished = alive.transition('bite finished', {isBiting}, isIdle);
late final onPounce = alive.transition('pounce', {isIdle, isWalking, isRunning}, isPouncing);
late final onPounceFinished = alive.transition('pounce finished', {isPouncing}, isIdle);
late final onHit = alive.ptransition('hit', any, isHit);
late final onRecovered = alive.transition('recover', {isHit}, isIdle);
}
Each state and transition is a callable function to either check the state or trigger the transition. Outlining how states behave long before I actually implement the behaviors associated with those states has been instrumental in letting my first implementation be already pretty correct.
Additionally, enemies have an automatically generated Storybook story for developing just that enemy. I show the state live at the top. Debugging is a breeze!
The player is similar, but about ten times more states and numerous nested machines.
Flame Hooks
I’ll likely release a second library before this game is over, which I’m calling flame_hooks
. Flame, the game engine, comes with a component-based system for defining game objects.
I don’t really know a good way to compose behavior using components. I think there’s an approach that makes a component out of every single stat, but I find it too heavy.
You can also compose things using classes, interfaces, and mixins, but these must be determined at compile time, rather than be driven by the presence (or not) of some data. My first game took this latter approach, and ended up with a massive, do-all monolith component.
As an ardent believer in React hooks - all my apps use Flutter hooks, too - I couldn’t help but want my hook-based API back, but for defining Flame component behavior instead. So I dreamed up something like this:
class Rat extends Enemy {
final state = RatState();
final status = RatStatus();
@override
Future<void> hook() async {
await super.hook();
// ...setup...
// idle
useFlameStateEnter(state.isIdle, () => loop('idle'));
// walk
state.isWalking.guard(() => !isBlocked);
useFlameStateEnter(state.isWalking, () => loop('walk'));
useFlameStateMovement(state.isWalking, status.walkSpeed);
// run
state.isRunning.guard(() => !isBlocked);
useFlameStateEnter(state.isRunning, () => loop('run'));
useFlameStateMovement(state.isRunning, status.runSpeed);
// ...continued...
}
}
Hooks are normally used for notifying the framework that something should be rendered, but in a normal game with a game loop, everything is always rendered anyway. Nonetheless, hooks remain an excellent way to pull together related code in a way that an object-oriented approach will always fall short.
There are a two things that let this happen: use functions to define behavior, rather than overridden methods; and allowing access to the “current component” from the ether.
Functions, Not Methods
I was able to rewrite the Flame Component
API by using a bunch of lists holding callbacks, then calling these fake “hooks” to add things to the current component. For example, here’s how updates are “hooked” into:
mixin FlameHooks on Component {
final _updateFns = <void Function(double)>[];
@override
@mustCallSuper
void update(double dt) {
super.update(dt);
for (final fn in _updateFns) {
fn.call(dt);
}
}
}
void useFlameUpdate(FlameHookUpdateFn fn) {
final component = useFlameComponent<FlameHooks>();
component._updateFns.add(fn);
}
There are similar lists for every single part of the component API. The advantage here is you can set up the contents of those methods at construction time, making it feasible to dictate behavior using functions rather than classes or mixins.
The hooks can then be composed to do things - and clean those things up - without the user being any wiser. In short, they share units of cohesive behavior across all components. The useFlameUpdate
hook is sort of like a core, “framework” hook, but I ended up with a wide variety of complex ones. For example, this is how useFlameStateMovement
is implemented:
void useFlameStateMovement(MachineState state, SpeedAttribute attribute) {
final target = useFlameComponent<GameComponent>();
useFlameStateEnter(state, () => target.move(attribute.value));
useFlameStateExit(state, () => target.stop());
}
It’s function composition at its best! Now you’ll never forget to stop moving when you leave a state in which you were moving.
What Is My Component
The magic that powers this contraption is useFlameComponent
. With this function, you can retrieve “the current component” when inside the hook()
setup method. But where does it come from?
I looked at how flutter_hooks
did it, and it seems like it uses a single static variable to store it. I tried this and ran into some problems - specifically that I didn’t want to expose that variable publicly, whereas flutter_hooks
had a neat place to put it privately. Instead, I used a Dart zone to hide it:
mixin FlameHooks on Component {
@override
@mustCallSuper
Future<void> onLoad() async {
await super.onLoad();
await runZoned(hook, zoneValues: {
#component: this,
});
}
@visibleForOverriding
FutureOr<void> hook();
}
C useFlameComponent<C extends Component>() {
// Some assertions here omitted for brevity.
return Zone.current[#component] as C;
}
With this mechanism, you can now write functions that, when run in the context of hook()
, can access the current component and attach various callbacks to the Flame component API.
I haven’t analyzed the performance ramifications of using zones this extensively, but if they become a problem I’ll probably just suck it up and use a public, global static variable instead.
Summary
Umbra is on track to actually get released. Developing it is fun and intuitive (for me at least, which is what matters!). The state machines and hooks for attaching behavior to them make the code extremely flexible, and I think these are concepts I want to carry on to future games too.
Lastly, I keep the latest branch deployed to Itch.io as a web application, so you can actually play it now if you like. Shoot me an email and I’ll invite you. The game loop is done, but progression, achievements, balance, and level design are still to come!
![](../../img/umbra-player.gif#center)