Replay Viewer

A Rocket League replay viewer in the browser


If you're like most people and haven't heard of the game Rocket League, let me give you the short version: soccer, but with cars. It's a little more nuanced than that; there is a massive distribution of players of all skill levels who have mastered the game's mechanics. Twice a year, hundreds of thousands of people gather to watch the world's greatest players go head-to-head in a three-day extravaganza.

Photo of the Prudential Center eventYes, they built an arena

Needless to say, there's a huge interest in this game. Like a modern first-person shooter or generic sports game, Rocket League is heavily dependent on teamwork. Teams of 2 or 3 rely on their teammates to communicate, rotate, pass, and work together to win. Hence, large communities have spawned as a result. Everything from national Collegiate Rocket League tournaments to local brackets where players like myself can compete.

One of these local communities went by the name of RLBot, a few programming enthusiasts who wanted to see if they could write bot players who could compete with one another, using these same mechanical skills and tactics that real players exercised. The RLBot team gained a following, receiving shoutouts from large content creators who played the game.

SunlessKhan, a YouTuber who covered an early version of RLBot

I was curious about building my own bot so I joined up and began poking around what other creators had built. One bot went by the name of Saltie, a deep reinforcement learning bot training on replays that the creators fed to it. Little did I know, one of the creators went to the same university I did, Northeastern, so I had to meet up.

He introduced me to the whole project and how the biggest difficulty they were facing was that they couldn't simulate enough games fast enough to produce a viable bot. So instead they launched, a website primarily aimed at collecting user-submitted replays from the game and performing analysis to discover what tactics and actions that top-tier players performed.

Calculated houses a wide number of statistics, predictions, and player analysis. At nearly 2.5 million replays to date, they are unmatched by any other community. They include information about your overall playstyles and how you perform individual matches. But I digress; let the site speak for itself.

The homepage of calculated.ggThe homepage of calculated.ggPreview of my play styleMy play stylePreview of my player statsJust one of the many tabs of information about a single replay of mine

The Viewer V1

I had very little game developer experience prior to this project, much less with Three.js. I initially began work on this project during a 12-hour hackathon with a few other members at a laboratory tucked away in a corner of Northeastern's library. We were able to get a very rough prototype displaying some crudely-designed cars, an object that represented the ball, and a basic field and set of goals.

A very early version of the replay viewerA very early version of the replay viewer

Since the site was early in development at this time, we spent more of our efforts on the backend side of things, getting a viable set of data to return to the frontend for parsing. I worked on two important systems during this time: an FPS Clock that allowed forward and reverse scrolling over frame data and an animation interpolation system.

The quick version about 3D animation in Three.js is that animations are made up of keyframes composited into a clip, linked together by a mixer, and run by an action. Each frame contains relevant information about every parameter of every object in the scene, including position and rotation—the two most important pieces of information in a replay.

Most 3D design software lets you easily build these animations and export them into a system like Three's for consumption. But that was not the case for us, since we parsed replay data from the game into JSON and then had to animate it manually ourselves.

Example of keyframes that are interpolatedEach of those little diamonds are keyframes and software like this (blender) interpolates it for you

The second piece of the equation was a global clock for tracking time. There's another issue with rolling data from one game engine into another: how do you represent time? In replay files from Rocket League, each frame of the game contained a plethora of information about each player's location, rotation, and state (like boosting or demolished). This frame also contains a delta, which is the amount of time that this frame existed for before transitioning to the next frame.

A delta is often a very small measure of time, something like 0.03605506s or 0.03618125s. As you may have noticed, these are not even fractions of time. We cannot rely on iterating over each frame on a fixed interval since the end result is a choppy mess that speeds up and slows down noticeably. Or if we missed a few frames and the delta is a large number, the animation would skip haphazardly.

Luckily, the browser keeps track of the time since the user loaded the page down to the nanosecond with a function called ``. Since we have one global game state, our one global clock can keep track of an "elapsed time" or the amount of time that has progressed in the replay animation. Given these deltas from each frame, we can convert them to represent an elapsed time by simply adding them up.

Run an example of converting deltas to elapsed frames

But this is just the easy part. The issue is that we have to convert between the browser time and the "elapsed frame" time, including differences, when telling the state to update. This means that in order to support pausing/playing and reverse scrubbing behavior, we have to keep track of our own delta. A delta of deltas if you will.

When playing the game, we can just set the delta of deltas, the "last delta", to the current browser time with a call to Each time we fire a frame update using the browser's built-in setInterval function, the subscribers of a frame update call to the clock's getDelta function. This returns the difference between the last delta and the current time, computes if we have elapsed any more frames, and sets the "last delta" to that current time.

There's one last piece of the puzzle: setting a frame. In order to do this, we have to have a notion of what the current frame is and the difference of time between that current frame and the frame we want to go to. We use this difference and push it onto a "delta queue". Since the browser is still animating during this time, we want to ensure that setting frames does not interfere with the computation between last frame and next frame. The queue is used to combine all operations that might have occurred between the last frame and the next frame and applies them at once. It reduces the overhead of recomputing if we rapidly change the frame and ensures that there are no side effects when playing or pausing the game.

That was certainly a mouthful, but if you'd like to get a closer look as to how this clock operates, check out this link to see the source code.

The Viewer V2

Some time elasped between the hackathon and the next time the project was picked up (for classes) but I picked it back up in the Spring of 2019. The goal was get the project into a presentable state, which meant using better models and controls that actually warranted getting added to the website.

I was able to extract models from the game, including the ball, the field, and the most popular car in the game: the Octane. With the help of the community, we cleaned up the materials for these objects. The field and vehicles started to take shape. We began customizing them, mostly to optimize for web performance, but adding our own flavor.

Extracted model assets

Performance wasn't amazing, so it warranted writing the most optimized set of managers I could come up with. I threw together a step-by-step loading system that lazily loaded assets (since there were over 26MB of them!), constructed the scene, configured the sizes of the objects, and initialized a handful of managers which each claimed their own responsibility.

To make the pieces as pluggable as possible, I moved everything to an event bus that dispatched and subscribed various managers with events that contained updates to cameras, game time progression, user interaction, and more. This optimization made the viewer performant enough to open for beta testing and we've had great success with testers.

We announced the viewer just in time for RLCS Season 7, showing it off to a number of professional players and game casters who were all excited to use it. By moving the viewer to its own NPM package, we can move quicker with updates and changes without being locked down by the rest of the constantly changing website. You can view the NPM package below.

Above-goal view of the replay viewerAbove-goal view of the replay viewerOrthographic view of the replay viewerOrthographic view of the replay viewer
Background photo