Physique de base des plateformes en 2D, Partie 6 Réponse de collision d'objet à objet

Dans la partie précédente de la série, nous avons implémenté un mécanisme de détection de collision entre les objets du jeu. Dans cette partie, nous utiliserons le mécanisme de détection de collision pour créer une réponse physique simple mais robuste entre les objets..

La démo montre le résultat final de ce tutoriel. Utilisez WASD pour déplacer le personnage. Le bouton du milieu de la souris génère une plate-forme à sens unique, le bouton droit de la souris crée une tuile pleine et la barre d'espace crée un clone de personnage. Les curseurs changent la taille du personnage du joueur. 

La démo a été publiée sous Unity 5.4.0f3 et le code source est également compatible avec cette version de Unity..

Réponse à la collision

Maintenant que nous avons toutes les données de collision du travail que nous avons effectué dans la partie précédente, nous pouvons ajouter une réponse simple aux objets en collision. Notre objectif ici est de permettre aux objets de ne pas se croiser comme s'ils se trouvaient sur un autre plan. Nous voulons qu'ils soient solides et qu'ils servent d'obstacle ou de plate-forme à d'autres objets. Pour cela, nous n’avons qu’une chose à faire: écarter l’objet d’un chevauchement, le cas échéant..

Couvrir les données supplémentaires

Nous aurons besoin de quelques données supplémentaires pour la MovingObject classe pour gérer l'objet par rapport à la réponse de l'objet. Tout d’abord, c’est bien d’avoir un booléen pour marquer un objet comme cinématique, c’est-à-dire que cet objet ne sera pas poussé par un autre objet.. 

Ces objets fonctionneront bien comme plates-formes, et ils peuvent également être des plates-formes mobiles. Ils sont censés être les objets les plus lourds qui soient, donc leur position ne sera pas corrigée de quelque manière que ce soit. Les autres objets devront s'éloigner pour leur laisser de la place.

public bool mIsKinematic = false;

Les autres données que j'aime bien sont des informations indiquant si nous nous tenons au-dessus d'un objet, à gauche ou à droite, etc. Jusqu'à présent, nous ne pouvions interagir qu'avec des tuiles, mais nous pouvons aussi interagir avec d'autres objets.. 

Pour apporter une certaine harmonie, nous aurons besoin d'un nouvel ensemble de variables décrivant si le personnage pousse quelque chose à gauche, à droite, en haut ou en bas..

public bool mPushesRight = false; public bool mPushesLeft = false; public bool mPushesBottom = false; public bool mPushesTop = false; public bool mPushedTop = false; public bool mPushedBottom = false; public bool mPushedRight = false; public bool mPushedLeft = false; public bool mPushesLeftObject = false; public bool mPushesRightObject = false; public bool mPushesBottomObject = false; public bool mPushesTopObject = false; public bool mPushedLeftObject = false; public bool mPushedRightObject = false; public bool mPushedBottomObject = false; public bool mPushedTopObject = false; public bool mPushesRightTile = false; public bool mPushesLeftTile = false; public bool mPushesBottomTile = false; public bool mPushesTopTile = false; public bool mPushedTopTile = false; public bool mPushedBottomTile = false; public bool mPushedRightTile = false; public bool mPushedLeftTile = false;

Cela fait beaucoup de variables. Dans un contexte de production, il serait intéressant de les transformer en drapeaux et d’avoir un seul entier au lieu de tous ces booléens, mais par souci de simplicité, nous nous en occuperons.. 

Comme vous le remarquerez peut-être, nous disposons de données très détaillées. Nous savons si le personnage a poussé ou poussé un obstacle dans une direction particulière, mais nous pouvons aussi facilement savoir si nous sommes à côté d'une tuile ou d'un objet..

Sortir du chevauchement

Créons le UpdatePhysicsResponse fonction, dans laquelle nous allons gérer l'objet par rapport à la réponse d'objet.

UpdatePhysicsResponse () 

Tout d'abord, si l'objet est marqué comme cinématique, nous retournons simplement. Nous ne gérons pas la réponse car l'objet cinématique n'a pas besoin de répondre à un autre objet. Les autres objets doivent y répondre..

si (mISKinematic) retourne;

Maintenant, cela suppose que nous n’aurons pas besoin d’un objet cinématique pour avoir les données correctes concernant le déplacement d’un objet du côté gauche, etc. Si ce n’est pas le cas, il faudrait alors le modifier un peu, ce que je ferais. touchons plus tard sur la ligne.

