roblox-datastores について
Robloxのサーバー開発者向けに、DataStoreServiceでプレイヤーの進行状況やランキングを保存する手引きです。エラーを考慮した読み書き、再試行、退出時や終了時の保存を扱い、データ損失や上書きの防止に役立ちます。
主な機能
- 01参加時の読み込みと退出時の保存
- 02UpdateAsyncによる安全なデータ更新
- 03pcallを使った失敗処理と再試行
- 04サーバー終了時の参加者データ保存
- 05OrderedDataStoreでのランキング作成
活用シーン
- 01コイン、所持品、レベルの永続化
- 02複数サーバー間の上書き防止
- 03一時的な失敗やスロットリングへの対応
- 04ランキングデータの取得と表示
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ gamedev-skills/ awesome-gamedev-agent-skills --skill roblox-datastoresインストール後、Claude Code で/roblox-datastoresを入力して読み込まれたか確認します。
同梱ファイルreferences/sessions-and-limits.md
詳細
Roblox DataStores
DataStoreService を使って Roblox でセッションをまたいでデータを永続化します。参加時の読み込み、退出時とシャットダウン時の保存、安全な更新、再試行、リーダーボード用の順序付きストアに対応します。サーバー側専用です。
使用する場面
- プレイヤーの進行状況(コイン、インベントリ、レベル)の保存や読み込み、永続的なリーダーボードの作成、データ損失、上書き、スロットリングの修正に使用します。
- サーバーコードで
DataStoreService、GetDataStore、GetAsync、SetAsync、UpdateAsync、またはGetOrderedDataStoreを呼び出す場合に使用します。
使用しない場面: 一般的なスクリプト、サービス、リモート、クライアントとサーバーの分担 → roblox-luau。高頻度の一時的な状態(マッチメイキング、ラウンド単位)→ メモリストア(別のサービス)。エンジンに依存しない永続化の理論 → save-systems。
基本的なワークフロー
- Studio のアクセスを一度有効にします。 File → Game Settings → Security → Enable Studio Access to API Services(テスト用の場所を使ってください。Studio はライブデータにアクセスします)。DataStore はサーバーの
Scriptからのみ使用でき、LocalScriptからは使用できません。 - ストアを取得してから、キーを使って読み書きします。
DataStoreService:GetDataStore("Name")を使います。プレイヤーごとのキーには通常"Player_" .. player.UserIdを使います。 - すべての呼び出しを
pcallで囲みます。GetAsync/SetAsync/UpdateAsyncは失敗する可能性があるネットワーク呼び出しです。保護されていない呼び出しが失敗するとスレッドがエラーになり、データ損失の危険があります。 PlayerAddedで読み込み、PlayerRemovingで保存し、さらにBindToCloseも使います。 プレイヤーの退出時とサーバーのシャットダウン時の両方で、最後に保存する必要があります。- 読み取りと変更を伴う書き込みには、
SetAsync(無条件の上書き)よりもUpdateAsync(複数サーバー間で安全)を優先します。 読み込みに失敗した場合は、デフォルト値で上書きしないでください。保存を中止し、既存のデータを消さないようにします。 - ランキングデータ(リーダーボード)には、
GetSortedAsyncを使うOrderedDataStoreを使用します。 参加し、データを変更してから再参加し、データが保持されていることを確認してテストします。
パターン
1. 参加時に読み込む(pcall で保護)
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("PlayerData")
local DEFAULT = { Coins = 0, Level = 1 }
Players.PlayerAdded:Connect(function(player)
local key = "Player_" .. player.UserId
local ok, data = pcall(function()
return store:GetAsync(key)
end)
if not ok then
-- Load FAILED (network). Do not treat as a new player; flag so we never save
-- over their real data with defaults.
warn("Load failed for", player.Name, data)
player:SetAttribute("DataLoaded", false)
return
end
player:SetAttribute("DataLoaded", true)
local profile = data or DEFAULT -- nil == genuinely new player
applyToLeaderstats(player, profile)
end)
2. UpdateAsync で保存する(複数サーバー間で安全)
-- UpdateAsync reads the latest value, then writes what the callback returns.
-- The callback MUST NOT yield (no task.wait, no further Async calls inside it).
local function savePlayer(player)
if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
local key = "Player_" .. player.UserId
local newData = gatherDataFor(player) -- a plain table of serializable values
local ok, err = pcall(function()
store:UpdateAsync(key, function(old)
-- merge/decide here; return nil to cancel the write
return newData
end)
end)
if not ok then warn("Save failed for", player.Name, err) end
end
3. 退出時とシャットダウン時に保存する
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- It has a limited time budget, so save in parallel and yield until done.
game:BindToClose(function()
local players = Players:GetPlayers()
local remaining = #players
if remaining == 0 then return end
for _, player in players do
task.spawn(function()
savePlayer(player)
remaining -= 1
end)
end
while remaining > 0 do task.wait() end
end)
4. バックオフ付きで再試行する(一時的な障害)
local function withRetry(fn, attempts)
attempts = attempts or 3
for i = 1, attempts do
local ok, result = pcall(fn)
if ok then return true, result end
if i < attempts then task.wait(2 ^ i) end -- 2s, 4s, ... backoff
end
return false
end
local ok, data = withRetry(function() return store:GetAsync(key) end)
5. カウンターを増加させる
-- IncrementAsync is a convenience for integer read-modify-write (still wrap it).
local ok, newTotal = pcall(function()
return store:IncrementAsync("Visits_" .. player.UserId, 1)
end)
6. OrderedDataStore を使ったリーダーボード
local boards = DataStoreService:GetOrderedDataStore("Coins")
-- Write a player's score (call when it changes, not every frame).
pcall(function() boards:SetAsync("Player_" .. player.UserId, coins) end)
-- Read the top 10, descending.
local ok, pages = pcall(function()
return boards:GetSortedAsync(false, 10) -- ascending=false → highest first
end)
if ok then
for rank, entry in ipairs(pages:GetCurrentPage()) do
print(rank, entry.key, entry.value) -- entry.value is the number
end
end
注意点
- 未処理の失敗によって進行状況が消える → Async の呼び出しは必ず
pcallで保護してください。読み込みに失敗した場合はセッションに印を付け、デフォルト値が実データを上書きしないよう、保存を拒否します。 - サーバー間で
SetAsyncが競合する → 2 つのサーバーが同じキーを書き込むと、互いのデータを上書きする可能性があります。各書き込みが最新の値を参照できるよう、読み取りと変更を伴う書き込みにはUpdateAsyncを使用します。 UpdateAsyncのコールバック内で待機する → コールバックからtask.waitや他の Async 関数を呼び出すことはできません。新しい値を事前に計算して返してください。BindToCloseで保存しない → シャットダウン時にサーバーにいるプレイヤーは、未保存の進行状況を失います。game:BindToCloseを追加し、その制限時間内に保存が完了するまで待ってください。- スロットリング/「リクエストが多すぎます」 → キーごとおよび分ごとの制限を守り、値が変わるたびに保存しないでください。まとめて、タイマーまたは退出時に保存します。
GetAsyncの結果は短時間キャッシュされるため、直後の再読み込みでは古い値が返ることがあります。 - シリアライズできない値を保存する → 永続化できるのは JSON にシリアライズ可能なデータのみです。数値、文字列、ブール値、およびキーが文字列または数値のテーブルが対象です。
Instance、Vector3、CFrame、関数は保存できないため、先に通常のテーブルにシリアライズしてください。 - API アクセスなしでテストする → Enable Studio Access to API Services が有効になるまで、Studio では DataStore を使用できません(また、
LocalScriptからも使用できません)。 - 順序付きストアで
DataStoreKeyInfoが nil になる →OrderedDataStoreはバージョン管理やメタデータに対応していません。これらが必要な場合は通常のDataStoreを使用してください。
参考資料
- セッションロック(サーバー間でデータが重複するのを防ぐ)、
DataStoreSetOptionsを使ったバージョン管理/メタデータ、順序付きストアのページネーション(AdvanceToNextPageAsync)、キーのエラーコードとリクエスト制限、および忘れられる権利への準拠については、references/sessions-and-limits.mdをお読みください。
関連スキル
roblox-luau— サービス、インスタンス、イベント、サーバー/クライアントモデル。save-systems— エンジンに依存しないシリアライズ、スロット、移行。
コメント
Markdown 対応