Speed Tech: The code behind a speedrun marathon

Introduction, background, problems to solve, solution(s)

This past August, I helped organize a small speedrun marathon called Licenseathon. As the name suggests, its focus is on licensed games - that is, games revolving around properties first appearing in novels or movies or other media. It was my second year contributing to the event, and like last year I volunteered to do technical work. Part of this was graphic design for our stream overlays, and part of it was the technology powering those overlays.

For those not in the know, most smaller events have a number of volunteer hosts who “restream” - that is, they stream to the marathon’s channel themselves, mirroring the showcase runner’s stream with an overlay.

A runner's livestream is captured by a restreamer, who then streams the run with an overlay to the viewer.

There are a few problems to solve here: For one, every host must have the same assets and even stream settings for consistency. There’s also many moving parts to a marathon overlay, some of which the host might need to update themselves. As an example, here’s a screenshot of the Licenseathon 2019 stream deck:

Look how many scenes there are!

And here’s a screenshot of the Licenseathon 2020 stream deck:

Much nicer!

Much nicer!

The first screenshot doesn’t even show the complexity of each overlay scene. There were dozens of components in each, and it was a mess. This year’s tech setup was much simpler. The approach to overlay design in 2020 was completely different, and it’s all thanks to one major piece of technology: Speedcontrol.

Problems to be solved:

  • Everything was done in OBS and with external programs
  • Duplicate timers
  • Updating run info
  • Lots of scenes
  • Required external programs, local assets

Problems

The streamdeck for Licenseathon 2019 was almost entirely built with OBS features. Scenes were comprised of images, text sources, and window captures. Any text was generated by an external program called Scoreboard Assistant, which was designed for fighting game tournaments. It let us update a text in bulk, but information about each run still had to be entered into the program manually beforehand. Timers were just copies of Livesplit being run on the host’s computer. If there was a race, the host would need to have two copies of livesplit open, with different hotkeys for stopping each one. Pausing and restarting one timer was basically impossible, and the host couldn’t use their numpad while a run was ongoing (and more than once, we forgot this fact and stopped a timer by accident). All the runners' streams were played in a browser window, which was carefully sized and cropped (but which required frequent adjustment between runs).

To make matters worse, this setup meant that every host’s streaming environment had to be identical, and there were lots of moving parts.

Designing the stream deck this way meant there was little in the way of technical knowledge required to operate the stream, but it also meant every host was required to run two external applications, open several browser windows, and hold nearly a gigabyte of assets on their C drive. That’s not even counting the fonts and plugins that had to be installed before importing the scenes into OBS. Everything was updated manually, which was slow and error-prone, and there were so many scenes that it was tough to find the correct one at a glance. Clearly, we needed a better solution.

The big solution came in the form of NodeCG and Speedcontrol, two technologies purpose-built for livestreaming events of this sort. NodeCG bills itself as a service that “enables you to write complex, dynamic broadcast graphics using the web platform.” To that end, it offers users the ability to create “bundles,” which are like plugins. Speedcontrol is one such bundle; It comes with a timer, the ability to import and edit a list of runs from Horaro or Oengus, and a javascript API for retrieving and updating information about runs. Overlays are displayed as webpages, and aren’t created by Speedcontrol or NodeCG themselves; they simply display the overlays you provide.

Now, my HTML was a little rusty and I’d never actually built something meaningful in javascript, so this was a somewhat daunting task. But it turned out to be rather straightforward. I won’t go over every page in our layout in this post, but I’ll describe the design of one of the more complex pages. If you would like to see the rest, the full repository will be linked at the end of the article.

Do you want to go over how the system registers and displays overlays here, or afterwards? It might be helpful in understanding why they had to be designed for a specific size first. Might be too small of a detail.

Layouts

My original plan was to create the overlays entirely with html and css, rather than create images like I’d done for the previous year. This would let me make changes to things like borders, margins, spacing rules, and colours without having to edit multiple images. Better yet, modern css properties like flex and grid would let me automatically size elements appropriately, so elements would be flexible. Plus, the logical structure of a marathon layout would be simple to create in code.

Unfortunately, this did not pan out. I didn’t want to hard-code the size of the game windows, instead leaving them to fit the largest possible space within their margins. Then, bordering elements like textboxes would fit the remaining space. However, I also wanted the game window to fit a constant aspect ratio. These two goals conflicted, and no matter what I tried I was unable to make the game window element both maintain aspect ratio and automatically fit itself to the page. That meant I was back to making my overlay images in Affinity.

