Deteksi Tembakan

*Konten ini diterjemahkan menggunakan AI (Beta) dan mungkin mengandung kesalahan. Untuk melihat halaman ini dalam bahasa Inggris, klik di sini.

Mendeteksi tembakan adalah proses mengidentifikasi kapan tembakan bertabrakan dengan pemain, kemudian mengurangi kesehatan mereka sesuai dengan itu. Secara umum, Anda dapat memikirkan pekerjaan ini sebagai salah satu dari dua hal:

  1. Pemeriksaan simulasi fisik apakah proyektil mengenai target.
  2. Pemeriksaan instan apakah blaster diarahkan ke target.

Jenis deteksi tembakan yang Anda gunakan tergantung pada kebutuhan gameplay permainan Anda. Misalnya, pemeriksaan simulasi fisik cocok untuk permainan dodgeball di mana bola perlu meninggalkan tangan dengan kecepatan tertentu, jatuh saat bergerak melalui udara, atau mengubah arah karena kondisi cuaca. Namun, pemeriksaan instan lebih cocok untuk permainan laser tag di mana sinar harus memiliki kecepatan hampir tak terbatas dan mengabaikan faktor lingkungan seperti gravitasi dan kecepatan angin.

Menggunakan permainan laser tag contoh sebagai referensi, bagian tutorial ini mengajarkan Anda tentang skrip di balik deteksi tembakan di ruang 3D, termasuk panduan tentang:

  • Mendapatkan arah tembakan dari nilai kamera saat ini dan jenis blaster pemain.
  • Mengarahkan sinar dalam jalur lurus dari blaster saat menembak.
  • Memvalidasi tembakan untuk mencegah eksploitasi data blaster.
  • Mengurangi kesehatan pemain sesuai dengan kerusakan tembakan dari setiap jenis blaster dan berapa banyak sinar yang mengenai pemain.

Setelah Anda menyelesaikan bagian ini, Anda dapat menjelajahi topik pengembangan tambahan untuk meningkatkan gameplay Anda, seperti audio, pencahayaan, dan efek khusus.

Dapatkan arah tembakan

Setelah seorang pemain menembakkan blasternya, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData memanggil dua fungsi untuk memulai proses deteksi tembakan: rayDirections() dan rayResults().

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

Input untuk rayDirections cukup sederhana: posisi dan nilai rotasi kamera saat ini, serta jenis blaster pemain. Jika permainan laser tag contoh hanya memberikan pemain blaster yang menghasilkan satu sinar laser, ReplicatedStorageLaserRaygetDirectionsForBlast akan menjadi tidak perlu karena Anda dapat menggunakan currentCamera.CFrame.LookVector untuk menghitung arah tembakan.

Namun, karena contoh tersebut menyediakan jenis blaster tambahan yang menghasilkan beberapa sinar laser dengan penyebaran horizontal yang lebar, getDirectionsForBlast harus menghitung arah untuk setiap sinar laser dari penyebaran sesuai dengan sudut mereka dalam konfigurasi blaster:

getDirectionsForBlast
if numLasers == 1 then
-- Untuk sinar tunggal, mereka diarahkan lurus
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Untuk beberapa sinar, sebarkan secara merata secara horizontal
-- di sekitar interval laserSpreadDegrees di sekitar pusat
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

Untuk menunjukkan konsep ini lebih lanjut, jika Anda ingin menyertakan jenis blaster ketiga dengan penyebaran vertikal yang lebar, Anda dapat membuat atribut blaster baru, seperti spreadDirection, kemudian menyesuaikan perhitungan CFrame untuk menggunakan sumbu yang berbeda. Misalnya, perhatikan perbedaan dalam perhitungan direction dalam skrip berikut untuk jenis blaster ketiga yang hipotetis ini.

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

Akhirnya, fungsi rayDirections() mengembalikan tabel Vectors yang mewakili arah setiap sinar laser. Jika perlu, Anda dapat menambahkan beberapa logging untuk mendapatkan gambaran tentang seperti apa data ini.

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)

Mengarahkan sinar

castLaserRay(), fungsi kedua di ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, melakukan operasi yang lebih kompleks dalam skrip. Ini dimulai dengan menentukan parameter sehingga dapat melakukan panggilan Workspace:Raycast() untuk tujuan raycasting. Raycasting adalah proses mengirimkan sinar tak terlihat dari titik Vector3 dalam arah tertentu dengan panjang yang ditentukan, kemudian memeriksa jalurnya untuk melihat di mana ia berinteraksi dengan objek lain.

