偵測擊中 是識別當爆炸碰撞到玩家時的過程,然後根據這些碰撞相應減少他們的生命值。在高層次上,你可以將這個工作理解為以下兩種方式:
- 一種物理模擬檢查,即投射物是否撞擊了目標。
- 一種瞬時檢查,即激光器是否瞄準了目標。
你使用的擊中偵測類型取決於你體驗的遊戲玩法需求。例如,對於需要球以一定速度離手、在空中移動時下墜或因天氣條件而改變方向的躲避球體驗,物理模擬檢查是合適的。然而,對於激光標籤體驗,瞬時檢查則更合適,因為光束必須具有幾乎無限的速度並忽略重力和風速等環境因素。
以 樣本激光標籤體驗 作為參考,本教程的這一部分將教你關於三維空間中擊中偵測背後的腳本,包括以下指導:
- 根據當前相機值和玩家的激光器類型獲取爆炸方向。
- 在激光器發射時從激光器發出直線射線。
- 驗證爆炸以防止對激光器數據的利用。
- 根據每種激光器的爆炸傷害和有多少射線擊中玩家來減少玩家的生命值。
完成這部分後,你可以探索其他開發主題來增強你的遊戲玩法,例如音頻、燈光和特效。
獲取爆炸方向
當玩家發射他們的激光器後,ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData 會呼叫兩個函數來開始擊中偵測過程: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
為了進一步演示這一概念,如果你要包含一種具有寬闊垂直散佈的第三種激光器,你可以創建一個新的激光器屬性,例如 spreadDirection,然後調整 CFrame 計算以使用不同的軸。例如,注意以下腳本中該假設的第三種激光器類型的 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 中的第二個函數,執行腳本中更複雜的操作。它開始時指定參數,以便能對射線進行 Workspace:Raycast() 呼叫。射線投射是從指定方向和定義的長度從 Vector3 點發射出隱形射線的過程,然後檢查其路徑以查看它與其他物體的交集位置。
這些信息對於第一人稱射擊體驗尤其有用,因為它使你能夠看到何時何處的爆炸與玩家或環境相交。例如,以下圖片顯示了兩條並行發射的射線。根據它們的起源點和方向,射線 A 沒有擊中牆壁,並繼續直到達到其最大距離,而射線 B 則撞擊到了牆壁。欲了解更多此過程的信息,請參見 射線投射。

castLaserRay() 的參數指定 Raycast() 呼叫必須考慮工作區中的每個部分 除了 射擊的角色。然後,腳本對 directions 表中的每個方向進行射線投射。如果射線擊中了某物,則會生成一個 RaycastResult,其具有五個屬性:
- Distance – 射線起點與交集點之間的距離。
- Material – 交集點的 Enum.Material。
對於樣本激光標籤體驗的遊戲玩法來說,Instance 值是這些屬性中最重要的,因為它傳達了射線何時與其他玩家相撞。要檢索這一信息,體驗使用 ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant 幫助函數。如果返回 nil,則實例不屬於玩家,意味著射線擊中了一個無生命的物體。
castLaserRay() 然後利用 Position 和 Normal 創建一個新的 CFrame,稱為射線的 destination。每條射線都有一個目的地,它要麼是射線在三維空間中擊中的位置,要麼是其最大距離的終點。根據你的玩家的瞄準精度,許多或大多數 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 來比較激光擴散的預期角度與客戶端發送的角度。這段代碼特別展示了使用 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 模型擁有 Health 屬性,預設值為 100。樣本激光標籤體驗未實施自己的系統,而是使用這個內建屬性來跟踪玩家在被標記出局之前需要多少傷害。
該體驗將傷害值存儲在每個激光器的 damagePerHit 屬性中。例如,發射單束激光的激光器造成 10 點傷害,因此使用這個激光器需要十次爆炸來標記出另一名玩家。要開始標記玩家出局的過程,LaserBlastHandler 調用 ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers,該過程檢查現在已驗證的 rayResults 表中的玩家,並將 damagePerHit 傳遞給 onPlayerTagged。

Health 不接受負值,因此 onPlayerTagged 有一些邏輯來保證玩家的生命值不低於零。在驗證玩家生命值高於零後,它將生命值與 damagePerHit 進行比較,並使用兩者中較小的值。例如,如果一名玩家擁有 10 點生命值並被 15 點傷害的激光束擊中,激光僅造成 10 點傷害。
這種解決問題的方法可能看起來有些錯綜複雜。例如,為什麼不在生命值會是負數時就直接將其設置為零?原因是,設置生命值會繞過力場。使用 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 在爆炸數據中包含發射者的玩家似乎不必要,但如果沒有這些信息,體驗將無法記錄玩家標記了誰。最後,被標記的玩家將重生回合,你可以在 重生 中查看此過程。
本課程的五個章節涵蓋了體驗的核心遊戲循環,但仍有很多領域可供探索,例如:
- 激光器視覺效果:查看 ReplicatedStorage ⟩ FirstPersonBlasterVisuals 和 ServerScriptService ⟩ ThirdPersonBlasterVisuals。
- 音頻:查看 ReplicatedStorage ⟩ SoundHandler。
- 自定義模式:你可以如何修改這個體驗以介紹新的目標類型,例如在時間耗盡之前獲得最多分數?
有關激光標籤體驗的擴展遊戲邏輯以及可重用的高質量環境資源,請查看 激光標籤模板。