Building a Final Fantasy-style ATB System in RxJS
In part because I’m trying to learn RxJS and in part because the soon-to-be-released Final Fantasy 7 (FF7) remake has me feeling nostalgic, I wanted to see if I could re-create the battle system (if not the graphics) from FF7 using HTML, CSS, JS, and RxJS. If you’ve never seen what this battle system looks like, here’s a YouTube video of the first boss in FF7. I’ll post a GIF below.
To see my rendition, check out the demo here. The code is up on GitHub.
For folks unfamiliar with it, FF7 is a 1997 role-playing game originally developed for the Sony PlayStation. I don’t know the full history of the Final Fantasy series’ various battle systems, but my understanding is that as of FF4, the series switched from a simple turn-based system to a more involved timer-based “Active Time Battle” (ATB) system. For this post, the main point is that ATB battles are complicated in that when a player-controlled character is allowed to do an action is dependent on a number of factors including whether or not the player is in a menu, what the value of a character’s current ATB gauge is (essentially a timer), and if particular animations are running.
To make matters even more confusing, the FF7 ATB system has three different user-selectable modes that change the behavior of each character’s ATB gauge. “Active” means characters’ gauges continue to fill regardless of what’s happening on the screen. “Recommended” means characters’ gauges stop filling during animations. Finally, “Wait” means characters’ gauges stop filling both during animations and when the player has selected an action like “Attack”.
If you’re a web developer, the complicated, asynchronous nature of ATB gauges may sound a bit like your day-to-day work. For example, if you’re making a single-page app, you may have to handle multiple requests loading in the background, the user clicking around menus unpredictably, and app logic waiting on UI animations to complete.
RxJS specifically and Reactive programming in general attempt to address some of the difficulties in wrangling asynchronous events into streams, but if you’re like me, you may find learning this style of programming fairly difficult. So to help me learn, I built the above version of an ATB system in RxJS using the terrific graphic assets from Mozilla’s BrowserQuest. While I’m not sure I coded everything in the canonical RxJS way, I do feel like I have a better grasp on what RxJS can offer in terms of thinking about async code.
For example, one requirement is that the characters’ gauges must fill based on the the selected ATB mode and whether or not the game is paused. In the non-RxJS version (partially implemented in index-norx.js
), the best way I could find to manage the gauges was the following:
// Map of ATB modes to related functions
const atbMap = {
Active: [],
Recommended: [getAnimating],
Wait: [getAnimating, getAction]
};
// Redraw and update if not paused
function draw() {
requestAnimationFrame(draw);
if (battleState.paused) return;
update();
}
// If we don't need to wait, update timers and set hero as ready if time is full
function update() {
if (!wait()) updateTimers();
state.heroes.forEach((hero, i) => {
if (hero.wait < 100) {
unsetHeroReady(i);
} else {
setHeroReady(i);
}
});
}
// Update timers unless the hero is animating
function updateTimers() {
state.heroes.forEach((hero, i) => {
hero.wait = Math.min(hero.wait + .15, 100);
});
}
// Don't update timers if any of the ATB related functions return true
function wait() {
return atbMap[state.settings.atbMode].some(shouldWait => shouldWait());
}
requestAnimationFrame(draw);
To my eye, the above code isn’t too rough, but as things get more complicated (e.g. handling when a player can click, dealing with enemy attacks, allowing the player to cancel actions, etc.), things get more complicated and the dreaded proliferation of if
statements becomes unavoidable.
In contrast, here is how I wrote the same logic using RxJS in index.js
:
// Map of ATB modes to related streams
const atbMap = {
Active: [of(false)],
Recommended: [animating$],
Wait: [animating$, actioning$]
};
// Don't tick if paused
const clock$ = of(null, animationFrameScheduler).pipe(
repeat(),
withLatestFrom(paused$),
filter(([_, paused]) => !paused),
share()
);
// Don't tick if the any of the related ATB streams' most recent values are true
const timerClock$ = clock$.pipe(
withLatestFrom(atbMode$, (_, mode) => mode),
switchMap(mode => combineLatest(atbMap[mode])),
filter(thingsToWaitOn => !thingsToWaitOn.some(m => m))
);
// Update each hero's timer whent the timer ticks
state.heroes.forEach(hero => {
timerClock$.pipe(
map(increase => Math.min(hero.wait + .15, 100))
).subscribe(time => (hero.wait = time));
// Stream to determine if the hero can attack
const heroReady$ = timerClock$.pipe(map(() => hero.wait === 100), distinctUntilChanged());
// Set hero as ready/not ready
heroReady$.pipe(filter(r => r)).subscribe(() => setHeroReady(i));
heroReady$.pipe(filter(r => !r)).subscribe(() => unsetHeroReady(i));
});
Annnnnnd, that’s a lot more complicated. So far, RxJS doesn’t seem like a win.
In my opinion, the benefit of the above code is that it sets up several streams from which more complex logic can be built. For example, if I want to limit when the player can click on a hero to only when the hero is “ready” (i.e. the hero’s gauge is full), I can combine the heroReady$
stream with a stream of clicks rather than have a conditional checking isHeroReady
inside a click handler. The value of composing streams rather than throwing around if
statements will be felt as the complexity of the application grows and the number of conditionals RxJS allows me to avoid also grows.
However, comparing these two implementations, I am still forced to admit that I’m not sure the RxJS version is clearer. For me, the biggest downside of RxJS is that while each operator appears small, there are a lot of concepts embedded in each one. The upshot is that while the resulting code is short, a quite a bit of background is required to unpack what is happening.
Despite my struggles with it, I’m interested in using RxJS again in a game setting to see how it can be helpful to keep async complexity from spiraling out of control. What do you think? Did I approach this problem from the wrong direction? Does this seem about right? Try the demo and let me know!