Détecter les coups est le processus d'identification lorsque des tirs entrent en collision avec des joueurs, puis de réduire leur santé en conséquence. À un niveau élevé, vous pouvez considérer ce travail comme soit :
- Un contrôle simulé physiquement pour déterminer si un projectile a frappé la cible.
- Un contrôle instantané pour déterminer si le blaster était visé sur la cible.
Le type de détection des coups que vous utilisez dépend des exigences de gameplay de votre jeu. Par exemple, un contrôle simulé physiquement est approprié pour un jeu de dodgeball 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 un jeu de laser tag où les faisceaux doivent avoir une vitesse presque infinie et ignorer des facteurs environnementaux comme la gravité et la vitesse du vent.
En utilisant le jeu 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 du tir à partir des valeurs de la caméra actuelle et du type de blaster du joueur.
- Lancer des rayons en ligne droite depuis le blaster pendant qu'il tire.
- Valider le tir pour empêcher l'exploitation des données du blaster.
- Réduire la santé du joueur en fonction des dégâts du tir de 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 du tir
Après qu'un joueur ait tiré avec son blaster, ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData appelle deux fonctions pour commencer le processus de détection des coups : rayDirections() et rayResults().
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 le jeu de laser tag exemple ne donnait aux joueurs que des blasters produisant un seul faisceau laser, ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast serait inutile car vous pourriez utiliser currentCamera.CFrame.LookVector pour calculer la direction du tir.
Cependant, comme l'exemple fournit un type de blaster supplémentaire qui produit plusieurs faisceaux laser avec une large dispersion horizontale, getDirectionsForBlast doit calculer la direction de chaque faisceau laser de la dispersion selon leurs angles dans la configuration du blaster :
if numLasers == 1 then
-- Pour les lasers simples, ils visent droit
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
endPour démontrer ce concept plus en détail, si vous deviez inclure un troisième type de blaster avec une large dispersion verticale, vous pourriez créer un nouvel attribut de blaster, tel que spreadDirection, puis ajuster le calcul 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 directionsEn fin de compte, la fonction rayDirections() retourne une table de Vectors qui représentent la direction de chaque faisceau laser. Si cela peut être utile, vous pouvez ajouter un peu de journalisation pour avoir une idée de ce à quoi ressemble ces données.
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 ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData, effectue les opérations plus complexes au sein du script. Il commence par spécifier des paramètres afin de pouvoir effectuer des appels Workspace:Raycast() à des fins 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 chemin pour voir où il intersecte d'autres objets.
Cette information est particulièrement utile pour les jeux de tir à la première personne car elle vous permet de voir quand et où les tirs 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.

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 ensuite un rayon pour chaque direction dans la table 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.
- Material – L'Enum.Material au point d'intersection.
La valeur Instance est la plus critique de ces propriétés pour le gameplay du jeu de laser tag exemple car elle communique quand les rayons entrent en collision avec d'autres joueurs. Pour récupérer cette information, le jeu utilise la fonction d'assistance ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. Si elle retourne nil, l'instance ne fait pas partie d'un joueur, ce qui signifie que le rayon a touché un objet inanimé dans l'environnement.
castLaserRay() utilise ensuite Position et Normal pour créer un nouveau CFrame qu'il appelle 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 à la fin de sa distance maximale. En fonction de la précision de vos joueurs, de nombreuses valeurs taggedPlayer sont nil.
if result then
-- Le tir a touché quelque chose, vérifiez si c'était un joueur.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- Le tir 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
endValider le tir
Pour prévenir la tricherie, le chapitre précédent Implémentation des Blasters explique comment blastClient notifie le serveur du tir en utilisant un RemoteEvent afin qu'il puisse vérifier toutes les données que chaque client envoie, telles que si oui ou non ils ont réellement touché un autre joueur avec leur blaster. Ce processus de validation des rayons se déroule dans ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults, et chaque vérification correspond à un script de module imbriqué :
Tout d'abord, getValidatedRayResults appelle validateRayResult pour vérifier que chaque entrée dans la table rayResults du client est un CFrame et un Player (ou nil).
Ensuite, il appelle isRayAngleFromOriginValid pour comparer les angles attendus de la dispersion laser à ceux du client. Ce code en particulier montre l'avantage d'utiliser ReplicatedStorage car le serveur peut appeler getDirectionsForBlast lui-même, stocker le retour comme les données "attendues", puis les comparer aux données du client.
Tout comme la validation du blaster du chapitre précédent, isRayAngleFromOriginValid s'appuie sur une valeur de tolérance pour déterminer ce qui constitue une différence "excessive" dans les angles :
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESRoblox abstrait les parties les plus complexes des mathématiques, donc le résultat est une courte fonction d'assistance hautement réutilisable avec une applicabilité à travers une gamme de jeux :
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)endLa 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 touché était près de la destination du faisceau :
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendLa dernière vérification isRayPathObstructed utilise une variation de l'opération de raycast pour vérifier si la destination du rayon est derrière un mur ou un autre obstacle depuis la position du client. Par exemple, si un joueur malveillant devait systématiquement retirer tous les murs du jeu pour toucher 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.
isRayPathObstructedlocal 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 les joueurs malveillants peuvent aborder votre jeu afin que vous puissiez mettre en place des vérifications que le serveur peut exécuter pour signaler un comportement suspect.
Réduire la santé du joueur
Après avoir vérifié qu'un joueur a touché un autre joueur, les étapes finales pour compléter la boucle de gameplay principale dans le jeu de laser tag exemple consistent à réduire la santé du joueur touché, à incrémenter le tableau des scores et à faire réapparaître le joueur dans le round.
En commençant par réduire la santé du joueur touché, Apparition et réapparition 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. Plutôt que d'implémenter son propre système, le jeu 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é du round.
Le jeu stocke les valeurs de dégâts dans l'attribut damagePerHit de chaque blaster. Par exemple, le blaster qui tire un seul 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 ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers, qui vérifie la table rayResults maintenant validée pour les joueurs et passe damagePerHit à onPlayerTagged.

Health n'accepte pas de valeurs négatives, donc onPlayerTagged a une logique pour maintenir la santé du joueur à zéro ou plus. Après avoir vérifié que la santé du joueur est supérieure à zéro, il compare la santé à damagePerHit et utilise la plus petite des deux valeurs. Par exemple, si un joueur a 10 de santé et est touché par un faisceau laser de 15 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 devait être négative ? La raison est que définir des 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 tant que leurs champs de force sont actifs.
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
-- Récompenser playerBlasted d'un point pour avoir éliminé playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
endL'étape suivante consiste à incrémenter le tableau des scores. Il pourrait sembler inutile pour LaserBlastHandler d'inclure le joueur qui a tiré avec les données du tir, mais sans cette information, le jeu ne peut pas créditer le joueur pour avoir éliminé quelqu'un. Enfin, le joueur éliminé réapparaît dans le round, ce que vous pouvez consulter dans Apparition et Réapparition.
Les cinq chapitres de ce curriculum couvrent la boucle de gameplay principale du jeu, mais il y a encore de nombreux domaines à explorer, tels que :
- Visuels du blaster : Voir ReplicatedStorage ⟩ FirstPersonBlasterVisuals et ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- Audio : Voir ReplicatedStorage ⟩ SoundHandler.
- Modes personnalisés : Comment pourriez-vous modifier ce jeu pour introduire de nouveaux types d'objectifs, comme marquer le plus de points avant la fin du temps ?
Pour une logique de gameplay étendue pour le jeu de laser tag, ainsi que des actifs environnementaux réutilisables et de haute qualité, consultez le modèle Laser Tag.