Licence CC BY-SA

Artillerie et physique dans un jeu vidéo

Quand un jeu vidéo imite la technologie du monde réel

Créer des armes pour un jeu vidéo est une entreprise complexe. Les armes doivent être efficaces, agréables à jouer et suffisamment crédibles, sans pour autant rendre leur coût en performances trop élevé. La plupart des jeux vidéo prennent donc des raccourcis :

  • les balles traversent souvent un niveau en une seule frame [^frame], permettant de simuler le tir en un seul test de collision ;
  • les dégâts dits de zone se contentent de causer des dégâts dans un rayon bien défini ;
  • la localisation des dégâts sur la victime se borne habituellement à des zones bonus qui causent plus de dégâts.

Est-il possible de faire quelque chose de plus réaliste avec les contraintes d'un jeu vidéo ? C'est le pari que nous avons fait ! Voyons ensemble quelques techniques pour contourner les limites des moteurs physiques.

Un peu de contexte

Cet article est rédigé par l'équipe du jeu vidéo Helium Rain et a initialement été publié en anglais sur notre Devblog. Notre jeu est une space sim qui inclut du combat dans l'espace, avec un objectif de réalisme. Nous avions besoin d'une arme antiaérienne [^AA] efficace pour défendre les vaisseaux majeurs des attaques de chasseurs, ce qui nous a conduit à développer un canon antiaérien en nous inspirant des technologies modernes.

L'artillerie antiaérienne à l'époque des guerres mondiales

Il y a historiquement deux options pour se débarrasser de chasseurs : utiliser des chasseurs en riposte, ou embarquer des canons antiaériens sur les cibles à protéger. La seconde option est celle qui nous intéresse ici. Ces canons répondent à des contraintes lourdes : il faut toucher une cible mouvante de quelques mètres de large qui évolue à plusieurs kilomètres d'altitude avec un obus qui met dix secondes à toucher sa cible, et une technologie de visée qui pendant les deux guerres mondiales se bornait essentiellement à l'estimation humaine.

Au cours de la Première Guerre mondiale, on comprend donc bien vite que les simples obus ne sont pas une solution et on développe des obus qui explosent près de la cible, projetant des fragments métalliques dans toutes les directions. A l'époque, il n'y a que deux façons de déterminer l'instant de l'explosion : soit un seuil de temps, soit un seuil d'altitude. Les deux méthodes exigent un réglage avant le tir.

Tout change avec la Seconde Guerre mondiale qui voit l'introduction des fusées de proximité, des obus explosant quand ils détectent une cible près d'eux. Ces projectiles détectent les objets métalliques et rendent les batteries antiaériennes extrêmement efficaces ; à l'époque, les Alliés prennent toutes les dispositions nécessaires pour que cette technologie reste secrète, quitte à limiter son déploiement. C'est cette technologie que nous simulons dans la vidéo ci-dessus - comme vous pouvez le voir, son efficacité est certaine.

Détonateur de proximité des années 50 (Wikimedia)

Contrairement à l'essentiel des jeux, nous avons décidé de simuler physiquement ces obus d'artillerie. La première étape est de recréer une fusée de proximité. Notre version est perfectionnée : l'obus explose quand il entre dans un rayon de détonation, mais il peut aussi exploser quand il détecte qu'il a atteint sa distance minimale à la cible.

On met donc en place une distance d'activation en dessous de laquelle l'obus est armé. Une fois armé, si l'obus détecte que sa distance à la cible augmente, il explose. Nos obus ont une distance d'activation de 50m et une distance de détonation de 3m.

16.7ms chrono

16.7 millisecondes, c'est le temps qui se déroule entre deux images (ou frames) d'un jeu vidéo quand ce dernier a un débit d'image de 60fps, la valeur habituellement visée par les développeurs. C'est un intervalle de temps très court pendant lequel on doit mettre à jour toute la logique du jeu, transmettre une scène au moteur de rendu 3D, effectuer un rendu et l'afficher. Le budget de temps pour les projectiles, lui, est de l'ordre d'une seule milliseconde : pas question de passer toute une frame à calculer des positions de projectiles !

