Jak zachować synchronizację zegarów serwer-klient dla precyzyjnych gier sieciowych, takich jak Quake 3?

15

Pracuję nad strzelanką 2D z góry i staram się kopiować koncepcje stosowane w grach sieciowych, takich jak Quake 3.

  • Mam autorytatywny serwer.
  • Serwer wysyła migawki do klientów.
  • Migawki zawierają znacznik czasu i pozycje encji.
  • Elementy są interpolowane między pozycjami migawek, dzięki czemu ruch wygląda płynnie.
  • Z konieczności interpolacja bytu zachodzi nieco „w przeszłości”, dzięki czemu mamy wiele migawek, w których można interpolować.

Problem, przed którym stoję, to „synchronizacja zegara”.

  • Dla uproszczenia, po prostu udawajmy przez chwilę, że nie ma żadnych opóźnień podczas przesyłania pakietów do i z serwera.
  • Jeśli zegar serwera jest 60 sekund przed zegarem klienta, znacznik czasu migawki będzie 60000 ms przed lokalnym znacznikiem czasu klienta.
  • Dlatego migawki jednostek będą zbierać się i siedzieć przez około 60 sekund, zanim klient zobaczy, że dana istota wykonuje swoje ruchy, ponieważ tak długo trwa, zanim zegar klienta dogoni.

Udało mi się temu zaradzić, obliczając różnicę między zegarem serwera i klienta za każdym razem, gdy otrzymywana jest migawka.

// For simplicity, don't worry about latency for now...
client_server_clock_delta = snapshot.server_timestamp - client_timestamp;

Określając, jak daleko od interpolacji jest jednostka, po prostu dodaję różnicę do bieżącego czasu klienta. Problem polega jednak na tym, że spowoduje to szarpnięcie, ponieważ różnica między dwoma zegarami gwałtownie się zmienia z powodu migawek przybywających szybciej / wolniej niż inne.

Jak mogę zsynchronizować zegary wystarczająco dokładnie, aby jedynym zauważalnym opóźnieniem było to, które jest zakodowane na sztywno do interpolacji i które jest spowodowane zwykłym opóźnieniem sieci?

Innymi słowy, w jaki sposób mogę zapobiec zbyt późnemu lub zbyt wczesnemu uruchomieniu interpolacji, gdy zegary ulegną znacznej desynchronizacji, bez powodowania szarpnięć?

Edycja: Według Wikipedii NTP można wykorzystać do synchronizacji zegarów przez Internet w ciągu kilku milisekund. Jednak protokół wydaje się skomplikowany, a może przesada w użyciu w grach?

Joncom
źródło
jak to jest skomplikowane ? To żądanie i odpowiedź, każda ze znacznikami czasu transmisji i przybycia, a następnie trochę matematyki, aby uzyskać różnicę
maniak zapadkowy
@ratchetfreak: Według ( mine-control.com/zack/timesync/timesync.html ) „Niestety, NTP jest bardzo skomplikowane i, co ważniejsze, powolne zbieganie się w dokładnym czasie. To sprawia, że ​​NTP nie jest idealny dla sieci gra, w której gracz spodziewa się, że gra rozpocznie się natychmiast ... ”
Joncom

Odpowiedzi:

10

Po przeszukaniu wydaje się, że synchronizacja zegarów 2 lub więcej komputerów nie jest trywialnym zadaniem. Protokół taki jak NTP wykonuje dobrą robotę, ale podobno jest powolny i zbyt skomplikowany, aby był praktyczny w grach. Ponadto używa UDP, który nie działa dla mnie, ponieważ pracuję z gniazdami internetowymi, które nie obsługują UDP.

Znalazłem tu jednak metodę , która wydaje się stosunkowo prosta:

Twierdzi, że synchronizuje zegary w odstępach 150 ms (lub lepszych).

Nie wiem, czy to wystarczy do moich celów, ale nie byłem w stanie znaleźć bardziej precyzyjnej alternatywy.

Oto algorytm, który zapewnia:

W przypadku gier wymagana jest prosta technika synchronizacji zegara. Idealnie powinien mieć następujące właściwości: dość dokładny (150 ms lub lepszy), szybki w konwergencji, prosty w implementacji, zdolny do działania na protokołach opartych na strumieniu, takich jak TCP.

Prosty algorytm z tymi właściwościami jest następujący:

  1. Klient stempluje bieżący czas lokalny na pakiecie „żądanie czasu” i wysyła do serwera
  2. Po otrzymaniu przez serwer serwer stempluje czas serwera i zwraca
  3. Po otrzymaniu przez klienta klient odejmuje bieżący czas od czasu wysłania i dzieli przez dwa, aby obliczyć opóźnienie. Odejmuje bieżący czas od czasu serwera, aby określić różnicę czasu między klientem a serwerem, i dodaje opóźnienie połowiczne, aby uzyskać prawidłową różnicę zegara. (Jak dotąd ten algothim jest bardzo podobny do SNTP)
  4. Pierwszego wyniku należy natychmiast użyć do aktualizacji zegara, ponieważ spowoduje to, że lokalny zegar znajdzie się w odpowiednim polu gry (przynajmniej we właściwej strefie czasowej!)
  5. Klient powtarza kroki od 1 do 3 pięć lub więcej razy, za każdym razem wstrzymując kilka sekund. Tymczasowo dozwolony może być inny ruch, ale należy go zminimalizować, aby uzyskać najlepsze wyniki
  6. Wyniki potwierdzeń pakietów są gromadzone i sortowane w kolejności od najniższego opóźnienia do największego opóźnienia. Mediana opóźnienia jest określana przez wybranie próbki punktu środkowego z tej uporządkowanej listy.
  7. Wszystkie próbki powyżej około 1 odchylenia standardowego od mediany są odrzucane, a pozostałe próbki są uśredniane przy użyciu średniej arytmetycznej.

