Vuruşları Tespit Et

*Bu içerik, yapay zekâ (beta) kullanılarak çevrildi ve hatalar içerebilir. Sayfayı İngilizce görüntülemek için buraya tıkla.

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:

  1. Bir merminin hedefe çarpıp çarpmadığını fiziksel olarak simüle eden bir kontrol.
  2. 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, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData iki işlev çağırarak vuruş tespit sürecini başlatır: rayDirections() ve rayResults().

generateBlastData

local 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, ReplicatedStorageLaserRaygetDirectionsForBlast 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:

getDirectionsForBlast

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, onları yatay olarak eşit şekilde yaymak
-- merkez etrafındaki 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
end

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

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.

generateBlastData

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ı gönder

castLaserRay(), ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData 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.

Işın A'nın duvarın içinden geçtiği ve Işın B'nin duvara çarptığı bir diyagram.

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:

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, ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant 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.

castLaserRay

if 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_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

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 ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults içinde gerçekleşir ve her kontrol, iç içe bir modül betiği ile ilişkilidir:

  1. Ö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.

  2. 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:

    isRayAngleFromOriginValid

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

    Roblox, 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:

    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. Bir 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:

    isPlayerNearPosition

    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. Son 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.

    isRayPathObstructed

    local 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 ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers 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: ReplicatedStorageFirstPersonBlasterVisuals ve ServerScriptServiceThirdPersonBlasterVisuals'e bakın.
  • Ses: ReplicatedStorageSoundHandler'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.

©2026 Roblox Corporation. Roblox, the Roblox logo and Powering Imagination are among our registered and unregistered trademarks in the U.S. and other countries.