Jak obsługiwać kod netto?

10

Interesuje mnie ocena różnych sposobów, w jakie kod sieci może „podłączyć się” do silnika gry. Projektuję teraz grę wieloosobową i do tej pory ustaliłem, że muszę (przynajmniej) mieć osobny wątek do obsługi gniazd sieciowych, inny niż reszta silnika, który obsługuje pętlę graficzną i skrypty.

Miałem jeden potencjalny sposób, aby stworzyć grę sieciową całkowicie jednowątkową, polegającą na sprawdzeniu sieci po renderowaniu każdej klatki przy użyciu nieblokujących gniazd. Jednak nie jest to optymalne, ponieważ czas oczekiwania na renderowanie ramki jest dodawany do opóźnienia sieci: Wiadomości przychodzące przez sieć muszą czekać, aż zakończy się renderowanie bieżącej ramki (i logiki gry). Ale przynajmniej w ten sposób gra pozostanie płynna, mniej więcej.

Posiadanie osobnego wątku do pracy w sieci pozwala grze całkowicie reagować na sieć, na przykład może odesłać pakiet ACK natychmiast po otrzymaniu aktualizacji stanu z serwera. Ale jestem trochę zdezorientowany co do najlepszego sposobu komunikacji między kodem gry a kodem sieciowym. Wątek sieci pchnie otrzymany pakiet do kolejki, a wątek gry będzie czytał z kolejki w odpowiednim czasie podczas pętli, więc nie pozbyliśmy się tego opóźnienia do jednej klatki.

Wygląda też na to, że chciałbym, aby wątek, który obsługuje wysyłanie pakietów, był inny niż ten, który sprawdza, czy pakiety wychodzą w dół potoku, ponieważ nie byłby w stanie wysłać jednego, gdy jest w środku sprawdzanie, czy są wiadomości przychodzące. Myślę o funkcjonalnościselect lub podobnej.

Wydaje mi się, że moje pytanie brzmi: jaki jest najlepszy sposób zaprojektowania gry pod kątem najlepszej reakcji sieci? Oczywiście klient powinien wysłać dane wejściowe użytkownika tak szybko, jak to możliwe, na serwer, aby kod wysyłania wiadomości był wysyłany natychmiast po pętli przetwarzania zdarzeń, zarówno wewnątrz pętli gry. Czy to ma jakiś sens?

Steven Lu
źródło

Odpowiedzi:

13

Zignoruj ​​czas reakcji. W sieci LAN ping jest nieznaczny. W Internecie 60-100 ms w obie strony jest błogosławieństwem. Módlcie się do bogów lagów, abyście nie dostali skoków> 3K. Twoje oprogramowanie musiałoby działać z bardzo małą liczbą aktualizacji / s, aby mógł to stanowić problem. Jeśli wykonujesz zdjęcia z szybkością 25 aktualizacji / s, masz maksymalnie 40 ms pomiędzy otrzymaniem pakietu a działaniem na nim. I to jest jednowątkowa obudowa ...

Zaprojektuj swój system pod kątem elastyczności i poprawności. Oto mój pomysł na podłączenie podsystemu sieciowego do kodu gry: Wiadomości. Rozwiązaniem wielu problemów może być „przesyłanie wiadomości”. Myślę, że wiadomości wyleczyły raka u szczurów laboratoryjnych. Wiadomości oszczędzają mi 200 USD lub więcej na moim ubezpieczeniu samochodu. Ale poważnie, przesyłanie wiadomości jest prawdopodobnie najlepszym sposobem na dołączenie dowolnego podsystemu do kodu gry przy jednoczesnym utrzymaniu dwóch niezależnych podsystemów.

Wiadomości należy używać do wszelkiej komunikacji między podsystemem sieciowym a silnikiem gry oraz w tym przypadku między dowolnymi dwoma podsystemami. Przesyłanie komunikatów między podsystemami może być proste jak kropla danych przekazywanych przez wskaźnik za pomocą std :: list.

Wystarczy mieć kolejkę wiadomości wychodzących i odniesienie do silnika gry w podsystemie sieciowym. Gra może zrzucać wiadomości, które chce wysłać do kolejki wychodzącej, i automatycznie wysyłać je automatycznie, a może po wywołaniu funkcji „flushMessages ()”. Jeśli silnik gry ma jedną dużą, współdzieloną kolejkę komunikatów, wówczas wszystkie podsystemy, które musiały wysyłać wiadomości (logika, sztuczna inteligencja, fizyka, sieć itp.), Mogą zrzucić do niej wszystkie wiadomości, w których główna pętla gry może następnie odczytać wszystkie wiadomości i działaj odpowiednio.

