🎲

【Roblox】DataStoreのデータをランダムに取得する

2024/10/18に公開

はじめに

今回の記事は、こちらの記事の続きの内容となっておりますので、まだ読まれていない方はこちらを先にご覧ください。
https://zenn.dev/landho_roblox/articles/d51ec009649be4

前回はDataStoreのデータを順位付けしましたが、もう一つDataStoreのできない事として、ランダムなデータの取得があります。

というわけで、その方法を考えてみました。
今回は、「キーに対してIDを割り振ったインデックスリストを作成・保存しておき、取得する際、1~総数の間でランダムに取得した値と一致するIDのキーのデータを取得する」という方法を使用します。

バージョン:0.641.2.6410741

1. 準備

前回のサンプルコードに追記していきますので、サンプルを真似して制作されている方は必要に応じて前回の記事も参照してください。

まずは、一旦前回保存したデータを削除しておきます。

-- 前回保存したデータを削除しておく.
for i = 1, 10, 1 do
	print(userDataStore:RemoveAsync("TestData_" .. i))
	print(scoreOrderedDataStore:RemoveAsync("TestData_" .. i))
end

そして、ソースコードの先頭あたりに、インデックスリストの型とキーを定義しておきます。

-- インデックスリストの型を定義.
type IndexList = {[number] :string}

-- インデックスリスト用のキーを文字列リテラル型として定数のように定義.
local KEY_INDEX_LIST :"IndexList" = "IndexList"

2. データの保存

準備ができたら、前回作成したアップロード用の関数に追記し、UpdataAsync()を使ってインデックスリストの情報も保存するようにします。
さらに、同じキーに対して複数回アップロードされると、インデックスリストと不整合が起きてしまうため、今回はGetAsync()で対象のキーにデータが存在するかチェックし、存在している場合はインデックスリストに追加しないようにしておきます。

-- 前回作成したアップロード用関数.
local function Upload(key :string, data :UserData) :(boolean, string?)
	------------------------------------ここから追記.
	-- インデックスリストに追加すべきかどうかのフラグ.
	local shouldAddToIndex = true

	-- データが既に存在するかチェック.
	do
		local result :{} = {pcall(userDataStore.GetAsync, userDataStore, key)}

		local success = result[1]::boolean
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end

		local data = result[2]

		-- すでにキーが存在していた場合、インデックスリストに追加しない.
		if data ~= nil then
			shouldAddToIndex = false
		end
	end
	
	if shouldAddToIndex then
		-- インデックスリストの末尾にキー情報を追加し保存する.
		local result :{} = {pcall(userDataStore.UpdateAsync, userDataStore, KEY_INDEX_LIST, function(indexList :IndexList)
			if indexList then
				table.insert(indexList, key)
				return indexList
			else
				-- データが無かったら空扱いとして最初にkeyを入れて保存する.
				return {key}
			end
		end)}

		local success = result[1]::boolean
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end
	end
	------------------------------------ここまで追記.

	-- dataStoreにdataを保存する.
	do
		local result :{} = {pcall(userDataStore.SetAsync, userDataStore, key, data)}

		local success = result[1]::boolean
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end
	end

	-- 同時にOrderedDataStoreにScoreを保存する.
	do
		local result :{} = {pcall(scoreOrderedDataStore.SetAsync, scoreOrderedDataStore, key, data.Score)}

		local success = result[1]::boolean
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end
	end

	return true
end

3. データの読み込み

前回同様、適当なデータを10個保存しておきます。

-- 適当なデータをアップロードする.
for i = 1, 10, 1 do
	local testData = {}::UserData
	testData.Score = i
	testData.PlayCount = 1
	testData.PlayerComment = "test"

	Upload("TestData_" .. i, testData)
end

データの読み込みでは、まずインデックスリストを取得した後、そのリストからランダムに一つキー情報を取得し、そのキー情報を用いてDataStoreからデータを取得します。

