การตรวจจับการโจมตี คือกระบวนการในการระบุเมื่อการยิงชนกับผู้เล่น จากนั้นลดพลังชีวิตของพวกเขาตามนั้น ในระดับสูง คุณสามารถคิดว่าการทำงานนี้เป็น:
- การตรวจสอบที่จำลองทางกายภาพว่ากระสุนชนเป้าหมายหรือไม่
- การตรวจสอบทันทีว่าปืนเลเซอร์ถูกเล็งไปที่เป้าหมายหรือไม่
ประเภทของการตรวจจับการโจมตีที่คุณใช้ขึ้นอยู่กับความต้องการของเกมของคุณ ตัวอย่างเช่น การตรวจสอบที่จำลองทางกายภาพเหมาะสำหรับเกมดอดจ์บอลที่ลูกบอลต้องออกจากมือด้วยความเร็วที่แน่นอน ตกลงขณะเคลื่อนที่ในอากาศ หรือเปลี่ยนทิศทางจากสภาพอากาศ อย่างไรก็ตาม การตรวจสอบทันทีเหมาะสมกว่าสำหรับเกมเลเซอร์แท็กที่ลำแสงต้องมีความเร็วเกือบไม่จำกัดและไม่สนใจปัจจัยด้านสิ่งแวดล้อมเช่นแรงโน้มถ่วงและความเร็วลม
โดยใช้ เกมเลเซอร์แท็กตัวอย่าง เป็นข้อมูลอ้างอิง ส่วนนี้ของบทเรียนจะสอนคุณเกี่ยวกับสคริปต์ที่อยู่เบื้องหลังการตรวจจับการโจมตีในพื้นที่ 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
-- สำหรับเลเซอร์หลายลำแสง ให้กระจายออกอย่างสม่ำเสมอในแนวนอน
-- รอบๆ มุม laserSpreadDegrees ที่อยู่กลาง
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 ชนกับผนัง สำหรับข้อมูลเพิ่มเติมเกี่ยวกับกระบวนการนี้ โปรดดูที่ Raycasting.

พารามิเตอร์ของ castLaserRay() ระบุว่าการเรียก Raycast() จะต้องพิจารณาทุกส่วนในพื้นที่ทำงาน ยกเว้น ตัวละครที่ยิง สคริปต์จะยิงรังสีสำหรับแต่ละทิศทางในตาราง directions หากรังสีชนกับบางสิ่ง มันจะสร้าง RaycastResult ซึ่งมีห้าคุณสมบัติ:
- Distance – ระยะห่างระหว่างจุดกำเนิดรังสีและจุดตัด
- Material – Enum.Material ที่จุดตัด
ค่าของ Instance เป็นคุณสมบัติที่สำคัญที่สุดในคุณสมบัติเหล่านี้สำหรับการเล่นเกมของเกมเลเซอร์แท็กตัวอย่าง เพราะมันสื่อสารเมื่อรังสีชนกับผู้เล่นคนอื่น เพื่อดึงข้อมูลนี้ เกมจะใช้ฟังก์ชันช่วย ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant หากมันส่งคืน nil หมายความว่า Instance นั้นไม่ใช่ส่วนหนึ่งของผู้เล่น หมายความว่ารังสีชนกับวัตถุที่ไม่มีชีวิตในสิ่งแวดล้อม
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 มีคุณสมบัติ Health ที่มีค่าเริ่มต้นเป็น 100 แทนที่จะใช้ระบบของตัวเอง เกมเลเซอร์แท็กตัวอย่างใช้คุณสมบัตินี้เพื่อติดตามความเสียหายที่ผู้เล่นต้องการก่อนที่จะถูกติดป้ายออกจากรอบ
เกมเก็บค่าความเสียหายในแอตทริบิวต์ 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
-- มอบคะแนนให้ playerBlasted สำหรับการติดป้าย playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
endขั้นตอนถัดไปคือการเพิ่มคะแนนในกระดานคะแนน อาจดูเหมือนไม่จำเป็นที่ LaserBlastHandler จะรวมผู้เล่นที่ยิงพร้อมกับข้อมูลการยิง แต่หากไม่มีข้อมูลนั้น เกมจะไม่สามารถให้เครดิตผู้เล่นที่ติดป้ายใครออกได้ สุดท้าย ผู้เล่นที่ถูกติดป้ายจะเกิดใหม่กลับเข้าสู่รอบ ซึ่งคุณสามารถตรวจสอบได้ใน การเกิดและการเกิดใหม่.
ห้าบทในหลักสูตรนี้ครอบคลุมวงจรการเล่นเกมหลักของเกม แต่ยังมีพื้นที่ให้สำรวจอีกมากมาย เช่น:
- ภาพลักษณ์ของปืนเลเซอร์: ดูที่ ReplicatedStorage ⟩ FirstPersonBlasterVisuals และ ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- เสียง: ดูที่ ReplicatedStorage ⟩ SoundHandler.
- โหมดที่กำหนดเอง: คุณจะปรับเปลี่ยนเกมนี้เพื่อแนะนำประเภทของวัตถุประสงค์ใหม่ๆ เช่น การทำคะแนนให้ได้มากที่สุดก่อนเวลาหมดได้อย่างไร?
สำหรับตรรกะการเล่นเกมที่ขยายสำหรับเกมเลเซอร์แท็ก รวมถึงทรัพย์สินสิ่งแวดล้อมที่มีคุณภาพสูงและนำกลับมาใช้ใหม่ได้ โปรดตรวจสอบ เลเซอร์แท็ก เทมเพลต