Aller au contenu principal

Gravity Platformer : brevet

Un grand bac à sable d’outils et de tests, avec un objectif de départ : construire un plateformer à gravité façon Mario Galaxy.

Cette page retrace les itérations qu’il a fallu pour y arriver : du calcul de la direction de la gravité sur une simple primitive jusqu’à une boîte à outils complète de level design : Attracteurs, GravityOverrides, éditeurs de lignes et Zones.

Philae

Les quatre secrets​

Rendre la gravité agréable dans un jeu, ça tient en quatre choses :

  1. GetClosestPoint() : le point le plus proche entre une position et un objet : sphère, cube, capsule, ligne, groupe de lignes, mesh 3D concave ou convexe… Selon les formes, c’est plus ou moins complexe. Ce vecteur, c’est la direction de la gravité.
  2. Les restrictions : le point le plus proche, c’est bien, mais parfois on ne veut pas de gravité une fois sorti des limites de l’objet. Il faut pouvoir appliquer des restrictions, facilement, directement dans le level design : c’est le rôle des GravityOverrides.
  3. Le level design d’abord : faire l’effort en plus de rendre les objets de gravité ultra faciles à manipuler. Ça absorbe la complexité, et n’importe qui peut ensuite créer des niveaux.
  4. L’optimisation : tout le système doit tourner avec 10 000 objets sans broncher, d’où les Zones, qui ne calculent que les Attracteurs utiles.

:::tip Pourquoi pas la gravité newtonienne ? La loi de Newton, c’est parfait dans la vraie vie, ou pour un simulateur, mais un jeu a besoin de calculs sur mesure. Aucun jeu n’a des planètes de la taille des vraies planètes, et aucun jeu n’a la hauteur de saut d’un vrai humain. Ça s’appelle une simulation… ou l’ennui. :::

La gravité suit l’environnement​

La direction de la gravité dépend de l’environnement, et doit être rapide à régler pour chaque partie d’un niveau. Pour garder une approche générique, la gravité pointe vers l’objet le plus proche du level design.

Première étape : calculer les points les plus proches sur une primitive cylindre. La gravité peut viser :

  • le tronc du cylindre,
  • le bord du disque du haut ou du bas,
  • l’intérieur du disque du haut ou du bas.
Gravité vers une primitive

Les Attracteurs​

La gravité doit ensuite combiner les forces de plusieurs cibles, les Attracteurs. Pour le design, l’Attracteur le plus proche applique toujours la même force X ; les autres, plus éloignés, appliquent leur propre force divisée par un ratio de distance.

Gravité avec plusieurs Attracteurs

GravityOverride​

Pour aller plus loin, seules certaines parties d’un Attracteur doivent appliquer la gravité. Un éditeur sur mesure permet de régler ces options directement dans la Scene view : les GravityOverrides. Ici, la gravité est désactivée quand le joueur se trouve au-dessus d’une partie donnée du cylindre :

GravityOverride sur un cylindre

Il y a le plus d’Attracteurs possible, chacun avec le plus de GravityOverrides possible. Les GravityOverrides sont séparés des Attracteurs sans override, pour économiser des performances quand on n’en a pas besoin.

GravityOverride sur un cube

Les Attracteurs se déplacent, tournent et se redimensionnent dans l’éditeur comme n’importe quel GameObject :

Déplacer, tourner, redimensionner

