rozwiązany, patrz na dole postu algorytm końcowegowykrywanie kolizji nie powinno uczynić przedmiot teleportować się
Tło: Pracuję na platformówki 2D przy użyciu JS i HTML elementu canvas. Mapa poziomów opiera się na kafelkach, ale gracz nie jest zaciśnięty na kafelkach. Używam algorytmu wykrywania kolizji przedstawionego w "Tiny Platformer" on Code inComplete. W dużej mierze działa z wyjątkiem przypadku jednej krawędzi (lub "półki").
Problem:
Gracz spada w dół, a także ruchu w prawo, w kierunku ściany. Upadając, teleportuje się na wysokość półki. Zamiast tego gracz powinien normalnie upaść bez teleportacji.
Czy istnieje sposób na zmianę algorytmu, aby zapobiec temu zjawisku? Jeśli nie, czy możesz zaproponować alternatywny algorytm wykrywania kolizji? Najlepiej, aby jakakolwiek poprawka nie zakładała, że gracz zawsze upada, ponieważ w grze kierunek upadku gracza zmienia się pomiędzy górą/dołem/lewo/prawo.
Algorytm:
nowa pozycja zawodnika jest obliczana przy założeniu, że nie ma kolizji. (Niepokazane w poniższym kodzie)
Funkcja o nazwie
getBorderTiles
pobiera obiekt (odtwarzacz) i zwraca płytki dotykające każdego z czterech rogów gracza. Ponieważ gracz nie jest większy niż kafelek, te płytki graniczne są koniecznie jedynymi płytkami, które dotyka gracz. Zauważ, że niektóre z tych płytek mogą być takie same. Na przykład, jeśli gracz zajmuje tylko jedną kolumnę, lewa górna/prawa górna płyta będzie taka sama, podobnie jak płytki z lewym dolnym/prawym dolnym. Jeśli tak się stanie,getBorderTiles
nadal zwraca wszystkie cztery kafelki, ale niektóre będą takie same.Sprawdza te płytki granicy w mapie poziomu (tablica 2D), aby sprawdzić, czy są one stałe. Jeśli kafelek jest pełny, obiekt koliduje z tym kafelkiem.
Testuje góra/dół/kolizji lewy/prawy. Jeśli gracz przesuwa się w dół i koliduje z płytką w dół, ale nie koliduje z odpowiadającym mu kafelkiem, gracz koliduje w dół. Jeśli gracz porusza się w lewo i koliduje z lewą płytką, ale nie koliduje z odpowiednią płytką po prawej stronie, koliduje w lewo. Etc. Kontrole góra/dół są przeprowadzane przed sprawdzeniem lewy/prawy. Zmienne przechowujące płytki graniczne są korygowane, jeśli występuje kolizja góra/dół przed wykonaniem testu lewej/prawej. Na przykład, jeśli gracz zderzy się, zostanie przesunięty do górnych płytek, więc płytki BL/BR są teraz takie same jak płytki TL/TR.
X, y i prędkość gracza są dostosowywane w zależności od kierunku, w którym się on koliduje.
Dlaczego algorytm kończy się niepowodzeniem:
dolna płytka prawo jest solidna, ale prawy górny nie jest, więc (krok 4) gracz zderza się i (krok 5) jest wypchnięty. Koliduje również z płytą BR, ale nie BL, więc koliduje w prawo i jest popychany w lewo. Pod koniec gracz jest renderowany tuż nad i na lewo od półki. W efekcie jest teleportowany.
Próba rozwiązania: Próbowałem to naprawić, ale to tylko stworzyło inny problem. Dodałem czek, aby gracz zderzył się tylko z płytką, jeśli odległość ta jest nieco większa (powiedzmy 3 piksele). Jeśli gracz znajdował się tylko w płytce BR, algorytm nie zarejestruje kolizji w dół, więc gracz nie będzie się teleportował. Jednakże, jeśli gracz spadł na ziemię w innym scenariuszu, nie uznał kolizji, dopóki gracz nie znalazł się dość daleko w ziemi. Gracz skakał, gdy spadł trochę na ziemię, został zepchnięty z powrotem na ziemię, znowu upadł, itp.
Dzięki za przeczytanie tej pory. Naprawdę doceniam twoją opinię.
Aktualny kod algorytm:
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;
}
UPDATE: rozwiązać za pomocą roztworu marakasy. Algorytm znajduje się poniżej. Zasadniczo testuje (x następnie y) i rozwiązuje kolizje, a następnie testuje (y, a następnie x) i rozstrzyga kolizje w ten sposób. Niezależnie od wyniku testu gracz poruszający się na krótszym dystansie jest tym, który w końcu zostanie użyty.
Co ciekawe, wymaga specjalnego przypadku, gdy gracz koliduje zarówno w kierunku górnym, jak i lewym. Być może jest to związane z faktem, że współrzędne gracza (x, y) znajdują się w lewym górnym rogu. W takim przypadku należy użyć testu, który spowoduje przesunięcie gracza o DŁUŻSZĄ odległość. Jest oczywiste, w tym GIF:
gracza jest czarna skrzynka, a żółta skrzynka reprezentuje której gracz byłby gdyby użył innego testu (test, który doprowadził do odtwarzacza ruchomy dłużej dystans). Najlepiej byłoby, gdyby gracz nie wchodził w ścianę, a zamiast tego powinien znajdować się tam, gdzie znajduje się żółte pole. Dlatego w tym scenariuszu należy zastosować test długodystansowy.
Oto szybka i brudna implementacja. Nie jest to zoptymalizowane, ale mam nadzieję, że pokazuje kroki algorytmu całkiem wyraźnie.
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;
}
}
Należy zauważyć, że istnieją również w dół, przeniesiona dzieje się, gdy w dolnej połowie wysokości płytek. Problem będzie jeszcze bardziej poważny, jeśli nie będzie żetonu, na którym lądujesz w tym przykładzie, zostaniesz teleportowany w dół do pozycji w powietrzu z prędkością 0, a następnie przesunięty nieco w lewo, po tym jak grawitacja zajmie Znowu myślę. – maraca