【Roblox】MemoryStore SortedMapを利用したグローバルランキング表示
はじめに
今回は、MemoryStoreのデータ構造の一つであるSortedMapを利用して、エクスペリエンス全体(サーバーの隔たりがない)のランキングの作成方法を紹介しようと思います。
MemoryStoreの概要については以下の記事をご覧ください。
DataStoreを利用したランキングの実装もあります。それぞれ一長一短なので、ぜひご覧ください。
Roblox バージョン: 0.661.0.6610708
MemoryStoreSortedMapについて
MemoryStoreのデータ構造は、SortedMap、Queue、HashMapと3種類あります。
共通して、設定するデータには有効期限があり最大で45日です。
利用するにあたって各種制限などもありますが、これは概要編にて記載したので省きます。
詳しく知りたい方は、冒頭の「MemoryStoreの概要」リンクからご覧いただけます。
このデータ構造の一つであるSortedMapでは、MemoryStoreSortedMapインスタンスを利用してデータをソートした状態で一時保存することができます。
また、Queueとは異なりSortedMapは登録順ではなくソートキーの順序で管理されるため、ランキング作成に適しています。
SortedMapを利用したランキング機能の実装の一例を紹介します。
SortedMapを管理するモジュールスクリプトを作成する
まずは、SortedMapを管理するモジュールスクリプトを作成します。
MemoryStoreSortedMapの取得
モジュールスクリプトを作成して、MemoryStoreSortedMap
を取得します。
-- MemoryStore SortedMap の管理
-- Service
local MemoryStoreService = game:GetService("MemoryStoreService")
-- 保存期間 一週間
local EXPIRATION = 86400 * 7
-- 使用するSortedMap
local sortedMap = MemoryStoreService:GetSortedMap("ScoreRanking")
local SortedMapManager = {}
return SortedMapManager
MemoryStoreSortedMap
のインスタンスを作成、取得するにはMemoryStoreService:GetSortedMap() を使用します。
GetSortedMap function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | name | string | MemoryStoreSortedMap |
戻り値 | MemoryStoreSortedMap | MemoryStoreSortedMap | 指定された名前のMemoryStoreSortedMapインスタンス。 |
これに加え、今回はデータの保存期間を仮として1週間とし定数で保持します。
データの取得
データを取得するにはMemoryStoreSortedMap:GetAsyncを利用します。
-- SortedMapに登録された値とソートキーを返す
function SortedMapManager:GetScore(player: Player): (string?, number?)
local key = tostring(player.UserId)
local success, value, sortKey = pcall(function()
return sortedMap:GetAsync(key)
end)
if success and value and sortKey then
print("GetAsync Success", key, value, sortKey)
else
print("GetAsync Error or No Data", key)
end
return value, sortKey
end
GetAsync function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | key | string | 取得したいデータのキー |
戻り値 | value, sortKey | Tuple(Variant, Variant) | キーに対して保存された値とソートキーを返す。キーに対して保存されたデータがない場合は、それぞれnilになる。 |
戻り値は設定された値とソートキーが返されます。
データを範囲で取得
データを範囲指定で取り出したい場合は、MemoryStoreSortedMap:GetRangeAsync() を使用します。
-- SortedMapに登録されたキー、値、ソートキーを範囲で取り出しテーブルで返す
function SortedMapManager:GetScoreInRange(
direction: Enum.SortDirection,
count: number
): { { key: string, value: string, sortKey: number } }
local exclusiveLowerBound = { sortKey = 0 }
local exclusiveUpperBound = nil
local success, items = pcall(function()
return sortedMap:GetRangeAsync(direction, count, exclusiveLowerBound, exclusiveUpperBound)
end)
if success and items then
print("GetRangeAsync Success", items)
return items
else
print("GetRangeAsync Error or No Data")
return {} -- 失敗時は空のテーブルを返す
end
end
GetRangeAsync function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | direction | Enum.SortDirection | 取得する方向。昇順or降順。 |
引数 | count | number | 取得したい個数。最大値は200。 |
引数 | exclusiveLowerBound | { key:string, sortKey: Variant } or nil | 返されるキー、ソートキーの下限を設定できる。(自身は含まれない。) |
引数 | exclusiveUpperBound | { key:string, sortKey: Variant } or nil | 返されるキー、ソートキーの上限を設定できる。 (自身は含まれない。) |
戻り値 | key, value, sortKeyの配列 | Array | キー、値、ソートキーを1要素としたカウント数分の配列 |
戻り値の配列は下記のような形になります。
{
[1] = {
["key"] = "1",
["sortKey"] = 8352,
["value"] = "Player3"
},
[2] = {
["key"] = "2",
["sortKey"] = 7518,
["value"] = "Player1"
},
[3] = {
["key"] = "3",
["sortKey"] = 3397,
["value"] = "Player2"
},
-- ... (指定した個数分データが続く)
}
引数のexclusiveLowerBound
とexclusiveUpperBound
を指定すると、取得するデータの範囲を制限できます。指定したキー・ソートキー自体は取得結果に含まれません。
例)local exclusiveLowerBound = { sortKey = 0 }
と設定した場合、1以上のデータが対象となる。
特に指定しない場合はnilを指定しましょう。
データを設定・保存する
データを設定したい場合は、MemoryStoreSortedMap:SetAsync() を利用します。
-- SortedMapにスコアを登録する
function SortedMapManager:SetScore(player: Player, score: number): boolean
local key = tostring(player.UserId)
local value = player.Name
local success, result = pcall(function()
return sortedMap:SetAsync(key, value, EXPIRATION, score)
end)
if success and result then
print("SetAsync Success", key, value, score)
return true
else
print("SetAsync Error", key)
return false
end
end
SetAsync function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | key | string | 設定したいキー。 |
引数 | value | Variant | 設定したい値。 |
引数 | expiration | number | 設定したアイテムの有効期限(秒単位)。有効期限が切れると、アイテムは自動的に削除される。最大有効期限は 45日間(3,888,000 秒)。 |
引数 | sortKey | Variant(number or string) | |
戻り値 | boolean | boolean | 処理が成功したかどうか。 |
expiration
として秒単位で有効期限を設定する必要があります。
有効期限を長くしてメモリサイズを抑えるか、有効期限を短くしてフルサイズに近いサイズを使用するか、ニーズによって決めましょう。
詳しくは前述した「MemoryStoreの概要編」の記事をご覧ください。
今回は、キーにUserId、値として表示用にプレイヤー名、ソート用にスコアを設定しています。
ここは場合によって変わると思うので臨機応変に対応しましょう。
他サーバーとの競合を考慮したデータの設定・保存
MemoryStoreSortedMap:SetAsync
はキーに対してデータを上書きします。
同一のMemoryStoreSortedMap
が他サーバーから呼ばれた際に、同じキーに対して同時に上書きが行われた時片方のデータが考慮されない場合があります。
これを解消すべく、DataStoreService同様MemoryStoreSortedMap:UpdateAsync() が用意されています。
-- SortedMapにUpdateAsyncを利用してスコアを登録する
function SortedMapManager:UpdateScore(player: Player, score: number): boolean
local key = tostring(player.UserId)
local value = player.Name
local success, newValue, newSortKey = pcall(function()
return sortedMap:UpdateAsync(
key,
function(oldValue: string?, oldSortKey: number?)
if oldValue == nil and oldSortKey == nil then
print("Save New Data")
end
return value, score
end,
EXPIRATION)
end)
if success and newValue and newSortKey then
print("UpdateAsync Success", key, newValue, newSortKey)
return true
else
print("UpdateAsync Error", key)
return false
end
end
UpdateAsync function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | key | string | 設定したいキー。 |
引数 | transformFunction | function | 変換関数。キーの古い値と古いソートキーを引数として受け取り、新しい値と新しいソートキーを返す。 |
引数 | expiration | number | 設定したアイテムの有効期限(秒単位)。有効期限が切れると、アイテムは自動的に削除される。最大有効期限は 45日間(3,888,000 秒)。 |
戻り値 | value, sortKey | Tuple(Variant, Variant) | 変換関数によって返された、最新の値とソートキーを返す。キーに対して保存されたデータがない場合は、それぞれnilになる。 |
transformFunction
は引数として更新前の古い値とソートキーを渡し、戻り値として設定した新しい値とソートキーを返します。
この時、キーが読み取られた後に他サーバーなどから値やソートキーが変更されると、UpdateAsync
はリトライを試みます。
このループは正しく値とソートキーが設定されるか、変換関数内からnilを返すまで続きます。
このリトライはAPIのアクセス制限数を消費します。
APIのアクセス制限などに不安がある場合は、ループ回数を計測し制限をかけるなどを行ってもよいかもしれません。
インベントリのような単独のプレイヤーからのみ呼ばれる場合はSetAsync
、ランキングのような複数のサーバーから呼ばれる場合はUpdateAsync
と使い分けましょう。
データの削除
データを削除したい場合は、MemoryStoreSortedMap:RemoveAsync() を利用します。
-- SortedMapからデータを削除する
function SortedMapManager:RemoveScore(player: Player): boolean
local key = tostring(player.UserId)
local success, errorMessage = pcall(function()
sortedMap:RemoveAsync(key)
end)
if not success and errorMessage then
print("RemoveAsync Error:", errorMessage)
else
print("Removed Data", key)
end
return success
end
RemoveAsync function | 名称 | 型 | 説明 |
---|---|---|---|
引数 | key | string | 削除したいキー。 |
戻り値 | - | - | なし。 |
データを設定する際に有効期限を設定していますが、それが経過する前に削除したい場合は利用しましょう。
エクスペリエンス全体で使用できるメモリ量が決まっているので無駄なデータを省くことで最適化できます。
全てのデータの削除が必要な場合は、GetRangeAsync
を利用してキーを取得し削除しましょう。
SortedMapを管理するModuleの完成
主にpcall
で囲んだくらいですが、これでSortedMapを管理するModuleが完成しました。
必要に応じて呼び出すことで、MemoryStoreへの保存・呼び出しが可能です。
最終的なコードは下記です。
SortedMapManager
-- MemoryStore SortedMap の管理
-- Service
local MemoryStoreService = game:GetService("MemoryStoreService")
-- 保存期間 一週間
local EXPIRATION = 86400 * 7
-- 使用するSortedMap
local sortedMap = MemoryStoreService:GetSortedMap("ScoreRanking")
local SortedMapManager = {}
-- SortedMapに登録された値とソートキーを返す
function SortedMapManager:GetScore(player: Player): (string?, number?)
local key = tostring(player.UserId)
local success, value, sortKey = pcall(function()
return sortedMap:GetAsync(key)
end)
if success and value and sortKey then
print("GetAsync Success", key, value, sortKey)
else
print("GetAsync Error or No Data", key)
end
return value, sortKey
end
-- SortedMapに登録されたキー、値、ソートキーを範囲で取り出しテーブルで返す
function SortedMapManager:GetScoreInRange(
direction: Enum.SortDirection,
count: number
): { { key: string, value: string, sortKey: number } }
local exclusiveLowerBound = { sortKey = 0 }
local exclusiveUpperBound = nil
local success, items = pcall(function()
return sortedMap:GetRangeAsync(direction, count, exclusiveLowerBound, exclusiveUpperBound)
end)
if success and items then
print("GetRangeAsync Success", items)
return items
else
print("GetRangeAsync Error or No Data")
return {} -- 失敗時は空のテーブルを返す
end
end
-- SortedMapにスコアを登録する
function SortedMapManager:SetScore(player: Player, score: number): boolean
local key = tostring(player.UserId)
local value = player.Name
local success, result = pcall(function()
return sortedMap:SetAsync(key, value, EXPIRATION, score)
end)
if success and result then
print("SetAsync Success", key, value, score)
return true
else
print("SetAsync Error", key)
return false
end
end
-- SortedMapにUpdateAsyncを利用してスコアを登録する
function SortedMapManager:UpdateScore(player: Player, score: number): boolean
local key = tostring(player.UserId)
local value = player.Name
local success, newValue, newSortKey = pcall(function()
return sortedMap:UpdateAsync(
key,
function(oldValue: string?, oldSortKey: number?)
if oldValue == nil and oldSortKey == nil then
print("Save New Data")
end
return value, score
end,
EXPIRATION)
end)
if success and newValue and newSortKey then
print("UpdateAsync Success", key, newValue, newSortKey)
return true
else
print("UpdateAsync Error", key)
return false
end
end
-- SortedMapからデータを削除する
function SortedMapManager:RemoveScore(player: Player): boolean
local key = tostring(player.UserId)
local success, errorMessage = pcall(function()
sortedMap:RemoveAsync(key)
end)
if not success and errorMessage then
print("RemoveAsync Error:", errorMessage)
else
print("Removed Data", key)
end
return success
end
return SortedMapManager
実際に使用する
実際に一定時間ごとに呼び出し表示してみます。
MemoryStoreSortedMap:GetRangeAsync()
を利用してデータを取得しUIへの表示を行っています。
長くなるので実際にランキングボードに表示する仕組みは省きます。
-- SortedMap を利用したランキング表示
-- Service
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
-- Module
local SortedMapManager = require(script.Parent:WaitForChild("SortedMapManager"))
-- BindableEvent
local UpdateRankingBindableEvent = script:WaitForChild("UpdateRanking")
-- ReadOnly
local UPDATE_INTERVAL = 60 -- 更新頻度(秒)
local DISPLAY_MAX_NUM = 100 -- 表示する最大個数
-- 次回更新時間
local nextUpdateTimestamp = DateTime.now().UnixTimestamp + UPDATE_INTERVAL
-- Player入場時の処理
local function onPlayerAdded(player: Player)
-- 乱数をスコアとして登録
local score = Random.new():NextInteger(0, 10000)
local saveSuccess = SortedMapManager:UpdateScore(player, score)
if saveSuccess then
local rankingItems = SortedMapManager:GetScoreInRange(Enum.SortDirection.Descending, DISPLAY_MAX_NUM)
-- ランキングボードの更新
UpdateRankingBindableEvent:Fire(rankingItems)
end
end
-- 更新処理
local function onUpdate(deltaTime: number)
local nowTimestamp = DateTime.now().UnixTimestamp
if nowTimestamp < nextUpdateTimestamp then
return
end
nextUpdateTimestamp = nowTimestamp + UPDATE_INTERVAL
local rankingItems = SortedMapManager:GetScoreInRange(Enum.SortDirection.Descending, DISPLAY_MAX_NUM)
-- ランキングボードの更新
UpdateRankingBindableEvent:Fire(rankingItems)
end
Players.PlayerAdded:Connect(onPlayerAdded)
RunService.Heartbeat:Connect(onUpdate)
よくあるランキングボードができた
まとめ
-
MemoryStoreSortedMap
を利用することで、データをソートした状態で一時保存することができる。 -
MemoryStoreService:GetSortedMap()
を使用することで、MemoryStoreSortedMap
を取得することができる。 -
MemoryStoreSortedMap:GetAsync()
を使用することで、設定されているデータ・ソートキーを取得できる。 -
MemoryStoreSortedMap:GetRangeAsync()
を使用することで、指定の範囲内のデータをキー・データ・ソートキーを要素としたテーブルの配列で取得できる。 -
MemoryStoreSortedMap:SetAsync()
を使用することで、キーに対応したデータ・ソートキーを保存できる。 -
MemoryStoreSortedMap:UpdateAsync()
はMemoryStoreSortedMap:SetAsync()
に比べて、他サーバーとの競合を回避ししつつ値を変更・追加できる。
ただし、APIのアクセス制限が肥大する可能性があるので注意が必要。 -
MemoryStoreSortedMap:RemoveAsync()
を使用することで、指定のキーに対応したデータ・ソートキーを削除できる。不要なデータは削除することで、エクスペリエンス内で利用できるデータ量を最適化できる。
長い記事になりましたが、MemoryStoreServiceのSortedMapについては以上になります。
Robloxでのグローバルなランキング表示は多く見られます。
ぜひ実装してプレイヤー間の競争が盛り上がるゲームを作りましょう!!!
参考

当社ではRobloxを活用したゲームの開発、 また企業の商品やサービスの認知度拡大に寄与する3Dワールドの制作など、 Robloxにおける様々な活用支援を行っております。 Robloxのコンテンツ開発をご検討されている企業様は、お気軽にご相談ください。 landho.co.jp/
Discussion