Phát hiện va chạm là quá trình xác định khi nào các tia laser va chạm với người chơi, sau đó giảm sức khỏe của họ tương ứng. Ở cấp độ cao, bạn có thể nghĩ về công việc này như là một trong hai điều:
- Một kiểm tra mô phỏng vật lý xem một projectile có va chạm với mục tiêu hay không.
- Một kiểm tra tức thời xem khẩu súng có được nhắm vào mục tiêu hay không.
Loại phát hiện va chạm mà bạn sử dụng phụ thuộc vào yêu cầu gameplay của trải nghiệm của bạn. Ví dụ, một kiểm tra mô phỏng vật lý là phù hợp cho một trải nghiệm dodgeball nơi các quả bóng cần phải thoát khỏi tay với một vận tốc nhất định, rơi xuống khi chúng di chuyển qua không khí, hoặc thay đổi hướng do điều kiện thời tiết. Tuy nhiên, một kiểm tra tức thời lại phù hợp hơn cho trải nghiệm laser tag nơi các tia laser phải có vận tốc gần như vô hạn và bỏ qua các yếu tố môi trường như trọng lực và tốc độ gió.
Sử dụng trải nghiệm laser tag mẫu làm tham chiếu, phần này của hướng dẫn dạy bạn về các script đứng sau việc phát hiện va chạm trong không gian 3D, bao gồm các hướng dẫn về:
- Lấy hướng tia từ các giá trị camera hiện tại và loại súng của người chơi.
- Phát tia trong một đường thẳng từ khẩu súng khi nó bắn.
- Xác thực tia để ngăn chặn việc khai thác dữ liệu của súng.
- Giảm sức khỏe của người chơi theo thiệt hại từ mỗi loại khẩu súng và số lượng tia va chạm với người chơi.
Sau khi bạn hoàn thành phần này, bạn có thể khám phá thêm các chủ đề phát triển để nâng cao gameplay của mình, chẳng hạn như âm thanh, ánh sáng và hiệu ứng đặc biệt.
Lấy hướng tia
Sau khi một người chơi bắn khẩu súng của họ, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData gọi hai hàm để bắt đầu quá trình phát hiện va chạm: rayDirections() và rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Các đầu vào cho rayDirections là rất đơn giản: vị trí và giá trị xoay của camera hiện tại, và loại súng của người chơi. Nếu trải nghiệm laser tag mẫu chỉ cung cấp cho người chơi những khẩu súng tạo ra một tia laser duy nhất, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast sẽ không cần thiết vì bạn có thể sử dụng currentCamera.CFrame.LookVector để tính toán hướng cho tia bắn.
Tuy nhiên, vì mẫu cung cấp thêm một loại súng khác tạo ra nhiều tia laser với sự phân tán rộng, getDirectionsForBlast phải tính toán hướng cho mỗi tia laser của sự phân tán theo các góc trong cấu hình của súng:
getDirectionsForBlastif numLasers == 1 then-- Đối với tia đơn, chúng nhắm thẳngtable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Đối với nhiều tia, phân tán chúng đều ở bên ngang-- qua một khoảng laserSpreadDegrees xung quanh trung tâmlocal 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
Để chứng minh thêm về khái niệm này, nếu bạn muốn bao gồm một loại súng thứ ba với sự phân tán đứng rộng, bạn có thể tạo một thuộc tính súng mới, chẳng hạn như spreadDirection, sau đó điều chỉnh phép tính CFrame để sử dụng một trục khác. Ví dụ, hãy lưu ý sự khác biệt trong các phép tính direction trong đoạn script giả định sau cho loại súng thứ ba này.
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
Cuối cùng, hàm rayDirections() trả về một bảng chứa Vectors đại diện cho hướng của mỗi tia laser. Nếu cảm thấy hữu ích, bạn có thể thêm một số ghi lại để cảm nhận xem dữ liệu này trông như thế nào.
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)
Phát tia
castLaserRay(), hàm thứ hai trong ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, thực hiện các thao tác phức tạp hơn trong script. Nó bắt đầu bằng cách chỉ định các tham số để có thể thực hiện các cuộc gọi Workspace:Raycast() cho mục đích raycasting. Raycasting là quá trình gửi ra một tia vô hình từ một điểm Vector3 theo một hướng cụ thể với một chiều dài xác định, sau đó kiểm tra đường đi của nó để xem nó giao nhau với các đối tượng khác ở đâu.
Thông tin này đặc biệt hữu ích cho các trải nghiệm bắn súng góc nhìn thứ nhất vì nó cho phép bạn thấy khi nào và ở đâu các tia bắn giao nhau với người chơi hoặc môi trường. Ví dụ, hình ảnh sau đây cho thấy hai tia đang được phát song song với nhau. Theo điểm gốc của chúng và hướng, Tia A bỏ lỡ bức tường và tiếp tục cho đến khi gặp khoảng cách tối đa của nó, trong khi Tia B va chạm với bức tường. Để biết thêm thông tin về quy trình này, xem Raycasting.

