roblox-physics について
Roblox の物理、衝突処理、空間クエリ、ネットワーク所有権を扱うゲーム開発者向けスキルです。現行 API とサーバー権威シミュレーションの設計に加え、安定性と性能の注意点も解説します。
主な機能
- 01レイキャストと空間範囲クエリ
- 02衝突サマリーと衝突グループ
- 03アセンブリ・ジョイント・駆動制約
- 04ネットワーク所有権の制御
- 05サーバー権威シミュレーションの設計
- 06物理の安定性に関する指針
活用シーン
- 01ヒット判定やゲームゾーンの実装
- 02車両や飛翔体の動きの作成
- 03衝突時の影響やエフェクトの処理
- 04サーバーシミュレーション型の対戦ゲーム設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-physicsインストール後、Claude Code で/roblox-physicsを入力して読み込まれたか確認します。
同梱ファイルreferences/recipes.mdreferences/server-authority.md
詳細
Roblox の物理演算と空間クエリ
空間クエリ(ゲームプレイの判定には、Touched ではなくこちらを使用)
--!strict
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {} -- e.g. { character } of the shooter
params.IgnoreWater = true
local function castRay(origin: Vector3, direction: Vector3): RaycastResult?
-- direction's length is the max distance
return workspace:Raycast(origin, direction, params)
end
local result = castRay(Vector3.new(0, 50, 0), Vector3.new(0, -100, 0))
if result then
print(result.Instance, result.Position, result.Normal, result.Material, result.Distance)
end
-- Swept volumes: thick bullets, melee arcs, ground checks with width.
local sphereHit = workspace:Spherecast(Vector3.new(0, 10, 0), 2, Vector3.new(0, 0, 50), params)
local boxHit = workspace:Blockcast(CFrame.new(0, 10, 0), Vector3.new(4, 4, 4), Vector3.new(0, 0, 50), params)
-- Volume overlap: explosions, hitboxes, zone checks.
local overlap = OverlapParams.new()
overlap.FilterType = Enum.RaycastFilterType.Exclude
overlap.MaxParts = 50
local partsInBox = workspace:GetPartBoundsInBox(CFrame.new(0, 5, 0), Vector3.new(10, 10, 10), overlap)
local partsInSphere = workspace:GetPartBoundsInRadius(Vector3.zero, 15, overlap)
print(sphereHit, boxHit, #partsInBox, #partsInSphere)
- 再利用:パラメーターオブジェクトを再利用し、呼び出しごとに割り当てるのではなく、
FilterDescendantsInstances(またはparams:AddToFilter(instance))を更新します。 - 非衝突パーツを無視するには
RespectCanCollide = trueを、グループとしてクエリするにはCollisionGroup = "Name"を指定します。 GetPartsInPart(part, params)は正確なジオメトリの重なりを調べます(負荷が高くなります)。GetPartBoundsIn*はバウンディングボックスを使用します。- 非推奨:
FindPartOnRay*、Ray.new、FindPartsInRegion3*→ 上記の API を使用してください。 - クライアントのクエリで検出できるのはストリーミング済みのパーツだけです。権威ある判定はサーバーで行ってください。
Touched:制限を把握する
Touched は物理エンジンによって発火し、物理的に接触したパーツに対してのみ発火します。パーツをシミュレートする側(所有するパーツについては、クライアントが発火を偽装または抑制できます)によって発火し、1 回の接触で何度も発火することがあります。デバウンスとサーバー側の検証を組み合わせて、トリガー(パッド、アイテムの取得)に使用してください。ヒット検出やゾーンには空間クエリを使用してください。
--!strict
local Players = game:GetService("Players")
local pad = Instance.new("Part")
pad.Anchored = true
pad.Parent = workspace
local recently: { [Player]: boolean } = {}
pad.Touched:Connect(function(hit)
local character = hit:FindFirstAncestorOfClass("Model")
local player = character and Players:GetPlayerFromCharacter(character)
if not player or recently[player] then
return
end
recently[player] = true
print(`{player.Name} stepped on the pad`)
task.delay(1, function()
recently[player] = nil
end)
end)
衝突サマリー(衝撃データ)
パーツの衝突の「仕方」(衝撃エフェクト、衝撃音、衝撃速度に応じたダメージ)を取得するには、Touched の代わりに BasePart:BindToCollisionSummaries(callback) を使用します。物理ステップ後、毎フレーム 1 回、そのパーツのすべての接触についてコールバックが呼び出されます。各接触には Part0(バインドされたパーツ)、Part1、Point、Normal(Part0 から離れる向き)、RelativeVelocity(衝突を解決する前の相対速度)、Area、Time が含まれます。
- パーツがシミュレートされる場所(ネットワーク所有者)でのみ発火し、何もレプリケートされません。他のクライアントに必要な情報は、独自のリモートを通じて送信し、サーバーで検証してください。
CanCollide = falseのパーツや、スリープ状態にある 2 つのパーツ間ではサマリーは取得されません。非凸パーツでは、パーツのペアごとに複数のサマリーが生成されることがあります。負荷は衝突の総数ではなく、バインドされたパーツの数に応じて増加します。
--!strict
local function bindImpacts(part: BasePart, onImpact: (point: Vector3, speed: number) -> ()): RBXScriptConnection
return part:BindToCollisionSummaries(function(summaries: { any })
for _, summary in summaries do
local speed = math.abs(summary.RelativeVelocity:Dot(summary.Normal)) -- head-on impact speed
if speed > 10 then
onImpact(summary.Point, speed)
end
end
end)
end
return bindImpacts
衝突グループ
--!strict
-- Collision group APIs now live on WorldRoot (workspace); the PhysicsService versions are superseded.
workspace:RegisterCollisionGroup("Players")
workspace:RegisterCollisionGroup("Ghosts")
workspace:CollisionGroupSetCollidable("Players", "Ghosts", false)
local ghost = Instance.new("Part")
ghost.CollisionGroup = "Ghosts" -- assign by name (CollisionGroupId/SetPartCollisionGroup are deprecated)
ghost.Parent = workspace
グループはサーバーの起動時に一度だけ登録してください(最大 32。workspace:GetMaxCollisionGroups() を参照)。CanCollide = false にするとすべての衝突が無効になります。CanQuery = false にするとレイキャストでパーツが検出されなくなります。CanTouch = false にすると Touched が無効になります(CPU 負荷を軽減)。NoCollisionConstraint は、特定の 2 つのパーツ間の衝突を無効にします。
MeshPart の衝突形状には、Box/Hull(低負荷)、Default、PreciseConvexDecomposition(高負荷)、または CollisionPrecision スライダーを備えた Tunable があります。メッシュごとの精度とパフォーマンスのバランスを調整できます。スライダーは Studio で設定します。スクリプトからは書き込めません。
アセンブリとジョイント
- アセンブリ とは、溶接、
WeldConstraint、Motor6D、剛体拘束によって剛結合されたパーツの集合です。1 つの物体としてシミュレートされ、ルートパーツ(part.AssemblyRootPart)によってネットワーク所有権が決まります。 - 静的なジオメトリはアンカーします。アンカーされていないパーツをアンカー済みパーツに溶接すると、アンカー済みアセンブリになります。
- 編集時に
WeldConstraintで溶接すると、現在のオフセットが維持されます。コードではPart0/Part1を設定し、親を指定します。 - モデル全体を移動するには
model:PivotTo(cframe)を使用します。多数のパーツを一度に移動するにはworkspace:BulkMoveToを使用します。 AssemblyLinearVelocity/AssemblyAngularVelocity、part:ApplyImpulse()、ApplyAngularImpulse()、AssemblyMassで物理プロパティを読み取り、設定します。Velocity/RotVelocityは非推奨です。CustomPhysicalProperties = PhysicalProperties.new(density, friction, elasticity, frictionWeight, elasticityWeight)。- 単位:1 stud ≈ 0.28 m、
workspace.Gravityのデフォルト値は 196.2 studs/s² です。
Moverの制約(BodyMoversを置き換えるもの)
| 目的 | 制約 | 置き換え対象 |
|---|---|---|
| 速度を維持/操作する | LinearVelocity(VelocityConstraintMode Vector/Plane/Line) | BodyVelocity |
| 位置に向かって移動する | AlignPosition(Mode = OneAttachment + Position、Responsiveness、MaxForce) | BodyPosition |
| 向きを合わせて回転する | AlignOrientation(CFrame/LookAtPosition) | BodyGyro |
| 一定の力を加える | VectorForce(RelativeTo = World) | BodyForce、BodyThrust |
| 回転させる | AngularVelocity | BodyAngularVelocity |
| 点に向かって引く | LineForce | RocketPropulsion |
| トルクを加える | Torque | — |
すべてにAttachment(Attachment0)が必要です。MaxForce/MaxTorqueは現実的な値に設定してください(意図的な場合を除き、math.hugeは使わないでください)。そうしないと、オブジェクトが吹き飛ぶことがあります。機械的な制約:HingeConstraint(車輪やドア用のMotor/Servoアクチュエーター)、PrismaticConstraint(スライダー、エレベーター)、SpringConstraint(サスペンション)、RopeConstraint、RodConstraint、BallSocketConstraint、CylindricalConstraint(車輪+ステアリング)、UniversalConstraint、RigidConstraint。
乗り物と飛翔体のレシピ、CFrameの手引き:references/recipes.md。
ネットワークオーナーシップ
- サーバーは、固定されていないアセンブリのオーナーシップを近くのクライアントに自動割り当てします(そのクライアントでは滑らかに動作しますが、悪用される可能性があります)。キャラクターはそのプレイヤーが所有します。
part:SetNetworkOwner(nil)=サーバーがシミュレーションします(安全ですが、プレイヤーには遅延が大きくなります)。乗り物の運転者にはSetNetworkOwner(player)を使い、元に戻すにはSetNetworkOwnershipAuto()を使います。アセンブリがWorkspaceに入った後、サーバー側でルートパーツに対して呼び出してください。固定されたパーツにはオーナーがいません。- オーナーシップを切り替えると引っかかりが発生するため、毎フレーム切り替えないでください。ビューポートの可視化オプション → ネットワークオーナーの切り替えで確認できます。
サーバー権限(対戦ゲーム)
WorkspaceのAuthorityMode = Serverを設定すると、サーバーがすべてをシミュレーションし、クライアントは数フレーム先を予測し、予測が外れた場合はロールバック/再シミュレーションします。必要な設定:NextGenerationReplication、PlayerScriptsUseInputActionSystem、SignalBehavior = Deferred、UseFixedSimulation、StreamingEnabled(AuthorityModeを設定すると、これらも設定されます)。基本ルール:
- ゲームロジックは、サーバーとクライアントの両方でrequireされるModuleScript内で、
RunService:BindToSimulation(fn)にバインドした関数に記述します。 - クライアントがゲームに影響を与える方法は入力アクション(
Playerを親とするInputContexts)のみです。UserInputServiceのイベントは使いません。 - 予測対象のインスタンスのカスタム状態は、シミュレーションのコールバック内でのみ書き込む属性を通じて同期します。
AnimationTrackをキャッシュせず、各ステップでAnimator:GetTrackByAnimationId()を呼び出します。- サーバー上でプレイヤーのカメラを読み取るには
player:GetCameraState()(CFrame、FieldOfView、ViewportSize)を使います。以前のカメラ用InputActionのレプリケーションでは同期されなくなりました。
2026年7月以降、オプトイン方式で正式リリース済みです。詳細と実装パターン:references/server-authority.md。
安定性に関するヒント
- ジッター:オーナーシップの競合、制約同士の干渉、大きな質量比(> 1:100)、または固定されていないパーツのCFrameを毎フレーム設定すること(代わりに制約を使います)。
- 吹き飛び:軽いパーツに
MaxForce = math.hugeを設定すること、スポーン時に衝突が重なること、NaN/極端に大きな速度。 - 休止中のアセンブリは、外部から作用を受けるまでシミュレーションが停止します。必要に応じて小さなインパルスを加えて再始動します。
- 適応型タイムステップ(
Workspace.PhysicsSteppingMethod = Adaptive)はCPU負荷を抑えます。精密な機構には固定タイムステップを使います。
関連スキル
roblox-security(オーナーシップの悪用)、roblox-characters-animation、roblox-networking、roblox-performance、roblox-input。