Vuruşları tespit etmek, patlamaların oyuncularla çarpıştığı zamanları belirleme sürecidir ve ardından sağlıklarını azaltma işlemidir. Genel anlamda, bu işlemi şöyle 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ş tespiti türü, deneyiminizin 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 yön değiştirmesi gereken bir yakan top deneyimi için fiziksel bir simülasyon kontrolü uygundur. Ancak, lazer tag deneyimi gibi ışınların neredeyse sonsuz bir hıza sahip olması ve yerçekimi ile rüzgar hızı gibi çevresel faktörleri göz ardı etmesi gereken bir durumda anlık bir kontrol daha iyi bir seçenek olacaktır.
Örnek lazer tag deneyimini referans alarak, bu öğretici bölüm, 3D alandaki vuruş tespiti arkasındaki betikler hakkında bilgi verir, şu konularla ilgili rehberlik eder:
- Patlama yönünü mevcut kamera değerlerinden ve oyuncunun blaster türünden alma.
- Blaster'dan patlarken düz bir yol boyunca ışın gönderme.
- Blaster verilerinin istismarını önlemek için patlamayı doğrulama.
- Her tür blaster'dan gelen patlama hasarına ve oyuncuya kaç ışının çarptığı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().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
rayDirections için girdiler basittir: mevcut kamera konumu ve döndürme değerleri ile oyuncunun blaster türü. Örnek lazer tag deneyimi, oyunculara tek bir lazer ışını üreten blaster'lar veriyorsa, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast gereksiz olur çünkü currentCamera.CFrame.LookVector kullanarak patlama yönünü hesaplayabilirsiniz.
Ancak, örnek, yatay olarak geniş bir yayılma ile birden fazla lazer ışını üreten ek bir blaster türü sağladığı için, getDirectionsForBlast, blaster konfigürasyonundaki açılara göre bu yayılmadaki her lazer ışını için yönü hesaplamak zorundadır:
getDirectionsForBlastif numLasers == 1 then-- Tek lazerler için, doğrudan nişan alırlartable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- Birden fazla lazer için, onları yatay olarak eşit şekilde yaymak-- merkez etrafındaki laserSpreadDegrees aralığındalocal 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
Bu kavramı daha iyi göstermek için, geniş, dikey bir yayılma ile üçüncü bir blaster türü eklemeyi düşünürseniz, yeni bir blaster özelliği oluşturabilir ve ardından CFrame hesaplamasını farklı bir eksen 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 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
Sonuç olarak, rayDirections() fonksiyonu her lazer ışınının yönünü temsil eden bir Vectors tablosunu döndürür. İsterseniz, bu verinin neye benzediğini anlamak için bazı günlüğe alma işlemleri ekleyebilirsiniz.
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)for _, direction in rayDirections do -- yeni satırprint(direction) -- yeni satırend -- yeni satırlocal rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
Işınları gönder
castLaserRay(), ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData içindeki ikinci işlevdir ve betikteki daha karmaşık işlemleri gerçekleştirir. Workspace:Raycast() çağrıları yapabilmesi için parametreleri belirleyerek başlar. Işın izi (raycasting), belirli bir yönde tanımlı bir uzunluk ile özel bir Vector3 noktasından görünmez bir ışın göndermeyi ve ardından bu ışının, diğer nesnelerle kesiştiği noktaları kontrol etmeyi içerir.
Bu bilgi, ilk kişi nişancı deneyimleri için özellikle faydalıdır çünkü patlamaların oyuncularla veya çevreyle ne zaman ve nerede kesiştiğini görmenizi sağlar. Örneğin, aşağıdaki görsel, birbirine paralel olarak iki ışının gönderildiğini gösterir. Köken noktalarına ve yönlerine göre, Işın A duvarı geçer ve maksimum mesafesine kadar devam eder, Işın B ise duvarla çarpışır. Bu süreç hakkında daha fazla bilgi almak için Işın İzleme sayfasına bakabilirsiniz.

