Mendeteksi tembakan adalah proses mengidentifikasi kapan ledakan bertubrukan dengan pemain, lalu mengurangi kesehatan mereka sesuai. Secara umum, Anda dapat memikirkan pekerjaan ini sebagai salah satu dari dua cara:
- Sebuah pemeriksaan yang disimulasikan secara fisik untuk menentukan apakah proyektil mengenai sasaran.
- Sebuah pemeriksaan instan untuk menentukan apakah blaster diarahkan ke sasaran.
Jenis deteksi tembakan yang Anda gunakan tergantung pada kebutuhan gameplay dari pengalaman Anda. Misalnya, pemeriksaan yang disimulasikan secara fisik sesuai untuk pengalaman 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 pengalaman laser tag di mana sinar harus memiliki kecepatan hampir tak terbatas dan mengabaikan faktor lingkungan seperti gravitasi dan kecepatan angin.
Menggunakan contoh pengalaman laser tag sebagai referensi, bagian tutorial ini mengajarkan Anda tentang skrip di balik deteksi tembakan dalam ruang 3D, termasuk panduan tentang:
- Mendapatkan arah ledakan dari nilai kamera saat ini dan jenis blaster pemain.
- Menembakkan sinar dalam jalur lurus dari blaster saat meledak.
- Memvalidasi ledakan untuk mencegah eksploitasi data blaster.
- Mengurangi kesehatan pemain sesuai dengan kerusakan ledakan 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 ledakan
Setelah pemain menembakkan blaster mereka, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData memanggil dua fungsi untuk memulai proses deteksi tembakan: rayDirections() dan rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Input untuk rayDirections cukup sederhana: posisi kamera saat ini dan nilai rotasi, serta jenis blaster pemain. Jika pengalaman laser tag contoh hanya memberikan pemain blaster yang memproduksi satu sinar laser, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast akan menjadi tidak perlu karena Anda dapat menggunakan currentCamera.CFrame.LookVector untuk menghitung arah untuk ledakan.
Namun, karena contoh memberikan jenis blaster tambahan yang memproduksi 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:
getDirectionsForBlastif numLasers == 1 then-- Untuk laser tunggal, mereka diarahkan lurustable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Untuk beberapa laser, sebarkan mereka secara merata secara horizontal-- selama interval laserSpreadDegrees di sekitar pusatlocal 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
Untuk mendemonstrasikan konsep ini lebih jauh, jika Anda ingin memasukkan jenis blaster ketiga dengan penyebaran yang lebar, vertikal, Anda dapat membuat atribut blaster baru, seperti spreadDirection, lalu 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 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
Akhirnya, fungsi rayDirections() mengembalikan tabel Vectors yang merepresentasikan arah dari setiap sinar laser. Jika merasa membantu, Anda dapat menambahkan beberapa logging untuk mendapatkan gambaran tentang seperti apa data ini.
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)
Menembakkan sinar
castLaserRay(), fungsi kedua di ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, 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, lalu memeriksa jalurnya untuk melihat di mana ia berpotongan dengan objek lain.
Informasi ini sangat berguna untuk pengalaman penembak orang pertama karena memungkinkan Anda untuk melihat kapan dan di mana ledakan berpotongan dengan pemain atau lingkungan. Misalnya, gambar berikut menunjukkan dua sinar yang menembak sejajar satu sama lain. Berdasarkan titik asal dan arah mereka, Sinar A melewatkan dinding dan terus sampai mencapai jarak maksimum, sementara Sinar B bertabrakan dengan dinding. Untuk informasi lebih lanjut tentang proses ini, lihat Raycasting.

