Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Jak działają multiplayerowe gry strategiczne?

Pamiętam jak dziś, kiedy po raz pierwszy zagrałem on-line w produkcję firmy Blizzard, strategię czasu rzeczywistego, grę Starcraft. Wtedy jako początkującego programisty, urzekł mnie fakt że to wszystko po prostu działa! Na modemie o oszałamiającej prędkości 3 KB/s dziesiątki jednostek poruszały się i walczył bez najmniejszego zająknięcia.

Po kilku latach przeczytałem artykuł podobny do tego wpisu, i zachwycił mnie geniusz ludzi którzy wymyślili w jaki sposób rozwiązać realizację protokół sieciowego w grach strategicznych. Może dzisiaj nie wydaje się to czymś bardzo odkrywczym, nie mniej, chciałbym podzielić się to wiedzą. Zakładam że bardzo dużo dzisiejszych gier działa w podobny sposób. Nawet jeśli nie jesteś programistą a jedynie graczem, zapraszam do zapoznania się z dalszą treścią i zrozumienia zasad na jakich bazują gry strategiczne.

Powróćmy do pierwotnego zadania, wprawić w ruch dziesiątki a często i setki, tysiące jednostek w taki sposób aby każdy z graczy widział na ekranie ten sam efekt, nie przesyłając przy tym ilości danych równych objętości samej gry. Oczywiście to co może wydawać się najłatwiejsze to wysyłanie stanu mapy/jednostek w określonych interwałach czasowych, jednakże danych takich było by bardzo dużo i niekoniecznie mogłyby dojść na czas, aby zapewnić płynną rozgrywkę.

Rozgrywka jako funkcja matematyczna

Patrząc nieco abstrakcyjnie, musimy potraktować cały mechanizm rozgrywki jako jedną dużą funkcję, prawdopodobnie wielu zmiennych. Tak jak funkcja sinus dla danego argumentu w każdym zakątku świata i czasie daje ten sam wynik, tak samo nasza gra, dla określonych danych wejściowych musi dawać zawsze ten sam wyjściowy stan gry/mapy/jednostek.

W takiej sytuacji, przesyłając jedynie argumenty tej funkcji, jesteśmy w stanie odtworzyć u wszystkich klientów sieciowych ten sam stan. Brzmi to za pewno jak przesyłanie całego stanu mapy, jednak tak nie jest.

Zagrajmy zatem

Przejdźmy do żywego przykładu który najlepiej zobrazuje realizację protokołu. Zakładamy że początkowy stan gry jest ogólnie znany wszystkim klientom gry, czyli posiadają oni informację o całej mapie i wszystkich graczach, nawet tych który gracz nie widzi (jest to pewna wada tego rozwiązania ale o tym później). Gra zaczyna toczyć się własnym życiem, jeśli żaden z graczy nie podejmował by interakcji, stan gry zmienia się w ustalony sposób, przykładowo nic się nie dzieje ;). Ok, to jednak była by dość nudna gra. Zatem nasz bohater postanawia wydać rozkaz jednostce X aby przemieściła się do innej lokacji. Co robimy my jako programiści? Wysyłamy tylko informacje o zdarzeniu jakie zaszło, i nie jest to nowa pozycja jednostki! Informujemy pozostałych klientów gry o samym rozkazie, czyli przykładowo że jednostka X została wysłana do lokalizacji o takich i takich współrzędnych, jednak w tej chwili czasu jeszcze się tam nie znajduje.

Co robią pozostali klienci gry? Odtwarzają sytuację na planszy jak film na bazie otrzymanej informacji. I tutaj dość istotna kwestia się pojawia. Algorytm prowadzący grę, zmieniający jej stan, musi być pozbawiony jakichkolwiek elementów czysto losowych. Nie może dojść do sytuacji w której na te same dane wejściowe gra u jednego z graczy zareaguje inaczej niż u innego - musi to być jak wspomniana funkcja matematyczna, jednemu argumentowi przypisana jest jedna i zawsze ta sama wartość. Wraz z upływem czasu, ilość napływających informacji jest coraz większa, a każda z nich mówi nam tylko co mamy zmodyfikować w stanie gry. Po kilkuset tysiącach takich paczek, wszyscy gracze muszą wciąż widzieć ten sam obraz rozgrywki.