Et pourtant, 16.7 millisecondes c'est énorme : pendant ce temps-là, un obus a le temps de parcourir près de quinze mètres, la taille d'un petit vaisseau. Chaque mise à jour du moteur physique va déplacer un obus de cette distance. On comprend mieux pourquoi les balles de jeux vidéo ne sont jamais simulées physiquement !

L'implémentation de notre mécanisme est donc très difficile. La simulation de la physique est faite pas à pas, chaque pas de simulation correspondant au calcul de tout ce qui s'est passé dans le monde du jeu entre le temps présent et le temps de la prochaine frame. Comme nos obus sont très rapides (près de 800 m/s), on ne peut pas se contenter de vérifier la proximité à la fin de chaque pas, puisqu'on pourrait traverser une cible pendant ce temps !

Trajectoire d'un obus vis-à-vis d'une cible

C'est ce qu'on appelle l'effet tunnel dans les jeux vidéo. Cette anomalie peut permettre à des balles de traverser des murs, par exemple. Les moteurs physiques pour le jeu vidéo ne permettent simplement pas de simuler de telles vitesses.

Pour corriger le problème, il nous faut calculer la distance minimale entre la cible et la trajectoire de l'obus, ainsi que le point associé sur la trajectoire. Si ce point n'est pas sur le segment (S0S1) correspondant à la frame courante , cette distance minimale n'est pas utilisable.

Différentes positions de l'obus pendant un tick

Si la distance minimale est inférieure à la distance de détonation, l'obus explose. La position initiale de l'explosion n'est en revanche pas le point de distance minimale D, car le projectile doit toujours exploser à la distance de détonation et jamais en dessous. En effet, ce point de distance minimale peut parfaitement être situé à l'intérieur du vaisseau - un cas typique d'effet tunnel. Une telle explosion causerait des dégâts irréalistes.

Pour éviter cela, on calcule la position où la distance à la cible est égale à la distance de détonation, pour exploser à cet endroit précis. Une conséquence amusante de cet ajustement est qu'il faut parfois faire revenir un obus en arrière pour exploser.

Explosion à la position précédente

Si on n'est pas encore à la distance de détonation, mais déjà en dessous de la distance d'activation, l'obus est armé. Dans ce pas de simulation ou dans les pas ultérieurs, si l'obus a été armé et que la distance augmente, l'obus explose.

Explosion au plus près

Pluie de métal

Dans la plupart des jeux vidéo, les obus explosent avec des dégâts dans un rayon fixe et tous les ennemis proches sont touchés. Habituellement, ils reçoivent plus de dégâts s'ils sont proches du centre. Dans le monde réel, l'explosion du projectile en elle-même ne cause pas de dégâts, c'est le nuage de fragments métalliques qu'elle projette qui cause le plus de dégâts. Plus une cible est proche du centre de l'explosion, plus la probabilité d'être touché est grande. Notre simulation reproduit ce comportement.

Dans Helium Rain, les vaisseaux n'ont pas de points de vie. Ils sont constitués de dizaines de composants individuels, chaque composant ayant son modèle de collision. Les obus simulent des centaines de fragments touchant les vaisseaux alentour, pour rendre les effets du tir aussi réalistes que possible. Une explosion sur le côté gauche d'un vaisseau endommagera donc des composants sur sa gauche, sans affecter son côté droit. La méthode la plus simple pour implémenter cette simulation serait de créer autant de traces [^trace] physiques qu'il y a de projectiles, mais elle serait bien trop coûteuse en performances, et inefficace, car très peu de rayons toucheraient leur cible.

Tracé de rayon pour des fragments

