roblox-data-stores について
Roblox 開発者向けに、プレイヤーデータの永続化、一時的なサーバー間状態、ランキング、メッセージングを扱うスキルです。サービスの選択からセッションロック、再試行、移行、各種制限まで説明します。
主な機能
- 01Roblox ストレージサービスの選択
- 02ProfileStore によるセッションロック
- 03DataStore の書き込み・再試行・移行
- 04OrderedDataStore のランキング設計
- 05MemoryStore のキューと一時状態
- 06MessagingService の利用上の注意
活用シーン
- 01プレイヤーの進行状況や所持品の保存
- 02全体・期間別ランキングの作成
- 03サーバー間のマッチメイキングキュー管理
- 04サーバー間アナウンスの送信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-data-storesインストール後、Claude Code で/roblox-data-storesを入力して読み込まれたか確認します。
同梱ファイルreferences/memory-and-messaging.mdreferences/raw-datastore.md
詳細
Roblox データストアとクラウドサービス
サービスの選択
| 用途 | サービス | 備考 |
|---|---|---|
| プレイヤーの進行状況、インベントリ、設定 | ProfileStore 経由の DataStore(セッションロック) | プレイヤーごとに 1 キー(User_{UserId})、4 MB 以下。 |
| グローバルランキング | OrderedDataStore | 整数値のみ。定期的な更新、読み取りはキャッシュされます。 |
| 高速で一時的な、サーバー間の状態 | MemoryStoreService | ソート済みマップ、キュー、ハッシュマップ。すべて有効期限があります(TTL ≤ 45 日)。 |
| マッチメイキングキュー、ライブイベント、サーバーリスト | MemoryStore のキュー / ソート済みマップ | 永続的ではありません。貴重なデータの唯一のコピーにしないでください。 |
| すべてのサーバーへのブロードキャスト | MessagingService | メッセージあたり 1 kB 以下、ベストエフォート、配信保証なし。 |
| 公開せずにライブオペレーションで変更する値 | Experience configs(ConfigService) | roblox-analytics-liveops を参照してください。 |
プレイヤーデータ: セッションロックを使用する
2 つのサーバーが同時に同じプレイヤーのデータを所有してはいけません(テレポート、素早い再参加)。そうしないとアイテムが複製され、進行状況が失われます。ProfileStore(loleris)はセッションロック、自動保存、BindToClose 時のフラッシュを実装しており、推奨されるデフォルトです。Wally / Creator Store からインストールし、ServerScriptService に配置してください。
-- ServerScriptService/Server/Services/DataService.luau (ProfileStore)
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.ServerPackages.ProfileStore)
local TEMPLATE = {
version = 1,
coins = 0,
inventory = {} :: { [string]: number },
}
type PlayerData = typeof(TEMPLATE)
local PlayerStore = ProfileStore.New("PlayerData", TEMPLATE)
local profiles: { [Player]: typeof(PlayerStore:StartSessionAsync("")) } = {}
local function onPlayerAdded(player: Player)
local profile = PlayerStore:StartSessionAsync(`User_{player.UserId}`, {
Cancel = function()
return player.Parent ~= Players -- stop waiting if the player left
end,
})
if profile == nil then
player:Kick("Could not load your data. Please rejoin.")
return
end
profile:AddUserId(player.UserId) -- GDPR / right-to-be-forgotten linkage
profile:Reconcile() -- fill in fields added to TEMPLATE since this profile was created
profile.OnSessionEnd:Connect(function()
profiles[player] = nil
player:Kick("Your data was loaded on another server. Please rejoin.")
end)
if player.Parent == Players then
profiles[player] = profile
else
profile:EndSession() -- left while loading
end
end
for _, player in Players:GetPlayers() do
task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(function(player)
local profile = profiles[player]
if profile then
profile:EndSession() -- saves and releases the lock
end
end)
local DataService = {}
function DataService.get(player: Player): PlayerData?
local profile = profiles[player]
return profile and profile.Data
end
return DataService
サーバー上で profile.Data を直接変更してください(メモリにキャッシュされ、自動保存されます)。変更のたびに SetAsync しないでください。あまり一般的でない機能(グローバル更新、MessageAsync、Studio 用のモックストア)に依存する前に、ProfileStore のドキュメントで正確な最新 API を確認してください。
生の DataStoreService のルール(ラッパーを使用しない場合)
- メモリにバッファリングする: 参加時に一度読み込み、インメモリのテーブルを変更し、定期的(例: ランダムなジッターを加えて 2〜5 分ごと)、退出時、チェックポイント時(購入)、および
game:BindToCloseで保存します。 - 書き込みが古い値に依存する場合、または複数のサーバーがそのキーに書き込む可能性がある場合は、
SetAsyncよりもUpdateAsyncを使用します。変換関数は純粋で高速(yield なし)でなければなりません。キャンセルするにはnilを返します。 - 一時的なエラーをリトライする: 指数バックオフ + ジッター、試行回数の上限を設けます。あるキーのリトライは順番に処理します(古いリトライが新しいデータを上書きしてはいけません)。失敗した書き込みは「結果不明」として扱います。
Instance、Vector3、CFrame、関数、混在した/疎なテーブルを保存しないでください。保存するのはプレーンなテーブル、数値、文字列、真偽値、bufferです。データ型をシリアライズします({ x, y, z })。UTF-8 文字列を検証します。- キーとデータストア名は 50 文字以下、値は JSON エンコード後に 4,194,304 文字以下です。
- 安定したキーパターン(
User_{UserId})を使用し、表示名は決して使わないでください。データストアは少なく、プレイヤーごとのキーも少なくします。 - Studio では File → Experience Settings → Security → Enable Studio Access to API Services が必要です。テスト用に別のユニバースまたは別のデータストア名を使用し、テストが本番データに触れないようにしてください。
完全な生の実装(セッションロック、自動保存、BindToClose、リトライ): references/raw-datastore.md。
予算と制限(2026)
| 制限 | 値 |
|---|---|
| Experience 全体の標準読み取り / 書き込み | 1 分あたり 300 + 40 × CCU / 300 + 20 × CCU(Open Cloud と共有) |
| サーバーごとのデフォルト読み取り/書き込み | それぞれ 1 分あたり 60 + 40 × players(設定可能) |
| サーバーごとの Ordered 書き込み | 1 分あたり 30 + 5 × players |
| キーごとのスループット | 読み取り 25 MB/分、書き込み 4 MB/分 |
| ストレージ(最新バージョン、圧縮済み) | Experience ごとに 500 MB + 1 MB × 累計ユーザー数 |
| リクエストタイプごとのキュー | 保留中のリクエストが 30 件、その後エラー 301〜306 |
UpdateAsyncは読み取りと書き込みの両方の予算を消費します。- 一括処理の前に
DataStoreService:GetRequestBudgetForRequestType(Enum.DataStoreRequestType.StandardWrite)を確認してください。起動時に一度SetRateLimitForRequestTypeでサーバーごとの制限を調整してください。 - データを事前に圧縮しないでください。Roblox が自動的に圧縮します。シャーディングは本当にホットなキーにのみ使用してください。
- バックアップキーに書き込む代わりに、バージョン(
ListVersionsAsync、GetVersionAtTimeAsync)を使用してデータを復元してください。
スキーマのバージョン管理とマイグレーション
version フィールドを保存します。読み込み時に、データが現在のバージョンに達するまで、順番にマイグレーション関数を実行し、その後デフォルト値を調整します。書き込みを停止するのと同じリリースでフィールドを削除してはいけません。また、異なる意味でフィールド名を再利用してはいけません。
--!strict
type Data = { [string]: any }
local MIGRATIONS: { (Data) -> () } = {
[1] = function(data) -- v1 -> v2: "gold" renamed to "coins"
data.coins = data.gold or 0
data.gold = nil
end,
[2] = function(data) -- v2 -> v3: inventory list -> counts
local counts: { [string]: number } = {}
for _, itemId in data.inventory or {} do
counts[itemId] = (counts[itemId] or 0) + 1
end
data.inventory = counts
end,
}
local CURRENT_VERSION = #MIGRATIONS + 1
local function migrate(data: Data): Data
local version = data.version or 1
while version < CURRENT_VERSION do
MIGRATIONS[version](data)
version += 1
end
data.version = CURRENT_VERSION
return data
end
print(migrate({ gold = 5, inventory = { "Sword", "Sword" } }))
リーダーボード(OrderedDataStore)
- 値は整数である必要があります。書き込みは一定のスケジュールに従うか、意味のある変更があったときに行い、増分のたびには行わないでください。
- 1つのサーバーループ(例:60秒ごと)で
GetSortedAsync(false, 100)を使って上位N件を読み込み、結果をキャッシュしてクライアントにレプリケートします。すべてのサーバーやクライアントが常に読み込み続ける状態にしないでください。 - 「今週」のボードでは、ストア名に期間を含める(
Wins_2026W39)か、MemoryStoreのソート済みマップを使用します。 - Robloxネイティブのリーダーボード(完全リリース済み):Creator Hub(ゲーム →
リーダーボード)で
OrderedDataStoreを登録し、「Active leaderboard」を切り替えて有効にします。Robloxはプレイヤーリストに、永続的な「Global」タブと「Friends」タブを表示します。ストアが引き続き信頼できる唯一の情報源となります(ゲームごとに有効なボードは1つ)。タブが正しく表示されるよう、参加時に各プレイヤーのスコアを初期化します。
MemoryStoreの基本
- クォータはエクスペリエンス全体で適用されます:メモリは
64 KB + 1.2 KB × users、リクエストは毎分1000 + 120 × CCU単位です。 MemoryStoreSortedMap:ランキングデータ、サーバーブラウザー、ライブリーダーボード(有効期限を指定したSetAsync、GetRangeAsync、UpdateAsync)。MemoryStoreQueue:マッチメイキング/作業キュー(AddAsync、ReadAsync。処理の成功後、返されたIDを使ってRemoveAsyncを実行し、クラッシュに備えて不可視タイムアウトを設定します)。MemoryStoreHashMap:ソートを必要としない高ボリュームのキーと値(SetAsync、GetAsync、UpdateAsync、ListItemsAsync)。- 必ず有効期限を設定し、スロットリングに対処し(
pcall+再試行)、アイテムのサイズを小さく保ちます。
MessagingServiceの基本
- メッセージは1 kB以下です。サーバーごとの上限は、毎分
600 + 240 × players件の送信、20 + 8 × players件のサブスクリプションです。 - 配信はベストエフォートであり、順序も保証されません。「キャッシュを更新してください」などの通知やグローバル告知に使用し、信頼できる唯一の情報源としては決して使わないでください。状態の管理にはDataStore/MemoryStoreと組み合わせます。
SubscribeAsync/PublishAsyncはpcallで囲みます。失敗したり、処理が中断されたりすることがあります。
その他のコード(MemoryStoreのキューマッチメイキング、サーバー間の告知、RTBF):references/memory-and-messaging.md。
忘れられる権利
静的なキーのパターンを使用すると、Creator Dashboardで設定された自動RTBFで、PlayerData/User_{userId}のようなテンプレートに従うプレイヤーデータを削除できます。そうでない場合は、消去権のWebhook(roblox-open-cloudを参照)を処理します。キーをユーザーIDに関連付けるには、SetAsync(key, value, { userId })を使うか、UpdateAsyncの変換関数から2つ目の値として返すか、ProfileStoreのAddUserIdを呼び出します。
関連スキル
roblox-security(複製やトレードの悪用)、roblox-monetization(レシート)、roblox-open-cloud(外部アクセス、一括操作)、roblox-teleport-matchmaking。