💽

【Roblox】DataStoreを活用した期間限定ランキングシステムの構築

2024/09/30に公開

はじめに

RobloxのDataStoreでは保存されているデータの一括削除ができないため、すべてをリセットしようと思ったらストア名を変更するか、すべてのキーを取得して削除するしかありません。
今回はその両方を使用せずに期間指定のランキングシステムを構築する方法を紹介します。

Robloxバージョン:0.638.1.6380615

システム設計

今回はプレイヤー個人のスコアを管理するPlayerScoreStoreとランキングを管理するRankingStoreの2つのDataStoreを用意しました。サンプルとしてウィークリーランキングを作成した例を記載します。

週はUnixTimeStampを使用した1970年1月1日からの経過した週数を使用しています。
RankingStoreに保存するランキングのデータは100人分のプレイヤーネームとスコアを持つ配列です。これを週が切り替わったタイミングで配列をリセットすることでウィークリーのランキングを作成しています。ランキングに使用するスコアはPlayerScoreStoreに保存されていますが、週を一緒に保存することで保存されているデータが前週以前のものだった時に保存されているスコアをリセットしています。またプレイヤーのデータが更新されたタイミングでランキングの更新を行っています。

実装例(サンプルコード)

セーブデータ更新用のSaveDataManagerモジュール

local WeekSeconds = 604800
local RankingNum = 100
local WeeklyStoreKey = "WeeklyRanking"
local DateKey = "Date"
local DataStoreService = game:GetService("DataStoreService")
local RankingStore = DataStoreService:GetDataStore("RankingStore")
local PlayerScoreStore = DataStoreService:GetDataStore("PlayerScoreStore")
--ランキングのデータの初期値 ウィークリーなので週数のみ保存
local Date = {
    Weeks = 0,
}
--プレイヤーのデータの初期値
local ScoreData = {
    Week = 0,
    Score = 0,
    RankIn = false
}
--ランキングのデータを取得
local function GetRankingData()
    local result
    local Success, Error = pcall(function()
        result = RankingStore:GetAsync(WeeklyStoreKey)
    end)
    if Success then
        return result
    else
        print(Error)
        GetRankingData()
    end
end
--ランキングのデータを更新
local function SaveRankingData(RankingData)
    while #RankingData > RankingNum do
        table.remove(RankingData, RankingNum + 1)
    end
    local Success, Error = pcall(function()
        RankingStore:SetAsync(WeeklyStoreKey, RankingData)
    end)
    if Success then
        print("Save RankingData")
    else
        print(Error)
        SaveRankingData(RankingData)
    end
end
--ランキングを更新する際に比較する範囲をチェックする
local function CheckCompareRange(currentRanking, Score, Max, Min)
    local range = Max - Min + 1
    local nextMin = Min
    local nextMax = Max
    if Score > currentRanking[math.round(range / 2) + Min - 1].Score then
        nextMin = Min
        nextMax = math.round(range / 2) + Min - 1
    else
        nextMin = math.round(range / 2) + Min
        nextMax = Max
    end
    --許容値(for文で回す回数)
    if nextMax - nextMin < 10 then
        return nextMax, nextMin
    else
        return CheckCompareRange(currentRanking, Score, nextMax, nextMin)
    end
end
--ランキングを更新する必要があるかをチェックする
local function CompareWeeklyScore(Name, Data)
    local currentRanking = GetRankingData()
    local max = RankingNum
    local min = 1
    if Data.Score > currentRanking[RankingNum].Score then
        max, min = CheckCompareRange(currentRanking, Data.Score, RankingNum, 1)
        for i = max, min, -1 do
            if Data.Score > currentRanking[i].Score then
            else	
                --入れ替え
                table.insert(currentRanking, i + 1, {Name = Name, Score = Data.Score})
                if not Data.RankIn then
                    table.remove(currentRanking, RankingNum + 1)
                else
                    for j = i + 2, RankingNum + 1  do
                        if currentRanking[j].Name == Name then
                            table.remove(currentRanking, j)
                            break
                        end
                    end
                end
                SaveRankingData(currentRanking)
                return true
            end
        end
        --入れ替え
        table.insert(currentRanking, min, {Name = Name, Score = Data.Score})
        if not Data.RankIn then
            table.remove(currentRanking, RankingNum + 1)
        else
            for j = min + 1, RankingNum + 1  do
                if currentRanking[j].Name == Name then
                    table.remove(currentRanking, j)
                    break
                end
            end
        end
        SaveRankingData(currentRanking)
        return true
    else
        --入れ替えなし
        return false
    end
