Jak zaimplementować system migawek stanu gry dla sieciowych gier w czasie rzeczywistym?

12

Chcę stworzyć prostą grę wieloosobową klient-serwer w czasie rzeczywistym jako projekt dla mojej klasy sieciowej.

Dużo czytałem o modelach sieciowych dla wielu graczy w czasie rzeczywistym i rozumiem relacje między klientem a serwerem oraz techniki kompensacji opóźnień.

To, co chcę zrobić, to coś podobnego do modelu sieciowego Quake 3: w zasadzie serwer przechowuje migawkę całego stanu gry; po otrzymaniu danych wejściowych od klientów serwer tworzy nową migawkę odzwierciedlającą zmiany. Następnie oblicza różnice między nową migawką a ostatnią i wysyła je do klientów, aby mogli być zsynchronizowani.

To podejście wydaje mi się naprawdę solidne - jeśli klient i serwer mają stabilne połączenie, tylko minimalna niezbędna ilość danych zostanie wysłana, aby je zsynchronizować. Jeśli klient nie jest zsynchronizowany, można również zażądać pełnej migawki.

Nie mogę jednak znaleźć dobrego sposobu na wdrożenie systemu migawek. Bardzo trudno mi odejść od architektury programowania dla jednego gracza i zastanowić się, jak mogę zapisać stan gry w taki sposób, aby:

  • Wszystkie dane są oddzielone od logiki
  • Różnice można obliczyć między migawkami stanów gry
  • Elementami gry można nadal łatwo manipulować za pomocą kodu

Jak wdrażana jest klasa migawki ? Jak przechowywane są podmioty i ich dane? Czy każda jednostka klienta ma identyfikator pasujący do identyfikatora na serwerze?

Jak obliczane są różnice w migawkach?

Ogólnie: w jaki sposób zostałby wdrożony system migawek stanu gry?

Vittorio Romeo
źródło
4
+1. Jest to nieco zbyt ogólne na jedno pytanie, ale IMO to interesujący temat, który można z grubsza ująć w odpowiedzi.
Kromster,
Dlaczego po prostu nie przechowasz 1 migawki (rzeczywistego świata), zapiszesz wszystkie nadchodzące zmiany w tym zwykłym stanie świata ORAZ zapiszesz zmiany na liście lub coś takiego. Następnie, gdy nadejdzie czas, aby wysłać zmiany do wszystkich klientów, wystarczy wysłać zawartość listy do wszystkich i wyczyścić listę, zaczynając od zera (zmiany). Być może nie jest to tak dobre, jak przechowywanie 2 migawek, ale dzięki takiemu podejściu nie musisz martwić się algorytmami, jak szybko różnicować 2 migawki.
tkausl
Czytałeś to: fabiensanglard.net/quake3/network.php - przegląd modelu sieci quake 3 obejmuje dyskusję na temat implementacji.
Steven
Jaką grę próbujesz zbudować? Konfiguracja sieci zależy w dużej mierze od rodzaju tworzonej gry. RTS nie zachowuje się jak FPS pod względem sieci.
AturSams,

Odpowiedzi:

3

Można obliczyć deltę migawki (zmiany do jej poprzedniego stanu synchronizacji), zachowując dwie instancje migawek: bieżącą i ostatnią synchronizowaną.

Po otrzymaniu danych wejściowych klienta modyfikujesz bieżącą migawkę. Następnie, gdy nadszedł czas, aby wysłać deltę do klientów, obliczasz ostatnio zsynchronizowaną migawkę z bieżącym jednym polem po polu (rekurencyjnie) oraz obliczasz i serializujesz deltę. Do serializacji można przypisać unikalny identyfikator do każdego pola w zakresie jego klasy (w przeciwieństwie do globalnego zakresu stanu). Klient i serwer powinny współużytkować tę samą strukturę danych dla stanu globalnego, aby klient rozumiał, do czego jest stosowany konkretny identyfikator.

Następnie, po obliczeniu delty, klonujesz bieżący stan i ustawiasz go jako ostatni zsynchronizowany, więc teraz masz identyczny aktualny i ostatni zsynchronizowany stan, ale różne wystąpienia, dzięki czemu możesz zmodyfikować aktualny stan i nie wpływać na drugi.

To podejście może być łatwiejsze do wdrożenia, szczególnie przy pomocy refleksji (jeśli masz taki luksus), ale może być powolne, nawet jeśli mocno zoptymalizujesz część refleksji (budując schemat danych, aby buforować większość wywołań refleksji). Głównie dlatego, że musisz porównać dwie kopie potencjalnie dużego stanu. Oczywiście zależy to od sposobu wdrożenia porównania i języka. Może być szybki w C ++ z zakodowanym komparatorem, ale nie jest tak elastyczny: każda zmiana globalnej struktury stanu wymaga modyfikacji tego komparatora, a zmiany te są tak częste na początkowych etapach projektu.