Around this point, I started to look for examples from other marathons. Valuethon, Hekathon, and Power Up With Pride all served as inspiration while designing the overlays. Ultimately, I took the easy but time-consuming approach of creating a background image for each page, then statically positioning elements to match the background. These were tedious to alter and less reusable than what I wanted, but it worked and ultimately it looked good.

There are ways to get aspect ratios to display properly but they involve padding tricks and you can’t automatically fit an element in a flexbox using those tricks, at least as far as I can tell. There would always be some static, absolute sizing variable which I wanted to avoid. In the future we’ll have the aspect-ratio keyword in css and then I can make everything relative.

The html behind the overlays is very simple. Here’s the entire body of the singleplayer overlays:

<body>
  <!-- Background image -->
  <div id="backgroundImage"></div>
  <!-- Player 1 information -->
  <div player-id="1" class="playerContainer flexContainer">
    <div class="nameContainer flexContainer">
      <div class="playerText"><!-- Player Name --></div>
    </div>
  </div>
  <!-- Timer -->
  <div id="timerInfoContainer" class="flexContainer">
    <div timer-id="1" class="timerContainer flexContainer">
      <div class="timer"><!-- Timer --></div>
    </div>
    <div id="estimateContainer" class="flexContainer">
      <div id="gameEstimate" class="timer"><!-- Game Estimate --></div>
    </div>
  </div>
  <!-- Run information -->
  <div id="runInfoContainer" class="flexContainer">
    <div id="gameTitle" class="flexContainer"><!-- Game Name --></div>
    <div id="categoryContainer" class="flexContainer">
      <div id="gameCategory"><!-- Game Category --></div>
    </div>
  </div>
</body>

Text elements are wrapped in flex containers, which allow them to fit whatever size box they’re in. The timer and player divs have a special id tag, which is used to identify them in multiplayer overlays. What’s nice about this approach is that containers can be rearranged and flex will ensure items are spaced properly. For example, another overlay has the category, estimate, and title all in one container separate from the timer, and it looks just as natural. Most css can be applied commonly to all overlays, with only minor changes added to each, for example overriding a container’s default font size.

Finally, there’s the matter of populating the overlays with information and updating them on the fly. This is where I was least comfortable, knowing very little about Javascript or Nodejs.

Code

  • Get into details of how Speedcontrol and NodeCG move around data
  • How does the timer code work?
  • How does the text-changing code work?
  • What bugs did you fix in the code that you took from valuethon?
var speedcontrolBundle = 'nodecg-speedcontrol';
var runDataActiveRun = nodecg.Replicant('runDataActiveRun', speedcontrolBundle);

These lines declare a replicant called runDataActiveRun, located in the bundle nodecg-speedcontrol. Anytime the data in the original changes, the replicant does what its name suggests and replicates the change in our bundle. NodeCG offers a handy listener function that lets us perform an action when this update occurs, so when the data in speedcontrol changes, we can do thinks like update the fields in our overlay.

runDataActiveRun.on('change', (newVal, oldVal) => {
  if (newVal)
   updateSceneFields(newVal);
});

From this point, you can do whatever you want. For example, take a look at this code from run-info.js:

// Select elements with JQuery
var gameTitle = $('#gameTitle');
var gameCategory = $('#gameCategory');
var gameSystem = $('#gameSystem'); 
var gameEstimate = $('#gameEstimate');

// Declare and watch replicant
var runDataActiveRun = nodecg.Replicant('runDataActiveRun', speedcontrolBundle);
runDataActiveRun.on('change', (newVal, oldVal) => {
  if (newVal)
    updateSceneFields(newVal);
});

// Sets information on the pages for the run.
function updateSceneFields(runData) {
  let fontSize = 20;
  if (gameTitle.height() > 57) {
    fontSize += Math.min((gameTitle.height() - 57) / 67 * 6, 6);
  }
  gameTitle.css('font-size', `${fontSize}px`);
  // Assign new values
  gameTitle.html(runData.game);
  gameCategory.html(runData.category);
  gameSystem.html(runData.system);
  gameEstimate.html(runData.estimate);

  gameTitle.fitText();
  gameCategory.fitText(1.25);
}

It’s astoundingly simple. First, we use JQuery to select the elements we want to update. In this case, it’s the divs containing the run information. Then, when the run information is updated, updateSceneFields() is called. This function sets some CSS properties and updates the selected elements with the new values from the replicant. That’s all there is to it!

comments powered by Disqus