O modelu sieciowym w grach dla początkujących

O modelu sieciowym w grach dla początkujących
Przez ostatnie dwa tygodnie pracowałem nad silnikiem sieciowym mojej gry. Wcześniej nie wiedziałem nic o sieci w grach, więc przeczytałem wiele artykułów i przeprowadziłem wiele eksperymentów, aby zrozumieć wszystkie koncepcje i móc napisać własny silnik sieciowy.

W tym przewodniku chciałbym podzielić się z Tobą różnymi koncepcjami, których musisz się nauczyć przed napisaniem własnego silnika gry, a także najlepszymi zasobami i artykułami, aby się ich nauczyć.

Ogólnie rzecz biorąc, istnieją dwa główne typy architektur sieciowych: peer-to-peer i klient-serwer. W architekturze peer-to-peer (p2p) dane są przesyłane pomiędzy dowolnymi parami połączonych graczy, natomiast w architekturze klient-serwer dane są przesyłane tylko pomiędzy graczami a serwerem.

Chociaż w niektórych grach nadal stosowana jest architektura peer-to-peer, standardem jest klient-serwer: jest łatwiejszy w implementacji, wymaga mniejszej szerokości kanału i ułatwia ochronę przed oszustwami. Dlatego w tym samouczku skupimy się na architekturze klient-serwer.

W szczególności najbardziej interesują nas serwery autorytarne: w takich systemach serwer ma zawsze rację. Przykładowo, jeśli gracz myśli, że jest na współrzędnych (10, 5), a serwer poinformuje go, że jest na (5, 3), to klient powinien zastąpić jego pozycję tą zgłoszoną przez serwer, a nie odwrotnie odwrotnie. Korzystanie z wiarygodnych serwerów ułatwia identyfikację oszustów.

Systemy gier sieciowych składają się z trzech głównych elementów:

  • Protokół transportowy: sposób przesyłania danych między klientami a serwerem.
  • Protokół aplikacji: co jest przesyłane od klientów do serwera i od serwera do klientów oraz w jakim formacie.
  • Logika aplikacji: w jaki sposób przesłane dane są wykorzystywane do aktualizacji stanu klientów i serwera.

Bardzo ważne jest zrozumienie roli każdej części i wyzwań z nią związanych.

Protokół transportowy

Pierwszym krokiem jest wybranie protokołu przesyłania danych pomiędzy serwerem a klientami. Do tego służą dwa protokoły internetowe: TCP и UDP. Można jednak stworzyć własny protokół transportowy w oparciu o jeden z nich lub skorzystać z biblioteki, która z nich korzysta.

Porównanie TCP i UDP

Zarówno protokół TCP, jak i UDP są oparte na IP. IP umożliwia przesłanie pakietu od źródła do odbiorcy, ale nie gwarantuje, że wysłany pakiet prędzej czy później dotrze do odbiorcy, że dotrze do niego przynajmniej raz i że sekwencja pakietów dotrze we właściwej kolejności zamówienie. Co więcej, pakiet może zawierać tylko ograniczoną ilość danych, określoną przez wartość MTU.

UDP to tylko cienka warstwa na IP. Dlatego ma takie same ograniczenia. Natomiast protokół TCP ma wiele funkcji. Zapewnia niezawodne, uporządkowane połączenie między dwoma węzłami ze sprawdzaniem błędów. Stąd TCP jest bardzo wygodny i stosowany w wielu innych protokołach, m.in. HTTP, FTP и SMTP. Ale wszystkie te funkcje mają swoją cenę: opóźnienie.

Aby zrozumieć, dlaczego te funkcje mogą powodować opóźnienia, musimy zrozumieć, jak działa protokół TCP. Kiedy węzeł nadawczy przesyła pakiet do węzła odbiorczego, oczekuje potwierdzenia (ACK). Jeśli po pewnym czasie go nie otrzyma (z powodu utraty pakietu, potwierdzenia lub z innego powodu), wówczas wysyła pakiet ponownie. Co więcej, protokół TCP gwarantuje, że pakiety są odbierane we właściwej kolejności, więc dopóki nie zostanie odebrany utracony pakiet, żadne inne pakiety nie mogą zostać przetworzone, nawet jeśli zostały już odebrane przez host odbierający.

Ale jak zapewne możesz sobie wyobrazić, opóźnienia w grach wieloosobowych są bardzo ważne, szczególnie w gatunkach pełnych akcji, takich jak FPS. Dlatego wiele gier korzysta z protokołu UDP z własnym protokołem.

