Dans cette partie de la série Physique des plateformes 2D, nous allons créer un tilemap et implémenter partiellement la détection et la réponse des collisions entre les objets..
Il existe deux approches de base pour la création de niveaux de plateforme. L'une d'elles consiste à utiliser une grille et à placer les carreaux appropriés dans des cellules, l'autre à une forme plus libre, dans laquelle vous pouvez placer la géométrie de niveau de manière lâche, comme et où vous le souhaitez..
Les deux approches présentent des avantages et des inconvénients. Nous allons utiliser la grille, voyons donc quel genre de pros il a par rapport à l'autre méthode:
Commençons par créer une classe Map. Il contiendra toutes les données spécifiques à la carte.
Carte de la classe publique
Nous devons maintenant définir toutes les tuiles que contient la carte, mais avant cela, nous devons savoir quels types de tuiles existent dans notre jeu. Pour l'instant, nous n'en prévoyons que trois: une tuile vide, une tuile pleine et une plate-forme à sens unique.
Énumération publique TileType Empty, Block, OneWay
Dans la démo, les types de tuiles correspondent directement au type de collision que nous aimerions avoir avec une tuile, mais dans un jeu réel, ce n'est pas nécessairement le cas. Comme vous avez plus de mosaïques visuellement différentes, il serait préférable d’ajouter de nouveaux types tels que GrassBlock, GrassOneWay, etc., afin de laisser l’énumération TileType définir non seulement le type de collision mais également l’aspect de la mosaïque..
Maintenant, dans la classe de carte, nous pouvons ajouter un tableau de tuiles.
Classe publique Map private TileType [,] mTiles;
Bien sûr, une tilemap que nous ne pouvons pas voir ne nous est pas d'une grande utilité, nous avons donc besoin de sprites pour sauvegarder les données de tuiles. Normalement, dans Unity, il est extrêmement inefficace d’avoir chaque mosaïque un objet distinct, mais comme nous ne l’utilisons que pour tester notre physique, il n’est pas difficile de le faire de cette façon dans la démo..
SpriteRenderer privé [,] mTilesSprites;
La carte a également besoin d'une position dans l'espace mondial, de sorte que si nous avons besoin de plus d'un seul, nous pouvons les écarter..
public Vector3 mPosition;
Largeur et hauteur, en dalles.
public int mWidth = 80; public int mHeight = 60;
Et la taille de la mosaïque: dans la démo, nous travaillerons avec une taille de mosaïque assez petite, de 16 x 16 pixels.
public const int cTileSize = 16;
Ce serait ça. Nous avons maintenant besoin de deux fonctions d’aide pour pouvoir accéder facilement aux données de la carte. Commençons par créer une fonction qui convertira les coordonnées du monde en coordonnées de tuile de la carte..
public Vector2i GetMapTileAtPoint (point Vector2)
Comme vous pouvez le voir, cette fonction prend un Vecteur2
en paramètre et retourne un Vector2i
, qui est essentiellement un vecteur 2D opérant sur des entiers au lieu de floats.
La conversion de la position du monde en position de la carte est très simple: nous devons simplement déplacer le point
par mPosition
donc nous retournons la tuile par rapport à la position de la carte puis divisons le résultat par la taille de la tuile.
public Vector2i GetMapTileAtPoint (point Vector2) renvoie le nouveau vecteur Vector2i ((int) ((point.x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize)), (int) ((point.y - mPosition.). y + cTileSize / 2.0f) / (float) (cTileSize)));
Notez que nous avons dû déplacer le point
en plus par cTileSize / 2.0f
, parce que le pivot de la tuile est en son centre. Faisons également deux fonctions supplémentaires qui renverront uniquement les composantes X et Y de la position dans l'espace de la carte. Ça sera utile plus tard.
public int GetMapTileYAtPoint (float y) return (int) ((y - mPosition.y + cTileSize / 2.0f) / (float) (cTileSize)); public int GetMapTileXAtPoint (float x) return (int) ((x - mPosition.x + cTileSize / 2.0f) / (float) (cTileSize));
Nous devrions également créer une fonction complémentaire qui, à partir d’une tuile, va revenir à sa position dans l’espace mondial..
public Vector2 GetMapTilePosition (int tileIndexX, int tileIndexY) retourne le nouveau Vector2 ((float) (tileIndexX * cTileSize) + mPosition.x, (float) (tileIndexY * cTileSize) + mPosition.y); public Vector2 GetMapTilePosition (Vector2i tileCoords) renvoie le nouveau vecteur Vector2 ((float) (tileCoords.x * cTileSize) + mPosition.x, (float) (tileCoords.y * cTileSize) + mPosition.y);
En plus de la traduction des positions, nous avons également besoin de quelques fonctions pour déterminer si une tuile à une certaine position est vide, est une tuile solide ou est une plate-forme à sens unique. Commençons par une fonction GetTile très générique, qui renverra le type d'une tuile spécifique.
TileType public GetTile (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) retourne TileType.Block; renvoyer mTiles [x, y];
Comme vous pouvez le constater, avant de renvoyer le type de mosaïque, nous vérifions si la position donnée est hors limites. Si c'est le cas, nous voulons le traiter comme un bloc solide, sinon nous retournons un vrai type.
Le prochain en file d'attente est une fonction permettant de vérifier si une tuile est un obstacle.
public bool IsObstacle (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) retourne vrai; return (mTiles [x, y] == TileType.Block);
De la même manière que précédemment, nous vérifions si la mosaïque est hors limites et si c'est le cas, nous retournons la valeur true. Toute mosaïque hors limites est alors traitée comme un obstacle..
Maintenant, vérifions si la tuile est une tuile au sol. Nous pouvons nous tenir à la fois sur un bloc et sur une plate-forme à sens unique. Nous devons donc retourner true si la tuile est l’une de ces deux.
public bool IsGround (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) renvoie false; return (mTiles [x, y] == TileType.OneWay || mTiles [x, y] == TileType.Block);
Enfin, ajoutons IsOneWayPlatform
et Est vide
fonctionne de la même manière.
public bool IsOneWayPlatform (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) renvoie false; return (mTiles [x, y] == TileType.OneWay); public bool IsEmpty (int x, int y) if (x < 0 || x >= mWidth || y < 0 || y >= mHeight) renvoie false; return (mTiles [x, y] == TileType.Empty);
C'est tout ce que nous avons besoin de notre classe de carte à faire. Maintenant, nous pouvons avancer et mettre en œuvre la collision de personnage contre elle.
Revenons à la MovingObject
classe. Nous devons créer quelques fonctions qui détecteront si le personnage entre en collision avec le tilemap.
La méthode par laquelle nous saurons si le personnage entre en collision avec une tuile est très simple. Nous allons vérifier toutes les tuiles qui existent juste à l'extérieur de l'AABB de l'objet en mouvement..
La boîte jaune représente le AABB du personnage, et nous allons vérifier les carreaux le long des lignes rouges. Si l’une d’entre elles chevauche une mosaïque, nous définissons une variable de collision correspondante sur true (telle que MOnGround
, mPushesLeftWall
, maCeiling
ou mPushesRightWall
).
Commençons par créer une fonction HasGround, qui vérifiera si le personnage entre en collision avec une tuile au sol.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottante Y)
Cette fonction retourne vrai si le caractère chevauche l'une des tuiles du bas. Il prend comme paramètres l'ancienne position, la position actuelle et la vitesse actuelle, et renvoie également la position Y du haut de la tuile avec laquelle vous entrez en collision et si la tuile en collision est une plate-forme à sens unique ou non..
La première chose que nous voulons faire est de calculer le centre de AABB.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, sortie de flottementY) var centre = position + MAABBOffset;
Maintenant que nous avons cela, pour la vérification de collision de fond, nous devrons calculer le début et la fin de la ligne de capteur de fond. La ligne de capteur est juste un pixel en dessous du contour inférieur de l'AABB.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var centre = position + MAABBOffset; var bottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nouveau Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);
le en bas à gauche
et en bas à droite
représentent les deux extrémités du capteur. Maintenant que nous en avons, nous pouvons calculer les carreaux à vérifier. Commençons par créer une boucle dans laquelle nous allons parcourir les tuiles de gauche à droite..
pour (var checkedTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize)
Notez qu'il n'y a pas de condition pour quitter la boucle ici, nous le ferons à la fin de la boucle..
La première chose à faire est de s’assurer que le vérifiéTile.x
n'est pas plus grande que l'extrémité droite du capteur. Cela peut être le cas parce que nous déplaçons le point vérifié par des multiples de la taille de la mosaïque. Par exemple, si le personnage a une largeur de 1,5 mosaïque, nous devons vérifier la mosaïque sur le bord gauche du capteur, puis une mosaïque à droite. , puis 1,5 carreaux à droite au lieu de 2.
pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x);
Maintenant, nous devons obtenir la coordonnée de la tuile dans l'espace de la carte pour pouvoir vérifier le type de la tuile.
int tileIndexX, tileIndexY; pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (vérifiéTile.x); tileIndexY = mMap.GetMapTileYAtPoint (vérifiéTile.y);
D'abord, calculons la position haute de la tuile.
int tileIndexX, tileIndexY; pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (vérifiéTile.x); tileIndexY = mMap.GetMapTileYAtPoint (vérifiéTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y;
Maintenant, si la tuile actuellement cochée est un obstacle, nous pouvons facilement renvoyer true.
int tileIndexX, tileIndexY; pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (vérifiéTile.x); tileIndexY = mMap.GetMapTileYAtPoint (vérifiéTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; if (mMap.IsObstacle (tileIndexX, tileIndexY)) renvoie la valeur true;
Enfin, vérifions si nous avons déjà examiné toutes les tuiles qui se croisent avec le capteur. Si tel est le cas, nous pouvons alors quitter la boucle en toute sécurité. Après avoir quitté la boucle sans trouver une tuile avec laquelle nous sommes entrés en collision, nous devons retourner faux
faire savoir à l'appelant qu'il n'y a pas de motif en dessous de l'objet.
int tileIndexX, tileIndexY; pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (vérifiéTile.x); tileIndexY = mMap.GetMapTileYAtPoint (vérifiéTile.y); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; if (mMap.IsObstacle (tileIndexX, tileIndexY)) renvoie la valeur true; if (checkedTile.x> = bottomRight.x) break; return false;
C'est la version la plus basique du chèque. Essayons de le faire fonctionner maintenant. Retour dans le UpdatePhysics
notre ancienne vérification au sol ressemble à ceci.
si (mPosition.y <= 0.0f) mPosition.y = 0.0f; mOnGround = true; else mOnGround = false;
Remplaçons-le en utilisant la méthode nouvellement créée. Si le personnage tombe et que nous avons trouvé un obstacle sur notre chemin, nous devons le sortir de la collision et régler le problème. MOnGround
à vrai. Commençons par la condition.
float groundY = 0; si (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY))
Si la condition est remplie, nous devons déplacer le personnage au-dessus de la tuile avec laquelle nous sommes entrés en collision..
float groundY = 0; si (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y;
Comme vous pouvez le constater, c’est très simple car la fonction renvoie le niveau de base sur lequel nous devrions aligner l’objet. Après cela, il suffit de régler la vitesse verticale à zéro et de régler MOnGround
à vrai.
float groundY = 0; si (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true;
Si notre vitesse verticale est supérieure à zéro ou si nous ne touchons aucun sol, nous devons définir la MOnGround
à faux
.
float groundY = 0; si (mSpeed.y <= 0.0f && HasGround(mOldPosition, mPosition, mSpeed, out groundY)) mPosition.y = groundY + mAABB.halfSize.y - mAABBOffset.y; mSpeed.y = 0.0f; mOnGround = true; else mOnGround = false;
Voyons maintenant comment cela fonctionne.
Comme vous pouvez le constater, cela fonctionne bien! La détection de collision pour les murs des deux côtés et en haut du personnage n'est toujours pas là, mais le personnage s'arrête chaque fois qu'il rencontre le sol. Nous avons encore besoin de mettre un peu plus de travail dans la fonction de vérification des collisions pour la rendre robuste.
L'un des problèmes que nous devons résoudre est visible si le décalage du personnage d'une image à l'autre est trop important pour détecter correctement la collision. Ceci est illustré dans l'image suivante.
Cette situation ne se produit pas maintenant car nous avons verrouillé la vitesse de chute maximale à une valeur raisonnable et actualisé la physique à une fréquence de 60 FPS, de sorte que les différences de positions entre les images sont plutôt faibles. Voyons ce qui se passe si nous mettons à jour la physique seulement 30 fois par seconde.
Comme vous pouvez le constater, dans ce scénario, notre contrôle de collision au sol nous échoue. Pour résoudre ce problème, nous ne pouvons pas simplement vérifier si le personnage a la terre au-dessous de lui à la position actuelle, mais nous avons plutôt besoin de voir s'il y avait des obstacles le long du chemin depuis la position de l'image précédente..
Revenons à notre HasGround
une fonction. Ici, en plus du calcul du centre, nous voudrons aussi calculer le centre de la trame précédente.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset;
Nous aurons également besoin d'obtenir la position du capteur de la trame précédente.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nouveau Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y);
Maintenant, nous devons calculer à quelle tuile verticalement nous allons commencer à vérifier s'il y a collision ou non, et à quel arrêt nous allons nous arrêter..
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var bottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var bottomRight = nouveau Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); int endY = mMap.GetMapTileYAtPoint (bottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY);
Nous commençons la recherche à partir de la mosaïque à la position du capteur de l'image précédente et la terminons à la position du capteur de l'image en cours. C'est bien sûr parce que lorsque nous vérifions une collision au sol, nous supposons que nous tombons, ce qui signifie que nous passons de la position la plus haute à la position la plus basse..
Enfin, nous devons avoir une autre boucle d'itération. Avant de renseigner le code de cette boucle externe, considérons le scénario suivant.
Ici vous pouvez voir une flèche se déplacer rapidement. Cet exemple montre que nous devons non seulement parcourir toutes les mosaïques que nous aurions besoin de passer verticalement, mais aussi interpoler la position de l'objet pour chaque mosaïque que nous parcourons afin de rapprocher le chemin de la position de l'image précédente à la position actuelle. Si nous continuions simplement à utiliser la position actuelle de l'objet, dans ce cas, une collision serait détectée, même si cela ne devrait pas être le cas..
Renommons le en bas à gauche
et en bas à droite
comme nouveauBottomLeft
et nouveauBottomRight
, nous savons donc que ce sont les positions du capteur du nouveau cadre.
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = new Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int tileIndexX; for (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) return false;
Maintenant, dans cette nouvelle boucle, interpolons les positions du capteur, de sorte qu'au début de la boucle, nous supposons que le capteur se trouve à la position de la trame précédente et qu'à la fin, il sera dans la position de la trame actuelle..
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = new Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endY - begY), 1); int tileIndexX; pour (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY - tileIndexY) / dist); var bottomRight = nouveau Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); return false;
Notez que nous interpolons les vecteurs en fonction de la différence de tuiles sur l'axe des ordonnées. Lorsque les anciennes et les nouvelles positions se trouvent dans la même tuile, la distance verticale sera égale à zéro. Dans ce cas, nous ne pourrions pas diviser par la distance. Donc, pour résoudre ce problème, nous voulons que la distance ait une valeur minimale de 1, de sorte que si un tel scénario se produisait (et cela se produira très souvent), nous utiliserons simplement la nouvelle position pour la détection de collision..
Enfin, pour chaque itération, nous devons exécuter le même code que nous avons déjà utilisé pour vérifier la collision au sol sur la largeur de l'objet..
public bool HasGround (Vector2 oldPosition, position Vector2, vitesse Vector2, virgule flottanteY) var oldCenter = oldPosition + mAABBOffset; var center = position + mAABBOffset; var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomLeft = center - mAABB.halfSize - Vector2.up + Vector2.right; var newBottomRight = new Vector2 (newBottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, newBottomLeft.y); int endY = mMap.GetMapTileYAtPoint (newBottomLeft.y); int begY = Mathf.Max (mMap.GetMapTileYAtPoint (oldBottomLeft.y) - 1, endY); int dist = Mathf.Max (Mathf.Abs (endY - begY), 1); int tileIndexX; pour (int tileIndexY = begY; tileIndexY> = endY; --tileIndexY) var bottomLeft = Vector2.Lerp (newBottomLeft, oldBottomLeft, (float) Mathf.Abs (endY - tileIndexY) / dist); var bottomRight = nouveau Vector2 (bottomLeft.x + mAABB.halfSize.x * 2.0f - 2.0f, bottomLeft.y); pour (var vérifiéTile = bottomLeft;; vérifiéTile.x + = Map.cTileSize) vérifiéTile.x = Mathf.Min (vérifiéTile.x, bottomRight.x); tileIndexX = mMap.GetMapTileXAtPoint (vérifiéTile.x); groundY = (float) tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y; if (mMap.IsObstacle (tileIndexX, tileIndexY)) renvoie la valeur true; if (checkedTile.x> = bottomRight.x) break; return false;
C'est à peu près tout. Comme vous pouvez l’imaginer, si les objets du jeu bougent très vite, cette méthode de contrôle des collisions peut coûter un peu plus cher, mais elle nous assure également qu’il n’y aura pas d’obstacles étranges lorsque des objets se déplacent à travers des murs solides..
Ouf, c'était plus de code que nous pensions avoir besoin, n'est-ce pas? Si vous repérez des erreurs ou des raccourcis possibles à prendre, faites-le moi savoir et tout le monde dans les commentaires! La vérification des collisions doit être suffisamment robuste pour que nous n'ayons pas à nous soucier d'événements malheureux d'objets glissant entre les blocs de tilemap..
Une grande partie du code a été écrite pour s’assurer qu’aucun objet ne passe à travers les mosaïques à grande vitesse, mais si cela ne pose pas de problème pour un jeu en particulier, nous pouvons supprimer le code supplémentaire en toute sécurité pour augmenter les performances. Il pourrait même être judicieux d’avoir un indicateur pour des objets spécifiques en mouvement rapide, afin que seuls ceux-ci utilisent les versions les plus chères des contrôles.
Nous avons encore beaucoup de choses à couvrir, mais nous avons réussi à effectuer un contrôle de collision fiable pour le sol, qui peut être reflété assez facilement dans les trois autres directions. Nous ferons cela dans la prochaine partie.