Pracuję nad izometryczną grą 2D z umiarkowaną skalą dla wielu graczy, około 20-30 graczy jednocześnie połączonych z trwałym serwerem. Miałem pewne trudności z wdrożeniem dobrej implementacji przewidywania ruchu.
Fizyka / ruch
Gra nie ma prawdziwej implementacji fizyki, ale wykorzystuje podstawowe zasady do implementacji ruchu. Zamiast ciągłego sprawdzania danych wejściowych zmiany stanu (tj. / Zdarzenia myszy w dół / w górę / ruch) są używane do zmiany stanu jednostki postaci, którą kontroluje gracz. Kierunek gracza (tj. / Północny-wschód) jest łączony ze stałą prędkością i zamieniany w prawdziwy wektor 3D - prędkość istoty.
W głównej pętli gry przed aktualizacją nazywa się „Aktualizacja”. Logika aktualizacji uruchamia „zadanie aktualizacji fizyki”, która śledzi wszystkie byty z niezerową prędkością, wykorzystuje bardzo podstawową integrację do zmiany pozycji bytów. Na przykład: entity.Position + = entity.Velocity.Scale (ElapsedTime.Seconds) (gdzie „Seconds” jest wartością zmiennoprzecinkową, ale to samo podejście działałoby w przypadku wartości całkowitych milisekundowych).
Kluczową kwestią jest to, że do ruchu nie stosuje się interpolacji - podstawowy silnik fizyki nie ma pojęcia „poprzedniego stanu” lub „bieżącego stanu”, a jedynie pozycję i prędkość.
Pakiety zmiany stanu i aktualizacji
Gdy prędkość jednostki postaci kontrolowanej przez gracza zmienia się, do serwera wysyłany jest pakiet „awatar ruchu” zawierający typ akcji istoty (stan, chód, bieg), kierunek (północno-wschodni) i aktualną pozycję. Różni się to od działania gier 3D w pierwszej osobie. W grze 3D prędkość (kierunek) może zmieniać się klatka po klatce, gdy gracz się porusza. Wysłanie każdej zmiany stanu skutecznie przesłałoby pakiet na ramkę, co byłoby zbyt drogie. Zamiast tego gry 3D wydają się ignorować zmiany stanu i wysyłają pakiety „aktualizacji stanu” w ustalonych odstępach czasu - powiedzmy, co 80-150 ms.
Ponieważ aktualizacje prędkości i kierunku występują znacznie rzadziej w mojej grze, mogę uniknąć wysyłania każdej zmiany stanu. Chociaż wszystkie symulacje fizyki odbywają się z tą samą prędkością i są deterministyczne, opóźnienie jest nadal problemem. Z tego powodu wysyłam rutynowe pakiety aktualizacji pozycji (podobne do gry 3D), ale znacznie rzadziej - teraz co 250 ms, ale podejrzewam, że z dobrą prognozą mogę z łatwością zwiększyć je do 500 ms. Największy problem polega na tym, że odeszłam od normy - cała inna dokumentacja, przewodniki i próbki online wysyłają rutynowe aktualizacje i interpolują oba te stany. Wydaje się to niezgodne z moją architekturą i muszę wymyślić lepszy algorytm przewidywania ruchu, który jest bliższy (bardzo podstawowej) architekturze „fizyki sieciowej”.
Serwer następnie odbiera pakiet i określa prędkość gracza na podstawie jego typu ruchu na podstawie skryptu (Czy gracz jest w stanie uruchomić? Uzyskaj prędkość biegania gracza). Gdy osiągnie prędkość, łączy ją z kierunkiem uzyskania wektora - prędkości bytu. Występuje pewne wykrywanie oszustw i podstawowe sprawdzanie poprawności, a jednostka po stronie serwera jest aktualizowana o bieżącą prędkość, kierunek i pozycję. Podstawowe dławienie jest również wykonywane, aby uniemożliwić graczom zalanie serwera żądaniami ruchu.
Po zaktualizowaniu własnego bytu serwer rozgłasza pakiet „aktualizacja pozycji awatara” do wszystkich innych graczy w zasięgu. Pakiet aktualizacji pozycji służy do aktualizacji symulacji fizyki po stronie klienta (stan świata) zdalnych klientów oraz do wykonywania prognoz i kompensacji opóźnień.
Prognozowanie i kompensacja opóźnień
Jak wspomniano powyżej, klienci są wiarygodni dla swojej pozycji. Z wyjątkiem przypadków oszukiwania lub anomalii, awatar klienta nigdy nie zostanie zmieniony przez serwer. Awatar klienta nie wymaga ekstrapolacji („przenieś teraz i popraw później”) - to, co gracz widzi, jest poprawne. Jednak dla wszystkich poruszających się jednostek zdalnych wymagana jest pewna ekstrapolacja lub interpolacja. Pewne przewidywanie i / lub kompensacja opóźnień jest wyraźnie wymagana w lokalnym silniku symulacji / fizyki klienta.
Problemy
Walczę z różnymi algorytmami i mam wiele pytań i problemów:
Czy powinienem ekstrapolować, interpolować, czy jedno i drugie? Moje „przeczucie” polega na tym, że powinienem stosować czystą ekstrapolację opartą na prędkości. Klient otrzymuje zmianę stanu, klient oblicza „przewidywaną” prędkość, która kompensuje opóźnienie, a normalny system fizyki zajmuje się resztą. Czuje się jednak w sprzeczności z innymi przykładowymi kodami i artykułami - wszystkie wydają się przechowywać szereg stanów i przeprowadzać interpolację bez silnika fizyki.
Kiedy nadchodzi pakiet, próbowałem interpolować pozycję pakietu z prędkością pakietu w określonym czasie (powiedzmy 200 ms). Następnie biorę różnicę między pozycją interpolowaną a bieżącą pozycją „błędu”, aby obliczyć nowy wektor i umieścić go na bycie zamiast prędkości, która została wysłana. Zakłada się jednak, że w tym przedziale czasowym nadejdzie kolejny pakiet i niezwykle trudno jest „zgadnąć”, kiedy nadejdzie następny pakiet - zwłaszcza, że nie wszystkie one docierają w ustalonych odstępach czasu (tj. Również zmiany stanu). Czy koncepcja jest zasadniczo wadliwa, czy jest poprawna, ale wymaga pewnych poprawek / korekt?
Co się stanie, gdy zdalny odtwarzacz się zatrzyma? Mogę natychmiast zatrzymać byt, ale będzie on ustawiony w „złym” miejscu, dopóki nie ruszy się ponownie. Jeśli oszacuję wektor lub spróbuję interpolować, mam problem, ponieważ nie zapisuję poprzedniego stanu - silnik fizyki nie może powiedzieć „musisz zatrzymać się po osiągnięciu pozycji X”. Po prostu rozumie prędkość, nic bardziej złożonego. Nie chcę dodawać informacji o stanie ruchu pakietu do encji lub silnika fizyki, ponieważ narusza to podstawowe zasady projektowania i upuszcza kod sieciowy w pozostałej części silnika gry.
Co powinno się stać, gdy byty zderzą się? Istnieją trzy scenariusze - kontrolujący gracz zderza się lokalnie, dwa byty zderzają się na serwerze podczas aktualizacji pozycji lub zdalna aktualizacja bytu zderza się z lokalnym klientem. We wszystkich przypadkach nie jestem pewien, jak poradzić sobie z kolizją - oprócz oszukiwania oba stany są „poprawne”, ale w różnych okresach. W przypadku jednostki zdalnej nie ma sensu rysować jej przechodząc przez ścianę, dlatego wykonuję wykrywanie kolizji na lokalnym kliencie i powoduję, że „zatrzymuje się”. W oparciu o punkt 2 powyżej mogę obliczyć „poprawiony wektor”, który nieustannie próbuje przenieść byt „przez ścianę”, co nigdy się nie powiedzie - zdalny awatar utknie tam, dopóki błąd nie stanie się zbyt wysoki i „zaskoczy” pozycja. Jak działają gry?