Physique de base des plateformes 2D, Partie 4

Dans cette partie de la série sur la physique des plateformes en 2D, nous ajouterons la saisie de rebords, la mécanique de la clémence des sauts et une capacité à redimensionner l'objet..

Accrocher les rebords

Maintenant que nous pouvons sauter, descendre à partir de plates-formes à sens unique et courir, nous pouvons également mettre en œuvre la saisie de rebords. Les mécanismes de prise de coin ne sont certainement pas indispensables dans tous les jeux, mais c'est une méthode très populaire pour étendre la plage de mouvements possible d'un joueur tout en évitant de faire quelque chose d'extrême comme un double saut..

Voyons comment déterminer si un rebord peut être saisi. Pour déterminer si le personnage peut saisir un rebord, nous vérifierons constamment le côté vers lequel le personnage se dirige. Si nous trouvons une tuile vide en haut de l'AABB, puis une tuile solide en dessous, le haut de cette tuile pleine est le rebord sur lequel notre personnage peut s'accrocher..

Configuration des variables

Allons à notre Personnage classe, où nous allons mettre en œuvre la saisie de rebord. Il ne sert à rien de faire cela dans le MovingObject classe, car la plupart des objets n'auront pas l'option de saisir un rebord, ce serait donc un gaspillage de faire n'importe quel traitement dans cette direction.

Premièrement, nous devons ajouter quelques constantes. Commençons par créer les constantes de décalage du capteur.

public const float cGrabLedgeStartY = 0.0f; public const float cGrabLedgeEndY = 2.0f;

le cGrabLedgeStartY et cGrabLedgeEndY sont des décalages par rapport au sommet de l'AABB; le premier est le premier point du capteur et le second est le point du capteur final. Comme vous pouvez le constater, le personnage devra trouver un rebord de moins de 2 pixels..

Nous avons également besoin d'une constante supplémentaire pour aligner le personnage sur la tuile qu'il vient de saisir. Pour notre personnage, ce sera mis à -4.

public const float cGrabLedgeTileOffsetY = -4.0f;

En dehors de cela, nous voudrons nous rappeler les coordonnées de la tuile que nous avons saisie. Sauvegardons ceux-ci en tant que variable membre du personnage.

public Vector2i mLedgeTile;

la mise en oeuvre

Nous aurons besoin de voir si nous pouvons saisir le bord de l'état de saut, alors allons-y. Juste après avoir vérifié si le personnage est tombé au sol, voyons si les conditions pour saisir un rebord sont remplies. Les conditions principales sont les suivantes:

  • La vitesse verticale est inférieure ou égale à zéro (le caractère est en baisse).
  • Le personnage n'est pas au plafond - inutile de saisir un rebord si vous ne pouvez pas en sauter.
  • Le personnage se heurte au mur et se dirige vers lui.
if (mOnGround) // s'il n'y a pas d'état de changement de mouvement en attente if (KeyState (KeyInput.GoRight) == KeyState (KeyInput.GoLeft)) mCurrentState = CharacterState.Stand; mSpeed ​​= Vector2.zero; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f);  else // soit on va vers la droite soit vers la gauche pour changer l'état en marche mCurrentState = CharacterState.Walk; mSpeed.y = 0.0f; mAudioSource.PlayOneShot (mHitWallSfx, 0.5f);  else if (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && KeyState(KeyInput.GoRight)) || (mPushesLeftWall && KeyState(KeyInput.GoLeft))))  

Si ces trois conditions sont remplies, nous devons rechercher le rebord à saisir. Commençons par calculer la position supérieure du capteur, qui sera soit le coin supérieur gauche ou supérieur droit de l'AABB.. 

Vector2 aabbCornerOffset; if (mPushesRightWall && mInputs [(int) KeyInput.GoRight]) aabbCornerOffset = mAABB.halfSize; else aabbCornerOffset = nouveau vecteur2 (-mAABB.halfSize.x - 1.0f, mAABB.halfSize.y);

Maintenant, comme vous pouvez l'imaginer, nous rencontrerons ici un problème similaire à celui rencontré lors de la mise en œuvre des contrôles de collision: si le personnage tombe très rapidement, il est en fait très susceptible de rater le point chaud où il peut saisir le rebord. . C'est pourquoi nous devrons vérifier la mosaïque que nous devons récupérer, non pas à partir du coin du cadre actuel, mais du précédent, comme illustré ici:


L'image du haut d'un personnage est sa position dans l'image précédente. Dans cette situation, nous devons commencer à chercher des occasions de saisir un rebord dans le coin supérieur droit de l'AABB de l'image précédente et de nous arrêter à la position de l'image actuelle..

Voyons les coordonnées des carreaux à vérifier, en commençant par déclarer les variables. Nous allons vérifier les carreaux dans une seule colonne, de sorte que tout ce dont nous avons besoin est la coordonnée X de la colonne ainsi que ses coordonnées Y supérieure et inférieure..

int tileX, topY, bottomY;

Prenons la coordonnée X du coin de l'AABB.

int tileX, topY, bottomY; tileX = mMap.GetMapTileXAtPoint (mAABB.center.x + aabbCornerOffset.x);

Nous voulons commencer à chercher un rebord à partir de la position de l'image précédente uniquement si nous nous déplacions déjà vers le mur poussé à cette époque. La position X de notre personnage n'a donc pas changé..

if ((mPushedLeftWall && mPushesLeftWall) || (mPushedRightWall && mPushesRightWall)) topY = mMap.GetMapTileYAtPoint (mOldPosition.y + mAABBOffset.y + aabbCornerOffset.y - Constantes). bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY); 

Comme vous pouvez le constater, dans ce cas, nous calculons le topY en utilisant la position de l'image précédente et celui du bas en utilisant celle de l'image en cours. Si nous n'étions pas près d'un mur, nous allons simplement voir si nous pouvons saisir un rebord en utilisant uniquement la position de l'objet dans le cadre actuel..

if ((mPushedLeftWall && mPushesLeftWall) || (mPushedRightWall && mPushesRightWall)) topY = mMap.GetMapTileYAtPoint (mOldPosition.y + mAABBOffset.y + aabbCornerOffset.y - Constantes). bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY);  else topY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeStartY); bottomY = mMap.GetMapTileYAtPoint (mAABB.center.y + aabbCornerOffset.y - Constants.cGrabLedgeEndY); 

Bon, maintenant que nous savons quelles mosaïques vérifier, nous pouvons commencer à les parcourir. Nous allons aller du haut vers le bas, parce que cet ordre est le plus logique, car nous autorisons la saisie de la corniche uniquement lorsque le personnage tombe.

pour (int y = topY; y> = bottomY; --y) 

Maintenant, vérifions si la tuile que nous sommes en train de remplir remplit les conditions permettant au personnage de saisir un rebord. Les conditions, comme expliqué précédemment, sont les suivantes:

  • La tuile est vide.
  • La tuile ci-dessous est une tuile solide (c'est la tuile que nous voulons saisir).
pour (int y = topY; y> = bottomY; --y) if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1)) 

La prochaine étape consiste à calculer la position du coin de la tuile que nous voulons saisir. C’est assez simple: il suffit d’obtenir la position de la tuile, puis de la compenser par la taille de la tuile..

if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1)) var tileCorner = mMap.GetMapTilePosition (tileX, y - 1); tileCorner.x - = Mathf.Sign (aabbCornerOffset.x) * Map.cTileSize / 2; tileCorner.y + = Map.cTileSize / 2; 

Maintenant que nous savons cela, nous devrions vérifier si le coin est entre nos points de capteur. Bien sûr, nous ne voulons le faire que si nous vérifions la mosaïque concernant la position du cadre actuel, qui est la mosaïque dont la coordonnée Y est égale au basY. Si ce n'est pas le cas, nous pouvons supposer en toute sécurité que nous avons franchi le rebord entre la trame précédente et la trame actuelle. Nous voulons donc saisir le rebord de toute façon..

if (! mMap.IsObstacle (tileX, y) && mMap.IsObstacle (tileX, y - 1)) var tileCorner = mMap.GetMapTilePosition (tileX, y - 1); tileCorner.x - = Mathf.Sign (aabbCornerOffset.x) * Map.cTileSize / 2; tileCorner.y + = Map.cTileSize / 2; if (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY)) 