Innym podejściem jest użycie brudnych flag. Za każdym razem, gdy przychodzi dane wejściowe klienta, stosujesz je do pojedynczej kopii stanu globalnego i zaznaczasz odpowiednie pola jako brudne. Następnie, gdy nadszedł czas na synchronizację klientów, serializujesz brudne pola (rekurencyjnie) przy użyciu tych samych unikalnych identyfikatorów. (Drobna) wada polega na tym, że czasami wysyłasz więcej danych niż jest to absolutnie wymagane: np. int field1Początkowo miał 0, potem przypisano 1 (i oznaczono jako brudny), a potem przypisano 0 ponownie (ale pozostaje brudny). Korzyścią jest to, że mając ogromną hierarchiczną strukturę danych, nie trzeba jej analizować całkowicie, aby obliczyć różnicę, a jedynie brudne ścieżki.

Ogólnie rzecz biorąc, to zadanie może być dość skomplikowane, zależy od tego, jak elastyczne powinno być ostateczne rozwiązanie. Np. Unity3D 5 (nadchodzące) użyje atrybutów, aby określić dane, które powinny być automatycznie synchronizowane z klientami (bardzo elastyczne podejście, nie musisz nic robić poza dodaniem atrybutu do swoich pól), a następnie wygenerowanie kodu jako krok po kompilacji. Więcej informacji tutaj.

Andrij Tylychko
źródło
2

Najpierw musisz wiedzieć, jak reprezentować odpowiednie dane w sposób zgodny z protokołem. Zależy to od danych istotnych dla gry. Jako przykład użyję gry RTS.

Do celów sieciowych wszystkie podmioty w grze są wyliczane (np. Odbiory, jednostki, budynki, zasoby naturalne, materiały do ​​zniszczenia).

Gracze muszą mieć odpowiednie dane (na przykład wszystkie widoczne jednostki):

  • Czy są żywi czy martwi?
  • Jakiego rodzaju są?
  • Ile pozostało zdrowia?
  • Bieżąca pozycja, obrót, prędkość (prędkość + kierunek), ścieżka w najbliższej przyszłości ...
  • Aktywność: atakowanie, chodzenie, budowanie, naprawianie, leczenie itp.
  • efekty statusu buff / debuff
  • i ewentualnie inne statystyki, takie jak mana, tarcze i co nie?

Najpierw gracz musi uzyskać pełny stan, zanim będzie mógł wejść do gry (lub ewentualnie wszystkie informacje dotyczące tego gracza).

Każda jednostka ma identyfikator liczby całkowitej. Atrybuty są wyliczane i dlatego mają również integralne identyfikatory. Identyfikatory jednostek nie muszą mieć długości 32 bitów (może tak być, jeśli nie jesteśmy oszczędni). Może to być bardzo dobrze 20 bitów (pozostawiając 10 bitów dla atrybutów). Identyfikator jednostek musi być unikalny, bardzo dobrze może być przypisany przez licznik, gdy jednostka zostanie utworzona i / lub dodana do świata gry (budynki i zasoby są uważane za nieruchome jednostki, a zasoby można przypisać identyfikatorowi, gdy mapa jest załadowana).

Serwer przechowuje bieżący stan globalny. Najnowszy zaktualizowany stan każdego gracza jest reprezentowany przez wskaźnik do listostatnich zmian (wszystkie zmiany po wskaźniku nie zostały jeszcze wysłane do tego gracza). Zmiany są dodawane do listmomentu ich wystąpienia. Gdy serwer zakończy wysyłanie ostatniej aktualizacji, może rozpocząć iterację po liście: serwer przesuwa wskaźnik gracza wzdłuż listy do ogona, zbierając wszystkie zmiany po drodze i umieszczając je w buforze, który zostanie wysłany do gracz (tzn. format protokołu może być mniej więcej taki: unit_id; attr_id; new_value) Nowe jednostki są również uważane za zmiany i są wysyłane ze wszystkimi wartościami atrybutów do otrzymujących graczy.

Jeśli nie używasz języka ze śmietnikiem, musisz ustawić leniwy wskaźnik, który będzie pozostawał w tyle, a następnie dogonić najbardziej przestarzały wskaźnik gracza na liście, uwalniając po drodze obiekty. Możesz zapamiętać, który gracz jest najbardziej przestarzały na stosie priorytetowym lub po prostu iterować i uwolnić, dopóki leniwy wskaźnik nie wyrówna się (tj. Wskazuje ten sam przedmiot, co jeden ze wskaźników gracza).

Niektóre pytania, których nie zadałeś i które są interesujące, to:

  1. Czy klienci powinni przede wszystkim otrzymać migawkę ze wszystkimi danymi? Co z przedmiotami poza ich polem widzenia? Co z mgłą wojny w grach RTS? Jeśli wyślesz wszystkie dane, klient może zostać zhakowany, aby wyświetlić dane, które nie powinny być dostępne dla gracza (w zależności od innych podjętych środków bezpieczeństwa). Jeśli prześlesz tylko odpowiednie dane, problem zostanie rozwiązany.
  2. Kiedy konieczne jest wysyłanie zmian zamiast wysyłania wszystkich informacji? Biorąc pod uwagę przepustowość dostępną na nowoczesnych maszynach, czy zyskujemy na wysyłaniu „delty” zamiast wysyłania wszystkich informacji, jeśli tak, to kiedy?
AturSams
źródło