Jedyną subtelnością tego algorytmu jest to, że pakiety powyżej jednego standardowego odchylenia powyżej mediany są odrzucane. Ma to na celu wyeliminowanie pakietów retransmitowanych przez TCP. Aby to zobrazować, wyobraź sobie, że próbka pięciu pakietów została wysłana przez TCP i nie było żadnej retransmisji. W takim przypadku histogram opóźnienia będzie miał pojedynczy tryb (klaster) wyśrodkowany wokół mediany opóźnienia. Teraz wyobraź sobie, że w innej próbie jeden pakiet pięciu jest retransmitowany. Retransmisja spowoduje, że jedna próbka spadnie daleko w prawo na histogramie opóźnienia, średnio dwa razy dalej niż mediana trybu podstawowego. Po prostu wycinając wszystkie próbki, które wypadają o więcej niż jedno odchylenie standardowe od mediany, te tryby błądzące można łatwo wyeliminować, zakładając, że nie stanowią one większości statystyk.

To rozwiązanie wydaje się dobrze odpowiadać na moje pytanie, ponieważ synchronizuje zegar, a następnie zatrzymuje się, umożliwiając liniowy przepływ czasu. Podczas gdy moja początkowa metoda ciągle aktualizowała zegar, powodując, że czas przeskakiwał nieco w miarę odbierania migawek.

Joncom
źródło
Jak to się wtedy sprawdzało? Jestem teraz w tej samej sytuacji. Korzystam ze struktury serwera obsługującej tylko protokół TCP, dlatego nie mogę używać protokołu NTP, który wysyła datagramy UDP. Z trudem znajduję algorytmy synchronizacji czasu, które twierdzą, że wykonują niezawodną synchronizację czasu przez TCP. Synchronizacja w ciągu sekundy wystarczyłaby jednak na moje potrzeby.
dynamokaj
@dynamokaj Działa dość dobrze.
Joncom
Chłodny. Czy to możliwe, że możesz udostępnić wdrożenie?
dynamokaj
@dynamokaj Wydaje się, że nie mogę znaleźć takiej implementacji w żadnym projekcie, o którym mogę teraz myśleć. Alternatywnie to, co działa dla mnie wystarczająco dobrze, to: 1) natychmiast użyj opóźnienia obliczonego na podstawie jednego żądania / odpowiedzi ping, a następnie 2) dla wszystkich przyszłych takich odpowiedzi w kierunku nowej wartości stopniowo, a nie natychmiast. Ma to efekt „uśredniania”, który był bardzo dokładny dla moich celów.
Joncom
Nie ma problemu. Korzystam z usługi zaplecza w Google App Engine, a więc w infrastrukturze Googles, gdzie serwery są synchronizowane za pomocą serwera Google NTP: time.google.com ( developers.google.com/time ) Dlatego używam następującego klienta NTP dla mojego klienta Xamarin Mobile aby uzyskać przesunięcie między klientem a serwerem. components.xamarin.com/view/rebex-time - Dziękujemy za poświęcenie czasu na udzielenie odpowiedzi.
dynamokaj
1

Zasadniczo nie możesz naprawić [całego] świata i ostatecznie będziesz musiał narysować linię.

Jeśli serwer i wszyscy klienci współużytkują tę samą liczbę klatek na sekundę, muszą tylko zsynchronizować się po połączeniu, a czasami później, szczególnie po zdarzeniu opóźnienia. Opóźnienie nie wpływa na upływ czasu ani na zdolność komputera do jego pomiaru, dlatego w wielu przypadkach zamiast interpolować, należy dokonać ekstrapolacji. Stwarza to równie niepożądane efekty, ale znowu jest to, co to jest i musisz wybrać najmniej dostępne zło.

Weź pod uwagę, że w wielu popularnych grach MMO opóźnieni gracze są wizualnie oczywisti. Jeśli widzisz, jak biegną w miejscu, bezpośrednio do ściany, twój klient dokonuje ekstrapolacji. Gdy klient otrzymuje nowe dane, gracz (na swoim kliencie) mógł się przesunąć na znaczną odległość i będzie „gumką” lub teleportować się w nowe miejsce („szarpnięcie”, o którym wspominałeś?). Dzieje się tak nawet w dużych markowych grach.

Technicznie jest to problem z infrastrukturą sieciową gracza, a nie z grą. Punktem, w którym przechodzi od jednego do drugiego, jest właśnie linia, którą musisz narysować. Twój kod na 3 osobnych komputerach powinien rejestrować mniej więcej tyle samo czasu, który upłynął. Jeśli nie otrzymasz aktualizacji, nie powinno to wpłynąć na szybkość odświeżania Update (); jeśli już, to powinno być szybsze, ponieważ prawdopodobnie jest mniej do zaktualizowania.

„Jeśli masz kiepski internet, nie możesz grać w tę konkurencję”.
To nie jest przewrotność ani błąd.

Jon
źródło