🛠️

【Roblox】UGC機能の実装(1)~ステージ作成からデータストアへの保存~

に公開

はじめに

今回は弊社からリリースした『UGC Romance Obby Fall Game』で実装したUGC機能について紹介します。
https://www.roblox.com/games/119782301016393/NEW-UGC-Romance-Obby-Fall-Game
https://www.youtube.com/watch?v=ByJM7H46rX0
※動画はアプデ前のものになっています。
このゲームで実装したUGC機能はユーザーがステージを作成し、ほかユーザーが遊べるようにするという機能です。この記事ではステージの作成方法から作成ステージ情報をDataStoreに保存する方法について紹介します。

Robloxバージョン 0.658.0.6580461

ステージ作成

ステージ作成ではオブジェクトの配置に加えて、プレイ中の動作命令(移動、回転、拡縮など)を追加できるようになっています。


生成するオブジェクトについて

生成するオブジェクトの子オブジェクトにDragDetectorModuleScriptを追加しています。
DragDetectorは生成したオブジェクトの位置を調整するのに使用し、ModuleScriptはオブジェクトに設定した命令情報を保持するのに使用します。
命令情報の保持の方法には他にもAttributeを使用したり、マネージャーなどを使用して一括管理したりといろいろな手法がありますが、今回はオブジェクトを破棄したときのデータの管理が不要になる点とデータの取得をrequireをするだけで一括で行える点でオブジェクト毎にデータ保持用のModuleScriptを持たせることにしました。
DragDetectorについては以前にこちらの記事で紹介しているので、読んでいない方はそちらも読んでいただけるとより分かりやすいかと思います。

DragDetectorの記事
https://zenn.dev/landho_roblox/articles/435a93cfca397b

オブジェクトの配置

オブジェクトの配置にはDragDetectorを使用します。
基本的にオブジェクトを動かしたりするのはDragDetectorのデフォルト機能を使用しているので、専用にスクリプトを組まなくてもステージの作成自体は行えるようになっています。

CreateStageManager.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--プレハブフォルダ
local prefabFolder = ReplicatedStorage:WaitForChild("Prefab")
--配置できるオブジェクトを格納しているフォルダ
local objectFolder = prefabFolder:WaitForChild("Object")
--配置したオブジェクトに追加するDragDetector
local objectDetector = prefabFolder:WaitForChild("ObjectDetector")
--配置したオブジェクトに追加するModuleScript
local directiveObject = prefabFolder:WaitForChild("DirectiveObject")

--生成したオブジェクトの親になるModelインスタンス
local BaseModel = workspace:WaitForChild("BaseModel")

--オブジェクトのドラッグ中フラグ
local isDrag = false

--選択中のオブジェクト
local selectedObject = nil

local CreateStageManager = {}

--オブジェクトの生成処理
function CreateStageManager:CreateObject(objectId:number)
    --オブジェクトを生成
    local object = prefabFolder:WaitForChild("Object_"..objectId):Clone()
    object.Parent = BaseModel
    --オブジェクト識別用のIDをAttributeに追加
    object:SetAttribute("ID", objectId)
    --オブジェクトにDragDetectorを追加
    local detector = objectDetector:Clone()
    detector.Parent = object
    local DragStartPos  --ドラッグ開始位置保持用変数
    detector.DragStart:Connect(function()
        DragStartPos = object.Position
        isDrag = true
    end)
    detector.DragEnd:Connect(function()
        --ドラッグしていないとみなしてオブジェクトを選択した判定とする。
        if (object.Position - DragStartPos).Magnitude < 1 then
            selectedObject = object
        end
        isDrag = false
    end)
    --オブジェクトにModuleScriptを追加
    local module = directiveObject:Clone()
    module.Parent = object
end

return CreateStageManager

上記のコードではドラッグ操作中のフラグとオブジェクトの選択処理を追加しました。
UGC機能を作成するときにはUndo,Redoといった機能を実装することも必要になってくることがあると思います。そういったときにはDragEnd内に現在のステージ情報を取得する処理を追加して、tableに持たせておくことで簡単に実装できます。

オブジェクトに命令を追加

