🗺️

【Roblox】ワープ先を事前に読み込む

2024/12/13に公開

1. はじめに

RobloxにはInstance Streamingと呼ばれる機能があり、デフォルト設定の場合、プレイヤーキャラクターからある程度近くにあるInstanceだけをクライアントに展開したり(Streaming in)、逆に遠くにあるInstanceはクライアントから破棄したり(Streaming out)、カメラからある程度近くにあるInstanceのみを描画したり、といった処理をエンジンが自動で行ってくれており、メモリ効率やパフォーマンスが最適化されています。
ただし、この機能が問題になる場合として、キャラクターを同じプレース内の遠くの場所にワープさせた時、ワープ先の地形の読み込みが追いつかず、ワープ先で地形の外に転落してしまったり、地形にめり込んでしまったりする場合があります。
一応、Roblox StudioのエクスプローラーからWorkspaceのStreamingEnabledプロパティをfalseにすることでこの機能を切ることもできますが、非常に小規模なプレースでない限りメモリ効率やパフォーマンスに問題が発生してしまうでしょう。
というわけで、今回はこの問題の解決策をご紹介します。

バージョン:0.652.0.6520764

2. 事前準備

まずは、触れたら単純にPivotToでワープするオブジェクトを作ってみましょう。
原点(0, 0, 0)付近にSpawnLocationがあり、そこからXが65535離れた位置にワープ先の地形を作りました。

原点付近の開始地点

Xが65535のワープ先

WarpScript
--!strict

local warpPoint = script.Parent

warpPoint.Touched:Connect(function(otherPart: BasePart)
	if otherPart.Name ~= "HumanoidRootPart" then
		return
	end
	
	-- キャラクターを取得.
	local character = otherPart:FindFirstAncestorOfClass("Model")::Model
	if not character then
		return
	end
	
	--[[
	GetBoundingBoxでキャラクターのサイズをざっくり取り、
	Pivotから足元までの距離の分上の位置をターゲットにする(厳密な数値ではない事に注意)
	]]
	local boundingCFrame, boundingSize = character:GetBoundingBox()
	
	-- ワープ.
	character:PivotTo(CFrame.new(65535, boundingSize.Y * 0.5, 0))
	
end)

今回はプリミティブで作成した簡素な地形なので転落等は無く普通にワープできますが、それでも一瞬Streaming待ちの警告が表示されたりします。
Meshで作成した地形だったり、周辺のInstanceが多くなると問題が起きそうです。
ここから、安全なワープを実装していきましょう。

3. 事前に読み込む

プレイヤーキャラクターの近く以外をStreaming inする方法として、Player:RequestStreamAroundAsync関数があります。

Player:RequestStreamAroundAsync

Parameters

position: Vector3
World location where streaming is requested.

timeOut: number
Optional timeout for the request.
Default Value: 0

この関数はサーバー側でのみ使用できる関数で、引数にVector3の値を取り、実行したPlayer Instanceのクライアントに対してその座標周辺をStreaming inするように要求します。
ワープする前にこれを実行すれば、事前にその周辺を読み込んでおけるわけです。

WarpScript
--!strict
local warpPoint = script.Parent

warpPoint.Touched:Connect(function(otherPart: BasePart)
	if otherPart.Name ~= "HumanoidRootPart" then
		return
	end
	
	local character = otherPart:FindFirstAncestorOfClass("Model")::Model
	if not character then
		return
	end
	
	-- Playerを取得.
	local player = game:GetService("Players"):GetPlayerFromCharacter(character)
	if not player then
		return
	end
	
	local boundingCFrame, boundingSize = character:GetBoundingBox()	
	local targetCFrame = CFrame.new(65535, boundingSize.Y * 0.5, 0)
	
	-- RequestStreamAroundAsyncでターゲットの周辺の事前Streamingを行う.
	player:RequestStreamAroundAsync(targetCFrame.Position)
	
	character:PivotTo(targetCFrame)
	
end)