La liste actuelle des Attracteurs, chacun avec son propre calcul du point le plus proche :

  • Sphère / demi-sphère : la planète classique ; la demi-sphère peut désactiver son dessus plat.
  • Cube : 6 faces, 12 arêtes et 8 coins, chacun activable ou non.
  • Capsule / demi-capsule : une ligne avec un rayon ; le tronc et chaque extrémité sont désactivables.
  • Cylindre : le tronc et les deux disques (leur face ou seulement leur bord).
  • Cône : un cône à base arrondie : pointe, base et tronc.
  • Disque : sa face, ou seulement son bord.
  • Donut : attire vers l’anneau intérieur, comme un monde en tore.
  • Quad / plan : un quad (face, 4 arêtes, 4 coins) ou un plan infini.
  • Triangle : face avant, face arrière, 3 arêtes et 3 coins.
  • Ligne : le tronc et les deux extrémités.
  • Spline : n’importe quelle courbe tracée avec l’éditeur de lignes ci-dessous.
  • Mesh convexe : n’importe quel modèle convexe, via le point le plus proche du moteur physique.
  • Mesh concave : n’importe quel modèle : une recherche en KD-tree dans un rayon max, mise en cache par objet et lissée pour que la gravité ne tremble jamais.

Chaque forme a une version Advanced qui ajoute ces options de GravityOverride, plus une portée min / max au-delà de laquelle elle n’attire plus. Les formes gardent leur transform en cache et ne recalculent que quand elles bougent.

Toutes les formes d’Attracteurs et leurs points les plus proches

Éditeur de lignes​

Un éditeur de lignes donne une gravité sur mesure à certains niveaux. Il est bien sûr compatible avec les GravityOverrides, et fonctionne de la même façon avec plusieurs lignes :

Éditeur de lignes
Éditeur de lignes avec gravité
Éditeur de polylignes

Les Zones​

Pour optimiser les calculs, le level design est découpé en morceaux, et seuls les Attracteurs du morceau courant sont calculés. Une Zone contient une liste d’Attracteurs ; elle peut être inclusive ou exclusive par rapport aux autres, et existe en plusieurs formes :

Zone
Zone soustractive
Formes de Zones

Sous le capot​

Le pipeline de la gravité : zones, points les plus proches, filtres, combinaison des forces

À chaque frame physique, chaque objet soumis à la gravité (un Graviton) passe par le même pipeline :

  1. Les zones décident qui compte. Les Attracteurs sont listés dans des zones déclencheurs : des colliders physiques, ou de simples tests de forme quand il n’y a pas de physique. Entrer dans une zone ajoute ses Attracteurs au Graviton ; en sortir les retire. Un Graviton ne calcule jamais que les Attracteurs autour de lui.
  2. Chaque Attracteur est une forme. 16 formes partagent les deux mêmes fonctions, GetClosestPoint() et IsInsideShape() : sphère et demi-sphère, cube, capsule et demi-capsule, cylindre, cône, disque, donut, quad, plan, triangle, ligne, spline, mesh convexe et mesh concave. Les meshes convexes s’appuient sur le moteur physique ; les meshes concaves sur un KD-tree, un cache par objet et un lissage optionnel pour que la gravité ne tremble jamais. Les formes mettent leur transform en cache et ne se recalculent que quand elles bougent.
  3. Les GravityOverrides la restreignent. Chaque forme a son propre override : un cube peut désactiver chacune de ses 6 faces, 12 arêtes et 8 coins ; un cylindre son tronc et la face ou le bord de chaque disque ; un triangle sa face, ses arêtes et ses coins… plus une portée maximale.
  4. Dans le volume ? La gravité peut continuer à attirer, s’arrêter, ou s’inverser. Un décalage de rotation optionnel peut aussi incliner sa direction.
  5. Filtrer. Dans un groupe d’Attracteurs, seul le plus proche compte (plusieurs formes peuvent former une seule planète), et les Attracteurs qui tirent presque dans la même direction sont fusionnés.
  6. Combiner, pas Newton. L’Attracteur le plus proche donne une force fixe (masse × facteur × 9,81), quelle que soit la distance ; les autres sont réduits selon qu’ils sont plus loin. Les facteurs peuvent varier selon le type d’entité (joueur, ennemis…).
  7. Appliquer. La force est envoyée au Rigidbody, l’objet tourne son « haut » à l’opposé de la gravité, et des zones de gravité inversée peuvent la retourner (avec une inclinaison optionnelle).

Le tout reste économe : les points les plus proches sont rafraîchis toutes les 0,1 s avec un premier décalage aléatoire, une option ne recalcule que l’Attracteur le plus proche entre deux passes complètes, et tout est visualisé dans l’éditeur avec des flèches et des formes colorées.

Dans les dessins du brevet​

La même méthode, dessinée comme l’exige un brevet : des traits noirs et des parties numérotées.

Dessin du brevet : un attracteur en cube, ses faces, arêtes et coins, et une face désactivée

Dessin du brevet : trois attracteurs, la force de référence et les forces réduites

Dessin du brevet : les étapes de la méthode, des zones jusqu’à la force appliquée

Trois corps : une base fixe, mélangée aux autres​

Trois cas : entre trois corps, pile au milieu, hors de toute zone

  • Entre trois corps, le plus proche sert de référence : sa force est toujours la base complète (masse × facteur × 9,81), quelle que soit la distance : courir et sauter donnent donc les mêmes sensations partout. Les autres sont réduits selon le rapport des distances au carré : deux fois plus loin, un corps attire quatre fois moins ; trois fois plus loin, neuf fois moins. La somme, c’est surtout le corps le plus proche, déviée vers les autres.
  • Pile au milieu de deux corps identiques, les deux forces sont égales et s’annulent : un point d’apesanteur entre les planètes. Un pas de plus vers l’un des deux, et il devient la référence.
  • Hors de toute zone, la gravité ne tombe jamais à zéro : la dernière direction est gardée, et un minuteur « hors gravité » démarre : pratique pour les longs sauts entre planètes.

La caméra​

Le rig de caméra : pivot de gravité, rotation, spline du dolly, zoom, caméra finale, zones de caméra

Un jeu façon Mario Galaxy tient ou s’effondre sur sa caméra : le sol peut être n’importe où, donc la caméra doit suivre la gravité, pas le monde. Elle est construite comme une pile de couches, chacune se lissant vers sa propre cible à chaque frame :

  1. Pivot de gravité : sur le joueur, son « haut » est l’opposé de la gravité, lissé (plus lentement en l’air qu’au sol). Quand la gravité change, une transition se déclenche ; pendant un saut, la caméra garde le « haut » du saut jusqu’à l’atterrissage, pour ne pas tourner en plein vol.
  2. Tourner à gauche / droite autour du joueur avec le stick, avec une zone morte et une courbe d’accélération : et quand le joueur n’y touche pas, la caméra revient d’elle-même vers l’objectif ou l’avant du joueur.
  3. Dolly sur une spline : l’axe vertical du stick déplace un pourcentage de 0 à 1 le long d’une spline derrière le joueur : la position de la caméra et le point visé glissent ensemble, donc en bas on regarde vers le haut, et en haut vers le bas.
  4. Zoom : la gâchette change l’échelle de tout le rig entre un minimum et un maximum, plus une inclinaison optionnelle.
  5. Caméra finale : position lissée vers le rig, et une visée dont le « haut » est lui-même interpolé, donc un changement de gravité ne fait jamais sauter l’image.

Les zones de caméra permettent aux level designers de reprendre la main localement : voir ci-dessous.

Zones de caméra​

Zones de caméra : n’importe quelle forme, n’importe quel réglage, restauré en sortie

Le rig de caméra est générique ; les zones de caméra permettent aux level designers de le diriger. Une zone de caméra est un déclencheur construit sur l’une des 16 formes (ou un simple collider), placé et redimensionné comme n’importe quel objet. Tant que le joueur est dedans, elle peut modifier n’importe quelle partie du système de caméra, chaque réglage est optionnel :

  • la référence du « haut » : gravité, sol, gravité inversée, ou un transform custom ;
  • ce que la caméra vise : une direction fixe, ou une cible à suivre ;
  • le profil d’auto-rotation : large ou serré, lent ou rapide, ou suivi continu de l’avant du joueur ;
  • une position cible de caméra custom, avec sa propre interpolation ;
  • la hauteur du dolly sur la spline (regarder en haut / en bas), le zoom, le FOV et l’offset ;
  • la liberté du joueur : autoriser ou interdire la rotation manuelle, activer le suivi haut/bas en saut, remettre à zéro le verrou d’input.

À l’entrée, la zone sauvegarde les valeurs actuelles et applique les siennes ; à la sortie, elle les restaure (optionnel selon la zone), puis réactive toute autre zone dans laquelle le joueur se trouve encore : les zones qui se chevauchent se passent le relais proprement. Quand plusieurs zones visent des cibles en même temps, la caméra vise leur moyenne. Les zones référencent les objets par GUID, donc elles fonctionnent entre scènes, et tous leurs réglages, cible, direction, « haut » custom, sont dessinés dans la Scene view.

Pathfinding sur des mondes non plats​

Marcher sur des planètes, c’est aussi faire marcher l’IA dessus. Les agents trouvent leur chemin à la surface de sphères, de capsules et de cubes, en suivant la gravité de chaque Attracteur : les points blancs montrent le chemin calculé entre chaque agent et sa cible.

Chemins à travers les planètes
Chemins à travers les planètes
Chemins autour des obstacles
Chemins autour des obstacles

Deux niveaux de graphes​

Pathfinding sur deux niveaux : la galaxie comme graphe de planètes, puis un chemin à la surface d’une planète

Le pathfinding fonctionne sur deux niveaux, grâce à la théorie des graphes :

  1. La galaxie est un graphe. Chaque planète est un nœud, et les liens entre planètes sont les arêtes. Un premier pathfinding sur ce graphe choisit par quelles planètes passer pour atteindre la destination.
  2. Chaque planète a sa propre navigation. Sur chaque planète, un nav mesh agent maison, construit et optimisé pour les mondes non plats, gère les sphères, les cubes, les objets convexes et concaves, et tout est généré au runtime.

Comment il est construit​

Construction de la grille de navigation d’une planète au runtime

Chaque planète construit sa propre grille de navigation au runtime, quelques instants après le chargement du niveau :

  • Échantillonnage cube-sphère : une grille de N×N points sur chacune des 6 faces d’un cube, projetée sur une sphère avec une formule à aires égales (N entre 5 et 20, en général 14 : environ 1 200 nœuds par planète).
  • Lancer vers l’intérieur : depuis chaque point, un sphere cast vers le centre de la planète. Le point d’impact devient un nœud. La grille épouse la surface, quelle qu’elle soit : sphère, cube, capsule ou mesh sur mesure. Les nœuds sur des obstacles sont marqués non praticables.
  • Voisins & coutures : chaque nœud est relié à ses 8 voisins praticables, et les bords des 6 faces sont cousus ensemble : les chemins font le tour de la planète sans couture.

Comment il tourne​

Boucle d’exécution d’une IA : planète, graphe de la galaxie, grille de la planète, pilotage

  1. Quelle planète ? Toutes les demi-secondes, la planète la plus proche de l’IA et de sa cible, selon la vraie forme de chaque planète.
  2. Graphe de la galaxie : les planètes sont les nœuds et les téléporteurs les arêtes ; un parcours en largeur trouve la route avec le moins de sauts, et l’IA vise son premier téléporteur.
  3. Grille de la planète : une recherche A* (tas binaire) de l’IA jusqu’à ce téléporteur, ou directement jusqu’à la cible si les deux sont sur la même planète.
  4. Pilotage : l’IA vise le point le plus proche de son chemin, projeté sur le plan de sa gravité actuelle.

Le chemin est entretenu à moindre coût : les points atteints sont retirés et de nouveaux ajoutés quand ça bouge, un recalcul complet n’a lieu que si l’IA ou sa cible s’est assez déplacée, et les minuteurs sont décalés au hasard pour que les IA ne recalculent jamais toutes sur la même frame.