Détecter les coups

*Ce contenu est traduit en utilisant l'IA (Beta) et peut contenir des erreurs. Pour consulter cette page en anglais, clique ici.

Détecter les coups est le processus consistant à identifier quand des éclats entrent en collision avec des joueurs, puis à réduire leur santé en conséquence. À un niveau élevé, vous pouvez considérer ce travail comme soit :

  1. Un contrôle simulé physiquement pour voir si un projectile a frappé la cible.
  2. Un contrôle instantané pour voir si le blaster était visé sur la cible.

Le type de détection de coup que vous utilisez dépend des exigences de gameplay de votre expérience. Par exemple, un contrôle simulé physiquement est approprié pour une expérience de balle au prisonnier où les balles doivent quitter la main à une certaine vitesse, tomber en se déplaçant dans l'air ou changer de direction en fonction des conditions météorologiques. Cependant, un contrôle instantané est mieux adapté pour une expérience de laser tag où les faisceaux doivent avoir une vélocité presque infinie et ignorer des facteurs environnementaux tels que la gravité et la vitesse du vent.

En utilisant l' expérience de laser tag exemple comme référence, cette section du tutoriel vous enseigne les scripts derrière la détection des coups dans l'espace 3D, y compris des conseils sur :

  • Obtenir la direction de l'éclat à partir des valeurs de caméra actuelles et du type de blaster du joueur.
  • Lancer des rayons dans une trajectoire droite à partir du blaster pendant qu'il tire.
  • Valider l'éclat pour prévenir l'exploitation des données du blaster.
  • Réduire la santé des joueurs en fonction des dommages de l'éclat dus à chaque type de blaster et du nombre de rayons qui touchent le joueur.

Après avoir terminé cette section, vous pouvez explorer d'autres sujets de développement pour améliorer votre gameplay, tels que l'audio, l'éclairage et les effets spéciaux.

Obtenir la direction de l'éclat

Après qu'un joueur tire avec son blaster, ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData appelle deux fonctions pour commencer le processus de détection des coups : rayDirections() et rayResults().

generateBlastData

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

Les entrées pour rayDirections sont simples : la position et les valeurs de rotation de la caméra actuelle, et le type de blaster du joueur. Si l'expérience de laser tag exemple ne donnait aux joueurs que des blasters produisant un unique faisceau laser, ReplicatedStorageLaserRaygetDirectionsForBlast serait superflu car vous pourriez utiliser currentCamera.CFrame.LookVector pour calculer la direction de l'éclat.

Cependant, comme l'exemple fournit un type de blaster supplémentaire qui produit plusieurs faisceaux laser avec une large répartition horizontale, getDirectionsForBlast doit calculer la direction pour chaque faisceau laser de la répartition selon leurs angles dans la configuration du blaster :

getDirectionsForBlast

if numLasers == 1 then
-- Pour les lasers uniques, ils visent droit devant
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- Pour plusieurs lasers, les répartir uniformément horizontalement
-- sur un intervalle laserSpreadDegrees autour du centre
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

Pour démontrer ce concept plus en détail, si vous deviez inclure un troisième type de blaster avec une large répartition verticale, vous pourriez créer un nouvel attribut de blaster, tel que spreadDirection, puis ajuster le calcul de CFrame pour utiliser un axe différent. Par exemple, notez la différence dans les calculs de direction dans le script suivant pour ce troisième type de blaster hypothétique.


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

En fin de compte, la fonction rayDirections() renvoie un tableau de Vectors qui représentent la direction de chaque faisceau laser. Si cela peut vous aider, vous pouvez ajouter un peu de journalisation pour vous faire une idée de ce à quoi ressemble ces données.

generateBlastData

local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- nouvelle ligne
print(direction) -- nouvelle ligne
end -- nouvelle ligne
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

Lancer des rayons

castLaserRay(), la deuxième fonction dans ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, effectue les opérations plus complexes dans le script. Elle commence par spécifier des paramètres afin qu'elle puisse effectuer des appels Workspace:Raycast() en vue de raycasting. Le raycasting est le processus d'envoi d'un rayon invisible depuis un point Vector3 dans une direction spécifique avec une longueur définie, puis de vérifier son parcours pour voir où il intersecte d'autres objets.

