ヒットの検出は、弾がプレイヤーに衝突したときにそれを特定し、その結果として彼らの健康を減少させるプロセスです。高レベルで考えると、この作業は次のいずれかと考えることができます。
- 弾がターゲットに当たったかどうかの物理的にシミュレートされたチェック。
- ブラスターがターゲットに向けられているかどうかの瞬時のチェック。
使用するヒット検出のタイプは、ゲームのゲームプレイ要件によって異なります。たとえば、ボールが特定の速度で手から離れ、空中を移動する際に落下したり、天候条件によって方向を変えたりする必要があるドッジボールゲームには、物理的にシミュレートされたチェックが適しています。しかし、レーザータグゲームでは、ビームがほぼ無限の速度を持ち、重力や風速などの環境要因を無視する必要があるため、瞬時のチェックがより適しています。
サンプルレーザータグゲームを参考にして、このチュートリアルのセクションでは、3D空間におけるヒット検出の背後にあるスクリプトについて学びます。以下のガイダンスが含まれています。
- 現在のカメラの値とプレイヤーのブラスタータイプから弾の方向を取得する。
- ブラスターから弾が発射される直線上にレイをキャストする。
- ブラスターのデータの悪用を防ぐために弾を検証する。
- 各タイプのブラスターからの弾のダメージに応じてプレイヤーの健康を減少させ、どれだけのレイがプレイヤーに当たったかを確認する。
このセクションを完了した後は、オーディオ、照明、特殊効果など、ゲームプレイを向上させるための追加の開発トピックを探求できます。
弾の方向を取得する
プレイヤーがブラスターを発射すると、ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastDataは、ヒット検出プロセスを開始するために2つの関数を呼び出します:rayDirections()とrayResults()。
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)rayDirectionsの入力は簡単です:現在のカメラの位置と回転値、そしてプレイヤーのブラスタータイプです。サンプルレーザータグゲームが単一のレーザービームを生成するブラスターのみをプレイヤーに提供している場合、ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlastは不要です。なぜなら、currentCamera.CFrame.LookVectorを使用して弾の方向を計算できるからです。
しかし、サンプルが広い水平分散を持つ複数のレーザービームを生成する追加のブラスタータイプを提供しているため、getDirectionsForBlastはブラスター構成内の角度に従って分散の各レーザービームの方向を計算する必要があります。
if numLasers == 1 then
-- 単一のレーザーの場合、まっすぐに狙います
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- 複数のレーザーの場合、中心の周りにレーザーSpreadDegreesの間隔で均等に広げます
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
table.insert(directions, direction)
end
endこの概念をさらに示すために、広い垂直分散を持つ3番目のブラスタータイプを含める場合は、spreadDirectionのような新しいブラスター属性を作成し、CFrame計算を異なる軸を使用するように調整できます。たとえば、以下のスクリプトでこの仮想の3番目のブラスタータイプのdirection計算の違いに注意してください。
if numLasers == 1 then
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction
if spreadDirection == "vertical" then
direction = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVector
else
direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
end
table.insert(directions, direction)
end
end
return directions最終的に、rayDirections()関数は、各レーザービームの方向を表すVectorsのテーブルを返します。役立つ場合は、データがどのように見えるかを把握するために、いくつかのログを追加できます。
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- 新しい行
print(direction) -- 新しい行
end -- 新しい行
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)レイをキャストする
castLaserRay()は、ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData内の2番目の関数で、スクリプト内のより複雑な操作を実行します。最初に、Workspace:Raycast()呼び出しを行うためのパラメータを指定します。レイキャスティングは、特定の方向に定義された長さでVector3ポイントから見えないレイを送信し、その経路をチェックして他のオブジェクトと交差する場所を確認するプロセスです。
この情報は、プレイヤーや環境と弾が交差する場所と時期を確認できるため、ファーストパーソンシューティングゲームに特に役立ちます。たとえば、以下の画像は、互いに平行にキャストされている2つのレイを示しています。起点と方向に応じて、レイAは壁を外し、最大距離に達するまで続き、レイBは壁に衝突します。このプロセスの詳細については、レイキャスティングを参照してください。