Natywny protokół oparty na UDP może być z różnych powodów bardziej wydajny niż TCP. Na przykład może oznaczyć niektóre pakiety jako zaufane, a inne jako niezaufane. Dlatego nie ma znaczenia, czy niezaufany pakiet dotrze do odbiorcy. Może też przetwarzać wiele strumieni danych, tak aby utracony pakiet w jednym strumieniu nie spowalniał pozostałych strumieni. Na przykład może istnieć wątek do wprowadzania danych przez graczy i inny wątek do wiadomości czatu. Jeśli wiadomość na czacie, która nie jest pilna, zostanie utracona, nie spowolni to wprowadzania pilnych komunikatów. Lub zastrzeżony protokół może implementować niezawodność inaczej niż TCP, aby być bardziej wydajnym w środowisku gier wideo.

Więc jeśli TCP jest do niczego, to stworzymy własny protokół transportowy oparty na UDP?

To trochę bardziej skomplikowane. Mimo że protokół TCP jest prawie nieoptymalny dla systemów sieciowych do gier, może całkiem dobrze działać w przypadku Twojej konkretnej gry i oszczędzać cenny czas. Na przykład opóźnienie może nie stanowić problemu w przypadku gier turowych lub gier, w które można grać wyłącznie w sieciach LAN, gdzie opóźnienia i utrata pakietów są znacznie mniejsze niż w Internecie.

Wiele udanych gier, w tym World of Warcraft, Minecraft i Terraria, korzysta z protokołu TCP. Jednak większość gier FPS korzysta z własnych protokołów opartych na UDP, dlatego omówimy je więcej poniżej.

Jeśli zdecydujesz się na użycie protokołu TCP, upewnij się, że jest on wyłączony Algorytm Nagle’a, ponieważ buforuje pakiety przed wysłaniem, co oznacza zwiększenie opóźnień.

Aby dowiedzieć się więcej o różnicach pomiędzy UDP i TCP w kontekście gier wieloosobowych, przeczytaj artykuł Glenna Fiedlera UDP kontra TCP.

Własny protokół

Chcesz stworzyć własny protokół transportowy, ale nie wiesz od czego zacząć? Masz szczęście, ponieważ Glenn Fiedler napisał na ten temat dwa niesamowite artykuły. Znajdziesz w nich wiele mądrych myśli.

Pierwszy artykuł Sieć dla programistów gier 2008, łatwiejszy niż drugi, Budowa protokołu sieciowego gier 2016. Radzę zacząć od starszego.

Należy pamiętać, że Glenn Fiedler jest wielkim zwolennikiem używania niestandardowego protokołu opartego na UDP. A po przeczytaniu jego artykułów prawdopodobnie zgodzisz się z jego opinią, że TCP ma poważne braki w grach wideo i będziesz chciał wdrożyć własny protokół.

Jeśli jednak dopiero zaczynasz przygodę z siecią, wyświadcz sobie przysługę i użyj protokołu TCP lub biblioteki. Aby skutecznie wdrożyć własny protokół transportowy, trzeba się wcześniej dużo nauczyć.

Biblioteki sieciowe

Jeśli potrzebujesz czegoś bardziej wydajnego niż TCP, ale nie chcesz męczyć się z implementacją własnego protokołu i wchodzeniem w wiele szczegółów, możesz skorzystać z biblioteki sieciowej. Jest ich wiele:

Nie wypróbowałem ich wszystkich, ale wolę ENet, ponieważ jest łatwy w użyciu i niezawodny. Ponadto posiada przejrzystą dokumentację i tutorial dla początkujących.

Protokół transportowy: Wniosek

Podsumowując: istnieją dwa główne protokoły transportowe: TCP i UDP. TCP ma wiele przydatnych funkcji: niezawodność, zachowanie kolejności pakietów, wykrywanie błędów. UDP nie ma tego wszystkiego, ale TCP ze swej natury zwiększa opóźnienia, co jest nie do przyjęcia w przypadku niektórych gier. Oznacza to, że aby zapewnić małe opóźnienia, możesz stworzyć własny protokół oparty na UDP lub skorzystać z biblioteki implementującej protokół transportowy na UDP i przystosowanej do gier wideo dla wielu graczy.

