2017-05-26 9 views
5

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:

gif of ledge issue

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:

  1. nowa pozycja zawodnika jest obliczana przy założeniu, że nie ma kolizji. (Niepokazane w poniższym kodzie)

  2. 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.

  3. 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.

  4. 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.

  5. 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:

See this image.

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:

gif showing why special case is needed

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; 
 
    } 
 
}

+0

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

Odpowiedz

1

Zdarza się, ponieważ najpierw wykryć kolizje w obu kierunkach, a potem dostosować pozycję. "góra/dół" jest aktualizowana jako pierwsza (kierunek grawitacji). Regulacja "lewo/prawo" najpierw tylko pogorszy problem (po każdej jesieni możesz zostać teleportowany w prawo lub lewo).

Tylko szybkie i brudne fix mogłem wymyślić (grawitacja-niezmienna):

  1. Oblicz zderzenie dwóch istotnych punktów w jednym kierunku (np gdy dzieje w lewo tylko lewe dwa punkty sprawa). Następnie dostosuj prędkość i pozycję w tym kierunku.

  2. Obliczyć kolizję dwóch (skorygowanych) istotnych punktów w innym kierunku. Dostosuj pozycję i prędkość tego directin podczas kolizji.

  3. Jeśli podczas pierwszego etapu nie doszło do kolizji, można kontynuować zmiany i powrócić. W przeciwnym razie obliczyć odległość dx + dy w porównaniu do pierwotnej pozycji przed krokiem 1.

  4. Powtórz kroki 1. do 3., ale tym razem zacznij od pierwszego kierunku w drugą stronę.

  5. Dokonaj zmiany z mniejszą odległością (chyba, że ​​już znalazłeś dobrą zmianę w kroku 3).

Edycja: Przykład

sizes: sTile = 50, sPlayer = 20 
old position (fine, top-left corner): oX = 27, oY = 35 
speeds: vX = 7, vY = 10 
new position: x = oX + vX = 34, y = oY + vY = 45 => (34, 45) 
solid: tile at (50, 50) 

1.1. Checking x-direction, relevant points for positive vX are the ones to the right: 
    (54, 45) and (54, 65). The latter gives a conflict and we need to correct the 
    position to p1 = (30, 45) and speed v1 = (0, 10). 

2.1. Checking y-direction based on previous position, relevant points: (30, 65) and 
    (50, 65). There is no conflict, p1 and v1 remain unchanged. 

3.1. There was a conflict in step 1.1. so we cannot return the current result 
    immediately and have to calculate the distance d1 = 4 + 0 = 4. 

1.2. Checking y-direction first this time, relevant points: (34, 65) and (54, 65). 
    Because the latter gives a conflict we calculate p2 = (34, 30) and v2 = (7, 0). 

2.2. Checking x-direction based on step 1.2., relevant points: (54, 30) and (54, 50). 
    There is no conflict, p2 and v2 remain unchanged. 

3.2. Because there was a conflict in step 1.2. we calculate the distance d2 = 15. 

5. Change position and speed to p1 and v1 because d1 is smaller than d2. 
+0

Cześć, dziękuję za odpowiedź! Niestety to samo zachowanie wciąż występuje, gdy podążam za twoim rozwiązaniem. Być może źle to zrozumiałem? Dodałem moją implementację w pierwszym poście. Jeśli nie, mogę przełączyć się na inny algorytm. – myohmywhoami

+0

Tak, wydaje się, że przegapiłeś to. Oddzieliłeś tylko czek, ale nie postępowałeś zgodnie z algorytmem opisanym powyżej. Zasadniczo musisz dwukrotnie skopiować oryginalną pozycję, a następnie najpierw skorygować najpierw kierunek x, a drugi kierunek y (i dostosować!). Na podstawie tych dwóch nowych pozycji wykonujesz sprawdzanie drugiego kierunku dla obu pozycji (i dostosowania). Teraz obliczyć odległość do oryginału dla obu możliwości i wybrać lepszą (w przykładowym przypadku jeden raz gracz zostanie przesunięty nieco w lewo, a drugi raz zostanie przesunięty w górę na większą odległość, więc powinno działać) – maraca

+0

@myohmywhoami dodany przykład. – maraca

Powiązane problemy