Pour augmenter la densité du nuage de fragments tout en diminuant la charge du processeur, il devient nécessaire d'optimiser. On sélectionne d'abord toutes les cibles dans un rayon raisonnable (en rouge sur les images suivantes), et pour chacune d'entre elles on calcule l'intersection entre leur sphère circonscrite [^bounding] (en magenta) et une sphère centrée sur l'obus (en jaune) qui représente la distance à la cible. Le ratio de surface entre la sphère jaune et l'intersection des deux sphères correspond au ratio entre le nombre de fragments que devrait émettre la munition et le nombre de fragments que l'on doit vraiment simuler (en bleu). Un dessin pour comprendre…

Tracé de rayon optimisé pour des fragments

Il suffit ensuite de simuler les fragments utiles et de propager les dégâts correspondants aux composants touchés. Voici le résultat en 3D, dans le jeu, avec la visualisation correspondant à l'image précédente.

Tracé de rayon pour des fragments, vu en 3D

Pour aller plus loin…

Ces obus sont encore très dangereux à utiliser, car ils ne font pas la différence entre alliés et ennemis, puisqu'ils se comportent comme des détecteurs de métaux. Plus simplement, tirer sur un ennemi proche peut causer des dégâts à son propre vaisseau. Pour pallier ce problème, les obus sont équipés d'un timer, qui ne permet à l'obus de s'armer que quand il est à une certaine distance du canon. Ce timer permet aussi de désactiver un obus qui aurait dépassé sa cible.

La vidéo au début de l'article montre l'efficacité de ces obus. Bien sûr, on pourrait encore développer le sujet en parlant des tourelles qui tirent ces projectiles, puisqu'elles doivent anticiper les mouvements des adversaires et compenser ceux du joueur ; ou encore de l'intelligence artificielle qui régit les pilotes des vaisseaux pour leur permettre d'échapper à ces obus… Ce qu'il faut retenir, c'est qu'il est possible de simuler très précisément des mécanismes réels dans un jeu vidéo.

En bonus, voici un petit extrait du code du jeu, issu du calcul du nombre de fragments à simuler.

1
2
3
4
5
6
7
    float ApparentRadius = FMath::Sqrt(FMath::Square(CandidateDistance) + FMath::Square(CandidateSize));
    float Angle = FMath::Acos(CandidateDistance/ApparentRadius);
    float ExposedSurface = 2 * PI * ApparentRadius * (ApparentRadius - CandidateDistance);
    float TotalSurface = 4 * PI * FMath::Square(ApparentRadius);
    float ExposedSurfaceRatio = ExposedSurface / TotalSurface;

    int FragmentCount = ShellDescription->GunCharacteristics.AmmoFragmentCount * ExposedSurfaceRatio;

On peut noter que le niveau de mathématiques requis pour ce genre de travail est très accessible. Les notions basiques de géométrie sont suffisantes.

Merci de votre lecture ! :)


  1. Une frame est une image faisant partie d'une vidéo. On parle de frames per seconds pour désigner le débit d'images à l'écran, par exemple.  

  2. On parle d'antiaérien parce que le terme est courant, il est inutile de préciser qu'il n'y a pas d'air dans l'espace. 

  3. Une trace, dans le jeu vidéo, est un segment entre deux points qui teste l'existence d'objets physiques. Imaginez une balle dans un jeu de tir. 

  4. La sphère circonscrite ou bounding sphere est la plus petite sphère qui contient tout un objet, en 3D. 


32 commentaires

C'est intéressant, merci pour cet article.

Du coup, les vaisseaux sont considérés sphériques quand vous calculer la distance entre celui-ci et l'obus ?

Et pour les fragments de l'obus, vous ne simulez que ceux qui seront éjectés dans un angle solide correspondant à la cible, en considérant une explosion homogène (ce qui n'est certainement pas délirant), mais visuellement, ça ne risque pas de faire bizarre de ne voir qu'une partie des fragments?

Comment est-ce que vous gérez la vitesse de ces fragments ? Distribution qui conserve la quantité de mouvement ?

