
W maju tego roku wziąłem udział jako gracz w . Zauważyłem, że kiedy liczba graczy osiąga pewien próg, co kilka minut część z nich "odłącza się". Na szczęście dla was (ale nie dla mnie), byłem jednym z tych graczy, którzy odłączali się za każdym razem, nawet przy dobrej łączności. Przyjąłem to jako osobiste wyzwanie i zacząłem szukać przyczyn problemu. Po trzech tygodniach debugowania, testowania i poprawek błąd w końcu został usunięty, ale ta podróż nie była wcale łatwa.
Problemy w grach wieloosobowych są bardzo trudne do zidentyfikowania. Zwykle występują w bardzo specyficznych warunkach ustawień sieciowych i przy bardzo szczególnych stanach gry (w tym przypadku - przy ponad 200 graczach). I nawet gdy uda się odtworzyć problem, trudno go odpowiednio debugować, ponieważ wstawienie punktów kontrolnych zatrzymuje grę, myli timery i zwykle prowadzi do zakończenia połączenia z powodu przekroczenia limitu czasu. Ale dzięki wytrwałości i niesamowitemu narzędziu o nazwie udało mi się ustalić, co się dzieje.
Pok krótce: z powodu błędu i niepełnej implementacji symulacji opóźnienia klient czasami znajdował się w sytuacji, w której musiał w jednym takcie wysłać pakiet sieciowy składający się z działań wprowadzanych przez gracza dotyczących około 400 bytów w grze (nazywamy to "megapakietem"). Po tym serwer musi nie tylko prawidłowo odebrać wszystkie te działania wprowadzania, ale także wysłać je wszystkim innym klientom. Jeśli masz 200 klientów, szybko staje się to problemem. Kanał do serwera szybko się zatyka, co prowadzi do utraty pakietów i kaskady ponownie żądanych pakietów. Opóźnienie działań wprowadzania prowadzi do tego, że jeszcze więcej klientów zaczyna wysyłać megapakiety, a ich lawina staje się jeszcze silniejsza. Szczęśliwi klienci udaje się zregenerować, a wszyscy inni "odłączają się".

Problem był dość fundamentalny i zajęło mi 2 tygodnie, aby go naprawić. Jest dość techniczny, więc poniżej wyjaśnię szczegółowe aspekty techniczne. Ale najpierw musicie wiedzieć, że od wersji 0.17.54, wydanej 4 czerwca, w warunkach problemów z połączeniem wieloosobowy stał się znacznie bardziej stabilny, a ukrywanie opóźnień – znacznie mniej zacięte (mniej lagów i teleportacji). Poza tym zmieniłem sposób, w jaki ukrywane są opóźnienia w bitwie i mam nadzieję, że dzięki temu będą one nieco płynniejsze.
Wieloosobowy Mega Pakiet – szczegóły techniczne
Jeśli wytłumaczyć to w prosty sposób, to tryb wieloosobowy w grze działa w następujący sposób: wszyscy klienci symulują stan gry, odbierając i wysyłając tylko dane wprowadzone przez gracza (nazywane "działaniami wejścia", Input Actions). Głównym zadaniem serwera jest przekazywanie Input Actions i kontrolowanie, czy wszyscy klienci wykonują te same działania w jednej turze. Więcej na ten temat można przeczytać w poście .
Ponieważ serwer musi podejmować decyzje o tym, jakie działania należy wykonać, działania gracza przechodzą mniej więcej taką drogę: działanie gracza -> klient gry -> sieć -> serwer -> sieć -> klient gry. Oznacza to, że każde działanie gracza jest wykonywane dopiero po przejściu tam i z powrotem przez sieć. Z tego powodu gra wydawałaby się strasznie zacięta, dlatego zaraz po wprowadzeniu trybu wieloosobowego w grze wprowadzono mechanizm ukrywania opóźnień. Ukrywanie opóźnień symuluje wprowadzenie gracza bez uwzględniania działań innych graczy i decyzji serwera.

W Factorio istnieje stan gry Game State – to pełny stan mapy, gracza, jednostek i wszystkiego innego. Jest deterministycznie symulowany we wszystkich klientach na podstawie działań otrzymanych od serwera. Stan gry jest święty i jeśli kiedykolwiek zacznie różnić się od serwera lub jakiegokolwiek innego klienta, to dochodzi do desynchronizacji.
Oprócz Game State mamy stan opóźnień Latency State. Zawiera on małe podzbiór głównego stanu. Latency State nie jest święty i po prostu przedstawia obraz tego, jak wyglądać będzie stan gry w przyszłości na podstawie wprowadzonych przez gracza Input Actions.
W tym celu przechowujemy kopię tworzonych Input Actions w kolejce opóźnień.

