リーダーボードをMomentoで実装する

リーダーボードをMomentoで実装する

Clock Icon2025.07.10

Introduction

オンラインゲームでは、プレイヤーのスコア更新やランキング表示などがリアルタイムに求められます。しかし、大量のプレイヤーデータを高速に処理するためのキャッシュ実装や、Pub/Subを利用したリアルタイム更新には、運用負荷やコストの課題があります。

この記事では、そういった課題に対して、データキャッシュサービスのMomentoを活用してリーダーボードのような機能を実装する方法を紹介します。

Comparing

従来技術との比較

従来のデータキャッシュソリューションと比較すると以下のような違いがあります。

項目 既存サービス Momento
コストモデル 時間課金(インスタンス稼働時間) 従量課金(使用した分だけ)
運用負荷 クラスター管理/スケーリング設定が必要 完全マネージド&自動スケーリング
可用性 レプリケーション設定など必要 標準で高可用性を実現
セットアップ時間 数時間 数分(APIキー取得のみ)

Momentoでは?

Momentoはフルマネージドな従量課金型サービスとして以下のような機能を提供します。

  • すぐ利用開始:APIキーを取得するだけで利用可能
  • 運用負荷ゼロ:パッチ適用、バックアップ、スケーリングが自動
  • コスト効率:使っていないときの料金が発生しない

本記事では、Momentoのキャッシュ機能を活用したリーダーボードのサンプル実装について解説します。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS : MacOS 14.5
  • Node : 20.19.0

必要なパッケージのインストール

Momentoモジュールをnpmでインストールしておきます。

# npmを使用する場合
% npm install @gomomento/sdk

APIキー取得

Momentoを使用するには、Momento ConsoleでAPIキーを生成する必要があります。
キーの作成はここで解説されてます。
参考にして作成し、JSONファイルをダウンロードしておいてください。

Try

では、よくあるリーダーボード的なものを実装してみましょう。
今回はMomentoのSortedSetを使ってみます。
MomentoのSortedSetコレクションは、RedisのSorted Setと同様の機能を持っており、
スコアに基づいて要素を自動的にソートします。
これにより、実装側で複雑な処理を行わずに済むため、実装がシンプルになります。

実装の全体像

以下のコードはリーダーボード実装例です。

import { 
  CacheClient, 
  CacheSortedSetPutElementResponse, 
  CacheSortedSetFetchResponse, 
  CreateCache,
  SortedSetOrder
} from "@gomomento/sdk";
import { promises as fs } from "fs";

interface MomentoKeyInfo {
  apiKey: string;
  refreshToken: string;
  validUntil: number;
  restEndpoint: string;
}

// 認証情報をJSONファイルから読み込み
async function loadMomentoKeyInfo(path: string): Promise<MomentoKeyInfo> {
  const data = await fs.readFile(path, "utf8");
  return JSON.parse(data);
}

// SortedSet にスコアを追加
async function updateScoreSortedSet(client: CacheClient, cacheName: string, player: string, score: number): Promise<void> {
  const addResponse = await client.sortedSetPutElement(cacheName, "leaderboard", player, score);

  switch (addResponse.type) {
    case CacheSortedSetPutElementResponse.Success:
      console.log(`Added/updated ${player} with score ${score}`);
      break;
    case CacheSortedSetPutElementResponse.Error:
      console.error(`Failed to add ${player}:`, addResponse.message());
      break;
  }
}

// SortedSet から上位 N 位を取得
async function displayLeaderboard(client: CacheClient, cacheName: string, topN: number): Promise<void> {
  const rangeResponse = await client.sortedSetFetchByRank(
    cacheName,
    "leaderboard",
    {
      startRank: 0,
      endRank: topN - 1,
      order: SortedSetOrder.Descending
    }
  );

  switch (rangeResponse.type) {
    case CacheSortedSetFetchResponse.Hit:
      console.log("Top Leaderboard:");
      const elements = rangeResponse.value();
      elements.forEach((element, index) => {
        console.log(`${index + 1}. ${element.value} - ${element.score}`);
      });
      break;
    case CacheSortedSetFetchResponse.Miss:
      console.log("Leaderboard not found");
      break;
    case CacheSortedSetFetchResponse.Error:
      console.error("Failed to retrieve leaderboard:", rangeResponse.message());
      break;
  }
}