Oui, les vaisseaux sont considérés comme sphériques pour le calcul de distance. Nous faisons cette approximation car ce calcul de distance doit être fait pour tous les vaisseaux, et surtout, toutes les 16 ms pendant toute la vie de l'obus. Ce calcul de distance doit être le plus rapide possible et cette approximation n'est pas très gênante. Par contre pour l'impact des fragments, nous utilisons bien la forme détaillée des vaisseaux.

En réalité, on ne voit jamais les éclats d'obus. Comme pour les balles de fusil (non traçantes) on ne voit que leur effet. Le cinéma donne un très mauvais exemple de l'effet des obus mais voici quelques vidéos d'explosion d'obus, on distingue clairement l'explosion qui est modérée et la pluie de fragments qui ne se repèrent qu'à leur impact :

À partir de 2m58s, 2 exemples de bombes de la seconde guerre mondiale avec des fusées de proximité au dessus d'un plan d'eau : https://youtu.be/33RhuzUrx_A?t=2m58s

À 2min24s, un exemple d'obus moderne explosant à quelques mètres du sol : https://youtu.be/Mj8ThMqjisA?t=2m24s

Dans notre cas nous n'avons pas d'eau ou de sol, donc tous les fragments qui ne partent pas en direction de vaisseaux restent invisibles.

Sur un obus classique la distribution n'est pas du tout homogène sur une sphère. 80% des fragments devraient partir sur les cotés, le reste droit devant ou droit dernière avec de zones presque dépourvus de fragments. De plus, les fragments devrait avoir comme vitesse initiale la vitesse de l'obus ce qui devrait décentrer la distribution de fragments vers l'avant de la sphère. Pour des raisons de performances, nous avons simplifié cette répartition en une distribution homogène et nous considérons la vitesse des fragments comme infinie. Les différences de tailles et vitesses des fragments sont simulées simplement en rendant aléatoire la quantité de dégâts que fait chaque fragment à l'impact.

Voilà un article très intéressant. :)

Je relève juste « leur bounding sphere » : on dira « sphère circonscrite » en français. De rien. ^^

Dominus Carnufex

Merci ! Je corrige.

Je veux joué à votre jeu :D où qu'on précommande ?

La source

Nous lancerons une campagne Greenlight cet été. :)

Je me posais une question en le lisant : comment est-ce que vous intégrez les questions et contraintes de gameplay lorsque vous vous lancez dans ce genre de simulation ?

SpaceFox

Nous avons un processus très rigoureux pour évaluer le meilleur compromis :

  1. Niavok propose une idée géniale de simulation précise qui rend le jeu bien plus réaliste ;
  2. je hurle jusqu'à ce que ça soit relativement transparent en gameplay ;
  3. on implémente.

Plus sérieusement, la règle générale est que les aspects de simulation doivent servir le gameplay et pas le définir. Deux exemples.

Exemple 1 : il y a un mécanisme de surchauffe dans le jeu : utiliser un moteur orbital pendant une dizaine de secondes chauffe les moteurs au rouge jusqu'à une température limite, à partir de laquelle le joueur est prévenu par un message sur le HUD, une alarme, des moteurs qui commencent à prendre des dégâts. Le rôle de ce mécanisme est de limiter l'usage de ces moteurs surpuissants, et ce mécanisme est servi par une simulation de la propagation de chaleur dans le vaisseau. Visible ici.

Exemple 2 : on a envisagé un mécanisme de carburant, on l'a mis dans les menus et dans les propriétés des vaisseaux… Et ensuite on l'a coupé du jeu parce que côté gameplay, ça posait énormément de problèmes, c'était réaliste mais ça ne servait aucun objectif pour le jeu lui-même.

Un superbe article que j'ai pris plaisir à valider. Premier article consacré au jeux vidéo par la même occasion.

Touché coulé pour Stranger et Niavok, en somme. :D

+3 -0

Merci pour vos retours ! Je n'étais pas sûr que le format de cet article soit intéressant, ça fait plaisir de lire que oui parce que j'aimerais remettre ça régulièrement.

Stranger