Maintenant, commençons à gérer les variables que nous venons de déclarer.

mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;

Nous sauvegardons les résultats de l'image précédente dans les variables appropriées et supposons pour l'instant que nous ne touchons aucun autre objet..

Commençons maintenant par parcourir toutes nos données de collision.

pour (int i = 0; i < mAllCollidingObjects.Count; ++i)  var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap; 

Tout d'abord, traitons les cas où les objets se touchent à peine, sans se chevaucher. Dans ce cas, nous savons que nous n'avons pas vraiment besoin de déplacer quoi que ce soit, il suffit de définir les variables. 

Comme mentionné précédemment, l'indicateur que les objets se touchent est que le chevauchement sur l'un des axes est égal à 0. Commençons par vérifier l'axe des x..

if (overlap.x == 0.0f) 

Si la condition est vraie, nous devons voir si l'autre objet est à gauche ou à droite de notre AABB..

if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x)  sinon 

Enfin, si c’est à droite, réglez le mPushesRightObject sur true et définit la vitesse afin qu'elle ne soit pas supérieure à 0, car notre objet ne peut plus se déplacer vers la droite car le chemin est bloqué.

if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  autre   

Gérons le côté gauche de la même manière.

if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f); 

Enfin, nous savons que nous n’avons pas besoin de faire autre chose ici, continuons donc à la prochaine itération de la boucle.

if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);  continuer; 

Gérons l'axe des y de la même manière.

if (overlap.x == 0.0f) if (other.mAABB.center.x> mAABB.center.x) mPushesRightObject = true; mSpeed.x = Mathf.Min (mSpeed.x, 0.0f);  else mPushesLeftObject = true; mSpeed.x = Mathf.Max (mSpeed.x, 0.0f);  continuer;  sinon if (overlap.y == 0.0f) if (other.mAABB.center.y> mAABB.center.y) mPushesTopObject = true; mSpeed.y = Mathf.Min (mSpeed.y, 0.0f);  else mPushesBottomObject = true; mSpeed.y = Mathf.Max (mSpeed.y, 0.0f);  continuer; 

C’est également un bon endroit pour définir les variables d’un corps cinématique, le cas échéant. Peu nous importe que le chevauchement soit égal à zéro ou non, car nous n'allons pas déplacer un objet cinématique de toute façon. Nous devrions également ignorer le réglage de la vitesse car nous ne voudrions pas arrêter un objet cinématique. Nous allons toutefois ignorer tout cela pour la démo, car nous n'allons pas utiliser les variables auxiliaires pour les objets cinématiques..

Maintenant que cela est couvert, nous pouvons gérer les objets qui se sont correctement chevauchés avec notre AABB. Avant de faire cela, permettez-moi d’expliquer l’approche que j’ai adoptée pour la réponse aux collisions.

Tout d’abord, si l’objet ne bouge pas et que nous y tombons, l’autre objet doit rester immobile. Nous le traitons comme un corps cinématique. J'ai décidé de suivre cette voie parce que j'estime que c'est plus générique et que le comportement de poussée peut toujours être traité plus loin dans la mise à jour personnalisée d'un objet particulier..

Si les deux objets se déplaçaient lors de la collision, nous avons divisé leur chevauchement en fonction de leur vitesse. Plus ils allaient vite, plus la valeur de chevauchement était grande, plus ils seraient reculés.

Le dernier point est, comme dans l’approche de réponse par tilemap, si un objet tombe et qu’en descendant, il gratte un autre objet même d’un pixel horizontalement, l’objet ne glisse pas et ne redescend pas, mais reste sur ce pixel..

Je pense que c'est l'approche la plus malléable, et sa modification ne devrait pas être très difficile si vous voulez gérer certaines réponses différemment.

Continuons l'implémentation en calculant le vecteur de vitesse absolue pour les deux objets lors de la collision. Nous aurons également besoin de la somme des vitesses pour que nous sachions quel pourcentage du chevauchement notre objet doit être déplacé.