Company of Heroes – setki jednostek, wybuchów i akcji w rozgrywce sieciowej

Synchronizacja

Wiemy już co przesyłać, pytanie tylko jak, aby wszystko to działo się w tej samej chwili czasu. Innymi słowy, nie tylko musimy reagować tak samo u wszystkich klientów na dane wejściowe, ale dane te musimy przetwarzać dokładnie w tej samej klatce czasowej rozgrywki aby wywołały ten sam efekt (niekonieczne w tej samej chwili czasu bezwzględnego). Wydany rozkaz w klatce A spowoduje inną reakcję gry niż ten sam rozkaz w klatce A+1, co jest rzeczą naturalną. Dodając do tego opóźnienie związane z przejściem pakietu po sieci internet, mamy niemal gwarancję, że nie uda się już przeliczyć danych gry w tej samej klatce co u klienta który je wysłał.

I tutaj dochodzimy do kwintesencji zagadnienia, gra musi reagować z opóźnieniem na rozkazy wydane przez gracza, nawet na maszynie na której on sam gra. Ten sztuczny czas opóźnienia potrzebny nam jest na wysłanie tego co gracz zrobił zanim u innych gra dotrze do tej samej klatki.

Mówiąc bardziej obrazowo, rozpoczynamy akcję ataku jednostką gry w klatce czasowej A, program jednak nie wykonuje jeszcze tego polecenia a jedynie zapisuje je sobie, wraz z czasem wykonania równym przykładowo A + 100. Wysyłamy je do innych graczy u których dane te trafiają do tej samej kolejki. Gdy licznik klatek osiągnie wartość A + 100, u wszystkich grających zostanie wykonana ta sama procedura postępowania.

Oczywiście to zaplanowane opóźnienie wprowadza pewien dyskomfort gry, im większe ono jest. Aby zapewnić bezproblemowy przebieg potyczki, czas ten musi być większy niż największe opóźnienie między graczami (ping). Nie mniej, w grach strategicznych nawet czas zagapienia się jednostek o 1 sekundę nie powinien być specjalnie uciążliwy (aczkolwiek zależy to od dynamiki gry). Wspomniana na początku gra Starcraft, posiadała ów czas regulowany dynamicznie przez graczy, sławne Extra high latency. Najzabawniejsze było gdy nieobeznane osoby zmieniały go właśnie na tę maksymalną wartość, nie wiedząc do czego to służy, podczas gdy na najniższej wartości gra radziła sobie doskonale a przy tym jednostki szybciej reagowały.

Idąc dalej, nie wysyłamy każdego rozkazu od razu po jego wystąpieniu. Aby to wszystko miało ręce i nogi musimy grupować rozkazy. Podsumujmy wszystko i rozpiszmy w etapy postępowania:

Etap. Klatka czasowa - Czynność

1. A - Zbieramy rozkazy wydane przez gracza ustawiając ich czas wykonania na klatkę A + 2t.
2. A + t - Wysyłamy zbiór zgromadzonych rozkazów do pozostałych klientów gry.
3. A + 2t - Wykonujemy zaplanowane wcześniej rozkazy własne oraz otrzymane.

Każdy z etapów trwa czas równy t, opóźnienie reakcji jednostki w stosunku do wydania jej rozkazu wynosi 2t. Istotną rzeczą o jakiej należy pamiętać to zazębianie się w/w etapów. W każdym przedziale czasu t, jednocześnie wykonujemy wszystkie 3 etapy jednocześnie. Czyli, zbieramy aktualne rozkazy (1), wysyłamy te zebrane etap wcześniej (2), oraz wykonujemy to co otrzymaliśmy dwa etapy temu (3). Jest to konieczne aby zapewnić ciągłość rozgrywki.

Opóźnienia

