Vuruşları tespit etme, patlamaların oyuncularla çarpıştığı anı tanımlama sürecidir ve ardından sağlıklarını buna göre azaltma işlemini içerir. Yüksek seviyede, bu işlemi iki şekilde düşünebilirsiniz:
- Bir merminin hedefe çarpıp çarpmadığını fiziksel olarak simüle eden bir kontrol.
- Blaster'ın hedefe nişan alıp almadığını anlık olarak kontrol eden bir kontrol.
Kullandığınız vuruş tespit türü, oyununuzun oyun gereksinimlerine bağlıdır. Örneğin, topların belirli bir hızda elden çıkması, havada hareket ederken düşmesi veya hava koşullarından dolayı yön değiştirmesi gereken bir yakan top oyunu için fiziksel olarak simüle edilen bir kontrol uygundur. Ancak, lazer tag oyunu için, ışınların neredeyse sonsuz bir hıza sahip olması ve yerçekimi ve rüzgar hızı gibi çevresel faktörleri göz ardı etmesi gerektiğinden, anlık bir kontrol daha iyi bir eşleşmedir.
örnek lazer tag oyunu referans alarak, bu öğreticinin bu bölümü, 3D alandaki vuruş tespiti arkasındaki scriptler hakkında bilgi verir ve aşağıdaki konularda rehberlik eder:
- Patlama yönünü mevcut kamera değerlerinden ve oyuncunun blaster türünden alma.
- Blaster'dan patlama sırasında düz bir yolda ışınlar atma.
- Blaster verilerinin kötüye kullanılmasını önlemek için patlamayı doğrulama.
- Her blaster türünden gelen patlama hasarına ve oyuncuya çarpan ışın sayısına göre oyuncu sağlığını azaltma.
Bu bölümü tamamladıktan sonra, ses, aydınlatma ve özel efektler gibi oyun deneyiminizi geliştirmek için ek geliştirme konularını keşfedebilirsiniz.
Patlama yönünü al
Bir oyuncu blaster'ını patlattıktan sonra, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData iki işlev çağırarak vuruş tespit sürecini başlatır: rayDirections() ve rayResults().
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)rayDirections için girişler basittir: mevcut kamera konumu ve dönüş değerleri ile oyuncunun blaster türü. Eğer örnek lazer tag oyunu, oyunculara yalnızca tek bir lazer ışını üreten blaster'lar verseydi, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast gereksiz olurdu çünkü currentCamera.CFrame.LookVector kullanarak patlama yönünü hesaplayabilirdiniz.
Ancak, örnek, geniş, yatay bir yayılım ile birkaç lazer ışını üreten ek bir blaster türü sağladığından, getDirectionsForBlast her lazer ışınının yayılım içindeki açılarına göre yönünü hesaplamak zorundadır:
if numLasers == 1 then
-- Tek lazerler için, doğrudan nişan alırlar
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Birden fazla lazer için, bunları yatay olarak eşit şekilde yay
-- merkez etrafında laserSpreadDegrees aralığında
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
endBu kavramı daha da göstermek için, geniş, dikey bir yayılım ile üçüncü bir blaster türü eklemeyi düşünürseniz, spreadDirection gibi yeni bir blaster niteliği oluşturabilir ve ardından CFrame hesaplamasını farklı bir ekseni kullanacak şekilde ayarlayabilirsiniz. Örneğin, aşağıdaki scriptte bu varsayımsal üçüncü blaster türü için direction hesaplamalarındaki farkı not edin.
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 directionsSonuç olarak, rayDirections() işlevi, her lazer ışınının yönünü temsil eden bir Vectors tablosu döndürür. Yardımcı olursa, bu verilerin nasıl göründüğünü anlamak için bazı günlük kayıtları ekleyebilirsiniz.
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- yeni satır
print(direction) -- yeni satır
end -- yeni satır
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)Işınları at
castLaserRay(), ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData içindeki ikinci işlev, script içinde daha karmaşık işlemleri gerçekleştirir. Raycasting amaçları için Workspace:Raycast() çağrıları yapabilmesi için parametreleri belirterek başlar. Raycasting, belirli bir yönde tanımlı bir uzunlukla bir Vector3 noktasından görünmez bir ışın göndermeyi ve ardından yolunu kontrol ederek diğer nesnelerle nerede kesiştiğini görmeyi içerir.
Bu bilgi, patlamaların oyuncularla veya çevreyle ne zaman ve nerede kesiştiğini görmenizi sağladığı için birinci şahıs nişancı oyunları için özellikle yararlıdır. Örneğin, aşağıdaki resim, birbirine paralel iki ışının nasıl atıldığını gösterir. Kaynak noktalarına ve yönlerine göre, Işın A duvarı kaçırır ve maksimum mesafesine kadar devam ederken, Işın B duvara çarpar. Bu süreç hakkında daha fazla bilgi için Raycasting sayfasına bakın.

castLaserRay() parametreleri, Raycast() çağrılarının patlama yapan karakter hariç, çalışma alanındaki her parçayı dikkate alması gerektiğini belirtir. Script, directions tablosundaki her yön için bir ışın atar. Eğer bir ışın bir şeye çarparsa, beş özelliğe sahip bir RaycastResult oluşturur:
- Distance – Işın kaynağı ile kesişim noktası arasındaki mesafe.
- Material – Kesişim noktasındaki Enum.Material.
Instance değeri, örnek lazer tag oyununun oyun deneyimi için bu özelliklerin en kritik olanıdır çünkü ışınların diğer oyuncularla çarpıştığını iletir. Bu bilgiyi almak için oyun, ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant yardımcı işlevini kullanır. Eğer nil dönerse, örnek bir oyuncunun parçası değildir, bu da ışının çevrede cansız bir nesneye çarptığı anlamına gelir.
castLaserRay() daha sonra Position ve Normal kullanarak yeni bir CFrame oluşturur ve buna ışının destination'ı denir. Her ışının bir hedefi vardır ve bu ya 3D alanda ışının çarptığı yer ya da maksimum mesafesinin sonundaki noktadır. Oyuncularınızın ne kadar iyi nişan aldığına bağlı olarak, birçok veya çoğu taggedPlayer değeri nil olabilir.
if result then
-- Patlama bir şeye çarptı, bunun bir oyuncu olup olmadığını kontrol et.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Patlama hiçbir şeye çarpmadı, bu yüzden hedefi
-- maksimum mesafesinin sonundaki noktadır.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
endPatlamayı doğrula
Hileleri önlemek için, önceki bölüm Blaster'ları Uygulama blastClient'in patlamayı sunucuya bildirdiğini ve her istemcinin gönderdiği verileri doğrulamak için bir RemoteEvent kullandığını açıklar; örneğin, gerçekten başka bir oyuncuyu blaster'larıyla etiketleyip etiketlemediklerini kontrol eder. Bu ışın doğrulama süreci ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults içinde gerçekleşir ve her kontrol, iç içe geçmiş bir modül scriptine karşılık gelir:
İlk olarak, getValidatedRayResults her bir rayResults tablosundaki girişin bir CFrame ve bir Player (veya nil) olduğunu kontrol etmek için validateRayResult çağrısını yapar.
Sonra, istemciden gelen lazer yayılımının beklenen açılarını karşılaştırmak için isRayAngleFromOriginValid çağrısını yapar. Bu kod, sunucunun getDirectionsForBlast'ı kendisinin çağırabilmesi, dönen veriyi "beklenen" veri olarak saklayabilmesi ve ardından istemciden gelen verilerle karşılaştırabilmesi açısından ReplicatedStorage kullanmanın avantajını gösterir.
Önceki bölümdeki blaster doğrulaması gibi, isRayAngleFromOriginValid açılardaki "aşırı" farkı belirlemek için bir tolerans değerine dayanır:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox, en karmaşık matematiksel işlemleri soyutlar, bu nedenle sonuç, bir dizi oyunda uygulanabilir, kısa ve yüksek derecede yeniden kullanılabilir bir yardımcı işlevdir:
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)endBir sonraki kontrol en sezgisel olandır. getValidatedBlastData, patlamayı yapan oyuncunun ışının kaynağına yakın olup olmadığını doğrulamak için DISTANCE_SANITY_CHECK_TOLERANCE_STUDS kullanırken, isPlayerNearPosition etiketlenen oyuncunun ışının hedefi yakınında olup olmadığını kontrol etmek için aynı mantığı kullanır:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendSon kontrol isRayPathObstructed, istemcinin konumundan ışının hedefinin bir duvarın veya diğer bir engelin arkasında olup olmadığını kontrol etmek için ışın atma işleminin bir varyasyonunu kullanır. Örneğin, kötü niyetli bir oyuncu, diğer oyuncuları etiketlemek için oyundaki tüm duvarları sistematik olarak kaldırırsa, sunucu, ışınların geçersiz olduğunu kontrol eder ve her nesnenin konumunu bildiği için bunu doğrular.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Hiçbir anti-hile stratejisi kapsamlı değildir, ancak kötü niyetli oyuncuların oyununuza nasıl yaklaşabileceğini düşünmek ve sunucunun şüpheli davranışları işaretlemek için çalıştırabileceği kontroller koymak önemlidir.
Oyuncu sağlığını azalt
Bir oyuncunun başka bir oyuncuyu etiketlediğini doğruladıktan sonra, örnek lazer tag oyunundaki ana oyun döngüsünü tamamlamanın son adımları, etiketlenen oyuncunun sağlığını azaltmak, liderlik tablosunu artırmak ve oyuncuyu tekrar tura geri getirmektir.
Etiketlenen oyuncunun sağlığını azaltarak başlayarak, Doğma ve Yeniden Doğma Player ve Player.Character arasındaki ayrımı kapsar; özellikle bir karakterin bir Humanoid modeli olduğunu belirtir. Humanoid modellerinin 100 varsayılan değerine sahip bir Health özelliği vardır. Kendi sistemini uygulamak yerine, örnek lazer tag oyunu, bir oyuncunun tura çıkmadan önce ne kadar hasar alması gerektiğini takip etmek için bu yerleşik özelliği kullanır.
Oyun, her blaster'ın damagePerHit niteliğinde hasar değerlerini saklar. Örneğin, tek bir lazer ışını patlatan blaster 10 puan hasar verir, bu nedenle bu blaster ile başka bir oyuncuyu etiketlemek için on patlama gerekir. Bir oyuncuyu etiketleme sürecini başlatmak için, LaserBlastHandler ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers'ı çağırır, bu da doğrulanan rayResults tablosunu oyuncular için kontrol eder ve damagePerHit'i onPlayerTagged'a geçirir.