オブジェクトに追加する命令の保持に関してですが、移動命令を例に解説します。
前述したようにこのモジュールはデータを保持するだけのために使用しているので、メソッドなどは一切含めず、外部から自由に参照できる値のみを保持するように作っています。

DirectiveObject.lua
local DirectiveObject = {}

DirectiveObject.Move =
    {
        IsActive = false,
        TargetPos = {0, 0, 0},
        Speed = 0
    }
--以下回転と拡縮も追加

return DirectiveObject

このように移動命令を実行するのに必要なデータだけを持たせておくことで、requireをして取得したtableをそのままステージ情報としてDataStoreに保存することができるようになっています。
このときにDataStoreで保存できるデータ型になっていないと別途変換処理を追加する必要が出てきます。DataStoreに保存できるデータ型と変換処理については次のトピックで紹介します。

DataStoreでのステージ情報の管理

RobloxではHttpServiceなどを利用してステージ情報などを外部データベースに保存することもできますが、今回はステージ情報をDataStoreに保存していきます。DataStoreに保存できるデータ型には制限があるので、データを保存できる形に修正する必要があります。

DataStoreで保存できるデータ型

  • number
  • string
  • boolean
  • table
  • nil

DataStoreでは上記の5種類のデータ型のみが保存できます。
ステージ情報にはオブジェクトの配置位置を含める必要がありますが、Robloxで使用されているCFrameはそのままだとDataStoreに保存できないので、保存できる形に変換する必要があります。

CFrameをtableに変換する

CFrameにはGetComponentというメソッドが存在します。このメソッドはオブジェクトのX座標、Y座標、Z座標、3x3回転行列(R00,R01,R02,R10,R11,R12,R20,R21,R22)を返します。
例 Position(0,10,20),Rotation(0,90,0)の場合、GetComponent
0 10 20 0.49995946884155273 0 0.8660488128662109 0 1 0 -0.8660488128662109 0 0.49995946884155273
をタプル型として返します。
このメソッドの戻り値を{}で囲んでtable型に変換することで、DataStoreで保存できるようになります。

配置されているオブジェクトの情報を取得し保存する

ステージ情報をデータストアに保存します。
オブジェクトはすべてBaseModel下に配置されているので、BaseModelの子オブジェクトを参照するだけでまとめてステージデータを取得することができます。

StageDataManager.lua
local DataStoreService = game:GetService("DataStoreService")
local StageDataStore = DataStoreService:GetDataStore("StageDataStore")

--生成したオブジェクトの親になっているModelインスタンス
local BaseModel = workspace:WaitForChild("BaseModel")

local StageDataManager = {}

function StageDataManager:SaveData(player)
    local stageData = {}

    local objects = BaseModel:GetChildren()
    for i,object in ipairs(objects) do
        local directiveObject = require(object:WaitForChild("DirectiveObject"))
        local objectData =
            {
                objectId = object:GetAttribute("ID") ,
                cframe = {object.CFrame:GetComponent()},
                directive = table.clone(directiveObject),
            }
        table.insert(stageData, objectData)
    end

    local key = "Stage_"..player.UserId.."_0001"
    StageDataStore:SetAsync(key, stageData)
end

return StageDataManager

これでステージデータをDataStoreに保存することができました。
keyにUserIdを含めることでListKeysAsyncのprefixにUserIdを設定するだけでプレイヤーが作成したステージのkeyをまとめて取得することが可能になっています。

まとめ

  • オブジェクトを配置するにはDragDetectorを使用すると便利
  • オブジェクトごとにデータを持たせるにはModuleScriptに必要なデータだけを持たせることで参照とオブジェクト破棄時の管理が楽になる
  • DataStoreに保存できるデータ型に注意し、必要があれば変換する
  • StageDataを保存するKeyにはフィルター要素(UserId)を含めておくと管理が楽になる

今回は弊社で制作したゲームで実装したUGC機能のステージを作成して、DataStoreに保存する方法について紹介しました。
次回の記事では今回保存したStageDataを呼び出して実際にステージを生成する方法について紹介したいと思います。
最後までお読みいただき、ありがとうございました!

参考

https://create.roblox.com/docs/cloud-services/data-stores
https://create.roblox.com/docs/reference/engine/datatypes/CFrame#GetComponents

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

Discussion