Cette information est particulièrement utile pour les expériences de tir à la première personne car elle vous permet de voir quand et où les éclats intersectent des joueurs ou l'environnement. Par exemple, l'image suivante démontre deux rayons qui se lancent parallèlement l'un à l'autre. Selon leur point d'origine et leur direction, le Rayon A manque le mur et continue jusqu'à atteindre sa distance maximale, tandis que le Rayon B entre en collision avec le mur. Pour plus d'informations sur ce processus, voir Raycasting.

Un diagramme où le Rayon A passe à travers le mur, et le Rayon B entre en collision avec le mur.

Les paramètres de castLaserRay() spécifient que les appels Raycast() doivent considérer chaque partie dans l'espace de travail sauf le personnage qui a tiré. Le script lance alors un rayon pour chaque direction dans le tableau directions. Si un rayon touche quelque chose, il génère un RaycastResult, qui a cinq propriétés :

  • Distance – La distance entre l'origine du rayon et le point d'intersection.
  • Instance – La BasePart ou la cellule Terrain avec laquelle le rayon entre en intersection.
  • Material – Le Enum.Material au point d'intersection.
  • Position – La position Vector3 de l'intersection entre le rayon et l'Instance.
  • Normal – Le Vector3 du vecteur normal de la face avec laquelle le rayon entre en intersection.

La valeur Instance est la plus critique parmi ces propriétés pour le gameplay de l'expérience de laser tag exemple car elle communique quand les rayons entrent en collision avec d'autres joueurs. Pour récupérer cette information, l'expérience utilise la fonction d'assistance ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. Si elle renvoie nil, cela signifie que l'instance ne fait pas partie d'un joueur, ce qui implique que le rayon a touché un objet inanimé dans l'environnement.

castLaserRay() utilise ensuite Position et Normal pour créer un nouveau CFrame appelé la destination du rayon. Chaque rayon a une destination, et c'est soit là où le rayon a touché dans l'espace 3D, soit le point à l'extrémité de sa distance maximale. Selon à quel point vos joueurs visent bien, de nombreuses ou la plupart des valeurs taggedPlayer sont nil.

castLaserRay

if result then
-- L'éclat a touché quelque chose, vérifiez s'il s'agissait d'un joueur.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- L'éclat n'a touché rien, donc sa destination est
-- le point à sa distance maximale.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

Valider l'éclat

Pour prévenir la tricherie, le chapitre précédent Implémentation des Blasters explique comment blastClient notifie le serveur de l'éclat à l'aide d'un RemoteEvent afin qu'il puisse vérifier toutes les données que chaque client envoie, telles que s'il a réellement marqué un autre joueur avec son blaster. Ce processus de validation des rayons se produit dans ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults, et chaque vérification correspond à un module script imbriqué :

  1. Tout d'abord, getValidatedRayResults appelle validateRayResult pour vérifier que chaque entrée dans le tableau rayResults du client est un CFrame et un Player (ou nil).

  2. Ensuite, il appelle isRayAngleFromOriginValid pour comparer les angles attendus de la répartition du laser à ceux du client. Ce code en particulier montre l'avantage de l'utilisation de ReplicatedStorage car le serveur peut appeler lui-même getDirectionsForBlast, stocker le retour comme les données "attendues" et ensuite les comparer aux données provenant du client.

    Tout comme la validation du blaster du chapitre précédent, isRayAngleFromOriginValid repose sur une valeur de tolérance pour déterminer ce qui constitue une "différence excessive" dans les angles :

    isRayAngleFromOriginValid

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

    Roblox abstrait les aspects les plus impliqués des mathématiques, donc le résultat est une fonction d'assistance courte et hautement réutilisable applicable à un éventail d'expériences :

    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. La vérification suivante est la plus intuitive. Alors que getValidatedBlastData utilise DISTANCE_SANITY_CHECK_TOLERANCE_STUDS pour vérifier que le joueur qui a tiré était près du point d'origine du faisceau, isPlayerNearPosition utilise une logique identique pour vérifier si le joueur marqué était près de la destination du faisceau :

    isPlayerNearPosition

    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. La dernière vérification isRayPathObstructed utilise une variation de l'opération de lancer de rayons pour vérifier si la destination du rayon se trouve derrière un mur ou toute autre obstruction depuis la position du client. Par exemple, si un joueur malveillant devait systématiquement retirer tous les murs de l'expérience pour marquer d'autres joueurs, le serveur vérifierait et confirmerait que les rayons sont invalides car il connaît chaque position d'objet dans l'environnement.

    isRayPathObstructed

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