Các tham số của castLaserRay() xác định rằng các cuộc gọi Raycast() phải xem xét mọi phần tử trong không gian làm việc trừ nhân vật đã bắn. Script sau đó phát một tia cho mỗi hướng trong bảng directions. Nếu một tia va chạm với một thứ gì đó, nó sẽ tạo ra một RaycastResult, có năm thuộc tính:
- Distance – Khoảng cách giữa gốc tia và điểm va chạm.
- Material – Enum.Material tại điểm giao nhau.
Giá trị Instance là thuộc tính quan trọng nhất trong số các thuộc tính này đối với gameplay của trải nghiệm laser tag mẫu vì nó truyền đạt khi nào các tia va chạm với những người chơi khác. Để lấy thông tin này, trải nghiệm sử dụng hàm hỗ trợ ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Nếu nó trả về nil, thì instance không phải là một phần của người chơi, có nghĩa là tia đã va chạm với một vật thể vô tri trong môi trường.
castLaserRay() sau đó sử dụng Position và Normal để tạo một CFrame mới mà nó gọi là điểm đích của tia. Mỗi tia có một điểm đích, và nó hoặc là nơi mà tia đã va chạm trong không gian 3D, hoặc là điểm ở cuối khoảng cách tối đa của nó. Tùy thuộc vào cách mà người chơi của bạn nhắm, giá trị taggedPlayer có thể là nil nhiều hoặc hầu hết thời gian.
castLaserRayif result then-- Tia đã va chạm với một thứ gì đó, kiểm tra xem có phải là người chơi không.destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- Tia không va chạm với gì cả, vì vậy điểm đích của nó là-- điểm ở khoảng cách tối đa của nó.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Xác thực tia bắn
Để ngăn chặn gian lận, chương trước Triển khai Súng giải thích cách blastClient thông báo cho máy chủ về tia bắn bằng cách sử dụng một RemoteEvent để có thể xác minh tất cả dữ liệu mà mỗi client gửi đến, chẳng hạn như việc họ có thực sự gắn cờ một người chơi khác bằng khẩu súng của họ hay không. Quy trình xác thực tia này diễn ra trong ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, và mỗi kiểm tra tương ứng với một module script lồng nhau:
Đầu tiên, getValidatedRayResults gọi validateRayResult để kiểm tra rằng mỗi mục trong bảng rayResults từ client là một CFrame và một Player (hoặc nil).
Tiếp theo, nó gọi isRayAngleFromOriginValid để so sánh các góc mong đợi của sự phân tán tia laser với những góc từ client. Đoạn mã này đặc biệt cho thấy lợi thế của việc sử dụng ReplicatedStorage vì máy chủ có thể gọi getDirectionsForBlast chính nó, lưu trữ giá trị trả về như là dữ liệu "mong đợi", và sau đó so sánh nó với dữ liệu từ client.
Giống như việc xác thực súng từ chương trước, isRayAngleFromOriginValid dựa vào một giá trị dung sai để xác định cái gì được coi là một sự khác biệt "quá mức" trong các góc:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox tóm tắt lại những phần toán học phức tạp nhất, để lại một hàm hỗ trợ ngắn gọn, có thể tái sử dụng rất cao và có thể áp dụng trên nhiều loại trải nghiệm khác nhau:
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)endKiểm tra tiếp theo là trực quan nhất. Trong khi getValidatedBlastData sử dụng DISTANCE_SANITY_CHECK_TOLERANCE_STUDS để xác minh rằng người chơi đã bắn gần điểm gốc của tia, isPlayerNearPosition sử dụng logic giống hệt để kiểm tra nếu người chơi bị gắn cờ ở gần điểm đích của tia:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendKiểm tra cuối cùng isRayPathObstructed sử dụng một biến thể của thao tác ray cast để kiểm tra nếu điểm đích của tia nằm sau một bức tường hoặc vật cản khác từ vị trí của client. Ví dụ, nếu một người chơi xấu tính toán hệ thống loại bỏ tất cả các bức tường khỏi trải nghiệm để gắn cờ các người chơi khác, máy chủ sẽ kiểm tra và xác nhận rằng các tia là không hợp lệ vì nó biết vị trí của mọi đối tượng trong môi trường.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Không có chiến lược chống khai thác nào là toàn diện, nhưng rất quan trọng để xem xét cách những người chơi xấu tính toán có thể tiếp cận trải nghiệm của bạn để bạn có thể đưa ra các kiểm tra mà máy chủ có thể thực hiện để đánh dấu hành vi đáng ngờ.
Giảm sức khỏe người chơi
Sau khi xác minh rằng một người chơi đã gắn cờ một người chơi khác, các bước cuối cùng trong quá trình hoàn thành vòng lặp gameplay chính trong trải nghiệm laser tag mẫu là giảm sức khỏe của người chơi bị gắn cờ, tăng điểm bảng xếp hạng, và hồi sinh người chơi đó trở lại vào vòng chơi.
Bắt đầu với việc giảm sức khỏe của người chơi bị gắn cờ, Hồi sinh và tái hồi sinh đề cập đến sự phân biệt giữa Player và Player.Character, cụ thể rằng một nhân vật là một mô hình Humanoid. Các mô hình Humanoid có thuộc tính Health với giá trị mặc định là 100. Thay vì triển khai hệ thống riêng của mình, trải nghiệm laser tag mẫu sử dụng thuộc tính có sẵn này để theo dõi lượng thiệt hại mà một người chơi cần trước khi họ bị gắn cờ ra khỏi vòng chơi.
Trải nghiệm lưu trữ các giá trị thiệt hại trong thuộc tính damagePerHit của mỗi khẩu súng. Ví dụ, khẩu súng gây ra một tia laser duy nhất gây ra 10 điểm thiệt hại, vì vậy cần mười tia bắn với khẩu súng này để gắn cờ một người chơi khác. Để bắt đầu quá trình gắn cờ một người chơi ra, LaserBlastHandler gọi ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, kiểm tra bảng rayResults đã được xác minh cho người chơi và truyền giá trị damagePerHit vào onPlayerTagged.

