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

castLaserRay()のパラメータは、Raycast()呼び出しが、発射を行ったキャラクター以外のワークスペース内のすべてのパーツを考慮する必要があることを指定します。その後、スクリプトはdirectionsテーブル内の各方向に対してレイをキャストします。レイが何かに当たった場合、RaycastResultを生成します。これには5つのプロパティがあります。
- Distance – レイの発生源と交差点との距離。
- Material – 交差点でのEnum.Material。
Instanceの値は、サンプルレーザータグ体験のゲームプレイにおいて最も重要なプロパティです。なぜなら、レイが他のプレイヤーに衝突したときを示すからです。この情報を取得するために、体験はReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendantヘルパー関数を使用します。これがnilを返す場合、インスタンスはプレイヤーの一部ではなく、レイは環境内の無生物に当たったことを意味します。
castLaserRay()は次に、PositionとNormalを使用して、新しいCFrameを作成し、それをレイのdestinationと呼びます。すべてのレイには目的地があり、それはレイが3D空間内で当たった場所か、最大距離の終点です。プレイヤーがどれだけうまく狙うかによって、多くのtaggedPlayerの値はnilになる場合があります。
castLaserRayif result then-- 弾は何かに当たった、プレイヤーであるか確認します。destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- 弾は何にも当たらなかったので、その目的地は-- 最大距離のポイントです。local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
弾を検証する
チートを防ぐため、前の章ブラスターの実装では、blastClientがRemoteEventを使用してサーバーに弾を通知して、各クライアントが送信するすべてのデータを検証できるようにしています。たとえば、プレイヤーが実際にブラスターで他のプレイヤーにタグを付けたかどうかです。このレイの検証プロセスは、ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResultsで行われ、各チェックは入れ子のモジュールスクリプトに対応しています。
最初に、getValidatedRayResultsがvalidateRayResultを呼び出し、クライアントからのrayResultsテーブルの各エントリがCFrameおよびPlayer(またはnil)であることを確認します。
次に、isRayAngleFromOriginValidを呼び出して、レーザーの散布の期待角度とクライアントからのものを比較します。このコードは特に、サーバーがgetDirectionsForBlastを直接呼び出し、「期待される」データとして返り値を保存し、クライアントからのデータと比較できるため、ReplicatedStorageを使用する利点を示しています。
前の章のブラスター検証と同様に、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はプレイヤーの健康を0以上に保つためのロジックを持っています。プレイヤーの健康が0を超えていることを確認した後、健康をdamagePerHitと比較し、2つの値のうち小さい方を使用します。例えば、プレイヤーが10の健康を持ち、15ダメージのレーザービームに当たった場合、レーザーはほんの10ポイントのダメージを与えるだけです。
このアプローチは少しややこしく見えるかもしれません。例えば、なぜ負の値になるときにプレイヤーの健康を0にしないのか?その理由は、健康値を設定するとフォースフィールドを回避するためです。Humanoid:TakeDamage()メソッドを使用すると、プレイヤーがフォースフィールドが有効な間にダメージを受けないことが保証されます。
onPlayerTagged
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を参照してください。
- カスタムモード: この体験をどのように変更して、タイムアップ前に最も多くのポイントを獲得するなどの新しい目標タイプを導入できますか?
レーザータグ体験のための拡張ゲームプレイ論理に加えて、再利用可能で高品質の環境資産については、レーザータグテンプレートを確認してください。