Justement, le format de ton article fait beaucoup penser à ce qu'on avait l'habitude de voir sur le site du zéro v3 en terme de "news". Quelque chose que j'aimais particulièrement lire et qui me faisait guetter ledit site au quotidien. :)

J'ai par ailleurs fait la même remarque sur cet article : http://zestedesavoir.com/articles/119/microsoft-et-lopen-source-comme-chien-et-chat/#p35718

Nous utilisons une loi uniforme. C'est facilement disponible quand on programme car les fonctions "rand" standard donnent des distributions uniformes. De plus cela suffit amplement dans ce cas car le joueur pas vraiment choisir exactement où les obus explosent.

Dans le cas des fragments, le moteur que nous utilisons, Unreal Engine 4 fournit même une fonction donnant un vecteur aléatoirement choisi dans un cône : FVector VRandCone (FVector Direction, float DemiAngle)

Article très intéressent, merci beaucoup. :)

Ils sont constitués de dizaines de composants individuels, chaque composant ayant son modèle de collision.

Si je comprends bien, chaque objet sur le jeu possède un ensemble de particules. Comment faites-vous pour stocker et charger autant de mémoire ? En 16.7ms, le moteur doit lire objet par objet, particule par particule et afficher la 3D en ajoutant texture et luminosité. Si vous devez faire un nouveau tutoriel, je suis preneur pour en savoir plus là dessus (plutôt axé graphisme cette fois). Ça m'intrigue et pas seulement pour ce jeu. :D A croire que le rendu graphique avec des atomes n'est pas un rêve.

+0 -0

Si je comprends bien, chaque objet sur le jeu possède un ensemble de particules. Comment faites-vous pour stocker et charger autant de mémoire ? En 16.7ms, le moteur doit lire objet par objet, particule par particule et afficher la 3D en ajoutant texture et luminosité. Si vous devez faire un nouveau tutoriel, je suis preneur pour en savoir plus là dessus (plutôt axé graphisme cette fois).

Yarflam

C'est vraiment pas grand chose pour un moteur de jeu moderne. Avoir autant de composants séparés nous coûte un peu parce que le moteur doit mettre à jour une hiérarchie complexe d'objets en mouvement, mais sur le plan graphique on a quelques centaines de milliers de triangles, ce qui est plutôt peu aujourd'hui. Une machine moderne peut afficher des dizaines de milliers d'objets avec des millions de triangles au total dans la scène, de l'éclairage dynamique, etc.

Voilà un aperçu de l'éditeur d'Unreal pour un chasseur, avec quelques composants sélectionnés. A gauche, la hiérarchie des composants.

D'accord. Je ne l'avais pas compris comme ça. Je pensais que les objets étaient des groupes de particules (ou de petit voxels) au vu des impacts sur la carroserie du vaisseau (les points noirs à la fin de la vidéo). C'est pour ça que je ne voyais pas comment on pouvait rendre le jeu aussi fluide.

Merci pour le screenshot et tes explications. J'ai hâte de tester ce jeu ! ;)

+0 -0

Plus simplement, tirer sur un ennemi proche peut causer des dégâts à son propre vaisseau. Pour pallier ce problème, les obus sont équipés d'un timer, qui ne permet à l'obus de s'armer que quand il est à une certaine distance du canon.

Dans l'espace, ca empêche pas le backfire d'un obus, même à grande distance. Car les éclats vont avoir une impulsion donnée par l'explosion. Si la distribution est sphérique, y'a des éclats qui vont revenir vers celui qui a tiré. Avec une distribution conique, c'est déjà moins le cas.

Et puis le truc dans l'espace, c'est qu'un fragment tiré qui ne percute rien continue jusqu'à percuter quelque chose… Il faudrait donc continuer à le simuler jusqu'à ce qu'il ait dépassé tous les vaisseaux.

A noter que dans l'espace, un simple projectile suffit déjà à faire beaucoup de dégât. On en parlait un peu ici : https://zestedesavoir.com/forums/sujet/3201/mass-effect-et-les-projectiles

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte