Alors que je travaillais sur un jeu dans lequel les vaisseaux spatiaux sont conçus par des joueurs et peuvent être partiellement détruits, j'ai rencontré un problème intéressant: déplacer un vaisseau à l'aide de propulseurs n'est pas une tâche facile. Vous pouvez simplement déplacer et faire pivoter le navire comme une voiture, mais si vous souhaitez que la conception et les dommages structurels du navire affectent le mouvement des navires de manière crédible, la simulation de propulseurs pourrait être une meilleure approche. Dans ce tutoriel, je vais vous montrer comment faire cela..
En supposant qu'un navire puisse avoir plusieurs propulseurs dans différentes configurations et que sa forme et ses propriétés physiques puissent changer (par exemple, certaines parties du navire pourraient être détruites), il est nécessaire de déterminer: lequel propulseurs à tirer pour déplacer et faire pivoter le navire. C'est le principal défi auquel nous devons nous attaquer ici.
La démo est écrite en Haxe, mais la solution peut facilement être implémentée dans n’importe quel langage. Un moteur physique similaire à Box2D ou Nape est supposé, mais tout moteur fournissant le moyen d'appliquer des forces et des impulsions et de rechercher les propriétés physiques des corps fera l'affaire..Cliquez sur le fichier SWF pour l'activer, puis utilisez les touches fléchées et les touches Q et W pour activer différents propulseurs. Vous pouvez passer à différentes conceptions de vaisseau spatial en utilisant les touches numériques 1-4, et vous pouvez cliquer sur n’importe quel bloc ou propulseur pour le retirer du vaisseau..
Ce diagramme montre les classes qui représentent le navire et leur relation entre elles:
BodySprite
est une classe qui représente un corps physique avec une représentation graphique. Il permet aux objets d’affichage d’être attachés aux formes et s’assure qu’ils se déplacent et pivotent correctement avec le corps..
le Navire
class est un conteneur de modules. Il gère la structure du navire et s'occupe de la fixation et du détachement des modules. Il contient un seul ModuleManager
exemple.
La liaison d’un module associe sa forme et son objet d’affichage au sous-jacent BodySprite
, mais retirer un module demande un peu plus de travail. Tout d'abord, la forme du module et l'objet d'affichage sont supprimés de la BodySprite
, et ensuite, la structure du navire est vérifiée afin que tous les modules non connectés au noyau (le module avec le cercle rouge) soient détachés. Cette opération est effectuée à l'aide d'un algorithme similaire au remplissage par inondation, qui prend en compte la manière dont chaque module peut se connecter à d'autres modules (par exemple, les propulseurs ne peuvent se connecter que d'un côté, en fonction de leur orientation)..
Le détachement des modules est quelque peu différent: leur forme et leur objet d’affichage sont toujours supprimés de la liste. BodySprite
, mais sont ensuite attachés à une instance de ShipDebris
.
Cette façon de représenter le navire n’est pas la plus simple, mais j’ai trouvé que cela fonctionnait très bien. L’alternative serait de représenter chaque module en tant que corps séparé et de les "coller" avec un joint de soudure. Bien que cela rendrait la décomposition du navire beaucoup plus facile, le navire se sentirait en caoutchouc et élastique s’il disposait d’un grand nombre de modules..
le ModuleManager
est un conteneur qui conserve les modules d'un navire dans une liste (permettant une itération facile) et une carte de hachage (permettant un accès facile via les coordonnées locales).
le ShipModule
La classe représente évidemment un module de navire. C'est une classe abstraite qui définit certaines méthodes et attributs pratiques de chaque module. Chaque sous-classe de module est responsable de la construction de son propre objet d'affichage et de sa propre forme, ainsi que de la mise à jour si nécessaire. Les modules sont également mis à jour lorsqu'ils sont attachés à ShipDebris
, mais dans ce cas la joinToShip
le drapeau est réglé sur faux
.
Ainsi, un navire n’est en réalité qu’un ensemble de modules fonctionnels: des blocs de construction dont le placement et le type définissent le comportement du navire. Bien sûr, avoir un joli vaisseau flottant comme une pile de briques serait un jeu ennuyeux. Nous devons donc trouver un moyen de le faire bouger de manière amusante et réaliste..
Faire pivoter et déplacer un navire en tirant sélectivement sur les propulseurs, en modifiant leur poussée en ajustant les gaz ou en les allumant et en les éteignant rapidement, est un problème difficile. Heureusement, il est également inutile.
Si vous souhaitez faire pivoter un navire avec précision, par exemple, vous pouvez le faire simplement en indiquant à votre moteur physique de faire pivoter tout le corps. Dans ce cas, cependant, je recherchais une solution simple, qui n’est pas parfaite mais qui est amusante à jouer. Pour simplifier le problème, je vais introduire une contrainte:
Les propulseurs ne peuvent qu'être allumés ou éteints et ils ne peuvent pas varier leur poussée.
Maintenant que nous avons abandonné la perfection et la complexité, le problème est beaucoup plus simple. Nous devons déterminer, pour chaque propulseur, s'il doit être allumé ou non, en fonction de sa position sur le vaisseau et de l'entrée du joueur. Nous pourrions attribuer une clé différente à chaque propulseur, mais nous obtiendrions un QWOP interstellaire. Nous utiliserons donc les touches fléchées pour tourner et se déplacer, ainsi que Q et W pour le mitraillage..
La première chose à faire consiste à déplacer le navire d'avant en arrière, car c'est le cas le plus simple possible. Pour déplacer le navire, nous allons simplement tirer les propulseurs dans la direction opposée à celle que nous voulons utiliser. Par exemple, si nous voulions aller de l'avant, nous allumions tous les propulseurs qui font face en arrière.
// Met à jour le propulseur, annule une fois par image la fonction publique update (): Void if (attachmentToShip) // Avancer ou reculer si ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse); // Strafing else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse);
Évidemment, cela ne produira pas toujours l'effet souhaité. En raison de la contrainte ci-dessus, si les propulseurs ne sont pas placés de manière uniforme, le déplacement du navire peut provoquer sa rotation. En plus de cela, il n'est pas toujours possible de choisir la bonne combinaison de propulseurs pour déplacer un navire selon les besoins. Parfois, aucune combinaison de propulseurs ne déplacera le navire comme nous le souhaitons. Ceci est un effet souhaitable dans mon jeu, car il rend très évident les dommages aux navires et la mauvaise conception des navires..
Dans cet exemple, il est évident que le lancement des propulseurs A, D et E entraînera une rotation du navire dans le sens des aiguilles d'une montre (et une légère dérive, mais le problème est tout à fait différent). La rotation du navire revient à savoir de quelle manière un propulseur contribue à la rotation du navire..
Il se trouve que ce que nous recherchons ici est l’équation suivante: couple - en particulier le signe et la magnitude du couple.
Voyons donc ce qu'est le couple. Le couple est défini comme une mesure de la force exercée par la force exercée sur un objet sur sa rotation:
Parce que nous voulons faire pivoter le navire autour de son centre de masse, notre [latex] r [/ latex] représente le vecteur distance de la position de notre propulseur au centre de masse de tout le navire. Le centre de rotation peut être n’importe quel point, mais le centre de masse est probablement celui auquel un joueur s’attendrait..
Le vecteur de force [latex] F [/ latex] est un vecteur de direction unitaire qui décrit l’orientation de notre propulseur. Dans ce cas, nous ne nous soucions pas du couple réel, mais seulement de son signe. Il est donc correct d'utiliser uniquement le vecteur direction..
Le produit croisé n'étant pas défini pour les vecteurs à deux dimensions, nous allons simplement travailler avec des vecteurs à trois dimensions et définir le composant [latex] z [/ latex] sur 0
, rendant les maths simplifier magnifiquement:
[latex]
\ tau = r \ times F \\
\ tau = (r_x, \ quadr_y, \ quad 0) \ times (F_x, \ quad F_y, \ quad 0) \\
\ tau = (-0 \ cdot F_y + r_y \ cdot 0, \ quad 0 \ cdot F_x - r_x \ cdot 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau = (0, \ quad 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau_z = r_x \ cdot F_y - r_y \ cdot F_x \\
[/latex]
Avec ceci en place, nous pouvons calculer comment chaque propulseur affecte le navire individuellement. Une valeur de retour positive indique que le propulseur entraînera une rotation du navire dans le sens des aiguilles d'une montre, et inversement. Implémenter cela dans le code est très simple:
// Calcule le couple pas mal à l'aide de l'équation ci-dessus, fonction privée CalculateTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); return distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x; // La mise à jour du propulseur annule la fonction publique update (): Void if (attachmentToShip) // Si le propulseur est attaché à un vaisseau, nous traitons l'entrée // du joueur et nous le déclenchons si nécessaire. var couple = CalculateTorque (); if ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse); else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse); else if ((Input.check (Key.LEFT) && couple < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > torqueThreshold)) fire (thrustImpulse); else thrusterOn = false; else // Si le propulseur n'est pas attaché à un vaisseau, il est // attaché à un débris. Si le propulseur était en train de tirer quand il était // détaché, il continuera à tirer pendant un moment. // détachéeThrustTimer est une variable utilisée comme minuterie simple, // et est définie lorsque le propulseur se détache d'un navire. if (detailedThrustTimer> 0) détachéeThrustTimer - = NapeWorld.currentWorld.deltaTime; feu (impulsion); else thrusterOn = false; animate (); // Lance le propulseur en appliquant une impulsion au corps parent, // avec la direction opposée à la direction du propulseur et // la magnitude passée en tant que paramètre. // L'indicateur thrusterOn est utilisé pour l'animation. fonction publique incendie (quantité: Float): néant var thrustVec = thrustDir.mul (- amount); var impulseVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true;
La solution présentée est facile à mettre en œuvre et fonctionne bien pour un jeu de ce type. Bien sûr, il y a place à amélioration: ce tutoriel et la démo ne prennent pas en compte le fait qu'un navire pourrait être piloté par autre chose qu'un joueur humain, et implémenter un pilote d'IA capable de piloter un navire à moitié détruit serait un défi très intéressant (un défi auquel je devrai faire face de toute façon).