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?
źródło
Odpowiedzi:
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 field1
Począ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.
źródło
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):
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
list
ostatnich zmian (wszystkie zmiany po wskaźniku nie zostały jeszcze wysłane do tego gracza). Zmiany są dodawane dolist
momentu 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:
źródło