Nie wspomniałem jeszcze o jednym ważnym szczególe. Wraz z wysyłanymi rozkazami musimy wysłać informację o tym do której klatki czasowej dany klient gry może dojść. W etapie drugim dodajemy dane mówiące o tym że timer gry może dokonać obliczeń do momentu A + 3t, gdyż to co aktualnie wysłaliśmy zawiera dane do tej chwili czasowej. Jeśli gra przebiega bez nieprzewidywalnych opóźnień, to zanim dotrzemy do tej klatki, powinniśmy otrzymać kolejne dane które wydłużą nam ten czas i tak non-stop.

Co jednak gdy nie otrzymamy opisanych pakietów na czas? Zatrzymujemy grę do czasu aż dotrze do nas brakująca paczka rozkazów. Jeśli jest to spowodowane nagłym pogorszeniem się połączenia z jednym tylko graczem, zamrożenie powinno nastąpić u całej reszty (jak i u samego winowajcy z racji tego że nie otrzyma on danych od reszty). Jeśli czas wstrzymania przekracza jakąś ustaloną wartość (np: 3 sekundy), wyświetlamy okienko informujące że ten a ten rozgrywający nie odpowiada.

Okno oczekiwania na gracza w grze Starcraft

Ograniczenia

Ten sposób realizacji mimo iż jest chyba najlepszym z możliwych ma pewne ograniczenia i wady. Po pierwsze, nie możemy stosować danych losowych gdyż musiały by one we wszystkich instancjach gry być identyczne. Niedogodność tą możemy ominąć inicjując generator liczb losowych tym samym ziarnem u wszystkich klientów, bądź samemu generować liczbę pseudo losową, na przykład za pomocą funkcji mieszających dane gry (które zmieniają się dość dynamicznie, a są u wszystkich takie same).

Drugim poważnym problemem jest desynchronizacja gry. Jako że obecny stan gry jest stanem gry w chwili poprzedniej zmienionym o aktualne dane, zatem im dalej tym większa szansa na to że coś się „rozjedzie”. Dobrym zwyczajem jest wprowadzenie sprawdzania jakiejś sumy kontrolnej stanu gry aby wykryć taki moment. Oczywiście jeśli protokół jest dobrze napisany a transmisja danych pewna, nie ma prawa dojść do takiej sytuacji. Niestety z doświadczenia wiem, że zdarzają się takie przypadki w wielu grach (np: Settlers 3, Company of Heroes). Po wystąpieniu błędu synchronizacji dalsza gra jest już niemożliwa. Jeśli porcja danych opisująca całościowy stan gry nie jest zbyt duża, możemy pokusić się o jej przesłanie i ponowną synchronizację. Jednak patrząc na to ile pamięci zajmują obecne gry (np.: 1GB), zakładając nawet że tylko 10% z tego stanowią dane opisujące świat gry, i tak jest to bardzo dużo.

Błąd synchronizacji w grze Company of Heroes

Jeśli gra oferuje system powtórek, to z pewnością nie umożliwia ich przewijania. Jest to konsekwencją tego że zapisywane są aktualne rozkazy, które służą do odtworzenia aktualnego stanu gry. Trzeba zatem przetworzyć wszystko po kolei aby zobaczyć ost. 10 sekund potyczki. Oczywiście możemy przyspieszyć obliczenia w zależności od skomplikowania gry. Jednakże jeśli produkcja jest tak wymagająca, że rozgrywka w czasie 1:1 obciąża znacznie najnowsze komputery, to nie ma fizycznej możliwości na przeskoczenie dalej.

Na sam koniec zostaje problem bezpieczeństwa. Gra musi dysponować pełnymi informacjami o całej sytuacji rozgrywki, czyli nawet o tych graczach których grający nie widzi na mapie. Niesie to ze sobą pewne niebezpieczeństwa w postaci modyfikacji gry i odczytania tych informacji. Patrząc z drugiej strony, niemożliwe jest wpływanie na te dane, gdyż wystąpiły by błędy synchronizacji.

Podsumowanie

Mam nadzieję że przybliżyłem ten temat w sposób dostatecznie jasny. Zagadnienie to wymaga własnego przemyślenia, aby dobrze zrozumieć co kiedy zebrać i wysłać by trybiki gry się sprawnie kręciły. 

internet programowanie gry

