数据库 101:如何为 100 万玩家的游戏建模排行榜。

2025-05-26

数据库 101:如何为 100 万玩家的游戏建模排行榜。

有没有想过像《英雄联盟》《堡垒之夜》甚至《摇滚乐队》这样的游戏是如何建模排行榜的?在本文中,我们将了解如何正确地建模一个模式,以便以一种极具表现力的方式处理它们!

如果您刚刚开始接触数据库或数据库基础知识,建议您先阅读我的首篇文章《数据库入门:面向初学者的数据一致性》。那篇文章记录了我对数据库范式的探索,我的视野远远超出了之前只使用 SQL 和 MySQL 的经验。我会持续关注这个数据库入门系列的学习进展。

距离我发表这个系列的第一篇文章已经过去快一年了!感谢大家在我学习这个主题的过程中一直陪伴着我。你们的评论和想法总是非常有帮助!

1. 序言

YARG 游戏截图

和大多数普通开发者一样,我从小就对游戏及其制作过程着迷。说到这,让我来介绍一下我童年最喜欢的游戏:《吉他英雄3:摇滚传奇》。

好吧,十多年后,我决定尝试在开源环境中贡献一些游戏,比如rust-ro(Rust Ragnarok Emulator)以及本文的主角:YARG(Yet Another Rhythm Game)

YARG 实际上是另一款节奏游戏,但这个项目的不同之处在于它是完全开源的,并且他们联合了游戏开发和设计领域的传奇贡献者来使这个项目发挥作用。

突然间,这款游戏开始被 Twitch 上的 Guitar Hero/Rockband 主播们选中并开始玩,我想:好吧,这是一个开源项目,所以也许我可以使用我的数据库技能来创建一个速度极快的排行榜或存储过去的游戏。

一开始只是他们在 Discord 上的一次简单聊天,后来演变成一场关于如何让这个项目更快发展的长时间讨论。

然后我决定和老板谈谈,问问他我是否可以和 YARG 的伙计们一起工作,条件是要创建一个足够酷的东西来实现ScyllaDB(NoSQL 宽列数据库),因为我在那里担任开发倡导者。你绝对想不到 ScyllaDB 带来的简洁性和可扩展性竟然完美契合了 YARG.in 的需求!

不管怎样,光说不练是没用的。我来给你展示一些代码和概念吧!

2. QDD - 查询驱动数据建模

NoSQL 与关系型数据库

当我们谈论使用NoSQL进行开发时,我们主要应该明白,根据范例(文档、图形、宽列等),您应该首先了解要运行哪个查询

虽然在 MySQL 中主要目标是了解一致性,但在 Scylla 中您应该关注查询并基于该查询创建模式。

在这个项目中,我们将处理两种类型的范例,即:

  • 键值
  • 宽列(聚类)

现在让我们讨论一下我们的建模的查询/特征。

2.1 功能:存储比赛

提交详情 YARG

每次完成 YARG 游戏时,最有趣的事情就是提交您的分数以及许多其他游戏内指标。

基本上它将是一个基于主索引的单一查询,仅此而已。

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

2.2 功能:排行榜

排行榜 Figma 文件

现在我们的主要目标是:一个超酷的排行榜,在良好的数据建模之后,你无需再关心它。排行榜是按歌曲排列的,所以每次你播放一首特定的歌曲,你的最佳成绩都会被保存并排名。

然而,这个界面有一个很大的特点,那就是有过滤器来准确知道要带来“哪个”排行榜:

  • song_id:必填
  • 仪器:必需
  • 修饰符:必需
  • 难度:必需
  • player_id:可选
  • 分数:可选

想象一下我们的查询看起来像这样,它返回按分数降序排序的结果:

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
----------------+-------
Enter fullscreen mode Exit fullscreen mode

现在我们知道了这里将要实现的功能,但是您能想象最终的模式会是什么样子吗?

没有?好吧,我来帮你!

3.数据建模时间!

是时候深入研究使用 ScyllaDB 进行数据建模并更好地了解如何扩展它了。

3.1 - 匹配建模

游戏结束屏幕

首先,让我们进一步了解一下游戏本身:

  • 这是一款节奏游戏;
  • 你一次播放一首特定的歌曲;
  • 您可以在游戏前激活“修改器”来让您的生活变得更轻松或更困难;
  • 您必须选择一种乐器(例如吉他、鼓、贝斯和麦克风)。
  • 游戏的每个方面都会被追踪,例如:
    • 分数;
    • 错过笔记;
    • 超速次数;
    • 播放速度(1.5倍~1.0倍);
    • 游戏日期/时间;
    • 还有其他很酷的东西。

考虑到这一点,我们可以轻松地开始我们的数据建模,它将变成如下的样子:

CREATE TABLE IF NOT EXISTS leaderboard.submissions (
    submission_id uuid,
    track_id text,
    player_id text,
    modifiers frozen<set<text>>,
    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)
);
Enter fullscreen mode Exit fullscreen mode

让我们跳过所有int/text值并跳转到set<text>

集合类型允许你存储特定类型的项目列表。我决定用这个列表来存储修饰符,因为它非常适合。看看查询是如何执行的:

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'
);
Enter fullscreen mode Exit fullscreen mode