Maintenant que nous sommes à la maison, nous avons trouvé le rebord que nous voulons saisir. Premièrement, sauvegardons la position de tuile du rebord saisi.

if (y> bottomY || ((mAABB.center.y + aabbCornerOffset.y) - tileCorner.y <= Constants.cGrabLedgeEndY && tileCorner.y - (mAABB.center.y + aabbCornerOffset.y) >= Constants.cGrabLedgeStartY)) mLedgeTile = nouveau Vector2i (tileX, y - 1); 

Nous devons également aligner le personnage avec le rebord. Ce que nous voulons faire est d’aligner le haut du capteur de bord du personnage avec le haut de la tuile, puis de décaler cette position de cGrabLedgeTileOffsetY.

mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY;

En dehors de cela, nous devons faire des choses comme mettre la vitesse à zéro et changer l'état en CharacterState.GrabLedge. Après cela, nous pouvons rompre avec la boucle car il est inutile de parcourir le reste des tuiles..

mPosition.y = tileCorner.y - aabbCornerOffset.y - mAABBOffset.y - Constants.cGrabLedgeStartY + Constants.cGrabLedgeTileOffsetY; mSpeed ​​= Vector2.zero; mCurrentState = CharacterState.GrabLedge; Pause;

Ça va être ça! Les rebords peuvent maintenant être détectés et saisis, il ne reste plus maintenant qu'à mettre en œuvre le GrabLedge état, que nous avons sauté plus tôt.

Contrôles de saisie de rebord

Une fois que le personnage s'empare d'un rebord, le joueur dispose de deux options: il peut sauter ou descendre. Sauter fonctionne normalement. le joueur appuie sur la touche de saut et la force du saut est identique à la force appliquée lors du saut depuis le sol. La descente se fait en appuyant sur le bouton bas ou sur la touche directionnelle qui pointe du rebord.

Mise en place des contrôles

La première chose à faire est de détecter si le bord est à gauche ou à droite du personnage. Nous pouvons le faire parce que nous avons enregistré les coordonnées du bord que le personnage est en train de saisir.

bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft;

Nous pouvons utiliser cette information pour déterminer si le personnage est supposé quitter le bord. Pour descendre, le joueur doit soit:

  • appuyez sur le bouton bas
  • appuyez sur le bouton gauche lorsque nous attrapons un rebord à droite, ou
  • appuyez sur le bouton droit lorsque nous saisissons un rebord sur la gauche
bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] && ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] && ledgeOnLeft))  

Il y a une petite mise en garde ici. Prenons une situation où nous tenons le bouton bas et le bouton droit, lorsque le personnage tient un rebord à droite. Cela entraînera la situation suivante:

Le problème ici est que le personnage s'empare du rebord immédiatement après l'avoir lâché.. 

Une solution simple consiste à verrouiller le mouvement vers le rebord pendant quelques images après le retrait du rebord. Pour cela, nous devons ajouter deux nouvelles variables. appelons-les mCannotGoLeftFrames et mCannotGoRightFrames.

public int mCannotGoLeftFrames = 0; public int mCannotGoRightFrames = 0;

Lorsque le personnage quitte le bord, nous devons définir ces variables et changer l'état pour sauter.

bool ledgeOnLeft = mLedgeTile.x * Map.cTileSize < mPosition.x; bool ledgeOnRight = !ledgeOnLeft; if (mInputs[(int)KeyInput.GoDown] || (mInputs[(int)KeyInput.GoLeft] && ledgeOnRight) || (mInputs[(int)KeyInput.GoRight] && ledgeOnLeft))  if (ledgeOnLeft) mCannotGoLeftFrames = 3; else mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump; 

Maintenant, revenons un peu à la Saut et assurons-nous qu'il respecte notre interdiction de déplacer à gauche ou à droite après avoir quitté le rebord. Réinitialisons les entrées juste avant de vérifier si nous devrions rechercher un rebord à saisir.

if (mCannotGoLeftFrames> 0) --mCannotGoLeftFrames; mInputs [(int) KeyInput.GoLeft] = false;  if (mCannotGoRightFrames> 0) --mCannotGoRightFrames; mInputs [(int) KeyInput.GoRight] = false;  if (mSpeed.y <= 0.0f && !mAtCeiling && ((mPushesRightWall && mInputs[(int)KeyInput.GoRight]) || (mPushesLeftWall && mInputs[(int)KeyInput.GoLeft])))  