Komentarze

0 nowych
tfl   8 #1 13.11.2011 09:18

Ortografia!

4lpha   10 #2 13.11.2011 09:33

Świetny wpis, dobrze, że wylądował na głównej.

Wszystko zależy od tego jak twórcy gry zaplanowali sobie multi.
Tyczy się to nie tylko strategii, bo niektóre FPS są mniej odporne na lagi od innych.

Może pokusisz się o podobny wpis dotyczący FPS? ;)

M@ster   17 #3 13.11.2011 11:10

@sunbeam96
FPSy działają nieco inaczej, o ile strategie tu opisane chodzą w trybie synchronicznym, tzn. że o każdego gracza jest stricte to samo na ekranie (pozycje jednostek itd), to gry akcji działają asynchronicznie.

Oznacza to nie mniej nie więcej że u każdego gracza pozycje tej samej jednostki mogą być różne (czasami o piksel ale jednak). W strzelankach nie może być opóźnienia u gracza między akcją oddania strzału a jej zobaczeniem na ekranie. To samo z samym poruszaniem :) Jeśli po naciśnięciu klawisza w lewo postać zareagowała by dopiero po np: 500ms to szlak by Cię trafił na dłuższą metę ;).

Nie znam przykładowych implementacji ale gdybym ja coś takiego kodził to prawdopodobnie oprócz przesyłania samych akcji aproksymował bym pozycję graczy na bazie jej aktualnego ruchu. Czyli jeśli wiem że sekundę temu gracz X biegł tędy prosto tzn. że jest duża szansa że w chwili obecnej dalej tu biegnie.

Ogółem opiera się to na pewnych przybliżeniach, dlatego czasami się widzi jak postać z grze FPS przeskakuje z miejsca na miejsce na skutek laga, albo nie zalicza nam strzału mimo iż na naszej maszynie widać było że trafiliśmy (tutaj amatorska gra Soldat miała z tym duże problemy ;)).

Aczkolwiek w niektórych grach FPS można włączyć synchronizację, np.: Soldier of Fortune II - ale wtedy praktycznie się grać nie da :p no jedynie poza hostem u którego wszystko jest liczone.

tfl   8 #4 13.11.2011 11:37

@up

Jest taki znany, rosyjski naukowiec Igor Wydajemniesie. Slynny jest z tego, ze swoje toerie prezentuje w formie pseudonaukowiej, ale sa one niepoparte zadnymi doswiadczeniami, czesto gesto sa tylko prezentacja wlasnego pumktu widzenia na tematy, o ktorych slyszal od kogos innego. Zawsze takie teorie przedstawia jako wlasne.

Klienci gry o dobrym silniku sieciowym nigdy nie przysylaja do pozostalych klientow informacji. Zawsze odbywa sie to tylko_i_wylacznie do hosta gry. Z dwoch podowdow: jest to bezpieczniejsze i szybsze.

Host nigdy nie generuje sztucznego opoznienia. Wyobraz sobie (a na podstawie tekstu mozna zgadywac, ze wyobraznie masz bardzo bogata), ze leci sobie pakiet od klienta do serwera, ten generuje opoznie rzedu 5ms, serwer wysyla pakiet do drugiego gracza, ten odpowiada reakcja, pakiet idzie do serwera, 5ms opoznienia. Po jakims czasie opoznienie miedzy graczami siega minuty. A co jesli graczy jest wiecej niz 2?

Gra dziala jak klient. Jak klient gadu gadu, jak dc++, jak wszystko, co laczy sie z serwerem. Odpowiedzialnosc za jakos lacza jest po stronie gracza, uruchamiajacego swojego klienta gry. "Synchronicznosc" jest tylko w tym elemencie, ze klient moze jednoczesnie (wiec "synchronicznie") odbierac i wysylac pakiety informacji.

