Résolu, voir en bas de poste pour algorithme finalDétection de collision ne doit pas faire l'objet téléporter jusqu'à
Contexte: Je travaille sur un jeu de plateforme 2D à l'aide JS et l'élément canvas HTML. La carte de niveau est basée sur les cases, mais le joueur n'est pas fixé aux tuiles. J'utilise un algorithme de détection de collision décrit dans "Tiny Platformer" on Code inComplete. Il fonctionne en grande partie à l'exception d'un cas de bord (ou «cas de« rebord »).
Le problème:
Le joueur est en train de tomber et se déplaçant aussi droit, dans le mur. En tombant, il se téléporte jusqu'à la hauteur du rebord. Au lieu de cela, le joueur devrait tomber normalement sans se téléporter.
Y at-il un moyen de modifier l'algorithme pour empêcher ce comportement? Si non, pouvez-vous suggérer un autre algorithme de détection de collision? Idéalement, toute correction ne supposerait pas que le joueur tombe toujours, car dans le jeu, la direction de chute du joueur bascule entre haut/bas/gauche/droite.
L'algorithme:
nouvelle position est calculée en supposant qu'aucune collision du joueur. (Non montré dans le code ci-dessous)
Une fonction appelée
getBorderTiles
prend un objet (le joueur) et retourne les tuiles touchant chacun des 4 coins du joueur. Puisque le joueur n'est pas plus grand qu'une tuile, ces tuiles de frontière sont nécessairement les seules tuiles que le joueur touche. Notez que certaines de ces tuiles peuvent être identiques. Par exemple, si le joueur n'occupe qu'une colonne, les tuiles gauche-haut/droite-haut seront les mêmes, tout comme les tuiles gauche-bas/droite-bas. Si cela se produit,getBorderTiles
renvoie toujours les quatre tuiles, mais certaines seront identiques.Il vérifie ces tuiles de bordure dans la carte de niveau (un tableau 2D) pour voir si elles sont solides. Si une tuile est solide, l'objet entre en collision avec cette tuile.
Il teste la collision haut/bas/gauche/droite. Si le joueur descend et entre en collision avec une tuile descendante sans entrer en collision avec la tuile montante correspondante, le joueur est en collision. Si le joueur se déplace à gauche et entre en collision avec une tuile gauche sans entrer en collision avec la tuile droite correspondante, il est en collision à gauche. Etc. Les contrôles haut/bas sont effectués avant les contrôles gauche/droite. Les variables qui stockent les tuiles de bordure sont ajustées s'il y a une collision haut/bas avant d'effectuer la vérification gauche/droite. Par exemple, si le joueur entre en collision, il sera poussé dans les tuiles vers le haut, de sorte que les tuiles BL/BR sont maintenant les mêmes que les tuiles TL/TR.
Les paramètres x, y et vitesse du lecteur sont ajustés en fonction des directions dans lesquelles il entre en collision.
Pourquoi l'algorithme échoue:
La tuile en bas à droite est solide, mais en haut à droite est pas, donc (étape 4), le joueur entre en collision vers le bas et (étape 5) il est poussé vers le haut. De plus, il entre en collision avec la tuile BR mais pas BL, donc il entre en collision droite et est poussé vers la gauche. A la fin, le joueur est rendu juste au-dessus et à gauche du rebord. En effet, il est téléporté.
Tentative à la solution: J'ai essayé de résoudre ce problème, mais cela n'a créé qu'un autre problème. J'ai ajouté un contrôle de sorte que le joueur ne se heurte à une tuile que s'il y a une certaine distance à l'intérieur de cette tuile (disons 3px). Si le joueur était à peine dans la tuile BR, l'algorithme n'enregistrerait pas de collision, donc le joueur ne se téléporterait pas. Cependant, si le joueur est tombé sur le sol dans un autre scénario, il n'a pas reconnu la collision jusqu'à ce que le joueur soit assez loin dans le sol. Le joueur a tremblé quand il est tombé un peu dans le sol, a été repoussé vers le haut du sol, est tombé à nouveau, etc.
Merci d'avoir lu jusqu'ici. J'apprécie vraiment vos commentaires.
Code de l'algorithme actuel:
var borderTiles = getBorderTiles(object), //returns 0 (a falsy value) for a tile if it does not fall within the level
tileTL = borderTiles.topLeft,
tileTR = borderTiles.topRight,
tileBL = borderTiles.bottomLeft,
tileBR = borderTiles.bottomRight,
coordsBR = getTopLeftXYCoordinateOfTile(tileBR), //(x, y) coordinates refer to top left corner of tile
xRight = coordsBR.x, //x of the right tile(s) (useful for adjusting object's position since it falls in middle of 4 tiles)
yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object's position since it falls in middle of 4 tiles)
typeTL = tileTL ? level.map[tileTL.row][tileTL.col] : -1, //if tileTL is in the level, gets its type, otherwise -1
typeTR = tileTR ? level.map[tileTR.row][tileTR.col] : -1,
typeBL = tileBL ? level.map[tileBL.row][tileBL.col] : -1,
typeBR = tileBR ? level.map[tileBR.row][tileBR.col] : -1,
collidesTL = typeTL == TILETYPE.SOLID, //true if the tile is solid
collidesTR = typeTR == TILETYPE.SOLID,
collidesBL = typeBL == TILETYPE.SOLID,
collidesBR = typeBR == TILETYPE.SOLID,
collidesUp = false,
collidesDown = false,
collidesLeft = false,
collidesRight = false;
//down and up
if (object.vy < 0 && ((collidesTL && !collidesBL) || (collidesTR && !collidesBR))) {
collidesUp = true;
/*The object is pushed out of the bottom row, so the bottom row is now the top row. Change the collides__
variables as this affects collision testing, but is it not necessary to change the tile__ variables. */
collidesTL = collidesBL;
collidesTR = collidesBR;
} else if (object.vy > 0 && ((collidesBL && !collidesTL) || (collidesBR && !collidesTR))) {
collidesDown = true;
/*The object is pushed out of the bottom row, so the bottom row is now the top row. Change the collides__
variables as this affects collision testing, but is it not necessary to change the tile__ variables. */
collidesBL = collidesTL;
collidesBR = collidesTR;
}
//left and right
if (object.vx < 0 && ((collidesTL && !collidesTR) || (collidesBL && !collidesBR))) {
collidesLeft = true;
} else if (object.vx > 0 && ((collidesTR && !collidesTL) || (collidesBR && !collidesBL))) {
collidesRight = true;
}
if (collidesUp) {
object.vy = 0;
object.y = yBottom;
}
if (collidesDown) {
object.vy = 0;
object.y = yBottom - object.height;
}
if (collidesLeft) {
object.vx = 0;
object.x = xRight;
}
if (collidesRight) {
object.vx = 0;
object.x = xRight - object.width;
}
MISE À JOUR: Résolu avec la solution de maracas. L'algorithme est ci-dessous. Fondamentalement, il teste (x puis y) et les collisions résolues, puis il teste (y puis x) et résout les collisions de cette façon. Quel que soit le résultat du test, le joueur qui se déplace sur une distance plus courte est celui qui finit par être utilisé.
Il est intéressant de noter qu'il faut un cas spécial lorsque le joueur entre en collision dans les deux sens. Peut-être cela est-il lié au fait que la coordonnée (x, y) du joueur est dans son coin supérieur gauche. Dans ce cas, le test qui entraîne le déplacement du joueur à une distance PLUS LONGUE doit être utilisé. Il est clair dans ce gif:
Le joueur est la boîte noire et la boîte jaune représente l'endroit où le joueur serait si elle avait utilisé l'autre test (le test qui a entraîné le joueur à déplacer une plus distance). Idéalement, le joueur ne devrait pas se déplacer dans le mur, et il devrait plutôt être là où se trouve la boîte jaune. Ainsi, dans ce scénario, le test à plus longue distance devrait être utilisé.
Voici l'implémentation rapide et sale. Ce n'est pas du tout optimisé mais j'espère que cela montre clairement les étapes de l'algorithme.
function handleCollision(object) {
var borderTiles = getBorderTiles(object), //returns 0 (a falsy value) for a tile if it does not fall within the level
tileTL = borderTiles.topLeft,
tileTR = borderTiles.topRight,
tileBL = borderTiles.bottomLeft,
tileBR = borderTiles.bottomRight,
coordsBR = getTopLeftXYCoordinateOfTile(tileBR), //(x, y) coordinates refer to top left corner of tile
xRight = coordsBR.x, //x of the right tile(s) (useful for adjusting object's position since it falls in middle of 4 tiles)
yBottom = coordsBR.y, //y of the bottom tile(s) (useful for adjusting object's position since it falls in middle of 4 tiles)
typeTL = tileTL ? level.map[tileTL.row][tileTL.col] : -1, //if tileTL is in the level, gets its type, otherwise -1
typeTR = tileTR ? level.map[tileTR.row][tileTR.col] : -1,
typeBL = tileBL ? level.map[tileBL.row][tileBL.col] : -1,
typeBR = tileBR ? level.map[tileBR.row][tileBR.col] : -1,
collidesTL = typeTL == TILETYPE.SOLID, //true if the tile is solid
collidesTR = typeTR == TILETYPE.SOLID,
collidesBL = typeBL == TILETYPE.SOLID,
collidesBR = typeBR == TILETYPE.SOLID,
collidesUp = false,
collidesDown = false,
collidesLeft = false,
collidesRight = false,
originalX = object.x, //the object's coordinates have already been adjusted according to its velocity, but not according to collisions
originalY = object.y,
px1 = originalX,
px2 = originalX,
py1 = originalY,
py2 = originalY,
vx1 = object.vx,
vx2 = object.vx,
vy1 = object.vy,
vy2 = object.vy,
d1 = 0,
d2 = 0,
conflict1 = false,
conflict2 = false,
tempCollidesTL = collidesTL,
tempCollidesTR = collidesTR,
tempCollidesBL = collidesBL,
tempCollidesBR = collidesBR;
//left and right
//step 1.1
if (object.vx > 0) {
if (collidesTR || collidesBR) {
vx1 = 0;
px1 = xRight - object.width;
conflict1 = true;
tempCollidesTR = false;
tempCollidesBR = false;
}
}
if (object.vx < 0) {
if (collidesTL || collidesBL) {
vx1 = 0;
px1 = xRight;
conflict1 = true;
tempCollidesTL = false;
tempCollidesBL = false;
collidesLeft = true;
}
}
//step 2.1
if (object.vy > 0) {
if (tempCollidesBL || tempCollidesBR) {
vy1 = 0;
py1 = yBottom - object.height;
}
}
if (object.vy < 0) {
if (tempCollidesTL || tempCollidesTR) {
vy1 = 0;
py1 = yBottom;
collidesUp = true;
}
}
//step 3.1
if (conflict1) {
d1 = Math.abs(px1 - originalX) + Math.abs(py1 - originalY);
} else {
object.x = px1;
object.y = py1;
object.vx = vx1;
object.vy = vy1;
return; //(the player's x and y position already correspond to its non-colliding values)
}
//reset the tempCollides variables for another runthrough
tempCollidesTL = collidesTL;
tempCollidesTR = collidesTR;
tempCollidesBL = collidesBL;
tempCollidesBR = collidesBR;
//step 1.2
if (object.vy > 0) {
if (collidesBL || collidesBR) {
vy2 = 0;
py2 = yBottom - object.height;
conflict2 = true;
tempCollidesBL = false;
tempCollidesBR = false;
}
}
if (object.vy < 0) {
if (collidesTL || collidesTR) {
vy2 = 0;
py2 = yBottom;
conflict2 = true;
tempCollidesTL = false;
tempCollidesTR = false;
}
}
//step 2.2
if (object.vx > 0) {
if (tempCollidesTR || tempCollidesBR) {
vx2 = 0;
px2 = xRight - object.width;
conflict2 = true;
}
}
if (object.vx < 0) {
if (tempCollidesTL || tempCollidesTL) {
vx2 = 0;
px2 = xRight;
conflict2 = true;
}
}
//step 3.2
if (conflict2) {
d2 = Math.abs(px2 - originalX) + Math.abs(py2 - originalY);
console.log("d1: " + d1 + "; d2: " + d2);
} else {
object.x = px1;
object.y = py1;
object.vx = vx1;
object.vy = vy1;
return;
}
//step 5
//special case: when colliding with the ceiling and left side (in which case the top right and bottom left tiles are solid)
if (collidesTR && collidesBL) {
if (d1 <= d2) {
object.x = px2;
object.y = py2;
object.vx = vx2;
object.vy = vy2;
} else {
object.x = px1;
object.y = py1;
object.vx = vx1;
object.vy = vy1;
}
return;
}
if (d1 <= d2) {
object.x = px1;
object.y = py1;
object.vx = vx1;
object.vy = vy1;
} else {
object.x = px2;
object.y = py2;
object.vx = vx2;
object.vy = vy2;
}
}
Notez qu'il existe également téléporte vers le bas qui se passe lorsque dans la moitié inférieure de la hauteur de la tuile. Le problème sera encore plus grave si la tuile sur laquelle vous atterrissez dans l'exemple est manquante, alors vous serez téléporté vers le bas à une position dans les airs avec la vitesse 0 et ensuite déplacé un peu vers la gauche, après que la gravité prendra encore, je suppose. – maraca