Informasi ini sangat berguna untuk permainan tembak-menembak orang pertama karena memungkinkan Anda melihat kapan dan di mana tembakan berinteraksi dengan pemain atau lingkungan. Misalnya, gambar berikut menunjukkan dua sinar yang memancarkan sejajar satu sama lain. Berdasarkan titik asal dan arah mereka, Sinar A melewatkan dinding dan terus sampai mencapai jarak maksimumnya, sementara Sinar B bertabrakan dengan dinding. Untuk informasi lebih lanjut tentang proses ini, lihat Raycasting.

Diagram di mana Sinar A terus melewati dinding, dan Sinar B bertabrakan dengan dinding.

Parameter castLaserRay() menentukan bahwa panggilan Raycast() harus mempertimbangkan setiap bagian di workspace kecuali karakter yang menembak. Skrip kemudian mengarahkan sinar untuk setiap arah dalam tabel directions. Jika sebuah sinar mengenai sesuatu, ia menghasilkan RaycastResult, yang memiliki lima properti:

Nilai Instance adalah yang paling penting dari properti ini untuk gameplay permainan laser tag contoh karena ini mengkomunikasikan kapan sinar bertabrakan dengan pemain lain. Untuk mengambil informasi ini, permainan menggunakan fungsi pembantu ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. Jika mengembalikan nil, instance tersebut bukan bagian dari pemain, yang berarti sinar mengenai objek mati dalam lingkungan.

castLaserRay() kemudian menggunakan Position dan Normal untuk membuat CFrame baru yang disebut sebagai destination sinar. Setiap sinar memiliki tujuan, dan itu adalah tempat sinar mengenai ruang 3D, atau titik di ujung jarak maksimumnya. Tergantung pada seberapa baik pemain Anda mengarahkan, banyak atau sebagian besar nilai taggedPlayer adalah nil.

castLaserRay
if result then
-- Tembakan mengenai sesuatu, periksa apakah itu adalah pemain.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Tembakan tidak mengenai apa pun, jadi tujuannya adalah
-- titik di jarak maksimumnya.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

Validasi tembakan

Untuk mencegah kecurangan, bab sebelumnya Implementasi Blaster menjelaskan bagaimana blastClient memberi tahu server tentang tembakan menggunakan RemoteEvent sehingga dapat memverifikasi semua data yang dikirim setiap klien, seperti apakah mereka benar-benar menandai pemain lain dengan blaster mereka. Proses validasi sinar ini terjadi di ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, dan setiap pemeriksaan berkorelasi dengan skrip modul bersarang:

  1. Pertama, getValidatedRayResults memanggil validateRayResult untuk memeriksa bahwa setiap entri dalam tabel rayResults dari klien adalah CFrame dan Player (atau nil).

  2. Selanjutnya, ia memanggil isRayAngleFromOriginValid untuk membandingkan sudut penyebaran laser yang diharapkan dengan yang dari klien. Kode ini khususnya menunjukkan keuntungan menggunakan ReplicatedStorage karena server dapat memanggil getDirectionsForBlast sendiri, menyimpan hasilnya sebagai data "yang diharapkan", dan kemudian membandingkannya dengan data dari klien.

    Sama seperti validasi blaster dari bab sebelumnya, isRayAngleFromOriginValid bergantung pada nilai toleransi untuk menentukan apa yang dianggap sebagai perbedaan sudut yang "berlebihan":

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

    Roblox mengabstraksi bagian matematika yang paling terlibat, sehingga hasilnya adalah fungsi pembantu yang pendek dan sangat dapat digunakan kembali dengan aplikasi di berbagai permainan:

    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. Pemeriksaan berikutnya adalah yang paling intuitif. Sementara getValidatedBlastData menggunakan DISTANCE_SANITY_CHECK_TOLERANCE_STUDS untuk memverifikasi bahwa pemain yang menembak berada dekat dengan titik asal sinar, isPlayerNearPosition menggunakan logika identik untuk memeriksa apakah pemain yang ditandai berada dekat dengan tujuan sinar:

    isPlayerNearPosition
    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. Pemeriksaan terakhir isRayPathObstructed menggunakan variasi dari operasi ray cast untuk memeriksa apakah tujuan sinar berada di belakang dinding atau halangan lain dari posisi klien. Misalnya, jika seorang pemain jahat secara sistematis menghapus semua dinding dari permainan untuk menandai pemain lain, server akan memeriksa dan mengonfirmasi bahwa sinar tersebut tidak valid karena ia mengetahui setiap posisi objek dalam lingkungan.

    isRayPathObstructed
    local scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)
    scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude

Tidak ada strategi anti-eksploitasi yang komprehensif, tetapi penting untuk mempertimbangkan bagaimana pemain jahat dapat mendekati permainan Anda sehingga Anda dapat menempatkan pemeriksaan yang dapat dijalankan server untuk menandai perilaku mencurigakan.

Mengurangi kesehatan pemain

Setelah memverifikasi bahwa seorang pemain menandai pemain lain, langkah terakhir dalam menyelesaikan loop gameplay utama dalam permainan laser tag contoh adalah mengurangi kesehatan pemain yang ditandai, meningkatkan papan peringkat, dan menghidupkan kembali pemain kembali ke ronde.

Dimulai dengan mengurangi kesehatan pemain yang ditandai, Spawning dan respawning membahas perbedaan antara Player dan Player.Character, khususnya bahwa karakter adalah model Humanoid. Model Humanoid memiliki properti Health dengan nilai default 100. Alih-alih menerapkan sistemnya sendiri, permainan laser tag contoh menggunakan properti bawaan ini untuk melacak berapa banyak kerusakan yang diperlukan pemain sebelum mereka ditandai keluar dari ronde.

Permainan menyimpan nilai kerusakan dalam atribut damagePerHit dari setiap blaster. Misalnya, blaster yang menembakkan satu sinar laser memberikan 10 poin kerusakan, jadi dibutuhkan sepuluh tembakan dengan blaster ini untuk menandai pemain lain. Untuk memulai proses menandai pemain keluar, LaserBlastHandler memanggil ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, yang memeriksa tabel rayResults yang sekarang telah divalidasi untuk pemain dan meneruskan damagePerHit ke onPlayerTagged.

Health tidak menerima nilai negatif, jadi onPlayerTagged memiliki beberapa logika untuk menjaga kesehatan pemain tetap di atas atau sama dengan nol. Setelah memverifikasi bahwa kesehatan pemain di atas nol, ia membandingkan kesehatan dengan damagePerHit dan menggunakan nilai yang lebih kecil dari keduanya. Misalnya, jika seorang pemain memiliki 10 kesehatan dan terkena sinar laser dengan kerusakan 15, laser hanya memberikan 10 poin kerusakan.

Pendekatan ini mungkin terlihat sedikit rumit. Misalnya, mengapa tidak langsung mengatur kesehatan pemain menjadi nol jika itu akan negatif? Alasannya adalah karena mengatur nilai kesehatan menghindari medan gaya. Menggunakan metode Humanoid:TakeDamage() memastikan bahwa pemain tidak menerima kerusakan saat medan gaya mereka aktif.

onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Larang tembakan antar tim
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Hindari kesehatan negatif
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage memastikan kesehatan tidak berkurang jika ForceField aktif
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Beri pemainBlasted satu poin untuk menandai playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

Langkah berikutnya adalah meningkatkan papan peringkat. Mungkin tampak tidak perlu bagi LaserBlastHandler untuk menyertakan pemain yang menembak bersama dengan data tembakan, tetapi tanpa informasi itu, permainan tidak dapat memberi kredit kepada pemain yang menandai seseorang keluar. Akhirnya, pemain yang ditandai keluar dihidupkan kembali ke ronde, yang dapat Anda tinjau di Spawning dan Respawning.

Lima bab dalam kurikulum ini mencakup loop gameplay inti permainan, tetapi masih ada banyak area untuk dijelajahi, seperti:

  • Visual blaster: Lihat ReplicatedStorageFirstPersonBlasterVisuals dan ServerScriptServiceThirdPersonBlasterVisuals.
  • Audio: Lihat ReplicatedStorageSoundHandler.
  • Mode Kustom: Bagaimana Anda dapat memodifikasi permainan ini untuk memperkenalkan jenis tujuan baru, seperti mencetak poin terbanyak sebelum waktu habis?

Untuk logika gameplay yang lebih luas untuk permainan laser tag, serta aset lingkungan berkualitas tinggi yang dapat digunakan kembali, tinjau template Laser Tag.

©2026 Roblox Corporation. Roblox, logo Roblox, dan Powering Imagination termasuk dalam merek dagang kami yang terdaftar dan tidak terdaftar di AS dan negara lainnya.