Wybór pomiędzy TCP, UDP i biblioteką zależy od kilku czynników. Po pierwsze, z potrzeb gry: czy potrzebuje ona małych opóźnień? Po drugie, z wymagań protokołu aplikacji: czy potrzebuje niezawodnego protokołu? Jak zobaczymy w następnej części, możliwe jest utworzenie protokołu aplikacji, dla którego całkiem odpowiedni będzie protokół niezaufany. Na koniec należy również wziąć pod uwagę doświadczenie programisty silnika sieciowego.

Mam dwie rady:

  • W miarę możliwości wyodrębnij protokół transportowy z reszty aplikacji, aby można go było łatwo zastąpić bez przepisywania całego kodu.
  • Nie przesadzaj z optymalizacją. Jeśli nie jesteś ekspertem w dziedzinie sieci i nie masz pewności, czy potrzebujesz niestandardowego protokołu transportowego opartego na UDP, możesz zacząć od protokołu TCP lub biblioteki zapewniającej niezawodność, a następnie przetestować i zmierzyć wydajność. Jeśli pojawią się problemy i masz pewność, że przyczyną jest protokół transportowy, być może nadszedł czas na stworzenie własnego protokołu transportowego.

Na koniec tej części polecam lekturę Wprowadzenie do programowania gier wieloosobowych autorstwa Briana Hooka, który obejmuje wiele omawianych tutaj tematów.

Protokół aplikacji

Teraz, gdy możemy wymieniać dane między klientami a serwerem, musimy zdecydować, jakie dane przesyłać i w jakim formacie.

Klasyczny schemat polega na tym, że klienci wysyłają dane wejściowe lub akcje do serwera, a serwer wysyła do klientów bieżący stan gry.

Serwer nie wysyła pełnego stanu, ale przefiltrowany stan z podmiotami, które znajdują się w pobliżu odtwarzacza. Robi to z trzech powodów. Po pierwsze, cały stan może być zbyt duży, aby można go było przesłać z dużą częstotliwością. Po drugie, klientów interesują głównie dane wizualne i dźwiękowe, ponieważ większość logiki gry jest symulowana na serwerze gry. Po trzecie, w niektórych grach gracz nie powinien znać pewnych danych, np. pozycji wroga po drugiej stronie mapy, w przeciwnym razie może wąchać pakiety i wiedzieć dokładnie, gdzie się ruszyć, aby go zabić.

Serializacja

Pierwszym krokiem jest przekonwertowanie danych, które chcemy przesłać (wejście lub stan gry) do formatu odpowiedniego do transmisji. Proces ten nazywa się serializacja.

Myśl, która od razu przychodzi na myśl, to użycie formatu czytelnego dla człowieka, takiego jak JSON lub XML. Ale będzie to całkowicie nieskuteczne i zmarnuje większość kanału.

Zamiast tego zaleca się użycie formatu binarnego, który jest znacznie bardziej zwarty. Oznacza to, że pakiety będą zawierać tylko kilka bajtów. Należy tu rozważyć pewien problem kolejność bajtów, które mogą się różnić na różnych komputerach.

Aby serializować dane, możesz użyć biblioteki, na przykład:

Upewnij się tylko, że biblioteka tworzy przenośne archiwa i dba o endianowość.

Alternatywnym rozwiązaniem jest zaimplementowanie go samodzielnie; nie jest to szczególnie trudne, szczególnie jeśli stosujesz podejście do kodu oparte na danych. Dodatkowo umożliwi wykonanie optymalizacji, które nie zawsze są możliwe przy korzystaniu z biblioteki.

Glenn Fiedler napisał dwa artykuły na temat serializacji: Pakiety do czytania i pisania и Strategie serializacji.

kompresja

Ilość danych przesyłanych pomiędzy klientami a serwerem jest ograniczona przepustowością kanału. Kompresja danych pozwoli Ci przesłać więcej danych w każdej migawce, zwiększyć częstotliwość aktualizacji lub po prostu zmniejszyć wymagania dotyczące kanału.

Opakowanie bitowe

Pierwszą techniką jest pakowanie bitów. Polega na użyciu dokładnie takiej liczby bitów, jaka jest niezbędna do opisania żądanej wartości. Na przykład, jeśli masz wyliczenie, które może mieć 16 różnych wartości, zamiast całego bajtu (8 bitów) możesz użyć tylko 4 bitów.

Glenn Fiedler wyjaśnia, jak to wdrożyć w drugiej części artykułu Pakiety do czytania i pisania.

Pakowanie bitów sprawdza się szczególnie dobrze w przypadku próbkowania, co będzie tematem następnej sekcji.