これで解決! ……とはいきません。
適当にtask.wait()すれば基本問題にはならないでしょうが、読み込み時間はデバイスや通信状況によっても異なり、Streamingを開始したからと言って読み込まれているとは限りません。
であれば、地形が確実に存在する事が保証されている状態になってからきちんとワープさせたいですよね。

4. 地形が存在する事を保証する

地形のInstanceをWaitForChildしてもよいのですが、存在を保証したい物全てに対して行うのは大変です。
そこで、Model.ModelStreamingModeというプロパティを利用しましょう。

Model.ModelStreamingMode

このプロパティは、そのModelのStreamingに関わる挙動を設定する事ができます。
選択肢がいくつかありますが、それぞれきちんと説明するとややこしくなってしまうので、今回使用するものについてのみ説明します。
このプロパティをAtomicに設定すると、そのModelに含まれるInstanceのいずれかがStreaming inの対象になった時、Modelに含まれる物全てが一度に読み込まれます。
つまり、ワープ時点で最低限確実に存在しておいてほしい床や壁を一つのModelにまとめておき、読み込みを開始した後にそのModelの存在だけを確認すれば、その子孫も含めて全て存在する事を保証できる!というわけです。

存在を保証したい物をModel下に配置

ModelStreamingModeをAtomicに設定する
今回は簡素な地形なので全てModelに含めていますが、実際に利用する際は最低限の物だけを含めるようにしてください。
あまり多くの物に対して一度に読み込みや破棄が行われると、その処理に時間がかかり、画面がガクついたりする事があります。

WarpScript
--!strict
local warpPoint = script.Parent

warpPoint.Touched:Connect(function(otherPart: BasePart)
	if otherPart.Name ~= "HumanoidRootPart" then
		return
	end
	
	local character = otherPart:FindFirstAncestorOfClass("Model")::Model
	if not character then
		return
	end
	
	local player = game:GetService("Players"):GetPlayerFromCharacter(character)
	if not player then
		return
	end
	
	local boundingCFrame, boundingSize = character:GetBoundingBox()	
	local targetCFrame = CFrame.new(65535, boundingSize.Y * 0.5, 0)
	
	player:RequestStreamAroundAsync(targetCFrame.Position)
	
	-- Atomicに設定したModelが読み込まれるまで待機する.
	workspace:WaitForChild("Area2")
	
	character:PivotTo(targetCFrame)
	
end)

これで安全なワープが可能になりました!

より厳密にロード待ちを行うなら、ループで一定時間毎にFindFirstChildで確認し、nilではなくなったらワープ、という方法もよいでしょう。
サーバー側とクライアント側が連動した処理を組む必要がありますが、ワープ前に画面が白くフェードアウトし、ワープ後にフェードインするような演出なども入れられるとより「それっぽく」なりますね。

ちなみに、Model下にModelを配置して、それぞれのModelStreamingModeが異なる場合、親によって子がStreaming inの対象になった時は、子のStreaming inの条件が満たされていなくても子も読み込まれるようになっています。

5. まとめ

  • Player:RequestStreamAroundAsync()で、特定座標の周辺を事前に読み込める
  • ModelStreamingModeをAtomicにすると、そのModelに含まれるInstanceは全て一度に読み込まれる
  • これらを利用して、地形が存在する事が保証されている状態でワープさせることができる
  • あまり読み込み対象を多くすると、ロードに時間がかかる可能性がある

Instance Streamingは、知らないうちに上手いことやってくれる非常に便利な機能ですが、 時々このように問題になる場合もあります。
対応策を身に着け、適切に利用していきたいですね。
他にもStreamingに関する設定は色々あるので、下記の参考ページなどから詳しく調べておくとより活用できるかと思います!
最後までお読みいただき、ありがとうございました!

6. 参考

https://create.roblox.com/docs/workspace/streaming
https://create.roblox.com/docs/reference/engine/classes/Player#RequestStreamAroundAsync
https://create.roblox.com/docs/reference/engine/classes/Model#ModelStreamingMode
https://create.roblox.com/docs/reference/engine/enums/ModelStreamingMode

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

Discussion