Health negatif değerleri kabul etmez, bu nedenle onPlayerTagged bazı mantıklara sahiptir ve oyuncu sağlığını sıfırın altında tutar. Oyuncu sağlığının sıfırdan büyük olduğunu doğruladıktan sonra, sağlığı damagePerHit ile karşılaştırır ve iki değerden daha küçük olanı kullanır. Örneğin, bir oyuncunun 10 sağlığı varsa ve 15 hasar veren bir lazer ışınına çarparsa, lazer yalnızca 10 puan hasar verir.
Bu sorunu ele alma şekli biraz karmaşık görünebilir. Örneğin, neden oyuncu sağlığını sıfıra ayarlamıyorsunuz, eğer negatif olacaksa? Bunun nedeni, sağlık değerlerini ayarlamanın kuvvet alanını aşmasıdır. Humanoid:TakeDamage() yöntemini kullanmak, oyuncuların kuvvet alanları aktifken hasar almamasını sağlar.
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Dost ateşine izin verme
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Negatif sağlığı önle
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage, kuvvet alanı aktifken sağlığın düşmesini sağlamaz
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- playerBlasted'a playerTagged'ı etiketlediği için bir puan ver
Scoring.incrementScore(playerBlasted, 1)
end
end
endSonraki adım, liderlik tablosunu artırmaktır. LaserBlastHandler'ın patlama verileriyle birlikte patlamayı yapan oyuncuyu dahil etmesi gereksiz görünebilir, ancak bu bilgi olmadan oyun, birini etiketlediği için oyuncuya kredi veremez. Son olarak, etiketlenen oyuncu tura geri doğar, bunu Doğma ve Yeniden Doğma sayfasında inceleyebilirsiniz.
Bu müfredattaki beş bölüm, oyunun temel oyun döngüsünü kapsar, ancak keşfedilecek birçok alan daha vardır, örneğin:
- Blaster görselleri: ReplicatedStorage ⟩ FirstPersonBlasterVisuals ve ServerScriptService ⟩ ThirdPersonBlasterVisuals'a bakın.
- Ses: ReplicatedStorage ⟩ SoundHandler'a bakın.
- Özel Modlar: Bu oyunu, zaman dolmadan önce en fazla puanı kazanmak gibi yeni türde hedefler eklemek için nasıl değiştirebilirsiniz?
Lazer tag oyunu için genişletilmiş oyun mantığı ve yeniden kullanılabilir, yüksek kaliteli çevresel varlıklar için Lazer Tag şablonunu inceleyin.