Announcing ScyllaDB 6.0 — True Elastic Scale | Learn More

See all blog posts

How to Model Leaderboards for 1M Player Game with ScyllaDB

Ever wondered how a game like League of Legends, Fortnite, or even Rockband models its leaderboards? In this article, we’ll explore how to properly model a schema for leaderboards…using a monstrously fast database (ScyllaDB)!

1. Prologue

Ever since I was a kid, I’ve been fascinated by games and how they’re made. My favorite childhood game was Guitar Hero 3: Legends of Rock.

Well, more than a decade later, I decided to try to contribute to some games in the open source environment, like rust-ro (Rust Ragnarok Emulator) and YARG (Yet Another Rhythm Game). YARG is another rhythm game, but this project is completely open source. It unites legendary contributors in game development and design. The game was being picked up and played mostly by Guitar Hero/Rockband streamers on Twitch. I thought: Well, it’s an open-source project, so maybe I can use my database skills to create a monstrously fast leaderboard for storing past games.

It started as a simple chat on their Discord, then turned into a long discussion about how to make this project grow faster. Ultimately, I decided to contribute to it by building a leaderboard with ScyllaDB. In this blog, I’ll show you some code and concepts!

2. Query-Driven Data Modeling

NoSQL vs Relational

With NoSQL, you should first understand which query you want to run depending on the paradigm (document, graph, wide-column, etc.). Focus on the query and create your schema based on that query.

In this project, we will handle two types of paradigms:

  • Key-Value
  • Wide Column (Clusterization)

Now let’s talk about the queries/features of our modeling.

2.1 Feature: Storing the matches

Every time you finish a YARG gameplay, you want to submit your scores plus other in-game metrics.

Basically, it will be a single query based on a main index.

SELECT score, stars, missed_notes, instrument, ...  
FROM leaderboard.submisisons  
WHERE submission_id = 'some-uuid-here-omg'

2.2 Feature: Leaderboard

Leaderboard Figma File

And now our main goal: a super cool leaderboard that you don’t need to worry about after you perform good data modeling. The leaderboard is per song: every time you play a specific song, your best score will be saved and ranked. The interface has filters that dictate exactly which leaderboard to bring:

  • song_id: required
  • instrument: required
  • modifiers: required
  • difficulty: required
  • player_id: optional
  • score: optional

Imagine our query looks like this, and it returns the results sorted by score in descending order:

SELECT
    player_id, score, ...
FROM
    leaderboard.song_leaderboard
WHERE
    instrument = 'guitar' AND
    difficulty = 'expert' AND
    modifiers = {'none'} AND
    track_id = 'dani-california'
LIMIT
    100;
-- player_id | score
----------------+-------
-- tzach | 12000
-- danielhe4rt | 10000
-- kadoodle | 9999
----------------+-------

Can you already imagine what the final schema will look like?

No? Ok, let me help you with that!

3. Data Modeling time!

It’s time to take a deep dive into data modeling with ScyllaDB and better understand how to scale it.

3.1 – Matches Modeling

End Game Screen

First, let’s understand a little more about the game itself:

  • It’s a rhythm game;
  • You play a certain song at a time;
  • You can activate “modifiers” to make your life easier or harder before the game;
  • You must choose an instrument (e.g. guitar, drums, bass, and microphone).
  • Every aspect of the gameplay is tracked, such as:
    • Score;
    • Missed notes;
    • Overdrive count;
    • Play speed (1.5x ~ 1.0x);
    • Date/time of gameplay;
    • And other cool stuff.

Thinking about that, let’s start our data modeling. It will turn into something like this:

CREATE TABLE IF NOT EXISTS leaderboard.submissions (
    submission_id uuid,
    track_id text,
    player_id text,
    modifiers frozen<set>,
    score int,
    difficulty text,
    instrument text,
    stars int,
    accuracy_percentage float,
    missed_count int,
    ghost_notes_count int,
    max_combo_count int,
    overdrive_count int,
    speed int,
    played_at timestamp,
    PRIMARY KEY (submission_id, played_at)
);

Let’s skip all the int/text values and jump to the set<text>.

The set type allows you to store a list of items of a particular type. I decided to use this list to store the modifiers because it’s a perfect fit. Look at how the queries are executed:

INSERT INTO leaderboard.submissions (
    submission_id,
    track_id,
    modifiers,
    played_at
) VALUES (
    some-cool-uuid-here,
    'starlight-muse'
    {'all-taps', 'hell-mode', 'no-hopos'},
    '2024-01-01 00:00:00'
);

With this type, you can easily store a list of items to retrieve later.

Another cool piece of information is that this query is a key-value like! What does that mean? Since you will always query it by the submission_id only, it can be categorized as a key-value.

3.2 Leaderboard Modeling

Leaderboard filters Figma

Now we’ll cover some cool wide-column database concepts.

In our leaderboard query, we will always need some dynamic values in the WHERE clauses. That means these values will belong to the Partition Key while the Clustering Keys will have values that can be “optional”.

A partition key is a hash based on a combination of fields that you added to identify a value. Let’s imagine that you played Starlight - Muse 100x times. If you were to query this information, it would return 100x different results differentiated by Clustering Keys like score or player_id.

SELECT
    player_id, score ---
FROM
    leaderboard.song_leaderboard
WHERE
    track_id = 'starlight-muse'
LIMIT
    100;

If 1,000,000 players play this song, your query will become slow and it will become a problem in the future because your partition key consists of only one field, which is track_id.

However, if you add more fields to your Partition Key, like mandatory things before playing the game, maybe you can shrink these possibilities for a faster query. Now do you see the big picture? Adding the fields like Instrument, Difficulty, and Modifiers will give you a way to split the information about that specific track evenly.

Let’s imagine with some simple numbers:

