Skip to content

Synchronizing a Multiplayer "Hard Mode" Feature

Mahmoud Bakir · · 3 min read

Background

A while back, I released a multiplayer mode for my music game Jingle. Shortly after, one of my players suggested a hard mode option.

Currently, singleplayer allows you to play with ‘hard mode’ enabled. This means you only get an x second segment of the song to make your guess.

Hard Mode

The Problem

Behind the scenes, a network request retrieves the mp3 file, browser reads the duration, client decides on a ‘start’ and ‘end’ time, trims the mp3 to that segment, and renders a custom ‘snippet player’ with that segment. If we take this approach and add it to multiplayer, we notice that all players get different segments of the song. This makes sense because start and end times are decided CLIENT SIDE. So we need to figure something out.


Storage

If we’re passing snippet start + end time from server -> client side, then these should be stored somewhere in our data tree. There are two options: This isn’t really complicated, but I want to share my thought process here.

Option 1: Store snippet times on the currentRound object.

export interface MultiRound { id: string; songName: string; pins: Array<{ userId: string; details: { clickedPosition: ClickedPosition; distance: number; confirmed: boolean; }; }>; results: Array<{ userId: string; score: number; }>; hardModeStartTime?: Date; hardModeEndTime?: Date; }

OR..

Option 2: store snippet times on the gameState object.

export interface MultiGameState { status: MultiLobbyStatus; currentRound: MultiRound; rounds: MultiRound[]; currentPhaseEndTime: Date | null; leaderboard: LeaderboardEntry[]; hardModeStartTime?: Date; hardModeEndTime?: Date; }


Pros and Cons

Option 1: memory and storage consumption 8 bytes per Firestore timestamp * 2 = 16 bytes; if 1000 rounds are played per day that’s an extra ~500KB of storage, which is like 9 cents. Not a big deal at all. In terms of memory occupation on the server, we’re running Jingle on a 500MB memory virtual machine, so it’s really nothing. This will allow us to have more granular game history and gather statistics if we want in the future (e.g. looks like players struggle a lot with x segment of y song)

Option 2: no memory/storage overhead, singular source of truth, but limited statistic analysis

Data is cheap, so we’ll go with Option 1. Now the easy part is out of the way.


Determining Time Segments

We need hardModeStartTime + hardModeEndTime to be determined SERVER SIDE, making our server the singular source of truth so all clients share the same exact snippet. Currently, audio files are loaded into a DOM reference on the client, which gives the browser access to metadata such as song length. We DON’T have that length metadata on the back end, so how can we generate snippet start + end times per song on our server? Options:

  1. Have a separate ‘song lengths’ data file that maps song titles to their durations in seconds.
  2. Make a network request on the back end to retrieve the mp3 metadata from Cloudflare (where it’s stored) (cons: redundancy, latency, network bandwidth)
  3. Pass metadata from client side to back end (doesn’t make sense, back end needs to be source of truth + we can’t afford a network request there due to latency + we don’t want all clients to send requests)

Let’s go with Solution 1.

To make this work, I went to the OSRS wiki music page listing, copied the 800 rows of song + durations, pasted that into Google Sheets, exported as a .csv, then wrote a simple JS script to convert that into a JSON (and by wrote I mean I coerced AI into doing it for me) I’ll also need to update this file every time Jagex releases a new song.

Now that we have this data on the back end, we can determine start + end times on the back end. This finally allows us to use the back end as our source of truth.