偵測擊中 是識別爆炸何時與玩家碰撞的過程,然後根據情況減少他們的健康值。從高層次來看,您可以將這項工作視為:
- 一個物理模擬檢查,檢查投射物是否擊中目標。
- 一個瞬時檢查,檢查爆炸器是否瞄準目標。
您使用的擊中檢測類型取決於遊戲的玩法需求。例如,物理模擬檢查適用於躲避球遊戲,其中球需要以一定的速度離開手,隨著在空中移動而下落,或因天氣條件而改變方向。然而,瞬時檢查更適合激光標籤遊戲,其中光束必須具有近乎無限的速度,並忽略重力和風速等環境因素。
以 示例激光標籤遊戲 為參考,本教程的這一部分將教您有關 3D 空間中擊中檢測背後的腳本,包括以下指導:
- 根據當前相機值和玩家的爆炸器類型獲取爆炸方向。
- 從爆炸器發射時沿直線投射光線。
- 驗證爆炸以防止利用爆炸器數據。
- 根據每種爆炸器的爆炸傷害和有多少光線擊中玩家來減少玩家的健康。
完成此部分後,您可以探索其他開發主題以增強您的遊戲體驗,例如音頻、燈光和特效。
獲取爆炸方向
在玩家發射他們的爆炸器後,ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData 會調用兩個函數以開始擊中檢測過程: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
-- 對於多個激光,均勻地水平擴散
-- 在中心周圍的激光擴散度範圍內
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為了進一步演示這一概念,如果您要包含一種具有寬廣 垂直 擴散的第三種爆炸器類型,您可以創建一個新的爆炸器屬性,例如 spreadDirection,然後調整 CFrame 計算以使用不同的軸。例如,請注意以下腳本中這種假設的第三種爆炸器類型的 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 -- new line
print(direction) -- new line
end -- new line
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)投射光線
castLaserRay(),在 ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData 中的第二個函數,執行腳本中的更複雜操作。它首先指定參數,以便可以進行 Workspace:Raycast() 調用以進行光線投射。光線投射是從 Vector3 點以特定方向發送一條不可見光線的過程,並具有定義的長度,然後檢查其路徑以查看它與其他物體的交集位置。
這些信息對於第一人稱射擊遊戲特別有用,因為它允許您查看何時以及何地爆炸與玩家或環境相交。例如,以下圖像演示了兩條光線平行投射。根據它們的起點和方向,光線 A 錯過了牆壁並繼續直到達到其最大距離,而光線 B 與牆壁相撞。要了解有關此過程的更多信息,請參見 光線投射。

castLaserRay() 的參數指定 Raycast() 調用必須考慮工作區中的每個部分 除了 發射的角色。然後,腳本為 directions 表中的每個方向投射一條光線。如果光線擊中某物,它會生成一個 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 點傷害,因此需要十次使用此爆炸器的爆炸才能標記出另一個玩家。為了開始標記玩家出局的過程,LaserBlastHandler 調用 ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers,該函數檢查現在已驗證的 rayResults 表中的玩家並將 damagePerHit 傳遞給 onPlayerTagged。

Health 不接受負值,因此 onPlayerTagged 有一些邏輯來保持玩家健康在零以上。在驗證玩家健康高於零後,它將健康值與 damagePerHit 進行比較,並使用兩者中較小的值。例如,如果玩家有 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
-- 為標記 playerTagged 的 playerBlasted 獲得一分
Scoring.incrementScore(playerBlasted, 1)
end
end
end下一步是增加排行榜。LaserBlastHandler 包含發射者和爆炸數據的玩家似乎不必要,但如果沒有這些信息,遊戲就無法給予玩家標記其他玩家的功勞。最後,被標記的玩家重新生成回到回合中,您可以在 生成和重生 中查看。
本課程的五個章節涵蓋了遊戲的核心遊戲循環,但仍有許多領域可以探索,例如:
- 爆炸器視覺效果:請參見 ReplicatedStorage ⟩ FirstPersonBlasterVisuals 和 ServerScriptService ⟩ ThirdPersonBlasterVisuals。
- 音頻:請參見 ReplicatedStorage ⟩ SoundHandler。
- 自定義模式:您如何修改這個遊戲以引入新的目標類型,例如在時間用完之前獲得最多分數?
有關激光標籤遊戲的擴展遊戲邏輯以及可重用的高質量環境資產,請查看 激光標籤 模板。