end
local SaveDataManager = {}
--ランキングデータに保存してある週数を更新(週が切り替わったかチェックする際に使用)
function SaveDataManager:UpdateStoreDate()
    local Weeks = os.time() // WeekSeconds
    local Success, Error = pcall(function()
        RankingStore:SetAsync(DateKey, {Weeks = Weeks})
    end)
    if Success then
        print("Save RankingData")
    else
        print(Error)
        SaveDataManager:UpdateStoreDate()
    end
end
--ランキングデータの週数を更新(週が切り替わった際に使用)
function SaveDataManager:GetStoreDate()
    local result
    local Success, Error = pcall(function()
        result = RankingStore:GetAsync(DateKey)
    end)
    if Success then
        print("Success")
        return result
    else
        print(Error)
        SaveDataManager:GetStoreDate(DateKey)
    end
end
--ランキングデータを初期化(週が切り替わった際に使用)
function SaveDataManager:InitRankingData()
    local data = {
        Name = "Not Data",
        Score = 0
    }
    local RankingData = {}
    for i = 1, 100 do
        RankingData[i] = table.clone(data)
    end
    SaveRankingData(RankingData)
end
--ランキングデータを取得
function SaveDataManager:GetRankingData()
    return GetRankingData()
end
--プレイヤーのデータを取得
function SaveDataManager:GetData(player:Player)
 local result
    local Success, Error = pcall(function()
        result = PlayerScoreStore:GetAsync(player.UserId)
    end)
    if Success then
        print("Success")
        return result
    else
        print("Error")
        print(Error)
        SaveDataManager:GetData(player)
    end
end
--プレイヤーのデータを保存する
function SaveDataManager:SaveData(player:Player, Data)
    local Success, Error = pcall(function()
        local rankIn = CompareWeeklyScore(player.Name, Data)
        Data.RankIn = rankIn
        PlayerScoreStore:SetAsync(player.UserId, Data)
    end)
    if Success then
        print("Saved "..player.Name..":"..Data.Score)
    else
        print(Error)
        SaveDataManager:SaveData(player, Data)
    end
end
--プレイヤーのデータを更新する(前週以前のデータかも確認する)
function SaveDataManager:Update(player:Player, AddScore)
    local currentData = SaveDataManager:GetData(player)
    if not currentData then
        currentData = table.clone(ScoreData)
    end
    local WeekNum = os.time() // WeekSeconds
    if currentData.Week == WeekNum then
        currentData.Score += AddScore
    else
        currentData.Week = WeekNum
        currentData.Score = AddScore
        currentData.RankIn = false
    end
    SaveDataManager:SaveData(player, currentData)
end
return SaveDataManager

日付の更新などを確認するスクリプト

local WeekSeconds = 604800
local RunService = game:GetService("RunService")
local SaveDataManager = require(script.Parent)
local Weeks = SaveDataManager:GetStoreDate()
local currentDay = DateTime.now():ToUniversalTime().Day
local Timer = 0
RunService.Heartbeart:Connect(function(deltaTime)
    Timer += deltaTime
    if Timer >= 60 then --1分に一回確認
        Timer = 0
        local NowTime = DateTime.now():ToUniversalTime() 
        if NowTime.Day ~= currentDay then --日にちが変わったときの処理
            local NowWeeks = os.time() // WeekSeconds
            if NowWeeks ~= Weeks then --週が変わったときの処理
                SaveDataManager:UpdateStoreDate()
                SaveDataManager:InitRankingData()
                Weeks = NowWeeks
            end
        end
    end
end)

以上が今回作成したランキングのコードになります。

ランキングをテーブルで保存しているので更新の際に多少比較する必要がありますが、最低限の比較で済むようにしてあります。

まとめ

今回はできるだけ処理として重くならないようにウィークリーランキングのシステムを構築してみました。今回作成したマネージャーに変更を加えればマンスリーランキングも作ることができます。日付更新のスクリプトはほかにも時間を扱うところがあればまとめてしまうのがいいと思います。
お読みいただきありがとうございました。

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

Discussion