Phát hiện va chạm

*Nội dung này được dịch bằng AI (Beta) và có thể có lỗi. Để xem trang này bằng tiếng Anh, hãy nhấp vào đây.

Phát hiện va chạm là quá trình xác định khi nào các vụ nổ va chạm với người chơi, sau đó giảm sức khỏe của họ tương ứng. Ở mức độ cao, bạn có thể nghĩ về công việc này như là:

  1. Một kiểm tra mô phỏng vật lý xem một vật thể có va chạm với mục tiêu hay không.
  2. 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 bạn sử dụng phụ thuộc vào yêu cầu gameplay của trò chơi 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ò chơi bóng ném nơi bóng cần rời khỏi tay với một vận tốc nhất định, rơi xuống khi 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à một sự phù hợp tốt hơn cho một trò chơi laser tag nơi các tia 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ò chơi 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 kịch bản phía sau phát hiện va chạm trong không gian 3D, bao gồm hướng dẫn về:

  • Lấy hướng vụ nổ từ các giá trị camera hiện tại và loại súng của người chơi.
  • Phát tia theo một đường thẳng từ khẩu súng khi nó bắn.
  • Xác thực vụ nổ để ngăn chặn việc khai thác dữ liệu khẩu súng.
  • Giảm sức khỏe của người chơi theo sát thương vụ nổ từ mỗi loại 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á các chủ đề phát triển bổ sung để nâng cao gameplay của bạn, chẳng hạn như âm thanh, ánh sáng và hiệu ứng đặc biệt.

Lấy hướng vụ nổ

Sau khi một người chơi bắn khẩu súng của họ, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData gọi hai hàm để bắt đầu quá trình phát hiện va chạm: rayDirections()rayResults().

generateBlastData
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

Các đầu vào cho rayDirections 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ò chơi 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, ReplicatedStorageLaserRaygetDirectionsForBlast 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 vụ nổ.

Tuy nhiên, vì mẫu cung cấp một loại súng bổ sung tạo ra nhiều tia laser với độ lan tỏa ngang rộng, getDirectionsForBlast phải tính toán hướng cho mỗi tia laser của độ lan tỏa theo các góc của chúng trong cấu hình súng:

getDirectionsForBlast
if numLasers == 1 then
-- Đối với tia đơn, chúng nhắm thẳng
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Đối với nhiều tia, phân tán chúng đều theo chiều ngang
-- qua một khoảng độ lan tỏa laser quanh trung tâm
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

Để minh họa thêm 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 độ lan tỏa dọc 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 mã sau cho loại súng thứ ba giả định này.

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

Cuối cùng, hàm rayDirections() trả về một bảng các Vectors đại diện cho hướng của mỗi tia laser. Nếu có ích, bạn có thể thêm một số ghi chú để có được cảm giác về dữ liệu này trông như thế nào.

generateBlastData
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)

Phát tia

castLaserRay(), hàm thứ hai trong ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, thực hiện các thao tác phức tạp hơn trong kịch bản. 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 phát tia. Phát tia 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ò chơi 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 vụ nổ giao nhau với người chơi hoặc môi trường. Ví dụ, hình ảnh sau đây minh họa hai tia đang phát song song với nhau. Theo điểm xuất phát và hướng của chú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, hãy xem Raycasting.

Một sơ đồ nơi Tia A tiếp tục xuyên qua bức tường, và Tia B va chạm với bức tường.

Các tham số của castLaserRay() chỉ định rằng các cuộc gọi Raycast() phải xem xét mọi phần trong không gian làm việc trừ nhân vật đã bắn. Kịch bản 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 cái gì đó, nó tạo ra một RaycastResult, có năm thuộc tính:

Giá trị Instance là thuộc tính quan trọng nhất trong số các thuộc tính này cho gameplay của trò chơi laser tag mẫu vì nó thông báo 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ò chơi sử dụng hàm trợ giúp ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. 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 PositionNormal để tạo ra một CFrame mới mà nó gọi là destination của tia. Mỗi tia có một điểm đến, và đó là nơi tia va chạm trong không gian 3D, hoặc đ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, nhiều hoặc hầu hết các giá trị taggedPlayernil.

castLaserRay
if result then
-- Vụ nổ đã va chạm với một cái gì đó, kiểm tra xem đó có phải là một người chơi không.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Vụ nổ không va chạm với cái gì cả, vì vậy điểm đến của nó là
-- điểm ở khoảng cách tối đa của nó.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

Xác thực vụ nổ

