Zajmuję się tworzeniem gry strategicznej w czasie rzeczywistym na kursie informatyki. Jednym z trudniejszych aspektów wydaje się być praca w sieci klient-serwer i synchronizacja. Przeczytałem na ten temat (w tym 1500 łuczników ), ale zdecydowałem się na podejście klient-serwer w przeciwieństwie do innych modeli (na przykład przez sieć LAN).
Ta gra strategiczna w czasie rzeczywistym ma pewne problemy. Na szczęście każde działanie gracza jest deterministyczne. Istnieją jednak zdarzenia, które mają miejsce w zaplanowanych odstępach czasu. Na przykład, gra składa się z płytek, a kiedy gracz bierze płytkę, „poziom energii”, wartość na tej płytce, powinna rosnąć o jedną sekundę po jej pobraniu. To bardzo szybkie wyjaśnienie, które powinno uzasadnić mój przypadek użycia.
W tej chwili robię cienkich klientów, którzy po prostu wysyłają pakiety do serwera i czekają na odpowiedź. Istnieje jednak kilka problemów.
Kiedy gry między graczami rozwijają się w grę końcową, często dochodzi do ponad 50 zdarzeń na sekundę (ze względu na zaplanowane zdarzenia, wyjaśnione wcześniej, gromadzenie się), a wtedy pojawiają się błędy synchronizacji. Moim największym problemem jest to, że nawet niewielkie odchylenie stanu między klientami może oznaczać różne decyzje, które podejmują klienci, co prowadzi do zupełnie oddzielnych gier. Innym problemem (który obecnie nie jest tak ważny) jest opóźnienie i trzeba poczekać kilka milisekund, nawet kilka sekund po wykonaniu ruchu, aby zobaczyć wynik.
Zastanawiam się, jakich strategii i algorytmów mógłbym użyć, aby uczynić to łatwiejszym, szybszym i przyjemniejszym dla użytkownika końcowego. Jest to szczególnie interesujące, biorąc pod uwagę dużą liczbę wydarzeń na sekundę, wraz z kilkoma graczami na grę.
TL; DR tworząc RTS z> 50 zdarzeniami na sekundę, jak synchronizować klientów?
Odpowiedzi:
Twój cel synchronizacji 50 zdarzeń na sekundę w czasie rzeczywistym brzmi dla mnie, jakby to nie było realistyczne. Właśnie dlatego mówiono o podejściu krok po kroku, o którym mowa w artykule o 1500 łuczników !
W jednym zdaniu: Jedynym sposobem na zsynchronizowanie zbyt wielu elementów w zbyt krótkim czasie w zbyt wolnej sieci jest NIE synchronizowanie zbyt wielu elementów w zbyt krótkim czasie w zbyt wolnej sieci, ale postęp deterministycznie na wszystkich klientach i synchronizacja tylko nagie potrzeby (dane wprowadzone przez użytkownika).
źródło
Myślę, że jest twój problem; Twoja gra powinna mieć tylko jedną oś czasu (dla rzeczy mających wpływ na rozgrywkę). Mówisz, że pewne rzeczy rosną w tempie X na sekundę ; dowiedz się, ile kroków gry zajmuje sekunda, i przelicz to na X na Y kroków gry . Wtedy nawet jeśli gra może zwolnić, wszystko pozostaje deterministyczne.
Niezależne uruchamianie gry w czasie rzeczywistym ma inne zalety:
Wspomniałeś również, że napotykasz problemy, gdy występuje> 50 zdarzeń lub występują opóźnienia do kilku sekund. Skala ta jest znacznie mniejsza niż scenariusz opisany przez 1500 łuczników , więc sprawdź, czy możesz profilować swoją grę i dowiedzieć się, gdzie jest spowolnienie.
źródło
Po pierwsze, aby rozwiązać problem z zaplanowanymi zdarzeniami, nie nadawaj wydarzeń, kiedy się zdarzają , ale kiedy są początkowo zaplanowane. Oznacza to, że zamiast wysyłać co sekundę komunikat „zwiększ energię kafelka ( x , y )”, po prostu wyślij pojedynczy komunikat z informacją: „zwiększ energię kafelka ( x , y ) raz na sekundę, aż będzie pełny lub do przerwane ". Każdy klient jest następnie odpowiedzialny za lokalne planowanie aktualizacji.
W rzeczywistości możesz posunąć tę zasadę dalej i przekazywać tylko działania gracza : wszystko inne może być obliczone lokalnie przez każdego klienta (i serwer, jeśli to konieczne).
(Oczywiście, prawdopodobnie powinieneś od czasu do czasu przesyłać sumy kontrolne stanu gry, aby wykryć przypadkową desynchronizację i mieć jakiś mechanizm ponownej synchronizacji klientów, jeśli tak się stanie, np. Poprzez ponowne przesłanie wszystkich danych gry z wiarygodnej kopii serwera do klientów Ale, mam nadzieję, powinno to być rzadkie wydarzenie, spotykane tylko podczas testowania lub podczas rzadkich awarii.)
Po drugie, aby synchronizować klientów, upewnij się, że gra jest deterministyczna. Inne odpowiedzi już dawały dobrą radę, ale pozwólcie, że przedstawię krótkie streszczenie tego, co należy zrobić:
Spraw, by twoja gra była wewnętrznie oparta na turach, z każdą turą lub „tyknięciem” zajmującym, powiedzmy, 1/50 sekundy. (W rzeczywistości prawdopodobnie mógłbyś uciec z 1/10 sekundową turą lub dłużej.) Wszelkie działania gracza występujące podczas jednej tury powinny być traktowane jako jednoczesne. Wszystkie wiadomości, przynajmniej z serwera do klientów, powinny być oznaczone numerem skrętu, aby każdy klient wiedział, która runda ma miejsce przy każdym zdarzeniu.
Ponieważ twoja gra korzysta z architektury klient-serwer, możesz sprawić, by serwer działał jako ostateczny arbiter tego, co dzieje się podczas każdej tury, co upraszcza niektóre rzeczy. Należy pamiętać jednak, że oznacza to, że klienci muszą także potwierdzić swoje własne działania z serwera: jeśli klient wysyła komunikat „przenieść jednostkę X jedna płytka na lewo”, a odpowiedź serwera nie mówi nic o jednostkowej X ruchu, klient należy założyć, że tak się nie stało, i być może anulować animację ruchu predykcyjnego, którą mogli już rozpocząć.
Zdefiniuj spójną kolejność „równoczesnych” zdarzeń występujących w tej samej turze, aby każdy klient wykonał je w tej samej kolejności. Ta kolejność może być dowolna, o ile jest deterministyczna i taka sama dla wszystkich klientów (i serwera).
Na przykład, możesz najpierw zwiększyć wszystkie zasoby (co można zrobić wszystko naraz, jeśli wzrost zasobów w jednym kafelku nie może kolidować z tym w innym), następnie przesuń jednostki każdego gracza w ustalonej kolejności, a następnie przesuń jednostki NPC. Aby być uczciwym wobec graczy, możesz zmieniać kolejność ruchów jednostek między turami, aby każdy z graczy mógł iść pierwszy tak samo; jest to w porządku, o ile odbywa się to deterministycznie (np. na podstawie numeru zakrętu).
Jeśli używasz matematyki zmiennoprzecinkowej, upewnij się, że używasz jej w trybie ścisłego IEEE. Może to nieco spowolnić, ale to niewielka cena za spójność między klientami. Upewnij się również, że nie dojdzie do przypadkowego zaokrąglenia podczas komunikacji (np. Klient przesyła zaokrągloną wartość do serwera, ale nadal wewnętrznie używa nieuzasadnionej wartości). Jak wspomniano powyżej, na wszelki wypadek dobrym rozwiązaniem jest posiadanie protokołu wykrywania i odzyskiwania po desynchronizacji.
źródło
Powinieneś uczynić logikę gier całkowicie niezależną od czasu rzeczywistego i zasadniczo uczynić ją turową. W ten sposób dokładnie wiesz, w którym momencie następuje „zmiana energii płytek”. W twoim przypadku każda tura to tylko 1/50 sekundy.
W ten sposób musisz martwić się tylko danymi wejściowymi graczy, wszystko inne jest zarządzane przez logikę gier i całkowicie identyczne na wszystkich klientach. Nawet jeśli gra utknie na chwilę z powodu opóźnienia sieci lub skomplikowanych obliczeń, wydarzenia będą się odbywać w synchronizacji dla wszystkich.
źródło
Przede wszystkim musisz zrozumieć, że zmiennoprzecinkowa / podwójna matematyka na PC NIE JEST deterministyczna, chyba że zdecydujesz się ściśle używać IEEE-754 do obliczeń (będzie powolny)
W ten sposób zaimplementowałbym to: klient łączy się z serwerem i synchronizuje czas (zadbaj o opóźnienie ping!) (W przypadku długiej rozgrywki może być konieczne ponowne zsynchronizowanie znacznika czasu / tury)
teraz, za każdym razem, gdy klient wykonuje akcję, zawiera znacznik czasu / zwrot i do serwera należy odrzucenie złego znacznika czasu / zwrotu. Następnie serwer odsyła akcję do klientów i za każdym razem, gdy kolej jest „zamykana” (inaczej serwer nie akceptuje tak starych znaczników / znaczników czasu), serwer wysyła i kończy akcję dla klientów.
Klienci będą mieli 2 „świat”: jeden jest zsynchronizowany z końcem tury, drugi jest obliczany od końca tury, sumując akcję przybywającą do kolejki, aż do aktualnej tury / znacznika czasu klienta.
ponieważ serwer zaakceptuje nieco starą akcję, klient może dodać własną akcję bezpośrednio w kolejce, więc czas podróży w obie strony przez sieć będzie ukryty, przynajmniej dla twojej własnej akcji.
ostatnią rzeczą jest kolejkowanie większej liczby akcji, aby można było wypełnić pakiet MTU, powodując mniejszy narzut protokołu; fajnym pomysłem jest zrobienie tego na serwerze, aby każde zdarzenie zakończenia zawierało akcję w kolejce.
używam tego algorytmu w grze strzelającej w czasie rzeczywistym i działa dobrze (z klientem i bez niego, ale sam z własnej akcji, ale z pingowaniem serwera na poziomie 20/50 ms), również każdy serwer X na końcu wysyła specjalne „wszystko pakiet map klienta ”, aby poprawić dryfowane wartości.
źródło