Próbowanie

Próbowanie to technika kompresji stratnej, która wykorzystuje tylko podzbiór możliwych wartości do zakodowania wartości. Najłatwiejszym sposobem wdrożenia dyskretyzacji jest zaokrąglenie liczb zmiennoprzecinkowych.

Glenn Fiedler (ponownie!) pokazuje w swoim artykule, jak zastosować sampling w praktyce Kompresja migawki.

Algorytmy kompresji

Następną techniką będą algorytmy kompresji bezstratnej.

Oto, moim zdaniem, trzy najciekawsze algorytmy, które musisz znać:

  • kodowanie Huffmana z wstępnie obliczonym kodem, który jest niezwykle szybki i może dawać dobre wyniki. Służył do kompresji pakietów w silniku sieciowym Quake3.
  • zlib to algorytm kompresji ogólnego przeznaczenia, który nigdy nie zwiększa ilości danych. Jak możesz zobaczyć tutaj, był używany w różnych zastosowaniach. Może to być zbędne w przypadku aktualizacji stanów. Może się jednak przydać, jeśli chcesz wysyłać zasoby, długie SMS-y lub teren do klientów z serwera.
  • Kopiowanie długości serii - Jest to prawdopodobnie najprostszy algorytm kompresji, ale jest bardzo skuteczny w przypadku niektórych typów danych i może być użyty jako etap wstępnego przetwarzania przed zlib. Szczególnie nadaje się do kompresji terenu złożonego z płytek lub wokseli, w którym powtarza się wiele sąsiadujących ze sobą elementów.

Kompresja delta

Ostatnią techniką kompresji jest kompresja delta. Polega ona na tym, że przesyłane są jedynie różnice pomiędzy aktualnym stanem gry, a ostatnim stanem otrzymanym przez klienta.

Po raz pierwszy zastosowano go w silniku sieciowym Quake3. Oto dwa artykuły wyjaśniające, jak z niego korzystać:

Glenn Fiedler użył go także w drugiej części swojego artykułu Kompresja migawki.

Szyfrowanie

Ponadto może być konieczne szyfrowanie przesyłania informacji między klientami a serwerem. Istnieje kilka powodów:

  • prywatność/poufność: wiadomości może przeczytać tylko odbiorca i żadna inna osoba węsząca w sieci nie będzie w stanie ich przeczytać.
  • uwierzytelnianie: osoba chcąca wcielić się w rolę gracza musi znać swój klucz.
  • Zapobieganie oszustwom: złośliwym graczom będzie znacznie trudniej stworzyć własne pakiety oszustw, będą musieli odtworzyć schemat szyfrowania i znaleźć klucz (który zmienia się przy każdym połączeniu).

Zdecydowanie polecam użycie do tego biblioteki. Sugeruję użycie libsodu, ponieważ jest szczególnie prosty i ma doskonałe samouczki. Szczególnie interesujący jest tutorial pt wymiana kluczy, co pozwala na generowanie nowych kluczy przy każdym nowym połączeniu.

Protokół stosowania: Wniosek

Na tym kończy się nasz protokół aplikacyjny. Uważam, że kompresja jest całkowicie opcjonalna i decyzja o jej zastosowaniu zależy wyłącznie od gry i wymaganej przepustowości. Szyfrowanie moim zdaniem jest obowiązkowe, ale w pierwszym prototypie można się bez niego obejść.

Logika aplikacji

Możemy teraz zaktualizować stan w kliencie, ale mogą wystąpić problemy z opóźnieniami. Gracz po zakończeniu wprowadzania danych musi poczekać, aż stan gry zaktualizuje się z serwera, aby zobaczyć, jaki wpływ miało to na świat.

Co więcej, pomiędzy dwoma aktualizacjami stanu świat jest całkowicie statyczny. Jeśli częstotliwość aktualizacji stanu jest niska, ruchy będą bardzo gwałtowne.

Istnieje kilka technik pozwalających zmniejszyć wpływ tego problemu. Omówię je w następnej sekcji.

Techniki wygładzania opóźnień

Wszystkie techniki opisane w tej sekcji zostały szczegółowo omówione w tej serii Szybka gra wieloosobowa Gabriel Gambetta. Gorąco polecam lekturę tej znakomitej serii artykułów. Zawiera także interaktywne demo, które pozwala zobaczyć, jak te techniki działają w praktyce.