使用此类型,您可以轻松存储项目列表以供稍后检索。

另一个很酷的信息是,这个查询是一个键值对查询!这是什么意思?

由于您总是通过submission_id唯一方式进行查询,因此它可以被归类为键值。

3.2 排行榜建模

排行榜过滤器 Figma

在本文的这一部分,您将了解一些很酷的宽列数据库概念。

在我们的排行榜查询中,如前所述,我们总是需要 WHERE 子句中的一些动态值,这意味着这些值将属于分区键,聚类键将具有可以“可选”的值。

分区键基于你添加的字段组合的哈希值,用于标识某个值。你明白了吗?还是不明白?好吧,我也花了一段时间才明白,不过我先给你演示一下:

假设你玩了Starlight - Muse100 次。如果你查询这些信息,将会得到 100 倍不同的结果,这些结果通过类似score或 的聚类键进行区分player_id

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

如果有 1,000,000 名玩家播放这首歌曲,您的查询将变得缓慢,并且将来会成为一个问题,因为您的分区键仅包含一个字段,即track_id

但是,如果您在分区键中添加更多字段,例如在玩游戏之前必须输入的内容,也许我们可以减少这些可能性,从而提高查询速度。现在您明白了吗?添加诸如“乐器”“难度”“修改器”之类的字段,可以使您能够均匀地划分有关特定曲目的信息。

让我们用一些简单的数字来想象:


-- 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;
Enter fullscreen mode Exit fullscreen mode

因此,如果您以特定形状构建查询,它将始终查找特定的标记并根据这些特定的分区键检索数据。

让我们看一下最终的建模并讨论聚类键和应用层:

CREATE TABLE IF NOT EXISTS leaderboard.song_leaderboard (
    submission_id uuid,
    track_id text,
    player_id text,
    modifiers frozen<set<text>>,
    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);
Enter fullscreen mode Exit fullscreen mode

分区键的定义如上所述,由我们所需的参数组成,例如:track_id、modifiers、difficulty 和 instrument。在聚类键上,我们添加了scoreplayer_id

请注意,默认情况下,聚类字段是按以下顺序排序的score DESC,并且如果玩家得分相同,则选择获胜者的标准将是alphabetical¯\ (ツ) /¯。

首先要理解的是,每个玩家只有一个分数,但是通过这样的建模,如果玩家以不同的分数两次通过相同的赛道,就会生成两个不同的条目。

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
----------------+-------
Enter fullscreen mode Exit fullscreen mode

那么我们该如何解决这个问题呢?嗯,这本身不是一个问题,而是一个功能!哈哈

作为开发人员,你必须根据项目需求创建自己的业务规则,这也不例外。这是什么意思呢?

您可以在插入新条目之前运行一个简单的DELETE查询,并保证在该特定的分区键组内,不会有来自player_id的特定数据低于新分数

-- 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...
Enter fullscreen mode Exit fullscreen mode

这样我们就完成了简单的排行榜系统,该系统在 YARG 中运行,也可以用于每秒数百万次输入的游戏 :D

4. 如何为 YARG 做出贡献

以下是我邀请您为这个精彩的开源项目做出贡献的文本部分!

今天,我们正在为所有玩家构建一个全新的平台,使用:

我们需要尽可能多的开发人员和测试人员与主要贡献者一起讨论游戏的未来实现!

YARG Discord

首先,一定要加入他们的Discord 社区。所有技术讨论都会在社区后台进行,之后才会进入开发板。

此外,除了 Discord 之外,YARG 社区主要关注EliteAsian(核心贡献者和项目所有者)的 Twitter 账号,用于展示开发成果。也请务必关注他。

仅供参考,该游戏的首席艺术家Kadu也是Elgato广播专家产品创新开发人员,曾与以下主播合作:

  • 忍者
  • 纳德肖特
  • 石山64
  • 以及传奇 DJ Marshmello。

Kadu 也在他的推特上分享了一些关于 YARG 新功能和实验的见解和早期预览。所以,别忘了在推特上关注他!

以下是一些有用的链接,可帮助您了解有关该项目的更多信息:

有趣的是:YARG 引起了《吉他英雄》项目负责人Brian Bright的注意,他很喜欢这个项目的开源特性。太棒了,对吧?

5. 结论

数据建模有时很有挑战性,这项研究花了 3 个月的时间,涵盖了许多新的 ScyllaDB 概念,并与我在 Twitch 的社区一起进行了大量测试。

我还发布了一个游戏排行榜演示,您可以从中获得有关如何使用NextJSScyllaDB实现同一项目的一些见解

此外,如果您喜欢 ScyllaDB 并想了解更多信息,我强烈建议您观看我们的免费大师班课程或访问ScyllaDB 大学

别忘了点赞这篇文章、在社交媒体上关注我,然后装满你的水瓶 xD

下篇文章再见!

在 Twitter 上关注我
在 Github 上关注我
在 Github 上关注我
在我的 Twitch 频道上关注和订阅

文章来源:https://dev.to/scylladb/database-101-how-to-model-leaderboards-for-1m-players-game-2pfa
PREV
数据库 101:面向初学者的 SSL/TLS
NEXT
学习如何高效学习