castLaserRay() parametreleri, Raycast() çağrılarının, patlayan karakter dahil tüm parçaları göz önünde bulundurması gerektiğini belirtir. Betik, directions tablosundaki her yön için bir ışın gönderir. Eğer bir ışın bir şeye çarparsa, beş özelliği olan bir RaycastResult üretir:
- Distance – Işın kökeni ile kesişim noktası arasındaki mesafe.
- Material – Kesişim noktasındaki Enum.Material.
Instance değeri, örnek lazer tag deneyiminin oyun dinamikleri için en kritik olanıdır çünkü ışınların diğer oyuncularla çarpıştığını bildirmektedir. Bu bilgiyi almak için deneyim, ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant yardımcı fonksiyonunu kullanır. Eğer nil dönerse, örnek bir oyuncunun parçası değildir ve ışın, çevredeki cansız bir nesneye çarpmıştır.
castLaserRay() ardından, Position ve Normal kullanarak ışının destination (hedef) olarak adlandırılan yeni bir CFrame oluşturur. Her ışının bir hedefi vardır ve bu ya 3D alandaki çarpılan nokta ya da maksimum mesafenin sonundaki noktadır. Oyuncularınızın ne kadar iyi nişan aldığına bağlı olarak, pek çok veya tüm taggedPlayer değerleri nil olabilir.
castLaserRayif result then-- Patlama bir şeye çarptı, 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 mesafedeki noktadır.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
Patlamayı Doğrula
Hileyi önlemek için, önceki bölüm Blaster'ların Uygulanması, blastClient'ın patlamayı sunucuya bildirdiğini açıklar. Sunucu, her istemcinin gönderdiği verileri, başka bir oyuncuyu gerçekten etiketleyip etkilemediği gibi doğrulamak için bu bilgiyi kullanır. Bu ışın doğrulama süreci ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults içinde gerçekleşir ve her kontrol, iç içe bir modül betiği ile ilişkilidir:
Öncelikle, getValidatedRayResults, istemciden gelen rayResults tablosundaki her bir girişin bir CFrame ve bir Player (veya nil) olduğunu kontrol etmek için validateRayResult çağrısını yapar.
Ardından, isRayAngleFromOriginValid'ı arayarak lazer yayılma açılarını istemciden gelenlerle karşılaştırır. Bu kod, sunucunun getDirectionsForBlast'ı kendisinin çağırabilmesi, sonucu "beklenen" veri olarak saklayabilmesi ve sonra bunu istemciden gelen verilerle karşılaştırabilmesi nedeniyle ReplicatedStorage'ın avantajını gösterir.
Önceki bölümdeki blaster doğrulamasında olduğu gibi, isRayAngleFromOriginValid, açıların "aşırı" bir fark oluşturup oluşturmadığını 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, böylece sonuç kısa, yüksek derecede yeniden kullanılabilir bir yardımcı işlev olur ve çok çeşitli deneyimlerde uygulanabilir:
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 köken noktasına yakın olduğunu doğrulamak için DISTANCE_SANITY_CHECK_TOLERANCE_STUDS kullanırken, isPlayerNearPosition, etiketlenmiş oyuncunun ışının hedef noktasına yakın olup olmadığını kontrol etmek için benzer 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 arkasında bir duvar veya başka bir engel olup olmadığını kontrol etmek için ışın izleme işleminin bir varyasyonunu kullanır. Örneğin, kötü niyetli bir oyuncunun diğer oyuncuları etiketlemek için sistematik olarak deneyimden tüm duvarları kaldırdı düşünülürse, sunucu, ışınların geçersiz olduğunu kontrol edip onaylar çünkü çevredeki her nesnenin konumunu bilir.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
Herhangi bir hile önleme stratejisi kapsamlı değildir, ancak kötü niyetli oyuncuların deneyiminize nasıl yaklaşabileceğini göz önünde bulundurmak ve sunucunun şüpheli davranışları işaretleyebileceği kontrolleri yerleştirmek önemlidir.
Oyuncu sağlığını azalt
Bir oyuncunun başka bir oyuncuyu etiketlediğini doğruladıktan sonra, örnek lazer tag deneyimindeki ana oyun döngüsünü tamamlama adımları, etiketlenen oyuncunun sağlığını azaltmak, liderliği artırmak ve oyuncuyu yeniden döngüye yerleştirmektir.
Öncelikle, etiketlenen oyuncunun sağlık seviyesini azaltmakla başlayarak, Yeniden doğma bölümünde Player ve Player.Character arasında, özellikle bir karakterin bir Humanoid modeli olduğu ayrımını kapsar. Humanoid modellerinin varsayılan değeri 100 olan bir Health özelliği vardır. Kendi sistemini uygulamak yerine, örnek lazer tag deneyimi, bir oyuncunun ne kadar hasar alması gerektiğini takip etmek için bu yerleşik özelliği kullanır.
Deneyim, her blaster için damagePerHit özelliğinde hasar değerlerini saklar. Örneğin, tek bir lazer ışını patlatan blaster 10 puan hasar verir, bu nedenle bu blaster ile bir oyuncuyu etiketlemek için on patlama almak gerekir. Bir oyuncuyu etiketleme sürecini başlatmak için, LaserBlastHandler ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers fonksiyonunu çağırır, bu fonksiyon, artık doğrulanmış rayResults tablosunu oyuncular için kontrol eder ve damagePerHit değerini onPlayerTagged'a geçirir.