Pierwsza technika polega na bezpośrednim zastosowaniu wyniku wejściowego, bez czekania na odpowiedź z serwera. Nazywa się to prognozowanie po stronie klienta. Jednak gdy klient otrzyma aktualizację z serwera, musi sprawdzić, czy jego przewidywania były prawidłowe. Jeśli tak nie jest, wystarczy, że zmieni swój stan zgodnie z tym, co otrzymał od serwera, ponieważ serwer jest autorytarny. Technika ta została po raz pierwszy zastosowana w Quake. Więcej na ten temat przeczytasz w artykule Przegląd kodu Quake Engine Fabien Sanglars [tłumaczenie na Habré].

Drugi zestaw technik służy do płynnego przemieszczania się innych obiektów pomiędzy dwoma aktualizacjami stanu. Istnieją dwa sposoby rozwiązania tego problemu: interpolacja i ekstrapolacja. W przypadku interpolacji brane są pod uwagę dwa ostatnie stany i pokazane jest przejście z jednego do drugiego. Jego wadą jest to, że powoduje niewielkie opóźnienie, ponieważ klient zawsze widzi, co wydarzyło się w przeszłości. Ekstrapolacja polega na przewidywaniu, gdzie powinny się teraz znajdować elementy, na podstawie ostatniego stanu otrzymanego przez klienta. Jego wadą jest to, że jeśli obiekt całkowicie zmieni kierunek ruchu, wówczas wystąpi duży błąd pomiędzy prognozą a rzeczywistą pozycją.

Najnowszą, najbardziej zaawansowaną techniką przydatną tylko w FPS-ach jest kompensacja opóźnienia. Korzystając z kompensacji opóźnienia, serwer bierze pod uwagę opóźnienia klienta podczas strzelania do celu. Na przykład, jeśli gracz wykonał strzał w głowę na swoim ekranie, ale w rzeczywistości jego cel znajdował się w innym miejscu z powodu opóźnienia, niesprawiedliwe byłoby odmówienie graczowi prawa do zabicia z powodu opóźnienia. Dlatego serwer cofa czas do momentu, w którym gracz oddał strzał, aby zasymulować to, co gracz zobaczył na ekranie i sprawdzić, czy jego strzał nie zderzył się z celem.

Glenn Fiedler (jak zawsze!) napisał artykuł w 2004 roku Fizyka sieci (2004), w którym położył podwaliny pod synchronizację symulacji fizycznych pomiędzy serwerem a klientem. W 2014 roku napisał nowy cykl artykułów Fizyka sieci, w którym opisano inne techniki synchronizacji symulacji fizycznych.

Na wiki Valve znajdują się także dwa artykuły: Źródło sieci dla wielu graczy и Metody kompensacji opóźnień w projektowaniu i optymalizacji protokołów w grze klient/serwer które uwzględniają rekompensatę za opóźnienia.

Zapobieganie oszustwom

Istnieją dwie główne techniki zapobiegania oszustwom.

Po pierwsze: utrudnianie oszustom wysyłania złośliwych pakietów. Jak wspomniano powyżej, dobrym sposobem na wdrożenie tego jest szyfrowanie.

Po drugie: autorytarny serwer powinien otrzymywać tylko polecenia/dane wejściowe/akcje. Klient nie powinien mieć możliwości zmiany stanu na serwerze inaczej niż poprzez wysłanie danych wejściowych. Następnie za każdym razem, gdy serwer otrzymuje dane wejściowe, musi sprawdzić, czy są one prawidłowe przed ich użyciem.

Logika zastosowania: wnioski

Zalecam wdrożenie sposobu symulowania dużych opóźnień i niskich częstotliwości odświeżania, aby móc przetestować zachowanie gry w złych warunkach, nawet gdy klient i serwer działają na tym samym komputerze. To znacznie uprości implementację technik wygładzania opóźnień.

Inne przydatne zasoby

Jeśli chcesz poznać inne zasoby dotyczące modeli sieci, możesz je znaleźć tutaj:

  • Blog Glenna Fiedlera – warto przeczytać cały jego blog, jest tam mnóstwo świetnych artykułów. Tutaj Zebrano wszystkie artykuły dotyczące technologii sieciowych.
  • Niesamowita gra sieciowa autorstwa M. Fatiha MAR to obszerna lista artykułów i filmów na temat silników gier wideo online.
  • В wiki subreddita r/gamedev Istnieje również wiele przydatnych linków.

Źródło: www.habr.com

Dodaj komentarz