Vector2 absSpeed1 = nouveau Vector2 (Mathf.Abs (data.pos1.x - data.oldPos1.x), Mathf.Abs (data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = nouveau Vector2 (Mathf.Abs (data.pos2.x - data.oldPos2.x), Mathf.Abs (data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;

Notez qu'au lieu d'utiliser la vitesse enregistrée dans les données de collision, nous utilisons le décalage entre la position au moment de la collision et la trame précédente. Cela sera simplement plus précis dans ce cas, car la vitesse représente le vecteur de mouvement avant la correction physique. Les positions elles-mêmes sont corrigées si l'objet a heurté une tuile solide, par exemple, donc si nous voulons obtenir un vecteur vitesse corrigé, nous devrions le calculer comme ceci.

Commençons maintenant à calculer le rapport de vitesse pour notre objet. Si l'autre objet est cinématique, nous allons définir le rapport de vitesse sur un, afin de nous assurer de déplacer tout le vecteur de recouvrement en respectant la règle selon laquelle l'objet cinématique ne doit pas être déplacé..

float speedRatioX, speedRatioY; si (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; autre  

Commençons par un cas étrange dans lequel les deux objets se chevauchent mais ne présentent aucune vitesse. Cela ne devrait pas vraiment arriver, mais si un objet est créé en superposant un autre objet, nous aimerions qu'ils se séparent naturellement. Dans ce cas, nous aimerions que les deux se déplacent de 50% du vecteur de recouvrement.

si (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f; 

Un autre cas est lorsque le speedSum sur l'axe des x est égal à zéro. Dans ce cas, nous calculons le rapport approprié pour l'axe des y et définissons le déplacement de 50% du chevauchement pour l'axe des x.

if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;  sinon si (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; 

De même, nous traitons le cas où le speedSum est zéro uniquement sur l'axe des y, et pour le dernier cas, nous calculons correctement les deux rapports.

si (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else if (speedSum.x == 0.0f && speedSum.y == 0.0f) speedRatioX = speedRatioY = 0.5f;  sinon si (speedSum.x == 0.0f) speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y;  else if (speedSum.y == 0.0f) speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0,5f;  else speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y; 

Maintenant que les ratios sont calculés, nous pouvons voir combien nous avons besoin de compenser notre objet.

float offsetX = overlap.x * speedRatioX; float offsetY = overlap.y * speedRatioY;

Maintenant, avant de décider s'il faut déplacer l'objet de la collision sur l'axe des x ou des y, calculons la direction à partir de laquelle le chevauchement s'est produit. Il y a trois possibilités: soit nous heurtons un autre objet horizontalement, verticalement ou en diagonale. 

Dans le premier cas, nous voulons sortir du chevauchement sur l'axe des x, dans le second cas, nous voulons sortir du chevauchement sur l'axe des y et dans le dernier cas, nous voulons sortir du chevauchement l'axe avait le moins de chevauchement.

Rappelez-vous que pour chevaucher un autre objet, nous avons besoin que les AABB se chevauchent sur les axes x et y. Pour vérifier si nous avons heurté un objet horizontalement, nous allons voir si l'image précédente était déjà superposée sur l'axe des y. Si c'est le cas, et que nous n'avons pas chevauché sur l'axe des x, le chevauchement doit s'être produit car, dans le cadre actuel, les AABB ont commencé à se chevaucher sur l'axe des x et nous en déduisons donc que nous avons heurté un autre objet horizontalement..

Tout d'abord, calculons si nous avons chevauché avec l'autre AABB dans le cadre précédent.

bool overlappedLastFrameX = Mathf.Abs (data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;

Définissons maintenant la condition pour sortir du chevauchement horizontalement. Comme expliqué précédemment, nous devions avoir un chevauchement sur l'axe des y et non un chevauchement sur l'axe des x dans l'image précédente.

if (! overlappedLastFrameX && overlappedLastFrameY) 

Si ce n'est pas le cas, nous allons sortir du chevauchement sur l'axe des y.

if (! overlappedLastFrameX && overlappedLastFrameY)  sinon 

Comme mentionné ci-dessus, nous devons également couvrir le scénario de collision en diagonale avec l'objet. Nous avons heurté l'objet en diagonale si nos AABB ne se chevauchaient pas dans le cadre précédent sur aucun des axes, car nous savons que dans le cadre actuel, ils se chevauchent sur les deux;.

if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY))  sinon 

Mais nous voulons sortir du chevauchement sur l'axe en cas de déformation diagonale uniquement si le chevauchement sur l'axe des x est inférieur au chevauchement sur l'axe des y.

if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))   else  

C'est tout les cas résolus. Maintenant, nous devons réellement sortir l'objet du chevauchement.

if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  

Comme vous pouvez le constater, nous le traitons de la même manière que dans le cas où nous touchions à peine un autre AABB, mais en plus nous déplaçons notre objet du décalage calculé..

La correction verticale se fait de la même manière.

if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX &&! overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  mPosition.y += offsetY; if (overlap.y < 0.0f)  mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);  else  mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);  

C'est presque ça; il y a juste une autre mise en garde à couvrir. Imaginez le scénario dans lequel nous atterrirons sur deux objets simultanément. Nous avons deux instances de données de collision presque identiques. En parcourant toutes les collisions, nous corrigeons la position de la collision avec le premier objet et nous remontons un peu.. 

Ensuite, nous gérons la collision pour le deuxième objet. Le chevauchement enregistré au moment de la collision n'est plus à jour, car nous nous sommes déjà éloignés de la position d'origine et si nous traitions la deuxième collision de la même façon que nous gérions la première, nous remonterions un peu plus loin. , faisant que notre objet soit corrigé deux fois la distance qu'il était censé.

Pour résoudre ce problème, nous garderons trace de combien nous avons déjà corrigé l'objet. Déclarons le vecteur offsetSum juste avant de commencer à itérer à travers toutes les collisions.

Vector2 offsetSum = Vector2.zero;

Maintenant, assurons-nous d'ajouter tous les décalages que nous avons appliqués à notre objet dans ce vecteur.

if ((! overlappedLastFrameX && overlappedLastFrameY) || (! overlappedLastFrameX &&! overlappedLastFrameY && Mathf.Abs (overlap.x) <= Mathf.Abs(overlap.y)))  mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f)  mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);  else  mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);   else  mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f)  mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);  else  mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);  