Health negatif değerler kabul etmez, bu nedenle onPlayerTagged bazı mantıklara sahiptir ve oyuncu sağlığının sıfırdan az olmasını engeller. 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 bu iki değerin daha küçüğünü kullanır. Örneğin, bir oyuncunun 10 sağlığı varsa ve 15 hasar veren bir lazer ışını ile vurulursa, lazer sadece 10 puan hasar verir.
Bu şekilde yaklaşım biraz karmaşık görünebilir. Örneğin, neden oyuncu sağlığını negatif olursa sıfıra ayarlamayalım? Bunun nedeni, sağlık değerlerini ayarlamanın kuvvet alanını geçersiz kılmasıdır. Humanoid:TakeDamage() yöntemini kullanmak, oyuncuların kuvvet alanları aktifken hasar almamalarını sağlar.
onPlayerTagged
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ı aktif olduğunda sağlığın düşmemesini sağlar
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- playerBlasted'a playerTagged'ı etiketlediği için bir puan ver
Scoring.incrementScore(playerBlasted, 1)
end
end
end
Sonraki adım, liderliği artırmaktır. LaserBlastHandler'ın patlama verileriyle birlikte patlayan oyuncuyu dahil etmesinin gereksiz gibi görünebilir; ancak bu bilgi olmadan deneyim, bir oyuncunun başkasını etiketlediğinde puan veremez. Son olarak, etiketlenen oyuncu yeniden döngüye yerleştirilir ve bunu Yeniden Doğma bölümünde inceleyebilirsiniz.
Bu müfredattaki beş bölüm, deneyimin temel oyun döngüsünü kapsar, ancak keşfedilecek hâlâ pek çok alan vardır, örneğin:
- Blaster görselleri: ReplicatedStorage ⟩ FirstPersonBlasterVisuals ve ServerScriptService ⟩ ThirdPersonBlasterVisuals'e bakın.
- Ses: ReplicatedStorage ⟩ SoundHandler'e bakın.
- Özel Modlar: Yeni tür hedefler tanıtmak için bu deneyimi nasıl değiştirebilirsiniz, örneğin süre dolmadan önce en çok puanı toplamak gibi?
Lazer tag deneyimi için genişletilmiş oyun mantığı ile birlikte yeniden kullanılabilir, yüksek kaliteli çevresel varlıklar için Lazer Tag şablonuna göz atın.