Parameter castLaserRay() menentukan bahwa panggilan Raycast() harus mempertimbangkan setiap bagian di dalam workspace kecuali karakter yang meledak. Skrip kemudian menembakkan sinar untuk setiap arah dalam tabel directions. Jika sebuah sinar mengenai sesuatu, ia menghasilkan RaycastResult, yang memiliki lima properti:
- Distance – Jarak antara titik asal sinar dan titik perpotongan.
- Material – Enum.Material di titik perpotongan.
Nilai Instance adalah properti yang paling kritis untuk gameplay pengalaman laser tag contoh ini karena mengkomunikasikan kapan sinar bertabrakan dengan pemain lain. Untuk mengambil informasi ini, pengalaman menggunakan fungsi pembantu ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Jika mengembalikan nil, itu berarti instansi bukan bagian dari seorang pemain, yang berarti sinar mengenai objek tak bernyawa di dalam lingkungan.
castLaserRay() kemudian menggunakan Position dan Normal untuk membuat CFrame baru yang disebut sebagai destination dari sinar. Setiap sinar memiliki tujuan, dan itu adalah tempat di mana sinar mengenai ruang 3D, atau titik di ujung jarak maksimumnya. Bergantung pada seberapa baik pemain Anda mengincar, banyak atau sebagian besar nilai taggedPlayer adalah nil.
castLaserRayif result then-- Ledakan mengenai sesuatu, periksa apakah itu pemain.destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- Ledakan tidak mengenai apapun, jadi tujuannya adalah-- titik di jarak maksimumnya.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Validasi ledakan
Untuk mencegah kecurangan, bab sebelumnya Mengimplementasikan Blaster menjelaskan bagaimana blastClient memberi tahu server tentang ledakan menggunakan RemoteEvent agar dapat memverifikasi semua data yang dikirim oleh setiap klien, seperti apakah mereka benar-benar menandai pemain lain dengan blaster mereka. Proses validasi sinar ini terjadi di ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, dan setiap pemeriksaan berkorelasi dengan skrip modul bersarang:
Pertama, getValidatedRayResults memanggil validateRayResult untuk memeriksa bahwa setiap entri dalam tabel rayResults dari klien adalah CFrame dan seorang Player (atau nil).
Selanjutnya, ia memanggil isRayAngleFromOriginValid untuk membandingkan sudut yang diharapkan dari penyebaran laser dengan yang dari klien. Kode ini khususnya menunjukkan keuntungan menggunakan ReplicatedStorage karena server dapat memanggil getDirectionsForBlast sendiri, menyimpan pengembalian sebagai data "yang diharapkan", dan kemudian membandingkannya dengan data dari klien.
Seperti validasi blaster dari bab sebelumnya, isRayAngleFromOriginValid bergantung pada nilai toleransi untuk menentukan apa yang dianggap sebagai perbedaan sudut "berlebihan":
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox mengabstraksi bagian matematika yang paling terlibat, jadi hasilnya adalah fungsi pembantu yang pendek dan sangat dapat digunakan kembali dengan aplikasi di berbagai pengalaman:
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)endPemeriksaan berikutnya adalah yang paling intuitif. Sementara getValidatedBlastData menggunakan DISTANCE_SANITY_CHECK_TOLERANCE_STUDS untuk memverifikasi bahwa pemain yang meledak berada dekat dengan titik asal sinar, isPlayerNearPosition menggunakan logika identik untuk memeriksa apakah pemain yang ditandai dekat dengan tujuan sinar:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendPemeriksaan terakhir isRayPathObstructed menggunakan variasi dari operasi sinar untuk memeriksa apakah tujuan sinar berada di belakang dinding atau halangan lain dari posisi klien. Misalnya, jika seorang pemain berbuat jahat secara sistematis menghapus semua dinding dari pengalaman untuk menandai pemain lain, server akan memeriksa dan mengonfirmasi bahwa sinar tersebut tidak valid karena ia mengetahui setiap posisi objek di dalam lingkungan.
isRayPathObstructedlocal 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 berbuat jahat dapat mendekati pengalaman Anda sehingga Anda dapat menempatkan pemeriksaan yang dapat dijalankan oleh server untuk menandai perilaku mencurigakan.
Mengurangi kesehatan pemain
Setelah memverifikasi bahwa seorang pemain telah menandai pemain lain, langkah terakhir dalam menyelesaikan loop gameplay utama di pengalaman laser tag contoh ini adalah mengurangi kesehatan pemain yang ditandai, menambahkan poin ke papan peringkat, dan memunculkan kembali pemain ke ronde.
Dimulai dengan mengurangi kesehatan pemain yang ditandai, Memunculkan dan memunculkan kembali membahas perbedaan antara Player dan Player.Character, khususnya bahwa karakter adalah model Humanoid. Model Humanoid memiliki properti Health dengan nilai default 100. Alih-alih mengimplementasikan sistemnya sendiri, pengalaman laser tag contoh ini menggunakan properti bawaan ini untuk melacak seberapa banyak kerusakan yang diperlukan sebelum seorang pemain ditandai keluar dari ronde.
Pengalaman tersebut menyimpan nilai kerusakan dalam atribut damagePerHit dari setiap blaster. Misalnya, blaster yang menembakkan satu sinar laser memberikan 10 poin kerusakan, sehingga diperlukan sepuluh ledakan dengan blaster ini untuk menandai pemain lain. Untuk memulai proses menandai pemain keluar, LaserBlastHandler memanggil ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, yang memeriksa tabel rayResults yang sekarang telah divalidasi untuk pemain dan melewatkan 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 ledakan laser dengan kerusakan 15, laser hanya memberikan 10 poin kerusakan.
Pendekatan ini mungkin tampak sedikit rumit. Misalnya, mengapa tidak langsung mengatur kesehatan pemain ke nol jika itu akan negatif? Alasannya adalah karena mengatur nilai kesehatan menghindari bidang kekuatan. Menggunakan metode Humanoid:TakeDamage() memastikan bahwa pemain tidak menerima kerusakan saat bidang kekuatan mereka aktif.
onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Larang tembakan teman
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 diturunkan jika ForceField aktif
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Beri poin kepada playerBlasted untuk menandai playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end
Langkah berikutnya adalah menambahkan poin ke papan peringkat. Mungkin tampak tidak perlu bagi LaserBlastHandler untuk menyertakan pemain yang meledak bersama data ledakan, tetapi tanpa informasi itu, pengalaman tidak dapat memberikan kredit kepada pemain karena telah menandai seseorang. Akhirnya, pemain yang ditandai keluar muncul kembali ke ronde, yang dapat Anda tinjau di Memunculkan dan Memunculkan Kembali.
Lima bab dalam kurikulum ini mencakup loop gameplay inti dari pengalaman, tetapi masih ada banyak area untuk dijelajahi, seperti:
- Visual blaster: Lihat ReplicatedStorage ⟩ FirstPersonBlasterVisuals dan ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio: Lihat ReplicatedStorage ⟩ SoundHandler.
- Mode Kustom: Bagaimana Anda dapat memodifikasi pengalaman ini untuk memperkenalkan jenis tujuan baru, seperti mencetak poin terbanyak sebelum waktu habis?
Untuk logika gameplay yang diperluas untuk pengalaman laser tag, serta aset lingkungan berkualitas tinggi yang dapat digunakan kembali, tinjau template Laser Tag.