castLaserRay()のパラメータは、Raycast()呼び出しがブラスターを発射したキャラクターを除外して、ワークスペース内のすべてのパーツを考慮する必要があることを指定します。スクリプトは、directionsテーブル内の各方向に対してレイをキャストします。レイが何かに当たると、5つのプロパティを持つRaycastResultが生成されます。
- Distance – レイの起点と交差点の間の距離。
- Material – 交差点のEnum.Material。
Instanceの値は、サンプルレーザータグゲームのゲームプレイにとって最も重要なプロパティです。なぜなら、レイが他のプレイヤーに衝突したときにそれを伝えるからです。この情報を取得するために、ゲームはReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendantヘルパー関数を使用します。もしそれがnilを返す場合、そのインスタンスはプレイヤーの一部ではなく、レイは環境内の無生物に当たったことを意味します。
castLaserRay()は次に、PositionとNormalを使用して、新しいCFrameを作成し、それをレイのdestinationと呼びます。すべてのレイには目的地があり、それはレイが3D空間で衝突した場所か、最大距離の終点です。プレイヤーの狙いがどれだけ良いかによって、多くのtaggedPlayer値はnilになります。
if result then
-- 弾が何かに当たったので、プレイヤーかどうかを確認します。
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- 弾は何にも当たらなかったので、その目的地は
-- 最大距離のポイントです。
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end弾を検証する
チートを防ぐために、前の章ブラスターの実装では、blastClientがRemoteEventを使用してサーバーに弾を通知し、各クライアントが送信するすべてのデータ(他のプレイヤーを本当にタグ付けしたかどうかなど)を検証できるようにする方法を説明しています。このレイ検証プロセスは、ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResultsで行われ、各チェックはネストされたモジュールスクリプトに関連しています。
最初に、getValidatedRayResultsはvalidateRayResultを呼び出して、クライアントからのrayResultsテーブル内の各エントリがCFrameおよびPlayer(またはnil)であることを確認します。
次に、isRayAngleFromOriginValidを呼び出して、レーザーの分散の期待される角度とクライアントからの角度を比較します。このコードは特にReplicatedStorageを使用する利点を示しています。なぜなら、サーバーがgetDirectionsForBlastを自ら呼び出し、戻り値を「期待される」データとして保存し、クライアントからのデータと比較できるからです。
前の章のブラスター検証と同様に、isRayAngleFromOriginValidは、角度の「過剰な」違いを決定するために許容値に依存しています:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRobloxは最も複雑な数学の部分を抽象化しているため、結果は短く、非常に再利用可能なヘルパー関数となり、さまざまなゲームで適用可能です:
getAngleBetweenDirectionslocal function getAngleBetweenDirections(directionA: Vector3, directionB: Vector3)local dotProduct = directionA:Dot(directionB)local cosAngle = math.clamp(dotProduct, -1, 1)local angle = math.acos(cosAngle)return math.deg(angle)end次のチェックは最も直感的です。getValidatedBlastDataがDISTANCE_SANITY_CHECK_TOLERANCE_STUDSを使用して、弾を発射したプレイヤーがビームの起点の近くにいたことを確認するのに対し、isPlayerNearPositionは、タグ付けされたプレイヤーがビームの目的地の近くにいたかどうかを確認するために同じロジックを使用します:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseend最後のチェックisRayPathObstructedは、レイの目的地がクライアントの位置から壁や他の障害物の後ろにあるかどうかを確認するためにレイキャスト操作のバリエーションを使用します。たとえば、悪意のあるプレイヤーが他のプレイヤーをタグ付けするためにゲームからすべての壁を系統的に取り除く場合、サーバーはレイが無効であることを確認します。なぜなら、環境内のすべてのオブジェクトの位置を知っているからです。
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
完全なアンチエクスプロイト戦略はありませんが、悪意のあるプレイヤーがゲームにどのようにアプローチするかを考慮し、サーバーが疑わしい行動をフラグ付けできるチェックを実行できるようにすることが重要です。
プレイヤーの健康を減少させる
プレイヤーが他のプレイヤーをタグ付けしたことを確認した後、サンプルレーザータグゲームのメインゲームプレイループを完了するための最終ステップは、タグ付けされたプレイヤーの健康を減少させ、リーダーボードを増加させ、プレイヤーをラウンドに再出現させることです。
タグ付けされたプレイヤーの健康を減少させることから始めます。スポーンと再出現では、PlayerとPlayer.Characterの違い、特にキャラクターがHumanoidモデルであることをカバーしています。Humanoidモデルには、デフォルト値が100のHealthプロパティがあります。独自のシステムを実装するのではなく、サンプルレーザータグゲームはこの組み込みプロパティを使用して、プレイヤーがラウンドからタグアウトするまでにどれだけのダメージが必要かを追跡します。
ゲームは、各ブラスターのdamagePerHit属性にダメージ値を保存します。たとえば、単一のレーザービームを発射するブラスターは10ポイントのダメージを与えるため、このブラスターで10回の発射が必要です。プレイヤーをタグアウトさせるプロセスを開始するために、LaserBlastHandlerはServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayersを呼び出し、今検証されたrayResultsテーブルをチェックして、damagePerHitをonPlayerTaggedに渡します。

Healthは負の値を受け入れないため、onPlayerTaggedにはプレイヤーの健康をゼロ以上に保つためのロジックがあります。プレイヤーの健康がゼロ以上であることを確認した後、健康をdamagePerHitと比較し、2つの値のうち小さい方を使用します。たとえば、プレイヤーが10の健康を持ち、15ダメージのレーザービームに当たった場合、レーザーは10ポイントのダメージしか与えません。
この問題へのアプローチは少し複雑に見えるかもしれません。たとえば、なぜ負の値になる場合はプレイヤーの健康をゼロに設定しないのか?その理由は、健康値を設定するとフォースフィールドを回避するからです。Humanoid:TakeDamage()メソッドを使用することで、プレイヤーがフォースフィールドがアクティブな間にダメージを受けないようにします。
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- フレンドリーファイアを禁止
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- 負の健康を避ける
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamageはフォースフィールドがアクティブな場合、健康が低下しないことを保証します
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- playerBlastedにplayerTaggedをタグ付けしたことでポイントを与えます
Scoring.incrementScore(playerBlasted, 1)
end
end
end次のステップはリーダーボードを増加させることです。LaserBlastHandlerが弾データとともに弾を発射したプレイヤーを含めることが不必要に思えたかもしれませんが、その情報がなければ、ゲームは誰かをタグ付けしたプレイヤーにクレジットを与えることができません。最後に、タグ付けされたプレイヤーはラウンドに再出現します。これについてはスポーンと再出現で確認できます。
このカリキュラムの5つの章は、ゲームのコアゲームプレイループをカバーしていますが、探求すべき領域はまだたくさんあります。たとえば:
- ブラスターのビジュアル:ReplicatedStorage ⟩ FirstPersonBlasterVisualsおよびServerScriptService ⟩ ThirdPersonBlasterVisualsを参照してください。
- オーディオ:ReplicatedStorage ⟩ SoundHandlerを参照してください。
- カスタムモード:このゲームをどのように変更して、時間が切れる前に最も多くのポイントを獲得するなどの新しい目的を導入できますか?
レーザータグゲームの拡張ゲームプレイロジックや再利用可能で高品質な環境アセットについては、レーザータグテンプレートを確認してください。