Powiedziałbym, że uruchamianie gniazd w innym wątku jest w porządku, choć nie jest wymagane. Jedynym problemem związanym z tym projektem jest to, że jest on generalnie asynchroniczny (nie wiadomo dokładnie, kiedy pakiety są wysyłane) i może to utrudnić debugowanie i sprawić, że problemy związane z czasem pojawią się / znikną losowo. Mimo to, jeśli wykonane prawidłowo, żadne z nich nie powinno stanowić problemu.

Z wyższego poziomu powiedziałbym osobne połączenie sieciowe z samym silnikiem gry. Silnik gry nie dba o gniazda ani bufory, dba o zdarzenia. Zdarzenia to takie rzeczy, jak „Gracz X oddał strzał” „Wybuch w grze T”. Mogą być one interpretowane bezpośrednio przez silnik gry. Nie ma znaczenia, skąd są generowane (skrypt, działanie klienta, odtwarzacz AI itp.).

Jeśli traktujesz swój podsystem sieciowy jako sposób wysyłania / odbierania zdarzeń, zyskujesz szereg korzyści w porównaniu z wywoływaniem recv () na gnieździe.

Można na przykład zoptymalizować przepustowość, biorąc na przykład 50 małych wiadomości (o długości 1-32 bajtów), a podsystem sieciowy pakuje je w jeden duży pakiet i wysyła. Może to skompresuje je przed wysłaniem, jeśli to była wielka sprawa. Z drugiej strony kod może ponownie rozpakować / rozpakować duży pakiet do 50 dyskretnych zdarzeń, aby silnik gry mógł go odczytać. Wszystko to może odbywać się w przejrzysty sposób.

Inne fajne rzeczy to tryb gry dla jednego gracza, który ponownie wykorzystuje kod sieci, mając czystego klienta + czysty serwer działający na tej samej maszynie, komunikujący się za pośrednictwem wiadomości w pamięci współdzielonej. Następnie, jeśli Twoja gra dla pojedynczego gracza działa poprawnie, działałby również zdalny klient (tj. Prawdziwy multiplayer). Ponadto zmusza Cię do rozważenia z wyprzedzeniem, jakie dane są potrzebne klientowi, ponieważ gra dla jednego gracza wyglądałaby dobrze lub byłaby całkowicie błędna. Miksuj i dopasowuj, uruchamiaj serwer ORAZ zostań klientem w grze wieloosobowej - wszystko działa równie łatwo.

PatrickB
źródło
Wspomniałeś o użyciu zwykłej listy std :: lub niektórych takich do przekazywania wiadomości. To może być temat dla StackOverflow, ale czy to prawda, że ​​wszystkie wątki mają tę samą przestrzeń adresową i dopóki nie zapobiegam wkręcaniu wielu wątków z pamięcią należącą do mojej kolejki jednocześnie, powinienem być w porządku? Mogę po prostu przydzielić dane do kolejki na stercie, tak jak normalnie, i po prostu użyć w niej niektórych muteksów?
Steven Lu
Tak, to jest poprawne. Muteks chroniący wszystkie wywołania std :: list.
PatrickB
Dzięki za odpowiedź! Dotychczas poczyniłem duże postępy w zakresie procedur wątkowania. To wspaniałe uczucie, mając własny silnik gry!
Steven Lu,
4
To uczucie zniknie. Te duże mosiężne, które zdobędziesz, pozostaną przy tobie.
ChrisE
@StevenLu Trochę [wyjątkowo] późno, ale chcę zauważyć, że zapobieganie jednoczesnemu wkręcaniu wątków w pamięć może być bardzo trudne, w zależności od tego, jak próbujesz to zrobić i jak wydajna powinna być. Czy robiąc to dzisiaj, wskazałbym jedną z wielu doskonałych implementacji współbieżnych kolejek open source, więc nie trzeba wymyślać skomplikowanego koła.
Pozew Fund Moniki w dniu
4

Potrzebuję (przynajmniej) osobnego wątku do obsługi gniazd sieciowych

Nie ty nie.

Miałem jeden potencjalny sposób, aby stworzyć grę sieciową całkowicie jednowątkową, polegającą na sprawdzeniu sieci po renderowaniu każdej klatki przy użyciu nieblokujących gniazd. Jednak nie jest to optymalne, ponieważ czas potrzebny do renderowania ramki jest dodawany do opóźnienia sieci:

Nie musi to mieć znaczenia. Kiedy twoja logika się aktualizuje? Nie ma sensu ściągać danych z sieci, jeśli nie możesz nic z tym zrobić. Podobnie nie ma sensu odpowiadać, jeśli nie masz jeszcze nic do powiedzenia.

na przykład może odesłać pakiet ACK natychmiast po otrzymaniu aktualizacji stanu z serwera.

Jeśli Twoja gra jest tak szybka, że ​​oczekiwanie na wyświetlenie następnej klatki jest znaczącym opóźnieniem, oznacza to, że będzie wysyłała wystarczającą ilość danych, że nie musisz wysyłać osobnych pakietów ACK - po prostu dołącz wartości ACK do swoich normalnych danych ładunki, jeśli w ogóle ich potrzebujesz.

W przypadku większości gier sieciowych istnieje możliwość stworzenia takiej pętli:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Możesz oddzielić aktualizacje od renderowania, co jest wysoce zalecane, ale wszystko inne może pozostać takie proste, chyba że masz konkretną potrzebę. Dokładnie jaką grę tworzysz?

Kylotan
źródło
7
Oddzielenie aktualizacji od renderowania jest nie tylko wysoce zalecane, jest wymagane, jeśli chcesz mieć silnik bez gówna.
AttackingHobo
Większość ludzi nie tworzy silników i prawdopodobnie większość gier nadal nie rozdziela tych dwóch. Ustawienie przekazywania wartości czasu, który upłynął, do funkcji aktualizacji działa w większości przypadków akceptowalnie.
Kylotan
2

Jednak nie jest to optymalne, ponieważ czas oczekiwania na renderowanie ramki jest dodawany do opóźnienia sieci: Wiadomości przychodzące przez sieć muszą czekać, aż zakończy się renderowanie bieżącej ramki (i logiki gry).

To wcale nie jest prawda. Wiadomość przechodzi przez sieć, podczas gdy odbiornik renderuje bieżącą ramkę. Opóźnienie sieci jest ograniczone do pełnej liczby ramek po stronie klienta; tak - ale jeśli klient ma tak mało FPS, że jest to wielka sprawa, mają większe problemy.

DeadMG
źródło
0

Komunikacja sieciowa powinna być grupowana. Powinieneś dążyć do tego, aby pojedynczy pakiet wysyłany był przy każdym tyknięciu gry (co często zdarza się, gdy ramka jest renderowana, ale tak naprawdę powinna być niezależna).

Twoje podmioty gry rozmawiają z podsystemem sieci (NSS). NSS grupuje wiadomości, ACK itp. I wysyła kilka (miejmy nadzieję) optymalnych rozmiarów pakietów UDP (zwykle ~ 1500 bajtów). NSS emuluje pakiety, kanały, priorytety, ponownie wysyła itp., Jednocześnie wysyłając tylko pojedyncze pakiety UDP.

Przeczytaj poradnik na temat gier lub po prostu skorzystaj z ENet, który implementuje wiele pomysłów Glenna Fiedlera.

Lub możesz po prostu użyć TCP, jeśli twoja gra nie wymaga reakcji drgań. Następnie wszystkie problemy związane z grupowaniem, wysyłaniem i ACK znikają. Jednak nadal chciałbyś, aby NSS zarządzał przepustowością i kanałami.

deft_code
źródło
0

Nie całkowicie „ignoruj ​​czas reakcji”. Dodanie kolejnych opóźnień 40 ms do już opóźnionych pakietów niewiele zyskuje. Jeśli dodajesz kilka klatek (przy 60 klatkach na sekundę), opóźniasz przetwarzanie pozycji, zaktualizuj kolejną parę klatek. Lepiej jest szybko akceptować pakiety i szybko je przetwarzać, aby zwiększyć dokładność symulacji.

Odniosłem wielki sukces w optymalizacji przepustowości, myśląc o minimalnych informacjach o stanie potrzebnych do przedstawienia tego, co jest widoczne na ekranie. Następnie patrząc na każdy kawałek danych i wybierając dla niego model. Informacje o pozycji mogą być wyrażane jako wartości delta w czasie. Możesz albo użyć do tego własnych modeli statystycznych i spędzić wieki na debugowaniu ich, albo możesz skorzystać z biblioteki, która ci pomoże. Wolę używać modelu zmiennoprzecinkowego tej biblioteki DataBlock_Predict_Float Ułatwia to optymalizację przepustowości wykorzystywanej na wykresie sceny gry.

Justin
źródło