Et enfin, compensons le chevauchement de chaque collision consécutive par le vecteur de corrections cumulatif que nous avons effectué jusqu'à présent..

var overlap = data.overlap - offsetSum;

Maintenant, si nous atterrissons sur deux objets de la même hauteur en même temps, la première collision serait traitée correctement et le chevauchement de la seconde collision serait décalé à zéro, ce qui ne déplacerait plus notre objet..

Maintenant que notre fonction est prête, assurons-nous de l'utiliser. Un bon endroit pour appeler cette fonction serait après la CheckCollisions appel. Cela nous obligera à diviser notre UpdatePhysics fonctionner en deux parties, alors créons la deuxième partie maintenant, dans le MovingObject classe.

public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; 

Dans la deuxième partie nous appelons notre fraîchement fini UpdatePhysicsResponse fonctionne et met à jour les pressions générales sur les variables gauche, droite, inférieure et supérieure. Après cela, il ne reste plus qu'à appliquer le poste.

public void UpdatePhysicsP2 () UpdatePhysicsResponse (); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; // met à jour l'aabb mAABB.center = mPosition; // applique les modifications à la transformation transform.position = new Vector3 (Mathf.Round (mPosition.x), Mathf.Round (mPosition.y), mSpriteDepth); transform.localScale = new Vector3 (ScaleX, ScaleY, 1.0f); 

Maintenant, dans la boucle principale de mise à jour du jeu, appelons la deuxième partie de la mise à jour physique après la CheckCollisions appel.

void FixedUpdate () for (int i = 0; i < mObjects.Count; ++i)  switch (mObjects[i].mType)  case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break;   mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2(); 

Terminé! Maintenant, nos objets ne peuvent pas se chevaucher. Bien sûr, dans un cadre de jeu, nous aurions besoin d'ajouter quelques éléments tels que des groupes de collision, etc., de sorte qu'il n'est pas obligatoire de détecter ou de réagir à une collision avec chaque objet. avoir les choses en place dans votre jeu, de sorte que nous n'allons pas plonger dans cette.

Résumé

Voilà pour une autre partie de la série de physique des plateformes 2D simples. Nous avons utilisé le mécanisme de détection de collision implémenté dans la partie précédente pour créer une réponse physique simple entre des objets.. 

Avec ces outils, il est possible de créer des objets standard tels que des plates-formes mobiles, des blocs repoussants, des obstacles personnalisés et de nombreux autres types d’objets qui ne peuvent pas vraiment faire partie du tilemap, mais qui doivent néanmoins faire partie du terrain plat. Il y a encore une caractéristique populaire qui manque à notre implémentation physique: ce sont les pentes. 

Espérons que dans la partie suivante, nous allons commencer à étendre notre tilemap avec le support correspondant, ce qui viendrait compléter l'ensemble des fonctionnalités de base qu'une implémentation physique simple devrait avoir, et mettre fin à la série.. 

Bien sûr, il y a toujours place à l'amélioration. Si vous avez une question ou un conseil sur la façon de faire mieux, ou si vous avez simplement une opinion sur le didacticiel, n'hésitez pas à utiliser la section commentaires pour me le faire savoir.!