Na końcu procesu obraz po stronie klienta wygląda mniej więcej tak:
- Zastosuj Input Actions wszystkich graczy do Game State tak, jak te działania wejściowe zostały otrzymane od serwera.
- Usuwamy z kolejki opóźnień wszystkie Input Actions, które, według danych serwera, zostały już zastosowane do Game State.
- Usuwamy Latency State i resetujemy go, aby wyglądał dokładnie tak samo, jak Game State.
- Zastosuj wszystkie działania z kolejki opóźnień do Latency State.
- Na podstawie danych Game State i Latency State renderujemy grę dla gracza.
Cały ten proces powtarza się w każdym takcie.
Za dużo? Nie martw się, to jeszcze nie wszystko. Aby zrekompensować niestabilność połączeń internetowych, stworzyliśmy dwa mechanizmy:
- Zgubione takty: gdy serwer decyduje, że Input Actions zostaną wykonane w takcie gry, a jeśli nie otrzyma Input Actions jakiegoś gracza (na przykład z powodu zwiększonego opóźnienia), to nie będzie czekać, tylko powie temu klientowi „nie uwzględniłem twoich Input Actions, postaram się dodać je w następnym takcie”. Zrobiono to, aby przez problemy z połączeniem (lub komputerem) jednego gracza aktualizacja mapy nie spowalniała wszystkich innych. Warto zauważyć, że Input Actions nie są ignorowane, a jedynie odkładane.
- Opóźnienie pełnej drogi tam i z powrotem: serwer próbuje oszacować, jakie jest opóźnienie transmisji danych tam i z powrotem między klientem a serwerem dla każdego klienta. Co 5 sekund omawia z klientem nowe opóźnienie (w zależności od tego, jak zachowało się połączenie w przeszłości) i odpowiednio zwiększa lub zmniejsza opóźnienie transmisji danych tam i z powrotem.
Same te mechanizmy są dość proste, ale gdy są stosowane razem (co często wydarza się w przypadku problemów z połączeniem), logika kodu staje się trudna do zarządzania i zawiera wiele przypadków granicznych. Ponadto, gdy te mechanizmy wchodzą w grę, serwer i kolejka opóźnień muszą prawidłowo wdrażać specjalne Input Action o nazwie StopMovementInTheNextTick. Dzięki temu w przypadku problemów z połączeniem postać nie będzie biegać sama (na przykład pod pociąg).
Teraz muszę wyjaśnić, jak działa wybór bytów. Jeden z przesyłanych typów Input Action — to zmiana stanu wyboru encji. Informuje wszystkich, na którą encję gracz najechał kursorem. Jak można się domyślić, to jedna z najczęściej przesyłanych akcji wejściowych przez klientów, dlatego w celu oszczędności przepustowości kanału zoptymalizowaliśmy ją, aby zajmowała jak najmniej miejsca. Zrealizowano to w ten sposób: podczas wyboru każdej encji, zamiast zapisywania absolutnych, precyzyjnych współrzędnych mapy, gra zapisuje niskoprecyzyjne, względne przesunięcie od poprzedniego wyboru. Działa to dobrze, ponieważ zazwyczaj zaznaczenie myszką występuje bardzo blisko poprzedniego zaznaczenia. Z tego powodu powstają dwa ważne wymagania: Input Actions nigdy nie można ich pominąć i muszą być wykonywane w poprawnej kolejności. Te wymagania są spełnione dla Game State. Ale ponieważ zadanie Stanu opóźnienia polega na tym, aby 'wyglądało wystarczająco dobrze' dla gracza, w stanie opóźnień nie są spełniane. Latency State nie uwzględnia , które są związane z pominięciem cykli i zmianą opóźnień przesyłu w obie strony.
Już możesz domyślić się, do czego to wszystko zmierza. W końcu zaczynamy dostrzegać przyczyny problemu megapakietu. Korzeń problemu polega na tym, że w podejmowaniu decyzji o tym, czy należy przesyłać akcję zmiany wyboru, logika wyboru encji polega na Latency State, a to stan nie zawsze zawiera prawidłowe informacje. Dlatego megapakiet generowany jest mniej więcej w ten sposób:
- Gracz ma problemy z połączeniem.
- Wchodzą w grę mechanizmy pomijania cykli i regulacji opóźnienia przesyłu w obie strony.
- Kolejka stanu opóźnień nie uwzględnia tych mechanizmów. Prowadzi to do tego, że niektóre akcje są usuwane przedwcześnie lub wykonywane w niewłaściwej kolejności, co prowadzi do błędnego Latency State.
- Gracz przestaje mieć problem z połączeniem i aby nadrobić zaległości z serwerem, symuluje do 400 cykli.
- W każdym cyklu generowana jest i przygotowywana do wysłania na serwer nowa akcja zmiany wyboru encji.
- Klient wysyła do serwera megapakiet zawierający ponad 400 zmian wyboru encji (i inne działania: stan strzelania, chodzenia itd. również cierpiały z tego powodu).
- Serwer wykonuje 400 działań wejściowych. Ponieważ nie ma pozwolenia na pominięcie żadnego działania wejściowego, nakazuje wszystkim klientom wykonać te działania i wysyła je przez sieć.
Ironia polega na tym, że mechanizm mający na celu oszczędność przepustowości kanału w rezultacie generował ogromne pakiety sieciowe.
Rozwiązaliśmy ten problem, poprawiając wszystkie graniczne przypadki aktualizacji i wsparcia dla kolejki opóźnień. Choć zajęło to sporo czasu, ostatecznie warto było wszystko wdrożyć poprawnie, a nie polegać na szybkich hackach.
Źródło: habr.com