async function main(): Promise<void> {
  // 認証情報load
  const keyInfo = await loadMomentoKeyInfo("./momento_key_info.json");

  // 環境変数に認証情報を設定
  process.env.MOMENTO_API_KEY = keyInfo.apiKey;

  // CacheClient生成
  const cacheClient = await CacheClient.create({
    defaultTtlSeconds: 300,  // 5分間キャッシュ保持
  });

  const cacheName = "my-cache";

  // リーダーボード用のキャッシュ作成
  const createCacheResponse = await cacheClient.createCache(cacheName);
  if (createCacheResponse instanceof CreateCache.Success) {
    console.log("Cache created successfully");
  } else if (createCacheResponse instanceof CreateCache.AlreadyExists) {
    console.log("Cache already exists");
  } else if (createCacheResponse instanceof CreateCache.Error) {
    console.error("Failed to create cache:", createCacheResponse.message());
  }

  // デモ用プレイヤーデータ(スコアは適当)
  const players = [
    { name: "Taro", score: Math.floor(Math.random() * 1000) },
    { name: "Hanako", score: Math.floor(Math.random() * 1000) },
    { name: "Mike", score: Math.floor(Math.random() * 1000) },
    { name: "Syuta", score: Math.floor(Math.random() * 1000) },
    { name: "Hoge", score: Math.floor(Math.random() * 1000) },
  ];

  // 各プレイヤーのスコアを SortedSet に登録
  for (const { name, score } of players) {
    await updateScoreSortedSet(cacheClient, cacheName, name, score);
  }

  // 上位5名のリーダーボードを表示
  await displayLeaderboard(cacheClient, cacheName, 5);

  await cacheClient.close();
}

main().catch((e) => {
  console.error("Error occurred:", e);
});

実行してみると以下のようになります。

% npx ts-node sorted_set.ts

Added/updated Taro with score 385
Added/updated Hanako with score 462
Added/updated Mike with score 573
Added/updated Syuta with score 989
Added/updated Hoge with score 590

Top Leaderboard:
1. Syuta - 989
2. Hoge - 590
3. Mike - 573
4. Hanako - 462

MomentoのAPIだけでリーダーボードデータの取得が完了しています。
楽ですね。

コード解説

スコアの更新

const addResponse = await client.sortedSetAdd("leaderboard", player, score);

sortedSetAddは、プレイヤー名とスコアを受け取り、
SortedSetに追加/更新します。
同じプレイヤーが既に存在する場合は、スコアが自動で更新されます。

ランキングの取得

const rangeResponse = await client.sortedSetRange(
  "leaderboard",
  0,
  topN - 1,
  //降順
  { order: SortedSetRangeOrder.Descending }
);

sortedSetRangeメソッドで、指定した範囲のランキングを取得できます。
ここでは降順を指定してスコアの高い順に結果が返すようにしています。

簡単に処理が記述できることがわかりました。
さきほどもいったように、Momentoでは自動スケールにより大量のアクセスにも対応しており、管理も不要です。
速度も高速なため、リアルタイム性を求められる製品ではとても有用です。

また、Momento Topicsを使えばリアルタイムなデータ配信やイベント通知も実現できます。

こういったシステムにおいては、スコアが更新されたタイミングですぐにユーザーに情報を配信したり、最新のランキング状況を反映させることができます。

Summary

本記事では、MomentoのSortedSetを使ったリーダーボードのサンプルについて解説しました。
Momentoの活用により、高速なリーダーボードを低コストで実現できます。
ぜひお試しください。

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.