Health không chấp nhận các giá trị âm, do đó onPlayerTagged có một số logic để giữ sức khỏe người chơi ở mức không âm. Sau khi xác minh rằng sức khỏe của người chơi lớn hơn không, nó so sánh sức khỏe với damagePerHit và sử dụng giá trị nhỏ hơn trong hai giá trị. Ví dụ, nếu một người chơi có 10 sức khỏe và bị va chạm bởi một tia laser gây 15 thiệt hại, tia laser chỉ gây 10 điểm thiệt hại.
Cách tiếp cận vấn đề này có thể có vẻ hơi phức tạp. Ví dụ, tại sao không chỉ đặt sức khỏe của người chơi bằng không nếu điều đó sẽ là giá trị âm? Lý do là vì việc thiết lập các giá trị sức khỏe sẽ vượt qua trường lực. Việc sử dụng phương pháp Humanoid:TakeDamage() đảm bảo rằng người chơi không bị thiệt hại khi trường lực của họ đang hoạt động.
onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Cấm bắn vào bạn hữu
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Tránh sức khỏe âm
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage đảm bảo sức khỏe không bị giảm nếu trường lực đang hoạt động
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Thưởng cho playerBlasted một điểm cho việc gắn cờ playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end
Bước tiếp theo là tăng điểm bảng xếp hạng. Có thể có vẻ không cần thiết cho LaserBlastHandler để bao gồm người chơi đã bắn cùng với dữ liệu của tia bắn, nhưng nếu không có thông tin đó, trải nghiệm sẽ không thể ghi nhận người chơi nào đã gắn cờ một ai đó. Cuối cùng, người chơi bị gắn cờ lại hồi sinh trở lại vào vòng chơi, mà bạn có thể xem trong Hồi sinh và Tái hồi sinh.
Năm chương trong chương trình học này đề cập đến vòng lặp gameplay cốt lõi của trải nghiệm, nhưng vẫn còn nhiều lĩnh vực để khám phá, chẳng hạn như:
- Hiệu ứng trực quan của súng: Xem ReplicatedStorage ⟩ FirstPersonBlasterVisuals và ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Âm thanh: Xem ReplicatedStorage ⟩ SoundHandler.
- Chế độ tùy chỉnh: Bạn có thể thay đổi trải nghiệm này như thế nào để giới thiệu những loại mục tiêu mới, chẳng hạn như ghi điểm nhiều nhất trước khi thời gian hết?
Để có logic gameplay mở rộng cho trải nghiệm laser tag, cũng như các tài sản môi trường chất lượng cao có thể tái sử dụng, hãy xem mẫu Laser Tag.