Comme vous pouvez le constater, de cette manière, nous ne remplirons pas les conditions nécessaires pour saisir un bord tant que la direction bloquée est la même que la direction du bord que le personnage peut essayer de saisir. Chaque fois que nous refusons une entrée particulière, nous décrémentons les images bloquantes restantes. Nous pourrons donc éventuellement nous déplacer à nouveau, dans notre cas, après 3 images..

Continuons maintenant à travailler sur le GrabLedge Etat. Depuis que nous avons réussi à descendre du rebord, nous devons maintenant permettre de sauter de la position de saisie.

Si le personnage n'a pas quitté le bord, nous devons vérifier si la touche de saut a été enfoncée. si c'est le cas, nous devons définir la vitesse verticale du saut et changer l'état:

if (mInputs [(int) KeyInput.GoDown] || (mInputs [(int) KeyInput.GoLeft] && ledgeOnRight) || (mInputs [(int) KeyInput.GoRight] && ledgeOnLeft)) || ; sinon mCannotGoRightFrames = 3; mCurrentState = CharacterState.Jump;  else if (mInputs [(int) KeyInput.Jump]) mSpeed.y = mJumpSpeed; mCurrentState = CharacterState.Jump; 

C'est à peu près ça! Maintenant, la prise de rebord devrait fonctionner correctement dans toutes sortes de situations.

Autoriser le personnage à sauter peu de temps après avoir quitté une plate-forme

Souvent, pour faciliter les sauts dans les jeux de plateforme, le personnage est autorisé à sauter s'il vient de sortir du bord d'une plate-forme et qu'il n'est plus au sol. Il s'agit d'une méthode populaire pour atténuer l'illusion que le joueur ait appuyé sur le bouton de saut mais que le personnage n'ait pas sauté, ce qui pourrait être dû à un décalage de la saisie ou au joueur qui a appuyé sur le bouton de saut juste après que le personnage soit sorti de la plateforme.

Mettons en place un tel mécanisme maintenant. Tout d'abord, nous devons ajouter une constante du nombre d'images après que le personnage ait quitté la plate-forme, il peut toujours effectuer un saut..

public const int cJumpFramesThreshold = 4;

Nous aurons également besoin d’un compteur de trames dans le Personnage classe, nous savons donc combien d'images le personnage est déjà dans l'air.

protected int mFramesFromJumpStart = 0;

Maintenant, mettons le mFramesFromJumpStart à 0 chaque fois que nous venons de quitter le sol. Faisons cela juste après l'appel  UpdatePhysics.

UpdatePhysics (); if (mWasOnGround &&! mOnGround) mFramesFromJumpStart = 0;

Et incrémentons chaque image où nous sommes dans l'état de saut.

case CharacterState.Jump: ++ mFramesFromJumpStart;

Si nous sommes dans l'état de saut, nous ne pouvons pas permettre un saut en l'air si nous sommes soit au plafond, soit à une vitesse verticale positive. Une vitesse verticale positive signifierait que le personnage n'a pas raté un saut.

++mFramesFromJumpStart; if (mFramesFromJumpStart <= Constants.cJumpFramesThreshold)  if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1; 

Si ce n'est pas le cas et que l'on appuie sur la touche de saut, il suffit de régler la vitesse verticale sur la valeur de saut, comme si on sautait normalement, même si le personnage est déjà dans l'état de saut..

if (mFramesFromJumpStart <= Constants.cJumpFramesThreshold)  if (mAtCeiling || mSpeed.y > 0.0f) mFramesFromJumpStart = Constants.cJumpFramesThreshold + 1; else if (KeyState (KeyInput.Jump)) mSpeed.y = mJumpSpeed; 

Et c'est tout! Nous pouvons régler le cJumpFramesThreshold à une grande valeur comme 10 cadres pour vous assurer que cela fonctionne.

L'effet ici est assez exagéré. Ce n'est pas très visible si nous permettons au personnage de ne sauter que 1 à 4 images après qu'il ne soit plus sur le terrain, mais globalement, cela nous permet de modifier la clémence avec laquelle nous voulons que nos sauts soient..

