Zadałem kilka podobnych pytań w ciągu ostatnich 8 miesięcy bez żadnej prawdziwej radości, więc sprawię, że pytanie będzie bardziej ogólne.
Mam grę na Androida, która jest OpenGL ES 2.0. w nim mam następującą pętlę gry:
Moja pętla działa na zasadzie stałego kroku czasowego (dt = 1 / ticksPerSecond )
loops=0;
while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){
updateLogic(dt);
nextGameTick+=skipTicks;
timeCorrection += (1000d/ticksPerSecond) % 1;
nextGameTick+=timeCorrection;
timeCorrection %=1;
loops++;
}
render();
Moja integracja działa w następujący sposób:
sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;
Teraz wszystko działa tak jak chciałbym. Mogę sprecyzować, że chciałbym, aby obiekt poruszał się na pewną odległość (powiedzmy na szerokości ekranu) w 2,5 sekundy, i zrobi to właśnie. Również z powodu przeskakiwania klatek, na które zezwalam w mojej pętli gry, mogę to zrobić na prawie każdym urządzeniu i zawsze zajmie to 2,5 sekundy.
Problem
Problem polega jednak na tym, że gdy ramka renderowania przeskakuje, grafika zacina się. To bardzo denerwujące. Jeśli usunę możliwość pomijania ramek, wszystko będzie gładkie, jak chcesz, ale będzie działać z różnymi prędkościami na różnych urządzeniach. Więc to nie jest opcja.
Nadal nie jestem pewien, dlaczego ramka przeskakuje, ale chciałbym zauważyć, że nie ma to nic wspólnego ze słabą wydajnością , wziąłem kod z powrotem do 1 małego duszka i nie ma logiki (oprócz logiki wymaganej do przenieś duszka), a ja wciąż pomijam klatki. I to jest na tablecie Google Nexus 10 (i jak wspomniano powyżej, muszę przeskakiwać ramki, aby utrzymać stałą prędkość na różnych urządzeniach).
Tak więc jedyną inną opcją, którą mam, jest użycie interpolacji (lub ekstrapolacji). Przeczytałem każdy artykuł, który tam jest, ale żadna naprawdę nie pomogła mi zrozumieć, jak to działa, i wszystkie moje próby implementacji zakończyły się niepowodzeniem.
Za pomocą jednej metody mogłem sprawnie poruszać się, ale było to niewykonalne, ponieważ popsuło mi to kolizję. Mogę przewidzieć ten sam problem z dowolną podobną metodą, ponieważ interpolacja jest przekazywana do (i działa w ramach) metody renderowania - w czasie renderowania. Więc jeśli pozycja kolizji koryguje (znak teraz stoi tuż obok ściany), a następnie renderujący może zmieniać swoją pozycję i wyciągnąć go na ścianie.
Więc jestem naprawdę zdezorientowany. Ludzie mówili, że nigdy nie należy zmieniać pozycji obiektu z poziomu metody renderowania, ale wszystkie przykłady online to pokazują.
Proszę więc o pchnięcie we właściwym kierunku, proszę nie zamieszczać linków do popularnych artykułów na temat pętli gry (deWitters, Fix timepep itp.), Ponieważ czytałem je wiele razy . Ja nie pytając nikogo, aby napisać kod dla mnie. Po prostu wyjaśnij proszę w prosty sposób, w jaki sposób interpolacja faktycznie działa na niektórych przykładach. Potem pójdę i spróbuję zintegrować jakieś pomysły z moim kodem i w razie potrzeby zadam bardziej szczegółowe pytania. (Jestem pewien, że jest to problem, z którym zmaga się wiele osób).
edytować
Niektóre dodatkowe informacje - zmienne używane w pętli gry.
private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;
źródło
Odpowiedzi:
Są dwie rzeczy niezbędne do uzyskania płynnego ruchu, po pierwsze, oczywiste jest, że to, co renderujesz, musi odpowiadać oczekiwanemu stanowi w momencie, w którym ramka jest prezentowana użytkownikowi, po drugie, musisz przedstawić klatki użytkownikowi w stosunkowo ustalonym odstępie czasu. Prezentowanie klatki w T + 10ms, potem kolejna w T + 30ms, a następnie w T + 40ms, wydaje się użytkownikowi, że ocenia, nawet jeśli to, co faktycznie pokazano dla tych czasów, jest zgodne z symulacją.
Wydaje się, że w głównej pętli brakuje mechanizmu bramkowania, aby zapewnić, że renderujesz tylko w regularnych odstępach czasu. Czasami możesz zrobić 3 aktualizacje między renderami, czasem możesz zrobić 4. Zasadniczo twoja pętla będzie renderować tak często, jak to możliwe, gdy tylko zasymulujesz wystarczająco dużo czasu, aby przesunąć stan symulacji przed bieżącym czasem, będziesz następnie render ten stan. Ale każda zmienność czasu potrzebnego do aktualizacji lub renderowania oraz odstępy między ramkami również będą się różnić. Masz ustalony czas symulacji, ale zmienny czas renderowania.
Prawdopodobnie potrzebujesz poczekać tuż przed renderowaniem, co gwarantuje, że rendering zaczniesz tylko na początku interwału renderowania. Idealnie byłoby, gdyby był adaptacyjny: jeśli zbyt długo trwało aktualizowanie / renderowanie, a początek interwału już minął, należy renderować natychmiast, ale także zwiększać długość interwału, dopóki nie będzie można konsekwentnie renderować i aktualizować, a następnie przejść do następne renderowanie przed zakończeniem interwału. Jeśli masz dużo czasu, możesz powoli zmniejszyć interwał (tj. Zwiększyć liczbę klatek na sekundę), aby ponownie renderować szybciej.
Ale tutaj jest kicker, jeśli nie wyrenderujesz ramki natychmiast po wykryciu, że stan symulacji został zaktualizowany do „teraz”, to wprowadzisz tymczasowe aliasing. Ramka prezentowana użytkownikowi jest prezentowana w nieco niewłaściwym czasie, a to samo w sobie będzie się jąkać.
Jest to powód „częściowego pomiaru czasu”, o którym wspominasz w artykułach, które przeczytałeś. Jest tam z dobrego powodu, a to dlatego, że dopóki nie ustawisz timepsu fizyki na jakąś stałą całkowitą wielokrotność twojego timeptu renderowania, po prostu nie możesz przedstawić ramek we właściwym czasie. W końcu albo prezentujesz je za wcześnie, albo za późno. Jedynym sposobem na uzyskanie stałej szybkości renderowania i nadal prezentowanie czegoś, co jest fizycznie poprawne, jest zaakceptowanie tego, że w momencie, gdy nadejdzie interwał renderowania, najprawdopodobniej będziesz w połowie drogi między dwoma ustalonymi czasami fizyki. Ale to nie znaczy, że obiekty są modyfikowane podczas renderowania, wystarczy, że rendering musi tymczasowo ustalić, gdzie znajdują się obiekty, aby mógł je renderować gdzieś pomiędzy tym, gdzie były przed i gdzie są po aktualizacji. To ważne - nigdy nie zmieniaj stanu świata do renderowania, tylko aktualizacje powinny zmieniać stan świata.
Aby umieścić go w pętli pseudokodu, myślę, że potrzebujesz czegoś więcej:
Aby to zadziałało, wszystkie aktualizowane obiekty muszą zachować wiedzę o tym, gdzie były wcześniej i gdzie są teraz, aby rendering mógł wykorzystać swoją wiedzę o tym, gdzie jest obiekt.
Nakreślmy oś czasu w milisekundach, mówiąc, że renderowanie zajmuje 3 ms, aktualizacja zajmuje 1 ms, krok aktualizacji jest ustalony na 5 ms, a czas renderowania rozpoczyna się (i pozostaje) na 16 ms [60 Hz].
Jest jeszcze jeden niuans dotyczący zbytniej symulacji z wyprzedzeniem, co oznacza, że dane wejściowe użytkownika mogą zostać zignorowane, nawet jeśli zdarzyły się przed faktycznym renderowaniem ramki, ale nie przejmuj się tym, dopóki nie upewnisz się, że pętla symuluje płynnie.
źródło
To, co wszyscy ci mówią, jest poprawne. Nigdy nie aktualizuj pozycji symulacji duszka w logice renderowania.
Pomyśl o tym w ten sposób, twój duszek ma 2 pozycje; gdzie symulacja mówi, że jest od ostatniej aktualizacji symulacji, i gdzie jest renderowany duszek. Są to dwie zupełnie różne współrzędne.
Duszek jest renderowany w jego ekstrapolowanej pozycji. Pozycja ekstrapolowana jest obliczana dla każdej ramki renderowania, używana do renderowania duszka, a następnie wyrzucana. To wszystko.
Poza tym wydaje się, że dobrze rozumiesz. Mam nadzieję że to pomoże.
źródło