Để ngăn chặn gian lận, chương trước Triển khai Khẩu Súng giải thích cách blastClient thông báo cho máy chủ về vụ nổ bằng cách sử dụng một RemoteEvent để nó có thể xác minh tất cả dữ liệu mà mỗi client gửi, chẳng hạn như liệu họ có thực sự đánh dấu 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 ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, và mỗi kiểm tra tương ứng với một kịch bản mô-đun lồng nhau:

  1. Đầ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).

  2. Tiếp theo, nó gọi isRayAngleFromOriginValid để so sánh các góc dự kiến của độ lan tỏa 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ư dữ liệu "dự kiến", và sau đó so sánh nó với dữ liệu từ client.

    Giống như việc xác thực khẩu súng từ chương trước, isRayAngleFromOriginValid dựa vào một giá trị dung sai để xác định điều gì cấu thành một sự khác biệt "quá mức" trong các góc:

    isRayAngleFromOriginValid
    local claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unit
    local directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)
    return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREES

    Roblox trừu tượng hóa những phần toán học phức tạp nhất, vì vậy kết quả là một hàm trợ giúp ngắn gọn, có thể tái sử dụng cao với khả năng áp dụng trên nhiều trò chơi:

    getAngleBetweenDirections
    local 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
  3. Kiể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 tương tự để kiểm tra xem người chơi bị đánh dấu có gần điểm đến của tia hay không:

    isPlayerNearPosition
    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. Kiểm tra cuối cùng isRayPathObstructed sử dụng một biến thể của thao tác phát tia để kiểm tra xem điểm đến của tia có bị che khuất bởi một bức tường hoặc chướng ngại vật khác từ vị trí của client hay không. Ví dụ, nếu một người chơi ác ý loại bỏ tất cả các bức tường khỏi trò chơi để đánh dấu những 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.

    isRayPathObstructed
    local 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 điều quan trọng là xem xét cách mà những người chơi ác ý có thể tiếp cận trò chơi của bạn để bạn có thể đặt các kiểm tra mà máy chủ có thể chạy để đánh dấu hành vi đáng ngờ.

Giảm sức khỏe của người chơi

Sau khi xác minh rằng một người chơi đã đánh dấu một người chơi khác, các bước cuối cùng trong việc hoàn thành vòng lặp gameplay chính trong trò chơi laser tag mẫu là giảm sức khỏe của người chơi bị đánh dấu, tăng điểm trên bảng xếp hạng, và hồi sinh người chơi trở lại vòng đấu.

Bắt đầu với việc giảm sức khỏe của người chơi bị đánh dấu, Hồi sinh và tái sinh đề cập đến sự khác biệt giữa PlayerPlayer.Character, cụ thể là 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 nó, trò chơi laser tag mẫu sử dụng thuộc tính tích hợp này để theo dõi bao nhiêu sát thương một người chơi cần trước khi họ bị đánh dấu ra khỏi vòng đấu.

Trò chơi lưu trữ các giá trị sát thương trong thuộc tính damagePerHit của mỗi khẩu súng. Ví dụ, khẩu súng bắn một tia laser đơn gây ra 10 điểm sát thương, vì vậy cần mười vụ nổ với khẩu súng này để đánh dấu một người chơi khác. Để bắt đầu quá trình đánh dấu một người chơi ra, LaserBlastHandler gọi ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, kiểm tra bảng rayResults đã được xác minh cho người chơi và truyền damagePerHit cho onPlayerTagged.

Health không chấp nhận giá trị âm, vì vậy onPlayerTagged có một số logic để giữ cho sức khỏe của người chơi ở mức bằng hoặc trên không. Sau khi xác minh rằng sức khỏe của người chơi trê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ị đánh bởi một tia laser gây 15 sát thương, tia laser chỉ gây ra 10 điểm sát thương.

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 thành 0 nếu nó sẽ là âm? Lý do là vì việc đặt giá trị sức khỏe sẽ vượt qua trường lực. Sử dụng phương thức Humanoid:TakeDamage() đảm bảo rằng người chơi không bị sát thương trong 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 đồng đội
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
-- Tặng điểm cho playerBlasted vì đã đánh dấu playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

Bước tiếp theo là tăng điểm trên 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 vụ nổ, nhưng nếu không có thông tin đó, trò chơi không thể ghi nhận người chơi đã đánh dấu ai. Cuối cùng, người chơi bị đánh dấu sẽ hồi sinh trở lại vòng đấu, mà bạn có thể xem trong Hồi sinh và Tái sinh.

Năm chương trong chương trình học này bao gồm vòng lặp gameplay chính của trò chơi, nhưng vẫn còn nhiều lĩnh vực để khám phá, chẳng hạn như:

  • Hình ảnh khẩu súng: Xem ReplicatedStorageFirstPersonBlasterVisualsServerScriptServiceThirdPersonBlasterVisuals.
  • Âm thanh: Xem ReplicatedStorageSoundHandler.
  • Chế độ tùy chỉnh: Làm thế nào bạn có thể sửa đổi trò chơi này để giới thiệu các 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 kết thúc?

Để có logic gameplay mở rộng cho trò chơi 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.

©2026 Roblox Corporation. Roblox, logo Roblox và Powering Imagination là các nhãn hiệu đã đăng ký và chưa đăng ký của chúng tôi tại Hoa Kỳ và các quốc gia khác.