Mise à l'échelle des objets

Faisons en sorte qu'il soit possible de redimensionner les objets. Nous avons déjà le mScale dans le MovingObject En fait, tout ce que nous avons à faire est de nous assurer que cela affecte correctement les décalages AABB et AABB..

Tout d’abord, éditons notre classe AABB pour qu’elle ait un composant scale.

structure publique AABB échelle publique Vector2; centre public Vector2; public Vector2 halfSize; AABB public (centre Vector2, demi-taille Vector2) scale = Vector2.one; this.center = center; this.halfSize = halfSize;  

Maintenant, éditons le moitié de la taille, de sorte que lorsque nous y accédons, nous obtenons en fait une taille réduite au lieu de celle non mise à l'échelle.

échelle publique Vector2; centre public Vector2; Vector2 privé halfSize; public Vector2 HalfSize set halfSize = value;  get return new Vector2 (halfSize.x * scale.x, halfSize.y * scale.y); 

Nous voudrons également pouvoir obtenir ou définir uniquement une valeur X ou Y de la demi-taille. Nous devons donc créer des getters et des setters séparés pour ceux-ci également..

public float HalfSizeX set halfSize.x = valeur;  get return halfSize.x * scale.x;  public float HalfSizeY set halfSize.y = valeur;  get return halfSize.y * scale.y; 

Outre la mise à l'échelle de l'AABB lui-même, nous devrons également adapter l'échelle mAABBOffset, de sorte qu'après que nous ayons redimensionné l'objet, son sprite correspondra toujours à l'AABB de la même manière que lorsque l'objet n'était pas mis à l'échelle. Revenons à la MovingObject classe pour le modifier.

privé Vector2 mAABBOffset; public Vector2 AABBOffset set mAABBOffset = valeur;  get return new Vector2 (mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y); 

Les mêmes que précédemment, nous voudrons avoir accès aux composants X et Y séparément aussi.

public float AABBOffsetX set mAABBOffset.x = valeur;  get return mAABBOffset.x * mScale.x;  public float AABBOffsetY set mAABBOffset.y = value;  get return mAABBOffset.y * mScale.y; 

Enfin, nous devons également nous assurer que lorsque l’échelle est modifiée dans le MovingObject, il est également modifié dans l'AABB. L'échelle de l'objet peut être négative, mais l'AABB elle-même ne devrait pas avoir une échelle négative, car nous comptons sur la demi-taille pour être toujours positifs. C'est pourquoi, au lieu de simplement transmettre la balance à l'AABB, nous allons passer à une balance qui a toutes les composantes positives.

privé Vector2 mScale; public Vector2 Scale set mScale = value; mAABB.scale = new Vector2 (Mathf.Abs (value.x), Mathf.Abs (value.y));  get return mScale;  public float ScaleX set mScale.x = valeur; mAABB.scale.x = Mathf.Abs (valeur);  get return mScale.x;  public float ScaleY set mScale.y = valeur; mAABB.scale.y = Mathf.Abs (valeur);  get return mScale.y; 

Il ne nous reste plus qu'à nous assurer que, où que nous utilisions les variables directement, nous les utilisions maintenant via les accesseurs et les calculateurs. Où que nous utilisions halfSize.x, nous voudrons utiliser HalfSizeX, partout où nous avons utilisé halfSize.y, nous voudrons utiliser HalfSizeY, etc. Quelques utilisations d’une fonction de recherche et remplacement devraient traiter ce problème.

Vérifier les résultats

La mise à l'échelle devrait bien fonctionner maintenant, et en raison de la manière dont nous avons construit nos fonctions de détection des collisions, peu importe si le personnage est géant ou minuscule: il doit bien interagir avec la carte..

Résumé

Cette partie conclut notre travail avec le tilemap. Dans les prochaines parties, nous organiserons les choses pour détecter les collisions entre objets. 

Cela a pris du temps et des efforts, mais le système en général devrait être très robuste. Une chose qui manque peut-être actuellement est le support des pentes. Beaucoup de jeux ne dépendent pas d'eux, mais beaucoup le font, alors c'est le plus gros objectif d'amélioration de ce système. Merci d'avoir lu jusqu'à présent, à la prochaine partie!