
Przez ostatnie dwa tygodnie pracowałem nad sieciowym silnikiem dla mojej gry. Wcześniej nie miałem żadnej wiedzy na temat technologii sieciowych w grach, więc przeczytałem wiele artykułów i przeprowadziłem liczne eksperymenty, żeby zrozumieć wszystkie koncepcje i móc napisać własny silnik sieciowy.
W tym przewodniku chciałbym podzielić się z Wami różnymi koncepcjami, które musicie poznać przed napisaniem własnego silnika gier, a także najlepszymi zasobami i artykułami do ich nauki.
Ogólnie istnieją dwa główne typy architektur sieciowych: peer-to-peer oraz klient-serwerowa. W architekturze peer-to-peer (p2p) dane są przesyłane między dowolnymi parami połączonych graczy, a w architekturze klient-serwerowej dane są przesyłane tylko między graczami a serwerem.
Chociaż architektura peer-to-peer wciąż jest używana w niektórych grach, standardem jest architektura klient-serwerowa: jest prostsza w realizacji, wymaga mniejszej szerokości łącza i ułatwia ochronę przed oszustwami. Dlatego w tym przewodniku skupimy się na architekturze klient-serwerowej.
Szczególnie interesują nas serwery autorytatywne: w takich systemach serwer ma zawsze rację. Na przykład, jeśli gracz myśli, że znajduje się w koordynatach (10, 5), a serwer mówi mu, że jest w (5, 3), to klient powinien zastąpić swoją pozycję tą, którą podaje serwer, a nie odwrotnie. Użycie serwerów autorytatywnych upraszcza rozpoznawanie oszustów.
W sieciowych systemach gier istnieją trzy podstawowe komponenty:
- Protokół transportowy: jak dane są przesyłane 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: jak przesyłane dane są używane do aktualizacji stanu klientów i serwera.
Bardzo ważne jest zrozumienie roli każdej części i związanych z nimi trudności.
Protokół transportowy
Pierwszym krokiem jest wybór protokołu do transportu danych między serwerem a klientami. Istnieją dwa protokoły internetowe: i . Ale można również stworzyć własny protokół transportowy na bazie jednego z nich lub zastosować bibliotekę, w której są one używane.
Porównanie TCP i UDP
Zarówno TCP, jak i UDP opierają się na . IP umożliwia przesyłanie pakietów od źródła do odbiorcy, ale nie daje gwarancji, że wysłany pakiet prędzej czy później dotrze do odbiorcy, że dotrze do niego przynajmniej raz i że sekwencja pakietów przyjdzie we właściwej kolejności. Co więcej, pakiet może zawierać tylko ograniczoną wielkość danych, określoną przez wartość .
UDP to tylko cienka warstwa nad IP. W związku z tym ma te same ograniczenia. W przeciwieństwie do tego, TCP ma wiele cech. Zapewnia niezawodne uporządkowane połączenie między dwoma węzłami z kontrolą błędów. W związku z tym TCP jest bardzo wygodne i jest używane w wielu innych protokołach, takich jak , i . Ale wszystkie te funkcje mają swoją cenę: .
Aby zrozumieć, dlaczego te funkcje mogą powodować opóźnienie, trzeba zrozumieć, jak działa TCP. Kiedy węzeł nadawczy przesyła pakiet do węzła odbiorczego, oczekuje na otrzymanie potwierdzenia (ACK). Jeśli po pewnym czasie go nie otrzyma (ponieważ pakiet lub potwierdzenie zostały utracone, lub z innych powodów), wysyła pakiet ponownie. Co więcej, TCP gwarantuje otrzymanie pakietów w poprawnej kolejności, dlatego dopóki utracony pakiet nie zostanie otrzymany, wszystkie pozostałe pakiety nie mogą być przetwarzane, nawet jeśli zostały już odebrane przez węzeł odbiorczy.
Ale jak zapewne rozumiesz, opóźnienie w grach wieloosobowych jest bardzo ważne, szczególnie w tak aktywnych gatunkach, jak FPS. Dlatego wiele gier korzysta z UDP z własnym protokołem.
Własny protokół oparty na UDP może być skuteczniejszy niż TCP z różnych powodów. Na przykład może oznaczać niektóre pakiety jako niezawodne, a inne jako niezawodne. Dlatego nie obchodzi go, czy niezawodny pakiet dotarł do odbiorcy. Może też obsługiwać kilka strumieni danych, aby utracony pakiet w jednym strumieniu nie spowalniał pozostałych strumieni. Na przykład może istnieć strumień dla wejścia gracza i jeszcze jeden strumień dla wiadomości czatu. Jeśli wiadomość czatu, która nie jest pilna, zostanie utracona, nie spowolni to reakcji na wejście, które jest pilne. Lub własny protokół może realizować niezawodność w inny sposób niż w TCP, aby być efektywniejszym w warunkach gier wideo.
Czy zatem, jeśli TCP jest takie kiepskie, będziemy tworzyć własny protokół transportowy oparty na UDP?
Rzeczy są nieco bardziej skomplikowane. Nawet jeśli TCP jest niemal suboptymalne dla systemów sieciowych w grach, może całkiem dobrze działać w konkretnej grze, oszczędzając czas. Na przykład opóźnienie może nie być problemem w grze turowej lub w grze, którą można grać tylko w sieciach LAN, gdzie opóźnienia i utrata pakietów są znacznie mniejsze niż w Internecie.
W wielu udanych grach, w tym World of Warcraft, Minecraft i Terraria, używa się TCP. Jednak w większości FPS stosowane są własne protokoły oparte na UDP, więc poniżej omówimy je bardziej szczegółowo.
Jeśli zdecydujesz się na użycie TCP, upewnij się, że wyłączony jest , ponieważ buforuje on pakiety przed wysłaniem, co zwiększa opóźnienie.
Aby dowiedzieć się więcej o różnicach między UDP a TCP w kontekście gier wieloosobowych, możesz przeczytać artykuł Glenn’a Fiedlera .
Własny protokół
Więc chcesz stworzyć własny protokół transportowy, ale nie wiesz od czego zacząć? Miałeś szczęście, bo Glenn Fiedler napisał o tym dwa wspaniałe artykuły. Znajdziesz w nich wiele mądrych przemyśleń.
Pierwszy artykuł, z 2008 roku, jest prostszy niż drugi, z 2016 roku. Polecam zacząć od starszego.
Pamiętaj, że Glenn Fiedler jest zwolennikiem używania własnego protokołu opartego na UDP. Po przeczytaniu jego artykułów na pewno przyjmiesz jego opinię, że TCP ma poważne wady w grach wideo i będziesz chciał wdrożyć własny protokół.
Jednak jeśli jesteś nowicjuszem w pracy z sieciami, zrób sobie przysługę i użyj TCP lub biblioteki. Aby pomyślnie wdrożyć własny protokół transportowy, musisz najpierw wiele się nauczyć.
Biblioteki sieciowe
Jeśli potrzebujesz czegoś bardziej wydajnego niż TCP, ale nie chcesz się martwić implementacją własnego protokołu i wnikać w wiele szczegółów, możesz skorzystać z biblioteki sieciowej. Jest ich bardzo dużo:
- Glenn’a Fiedlera
- , która już nie jest wspierana, ale jej fork wygląda na to, że jest nadal aktywny.
- to biblioteka stworzona dla wieloosobowego FPS
- firmy Valve
Nie próbowałem wszystkich, ale preferuję ENet, ponieważ jest prosty w użyciu i niezawodny. Ponadto ma zrozumiałą dokumentację i samouczek dla początkujących.
Protokół transportowy: podsumowanie
Podsumowując, istnieją dwa główne protokoły transportowe: TCP i UDP. TCP ma wiele użytecznych cech: niezawodność, zachowanie kolejności pakietów, wykrywanie błędów. UDP nie ma tego wszystkiego, ale TCP z natury ma zwiększone opóźnienia, które są niedopuszczalne w niektórych grach. To znaczy, aby zapewnić niskie opóźnienia, można stworzyć własny protokół oparty na UDP lub użyć biblioteki implementującej protokół transportowy na UDP, dostosowanej do gier wieloosobowych.
Wybór między TCP, UDP a biblioteką zależy od kilku czynników. Po pierwsze, od potrzeb gry: czy wymagane są niskie opóźnienia? Po drugie, od wymagań protokołu aplikacji: czy potrzebny jest niezawodny protokół? Jak zobaczymy w następnej części, można stworzyć protokół aplikacji, do którego w pełni pasuje niezawodny protokół. Na koniec trzeba też uwzględnić doświadczenie programisty silnika sieciowego.
Mam dwie rady:
- Maksymalnie abstrahuj protokół transportowy od reszty aplikacji, aby można go było łatwo wymienić, nie przepisywając całego kodu.
- Nie zajmuj się przedwczesną optymalizacją. Jeśli nie jesteś specjalistą w dziedzinie sieci i nie masz pewności, czy potrzebujesz własnego protokołu transportowego opartego na UDP, to możesz zacząć od TCP lub biblioteki zapewniającej niezawodność, a następnie przetestować i zmierzyć wydajność. Jeśli występują problemy i jesteś pewien, że przyczyna leży w protokole transportowym, to być może nastał czas na stworzenie własnego protokołu transportowego.
Na zakończenie tej części polecam przeczytać Briana Hooka, który omawia wiele tematów poruszanych tutaj.
Protokół aplikacji
Teraz, gdy możemy wymieniać dane między klientami a serwerem, należy zdecydować, jakie dokładnie dane przesyłać i w jakim formacie.
Klasyczny schemat polega na tym, że klienci wysyłają do serwera dane wejściowe lub działania, a serwer wysyła klientom bieżący stan gry.
Serwer wysyła niepełny, a filtrowany stan z encjami, które znajdują się w pobliżu gracza. Dzieje się tak z trzech powodów. Po pierwsze, pełny stan może być zbyt duży do przesyłania z dużą częstotliwością. Po drugie, klientów głównie interesują dane wizualne i audio, ponieważ większa część logiki gry jest symulowana na serwerze gry. Po trzecie, w niektórych grach gracz nie powinien znać pewnych danych, na przykład pozycji przeciwnika po drugiej stronie mapy, ponieważ w przeciwnym razie może podsłuchiwać pakiety i dokładnie wiedzieć, dokąd się udać, aby go zabić.
Serializacja
Pierwszym krokiem jest przekształcenie danych, które chcemy wysłać (wejście lub stan gry), w odpowiedni format do przesyłania. Proces ten nazywa się .
Od razu przychodzi na myśl użycie formatu czytelnego dla człowieka, takiego jak JSON lub XML. Ale byłoby to całkowicie nieefektywne i zmarnowałoby dużą część 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 tutaj wziąć pod uwagę problem , która na różnych komputerach może się różnić.
Do serializacji danych można użyć biblioteki, na przykład:
- firmy Google
- firmy Sandstorm
- Shayna Granta i Randolpha Vuurhisa
Upewnij się tylko, że biblioteka tworzy przenośne archiwa i dba o kolejność bajtów.
Alternatywnym rozwiązaniem może być samodzielna implementacja, która nie jest szczególnie skomplikowana, zwłaszcza jeśli w kodzie stosujesz podejście zorientowane na dane. Dodatkowo pozwoli to na przeprowadzanie optymalizacji, które nie zawsze są możliwe przy użyciu biblioteki.
Glenn Fidler napisał dwie artykuły na temat serializacji: i .
Kompresja
Ilość danych przesyłanych między klientami a serwerem jest ograniczona przepustowością kanału. Kompresja danych pozwoli na przesyłanie w każdym zrzucie większej ilości danych, zwiększenie częstotliwości aktualizacji lub po prostu zmniejszenie wymagań dotyczących kanału.
Pakowanie bitowe
Pierwsza technika to kompresja bitowa. Polega ona na wykorzystaniu dokładnie takiej liczby bitów, jaka jest potrzebna do opisu żądanej wielkości. Na przykład, jeśli masz enumerację, która może mieć 16 różnych wartości, zamiast pełnego bajtu (8 bitów) można użyć zaledwie 4 bitów.
Glenn Fiedler wyjaśnia, jak to zrealizować, w drugiej części artykułu. .
Kompresja bitowa szczególnie dobrze sprawdza się w dyskretyzacji, która będzie tematem następnej sekcji.
Dyskretyzacja
to technika kompresji stratnej, która polega na tym, że do kodowania wielkości wykorzystywana jest tylko podzespół możliwych wartości. Najłatwiej zaimplementować dyskretyzację przez zaokrąglanie liczb zmiennoprzecinkowych.
Glenn Fiedler (znowu!) pokazuje, jak stosować dyskretyzację w praktyce w swoim artykule. .
Algorytmy kompresji
Następną techniką będą algorytmy kompresji bezstratnej.
Oto, moim zdaniem, trzy najciekawsze algorytmy, które warto znać:
- z wcześniej obliczonym kodem, co jest niezwykle szybkie i może dawać dobre wyniki. Był on wykorzystywany do kompresji paczek w silniku sieciowym Quake3.
- to algorytm kompresji ogólnego przeznaczenia, który nigdy nie zwiększa rozmiaru danych. Jak można zauważyć, był stosowany w wielu obszarach. Dla aktualizacji stanów może okazać się zbędny. Ale może też być przydatny, jeśli musisz przesyłać klientom z serwera zasoby, długie teksty lub tereny.
- to prawdopodobnie najprostszy algorytm kompresji, ale jest bardzo skuteczny dla określonych typów danych i może być używany jako etap wstępnego przetwarzania przed zlib. Jest szczególnie odpowiedni do kompresji terenu złożonego z płytek lub wokseli, w których wiele sąsiadujących elementów się powtarza.
Kompresja delta
Ostatnia metoda kompresji to kompresja delta. Polega ona na tym, że przekazywane są tylko różnice między bieżącym stanem gry a ostatnim stanem odebranym przez klienta.
Po raz pierwszy została zastosowana w silniku sieciowym Quake3. Oto dwa artykuły, które wyjaśniają sposób jej użycia:
- Briana Hooka
- Fabiena Sanglara [ artykuły na Habrze, patrz sekcja „Model sieciowy”]
Glen Fiedler również użył jej w drugiej części swojego artykułu .
Szyfrowanie
Może być również konieczne szyfrowanie przesyłania informacji między klientami a serwerem. Istnieje kilka powodów, dla których warto to zrobić:
- prywatność: wiadomości mogą być odczytane tylko przez odbiorcę, a żadna inna osoba, która dokonuje podsłuchiwania sieci, nie będzie w stanie ich odczytać.
- uwierzytelnienie: osoba, która chce pełnić rolę gracza, musi znać jego klucz.
- zapobieganie oszustwom: złośliwym graczom znacznie trudniej będzie tworzyć własne pakiety do oszustwa; będą musieli odtworzyć schemat szyfrowania i znaleźć klucz (który zmienia się przy każdym połączeniu).
Zalecam stanowczo skorzystanie z biblioteki do tego celu. Proponuję używać , ponieważ jest szczególnie prosta i ma świetne tutoriale. Szczególnie interesujący jest tutorial dotyczący , który umożliwia generowanie nowych kluczy przy każdym nowym połączeniu.
Protokół aplikacji: zakończenie
Na tym zakończymy temat protokołu aplikacji. Uważam, że kompresja jest zupełnie niekonieczna, a decyzja o jej użyciu zależy wyłącznie od gry i wymaganej przepustowości kanału. Szyfrowanie moim zdaniem jest obowiązkowe, ale w pierwszym prototypie można się bez niego obejść.
Logika aplikacji
Teraz jesteśmy w stanie aktualizować stan na kliencie, ale możemy napotkać problemy z opóźnieniami. Gracz, po wprowadzeniu danych, musi czekać na aktualizację stanu gry od serwera, aby zobaczyć, jaki wpływ wywarł na świat.
Co więcej, między dwoma aktualizacjami stanu świat jest całkowicie statyczny. Jeśli częstotliwość aktualizacji stanów jest niska, ruchy będą bardzo szarpane.
Istnieje kilka technik, które mogą pomóc zmniejszyć wpływ tego problemu, a w następnej sekcji o nich opowiem.
Techniki wygładzania opóźnień
Wszystkie opisane w tej sekcji techniki zostały szczegółowo omówione w serii Gabriela Gambetty. Zdecydowanie polecam przeczytać tę doskonałą serię artykułów. Zawiera ona również interaktywne demo, które pozwala zobaczyć, jak te techniki działają w praktyce.
Pierwsza technika polega na zastosowaniu wyniku wprowadzenia bez czekania na odpowiedź od serwera. Nazywa się to prognozowaniem po stronie klienta. Jednak gdy klient otrzymuje aktualizację z serwera, musi upewnić się, że jego prognoza była poprawna. Jeśli tak nie jest, musi po prostu zmienić swój stan zgodnie z tym, co otrzymał od serwera, ponieważ serwer jest autorytarny. Ta technika została po raz pierwszy użyta w Quake. Szczegóły można znaleźć w artykule Fabiena Sanglara [ na Habrze].
Drugi zestaw technik służy do wygładzania ruchu innych bytów pomiędzy dwoma aktualizacjami stanu. Istnieją dwa sposoby rozwiązania tego zadania: interpolacja i ekstrapolacja. W przypadku interpolacji bierzemy dwa ostatnie stany i pokazujemy przejście z jednego do drugiego. Jej wadą jest to, że powoduje niewielką opóźnienie, ponieważ klient zawsze widzi to, co działo się w przeszłości. Ekstrapolacja polega na prognozowaniu, gdzie obecnie powinny znajdować się byty na podstawie ostatniego stanu, który otrzymał klient. Jej wadą jest to, że jeśli byt w pełni zmienia kierunek ruchu, pojawi się duża rozbieżność między prognozą a rzeczywistą pozycją.
Ostatnia, najbardziej zaawansowana technika, użyteczna tylko w FPS - to kompensacja opóźnienia. Przy użyciu kompensacji opóźnienia serwer uwzględnia opóźnienia klienta, gdy ten strzela do celu. Na przykład, jeśli gracz wykonał headshota na swoim ekranie, ale w rzeczywistości jego cel z powodu opóźnienia znajdował się w innym miejscu, byłoby niesprawiedliwie odmawiać graczowi prawa do zabicia z powodu opóźnienia. Dlatego serwer cofa czas do momentu, w którym gracz strzelił, aby zasymulować to, co gracz widział na swoim ekranie, i sprawdzić kolizję między jego strzałem a celem.
Glenn Fiddler (jak zawsze!) napisał w 2004 roku artykuł , w którym położył podwaliny pod synchronizację symulacji fizyki między serwerem a klientem. W 2014 roku napisał nową serię artykułów , w której opisał inne techniki synchronizacji symulacji fizyki.
W wiki firmy Valve znajdują się również dwa artykuły, i , w których omawia się kompensację opóźnień.
Zapobieganie oszustwom
Istnieją dwie podstawowe techniki zapobiegania oszustwom.
Pierwsze: komplikacja w wysyłaniu złośliwych pakietów przez cheaterów. Jak wspomniano wcześniej, dobrym sposobem realizacji jest szyfrowanie.
Drugie: autorytarny serwer powinien otrzymywać tylko polecenia / wejścia / działania. Klient nie powinien mieć możliwości zmiany stanu na serwerze, poza wysyłaniem wejścia. Wówczas serwer za każdym razem przy odbiorze wejścia powinien przed jego zastosowaniem sprawdzać jego dopuszczalność.
Logika aplikacji: podsumowanie
Zalecam wdrożenie sposobu symulacji dużych opóźnień i niskiej częstotliwości aktualizacji, aby móc przetestować zachowanie swojej gry w złych warunkach, nawet gdy klient i serwer uruchomione są na tym samym komputerze. To znacznie uprości realizację metod wygładzania opóźnień.
Inne przydatne zasoby
Jeśli chcesz zbadać inne zasoby poświęcone modelom sieciowym, można je znaleźć tutaj:
- — warto przeczytać cały jego blog, który zawiera wiele doskonałych artykułów. zebrane są wszystkie artykuły dotyczące technologii sieciowych.
- autorstwa M. Fatih MAR — to szczegółowa lista artykułów i wideo dotyczących sieciowych silników gier wideo.
- W również zawiera wiele przydatnych linków.
Źródło: habr.com