-- ランダムなデータを一つ取得.
local function GetRandomData() :(boolean, any)
	local indexList :IndexList?

	-- インデックスリストを取得.
	do
		local result :{} = {pcall(userDataStore.GetAsync, userDataStore, KEY_INDEX_LIST)}

		local success = result[1]
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end

		indexList = result[2]::IndexList?
		
		if not indexList then
			warn("Index List is nil")
			return false, "Index List is nil"
		end

		if #indexList <= 0 then
			warn("No data in Index List")
			return false, "No data in Index List"
		end
	end

	-- インデックスリストからランダムなデータを一つ取得.
	local returnData = {}::{key :string, data :UserData}	

	do
		local id = math.random(#indexList)
		local key = indexList[id]

		-- 対象のキーのデータを取得.
		local result :{} = {pcall(userDataStore.GetAsync, userDataStore, key)}

		local success = result[1]
		if not success then
			local errorMassage = result[2]::string
			warn(errorMassage)
			return false, errorMassage
		end

		local userData = result[2]::UserData?
		
		if not userData then
			warn(key .. " User Data is nil")
			return false, key .. " User Data is nil"
		end
		
		returnData.key = key
		returnData.data = userData
	end

	return true, returnData
end

-- 試しに3つランダムに取得してみる.
local randomData = {}
for i = 1, 3, 1 do
	local success, data = GetRandomData()
	table.insert(randomData, data)
end
print(randomData)
実行結果
▼  {
	[1] =  ▼  {
		["data"] =  ▼  {
			["PlayCount"] = 1,
			["PlayerComent"] = "test",
			["Score"] = 5
		},
		["key"] = "TestData_5"
	},
	[2] =  ▼  {
		["data"] =  ▼  {
			["PlayCount"] = 1,
			["PlayerComent"] = "test",
			["Score"] = 9
		},
		["key"] = "TestData_9"
	},
	[3] =  ▼  {
		["data"] =  ▼  {
			["PlayCount"] = 1,
			["PlayerComent"] = "test",
			["Score"] = 4
		},
		["key"] = "TestData_4"
	}
}

ランダムなデータを取得することができました!
普段から複数のランダムなデータを取得することがある場合は、毎回インデックスリストを取得しなおすのは無駄なので、引数から取得するデータの数を指定できるようにしておくと良いかもしれませんね。

4. 注意点

今回は簡易的にするため対応を省略しましたが、実際に使用する場合、いくつかの注意点があります。

データの削除

削除されうるデータである場合、追加した時と同じようにインデックスリストと整合性を取る必要があります。
削除する際にインデックスリストを逆引きできるよう、保存しておくデータにインデックス情報を追加しておく必要がありますね。
また、インデックスリストに空きができることになるので、空きを埋める処理も必要になります。
削除するキーの位置に末尾のキーを入れ、末尾を削除した後、末尾だったキーのデータのインデックス情報を更新すればよいかと思います。

データのサイズ上限

インデックスリストのデータサイズにも注意する必要があります。
DataStoreではキーの数に上限はありませんが、一つのデータとして保存できるサイズの上限は4MB(4,194,304バイト)のため、仮にIDに10文字分の文字列を使用した場合、インデックスリストは322,638件までしか保存できません。
ユーザー一人当たりのデータだったとしても、運よく人気のエクスペリエンスになったら十分に到達しそうな数字です。
必要に応じてインデックスリストを複数に分割し、各リストの総数を別で保存しておくなど、管理する方法も必要になるでしょう。

あるいは、ランダムに取得したいデータを保存するキー自体を通し番号にし、総数だけ保存しておいてインデックスリストは持たないという手段もあります。
こちらはランダムなデータの取得や空きデータを埋めるのは簡単ですが、「特定のデータを取り出したい」という場合に困らないような仕組みが必要です。

エラー処理

インデックスリストに追加はされたもののデータの保存には失敗した、といった事が起きるとやはり不整合が起きるため、保存時や読み込み時に適切にリトライする仕組みや、それでも不整合が出てしまった場合にメンテナンスで対応可能な仕組みにしておく必要もあります。

5. まとめ

  • インデックスリストを作成しておくことで、ランダムにデータを取得することができる
  • 対象のキーの初回のアップロードの際に、データと同時にインデックスリストに書き込んでおく
  • インデックスリストからランダムに取得したキーを使って、DataStoreのデータを取得する
  • 実際に利用するなら、データとインデックスリストの不整合やデータサイズ上限への対応が必須

DataStoreは、機能的には融通が利かない所もありつつも、開発・運用で追加の費用がかからないという利点があります。
うまく活用して、低コストで効果的な開発をめざしましょう!

6. 参考

https://create.roblox.com/docs/cloud-services/data-stores

ランド・ホー Roblox開発チーム

Discussion