Wierutna bzdura jest to co napisales o FPSowych grach i "aproksymowaniu ruchu". Dziala to dokladnie tak samo. Przeskoki postaci (tzw warpy) czy inne efekty sa zwiazane z opoznieniami pakietow miedzy klientem a serwerem i moga byc wrecz sztucznie generowane. Problem pochodzi stad, ze klient nie generuje ruchu postaci (przeciwnika) jesli nie otrzymal informacji od serwera, a potem "nadrabia" (koryguje bedzie lepszym slowem) przestawiajac go w odpowiednie miejsce. A ze fakeshootami jest tak, ze klient reaguje na informacje od myszki. Zanim wysle pakiet.

M@ster   17 #5 13.11.2011 12:00

Początkowej wycieczki osobistej komentować nie będę.

[[ Klienci gry o dobrym silniku sieciowym nigdy nie przysylaja do pozostalych klientow informacji. Zawsze odbywa sie to tylko_i_wylacznie do hosta gry. Z dwoch podowdow: jest to bezpieczniejsze i szybsze. ]]

Jaki typ gry? Akurat pierwszy Starcraft nie miał ogóle hosta gry, tylko tworzył połączenia każdy z każdym. Między innymi dlatego jeden gracz z kiepskim łączem potrafił zabić całą grę. Miało to też swoje plusy, nie było sytuacji takich że host opuszcza grę i reszta w związku z tym też.
Nie wiem jak to dziś wygląda, w FPSach na pewno wszyscy do hosta się łączą, ale w strategiach niekoniecznie - to są całkowicie różne mechanizmy rozgrywki.

[[ Host nigdy nie generuje sztucznego opoznienia. ]]

Gdzie ja coś takiego napisałem? W ogóle nie rozumiem tego akapitu, misz masz.

[[ Wierutna bzdura jest to co napisales o FPSowych grach i "aproksymowaniu ruchu". ]]

Dlatego poprzedziłem swoją wypowiedź słowami "gdybym ja coś takiego kodził". A tak poza tym to napisałeś w tym akapicie dokładnie to samo co ja - może po prostu czytaj ze zrozumieniem. Pakiet wychodzi po dokonaniu akcji u gracza a w związku z tym inni to co zrobił widzą z opóźnieniem - a do tego czasu coś z postacią trzeba zrobić, no bo przecież gracze nie przeskakują Ci z miejsc na miejsce tak jak przychodzą pakiety tylko idą/biegną płynnie - zatem pozycje "między pakietowe" są aproksymowane.

tfl   8 #6 13.11.2011 13:05

@up

1 "Jaki typ gry? Akurat pierwszy Starcraft nie miał ogóle hosta gry"

Miał. Czy to po lanie (ipx/spx) czy to po tcp, czy to po battlenecie. Zawsze byl host, ktory stawial gre i klienci, ktorzy sie laczyli. Screena zalaczyc?

2. "Gdzie ja coś takiego napisałem? W ogóle nie rozumiem tego akapitu, misz masz. "

Tutaj: "I tutaj dochodzimy do kwintesencji zagadnienia, gra musi reagować z opóźnieniem na rozkazy wydane przez gracza, nawet na maszynie na której on sam gra. Ten sztuczny czas opóźnienia potrzebny nam jest na wysłanie tego co gracz zrobił zanim u innych gra dotrze do tej samej klatki. "


3 (w calosci, szkoda miejsca na wklejanki).

nie sa aproksymowane. Nie napisalismy tego samego. Nie rozumiesz istoty warpow, nie wiesz z czego wynikaja, na sile dorabiasz toerie Igorze Wydajemisiewiczu!

M@ster   17 #7 13.11.2011 13:29

1. Nie, w SC nie było hosta w trakcie gry - był host tworzący grę (no bo jakiś musi być), ale służył on tylko do inicjacji połączeń/gry. Sama rozgrywka nie miała jednego scentralizowanego miejsca obliczeń że tak to ujmę. SC w miarę możliwości (NAT itd.) tworzył połączenia każdy z każdym (UDP). Prosty test, stwórz grę dla 3 graczy, niech jeden będzie tym Twoim hostem, potem niech wyjdzie w trakcie gry i tamci dwaj dalej będą sobie grać. Gdyby był podział na hosta nie miało by coś takiego miejsca.

Sorry ale nie znasz tematu, to jak UI SC'ta wygląda to ja doskonale wiem, ale nie działa to tak jak Ci się wydaje.