-- Query Partition ID: '1'
SELECT
    player_id, score, ...
FROM
    leaderboard.song_leaderboard
WHERE
    instrument = 'guitar' AND
    difficulty = 'expert' AND
    modifiers = {'none'} AND -- Modifiers Changed
    track_id = 'starlight-muse'
LIMIT
    100;
-- Query Partition ID: '2'
SELECT
player_id, score, ...
FROM
leaderboard.song_leaderboard
WHERE
instrument = 'guitar' AND
difficulty = 'expert' AND
modifiers = {'all-hopos'} AND -- Modifiers Changed
track_id = 'starlight-muse'
LIMIT
100;

So, if you build the query in a specific shape it will always look for a specific token and retrieve the data based on these specific Partition Keys.

Let’s take a look at the final modeling and talk about the clustering keys and the application layer:


CREATE TABLE IF NOT EXISTS leaderboard.song_leaderboard (
    submission_id uuid,
    track_id text,
    player_id text,
    modifiers frozen<set>,
    score int,
    difficulty text,
    instrument text,
    stars int,
    accuracy_percentage float,
    missed_count int,
    ghost_notes_count int,
    max_combo_count int,
    overdrive_count int,
    speed int,
    played_at timestamp,
    PRIMARY KEY ((track_id, modifiers, difficulty, instrument), score, player_id)
) WITH CLUSTERING ORDER BY (score DESC, player_id ASC);

The partition key was defined as mentioned above, consisting of our REQUIRED PARAMETERS such as track_id, modifiers, difficulty and instrument. And for the Clustering Keys, we added score and player_id.

Note that by default the clustering fields are ordered by score DESC and just in case a player has the same score, the criteria to choose the winner will be alphabetical ¯\(ツ)/¯.

First, it’s good to understand that we will have only ONE SCORE PER PLAYER. But, with this modeling, if the player goes through the same track twice with different scores, it will generate two different entries.

INSERT INTO leaderboard.song_leaderboard  (
    track_id,
    player_id,
    modifiers,
    score,
    difficulty,
    instrument,
    stars,
    played_at
) VALUES (
    'starlight-muse',
    'daniel-reis',
    {'none'},
    133700,
    'expert',
    'guitar',
    '2023-11-23 00:00:00'
);
INSERT INTO leaderboard.song_leaderboard (
    track_id,
    player_id,
    modifiers,
    score,
    difficulty,
    instrument,
    stars, 
    played_at
) VALUES (
    'starlight-muse',
    'daniel-reis', 
    {'none'}, 
    123700, 
    'expert', 
    'guitar', 
    '2023-11-23 00:00:00'
);


SELECT 
    player_id, score
FROM 
    leaderboard.song_leaderboard 
WHERE 
    instrument = 'guitar' AND
    difficulty = 'expert' AND
    modifiers = {'none'} AND
    track_id = 'starlight-muse' 
LIMIT 
    2;

--   player_id  | score
----------------+-------
--  daniel-reis | 133700
--  daniel-reis | 123700
----------------+-------

So how do we fix this problem? Well, it’s not a problem per se. It’s a feature!

As a developer, you have to create your own business rules based on the project’s needs, and this is no different. What do I mean by that?

You can run a simple DELETE query before inserting the new entry. That will guarantee that you will not have specific data from the player_id with less than the new score inside that specific group of partition keys.

-- Before Insert the new Gampleplay

DELETE FROM 
    leaderboard.song_leaderboard 
WHERE 
    instrument = 'guitar' AND
    difficulty = 'expert' AND
    modifiers = {'none'} AND
    track_id = 'starlight-muse' AND
    player_id = 'daniel-reis' AND
    score <= 'your-new-score-here';

-- Now you can insert the new payload...

And with that, we finished our simple leaderboard system, the same one that runs in YARG and can also be used in games with MILLIONS of entries per second 😀

4. How to Contribute to YARG

Want to contribute to this wonderful open-source project? We’re building a brand new platform for all the players using:

We will need as many developers and testers as possible to discuss future implementations of the game together with the main contributors!

YARG Discord

First, make sure to join this Discord Community. This is where all the technical discussions happen with the backing of the community before going to the development board.

Also, outside of Discord, the YARG community is mostly focused on the EliteAsian (core contributor and project owner) X account for development showcases. Be sure to follow him there as well.

And FYI, the Lead Artist of the game, (aka Kadu) is also a Broadcast Specialist and Product Innovation Developer at Elgato who worked with streamers like:

  • Ninja
  • Nadeshot
  • StoneMountain64
  • and the legendary DJ Marshmello.

Kadu also uses his X to share some insights and early previews of new features and experimentations for YARG. So, don’t forget to follow him as well!

Here are some useful links to learn more about the project:

Fun fact: YARG got noticed by Brian Bright, project lead on Guitar Hero, who liked the fact that the project was open source. Awesome, right?

5. Conclusion

Data modeling is sometimes challenging. This project involved learning many new concepts and a lot of testing together with my community on Twitch.

I have also published a Gaming Leaderboard Demo, where you can get some insights on how to implement the same project using NextJS and ScyllaDB!

Also, if you like ScyllaDB and want to learn more about it, I strongly suggest you watch our free Masterclass Courses or visit ScyllaDB University!

 

About Daniel Reis

Daniel is a Developer Advocate at ScyllaDB. His goal is to make content about Science and Technology more accessible to everyone and create the best environment for new developers who wants to follow on this journey. In 2018, Daniel founded a developer community called He4rt Developers that is now known by almost every Brazilian developer.

Blog Subscribe Mascots in Paper Airplane

Subscribe to the ScyllaDB Blog

For Engineers Only. Subscribe to the ScyllaDB Blog. Receive notifications about database-related technology articles and developer how-tos.