Aucune stratégie anti-exploitation n'est complète, mais il est important de considérer comment des joueurs malveillants peuvent approcher votre expérience pour que vous puissiez mettre en place des vérifications que le serveur peut exécuter pour signaler des comportements suspects.

Réduire la santé des joueurs

Après avoir vérifié qu'un joueur a marqué un autre joueur, les dernières étapes pour compléter la boucle de gameplay principale dans l'expérience de laser tag exemple sont de réduire la santé du joueur marqué, d'incrémenter le leaderboard et de respawner le joueur dans la ronde.

En commençant par réduire la santé du joueur marqué, Spawn et respawn couvre la distinction entre Player et Player.Character, spécifiquement qu'un personnage est un modèle Humanoid. Les modèles Humanoid ont une propriété Health avec une valeur par défaut de 100. Au lieu d'implémenter son propre système, l'expérience de laser tag exemple utilise cette propriété intégrée pour suivre combien de dégâts un joueur doit subir avant d'être éliminé de la ronde.

L'expérience stocke les valeurs de dégâts dans l'attribut damagePerHit de chaque blaster. Par exemple, le blaster qui tire un unique faisceau laser inflige 10 points de dégâts, donc il faut dix tirs avec ce blaster pour éliminer un autre joueur. Pour commencer le processus d'élimination d'un joueur, LaserBlastHandler appelle ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers, qui vérifie le tableau maintenant validé rayResults pour les joueurs et passe damagePerHit à onPlayerTagged.

Health n'accepte pas de valeurs négatives, donc onPlayerTagged a une certaine logique pour maintenir la santé du joueur à ou au-dessus de zéro. Après avoir vérifié que la santé du joueur est supérieure à zéro, elle compare la santé à damagePerHit et utilise la plus petite des deux valeurs. Par exemple, si un joueur a 10 points de santé et est touché par un faisceau laser infligeant 15 points de dégâts, le laser n'inflige que 10 points de dégâts.

Cette approche du problème peut sembler un peu compliquée. Par exemple, pourquoi ne pas simplement définir la santé du joueur à zéro si elle serait négative ? La raison est que définir les valeurs de santé contourne le champ de force. Utiliser la méthode Humanoid:TakeDamage() garantit que les joueurs ne subissent pas de dégâts pendant que leurs champs de force sont actifs.

onPlayerTagged

local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- Interdire le tir ami
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- Éviter la santé négative
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage garantit que la santé n'est pas abaissée si le champ de force est actif
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- Accorder un point à playerBlasted pour avoir marqué playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

L'étape suivante consiste à incrémenter le leaderboard. Il pourrait sembler inutile pour LaserBlastHandler d'inclure le joueur qui a tiré avec les données de l'éclat, mais sans cette information, l'expérience ne peut pas créditer le joueur pour avoir éliminé quelqu'un. Enfin, le joueur éliminé respawn dans la ronde, ce que vous pouvez revoir dans Spawn et respawn.

Les cinq chapitres de ce curriculum couvrent la boucle de gameplay centrale de l'expérience, mais il existe encore de nombreuses zones à explorer, telles que :

  • Visuels du blaster : Voir ReplicatedStorageFirstPersonBlasterVisuals et ServerScriptServiceThirdPersonBlasterVisuals.
  • Audio : Voir ReplicatedStorageSoundHandler.
  • Modes personnalisés : Comment pourriez-vous modifier cette expérience pour introduire de nouveaux types d'objectifs, comme marquer le plus de points avant l'écoulement du temps ?

Pour une logique de gameplay étendue pour l'expérience de laser tag, ainsi que des actifs environnementaux réutilisables et de haute qualité, consultez le Laser Tag modèle.

©2026 Société Roblox. Roblox, le logo Roblox et Powering Imagination font partie de nos marques déposées aux États-Unis et dans d'autres pays.