2. No zgadza się, polecam empiryczne testy, SC, Settlers 3, Company of heroes po necie - wydaj rozkaz jednostce i na 100% nie zareaguje ona od razu, tylko będzie wymuszone opóźnienie zarezerwowane na rozesłanie tej informacji (wszystko jedno czy p2p czy via host) do pozostałych. Potem dla porównania sprawdź jak to wygląda vs CPU (czyli wszystko lokalnie), reakcja będzie natychmiastowa.

Porównaj to sobie do umawiania się z kumplem na piwo, jeśli masz na nie ochotę i jest godzina 16 to nie mówisz kumplowi z drugiego końca miasta przyjdź do baru na 16 bo na bank nie zdąży. Umawiacie się na 17, czyli i on i Ty czekacie 1h, tzn. Ty czekasz bo bar masz za rogiem a jemu ta 1h potrzebna jest na dotarcie.

Tak samo w tym co opisałem, ten czas jest rezerwowany na przejście pakietu, gdyż w strategiach rozgrywka jest prowadzona synchronicznie! tj. każdy grać musi widzieć dokładnie to samo "piksel w piksel". No nie chce mi się 2x tego samego tłumaczyć.

3. Dlatego w temacie gier FPS wyraziłem swoi przypuszczenia i jak ja jako programista bym ew. podszedł do tematu który nie jest rzeczą prostą.

Na dorabiam teorii żadnej, po prostu myślę logicznie i próbuje rozwiązać problem a nie powtarzam ślepo jednego słówka.

soanvig   10 #8 13.11.2011 13:56

Dobry tekst, dobrze, że uświadomiłeś mnie co do sztucznego opóźnienia. Ale napisałeś, że w grach strategicznych 1s nie robi różnicy, ale w strzelankach typu counterstrike?

M@ster   17 #9 13.11.2011 14:07

@soanvig
Tam się oczywiście takich mechanizmów nie stosuje :) bo była by to niezła sieczka. To zupełnie inny rodzaj gry a co za tym idzie sposób realizacji multiplayera.

Tak w skrócie zobacz mój pierwszy komentarz: http://www.dobreprogramy.pl/Jak-dzialaja-multiplayerowe-gry-strategiczne,Blog,28...

marcin_k   3 #10 13.11.2011 16:06

a co z grami mmo typu World of Warcraft. Tam na serwerze gra nieraz po kilka tysięcy osób, i nie bardzo moge sobie wyobrazić jak przedstawiony przez Ciebie algorytm mógłby się tam sprawdzić:)

command-dos   18 #11 13.11.2011 20:48

http://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking - tu jest, co nieco, napisane o działaniu sieciowych gier - nie chce mi się czytać, ale może kogoś zainteresuje...

M@ster   17 #12 13.11.2011 22:26

@marcin_k
Hmm, dobre pytanie :) To jest już MORPG (sry nie wiem jak te skróty lecą dokładnie ;)) no i z pewnością wygląda to inaczej, chociażby z tego względu że świat gry jest ogromny i podłączający się klient nie zna jego całkowitego stanu.

Strzelam że tutaj bardziej podobne to jest do typowego FPSa a niżeli strategii pod względem protokołu. Zasadniczo chodzisz tylko jedną postacią a w zasięgu wzroku masz co najwyżej kilkunastu graczy. Dodatkowo pełna synchronizacja nie jest tam potrzebna. Widok wprawdzie jest "strategiczny" ale zasada działania podchodzi w mojej opinii pod strzelankę, aczkolwiek nigdy w WoWa nie grałem.

Axles   17 #13 14.11.2011 14:56

Wpis warty głównej. Czekamy M@ster na kolejne. Pozdrawiam.

mgr.inz.Player   6 #14 21.11.2011 00:51

command-dos | 13.11.2011 20:48

Dzięki za link. Przydatny, szkoda że nie ma wersji w języku polskim.

  #15 26.05.2012 14:20

Polecam tą gierkę :) mnie pochłonęła :D
http://pl.desert-operations.com/?recruiter=63d41&world=d29ybGQx