Jak zaprojektować system powtórek


75

Jak więc zaprojektować system powtórek?

Możesz go znać z niektórych gier, takich jak Warcraft 3 lub Starcraft, w których możesz ponownie obejrzeć grę po jej rozegraniu.

Otrzymujesz stosunkowo mały plik powtórki. Więc moje pytania to:

  • Jak zapisać dane? (format niestandardowy?) (mały rozmiar pliku)
  • Co zostanie zapisane?
  • Jak sprawić, by był ogólny, aby można go było używać w innych grach do rejestrowania przedziału czasu (na przykład nie pełnego dopasowania)?
  • Umożliwić przewijanie do przodu i do tyłu (WC3 nie było do tyłu, o ile pamiętam)

3
Mimo że poniższe odpowiedzi dostarczają wielu cennych informacji, chciałem tylko podkreślić , jak ważne jest, aby twoja gra / silnik były wysoce deterministyczne ( en.wikipedia.org/wiki/Deterministic_algorytm ), ponieważ jest to niezbędne do osiągnięcia celu.
Ari Patrick

2
Zauważ też, że silniki fizyki nie są deterministyczne (Havok twierdzi, że to ...), więc rozwiązanie polegające na przechowywaniu danych wejściowych i znaczników czasu przyniesie różne wyniki za każdym razem, jeśli gra wykorzystuje fizykę.
Samaursa

5
Większość silników fizykalnych jest deterministycznych, o ile używasz stałego timestepu, co i tak powinieneś robić. Byłbym bardzo zaskoczony, gdyby Havok nie był.

4
Deterministyczny oznacza te same dane wejściowe = te same dane wyjściowe. Jeśli masz zmiennoprzecinkowe na jednej platformie i podwaja się na innej (na przykład) lub umyślnie wyłączyłeś standardową implementację zmiennoprzecinkową IEEE, oznacza to, że nie korzystasz z tych samych danych wejściowych, nie oznacza to, że nie jest deterministyczna.

3
Czy to ja, czy to pytanie dostaje nagrodę co drugi tydzień?
Kaczka komunistyczna

Odpowiedzi:


39

Ten znakomity artykuł obejmuje wiele problemów: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php

Kilka rzeczy, o których artykuł wspomina i robi dobrze:

  • twoja gra musi być deterministyczna.
  • rejestruje początkowy stan systemów gry na pierwszej klatce i tylko dane wejściowe gracza podczas gry.
  • kwantyzować dane wejściowe do niższej liczby bitów. To znaczy. reprezentują zmiennoprzecinkowe w różnych zakresach (np. [0, 1] lub [-1, 1] zakres w mniejszej liczbie bitów. Skwantyzowane dane wejściowe należy uzyskać również podczas rzeczywistej gry.
  • użyj jednego bitu, aby ustalić, czy strumień wejściowy ma nowe dane. Ponieważ niektóre strumienie nie zmieniają się często, wykorzystuje to czasową spójność danych wejściowych.

Jednym ze sposobów dalszego ulepszenia współczynnika kompresji w większości przypadków byłoby oddzielenie wszystkich strumieni wejściowych i pełne ich zakodowanie niezależnie. To będzie zwycięstwo nad techniką kodowania delta, jeśli zakodujesz swój bieg w 8-bitach, a sam bieg przekroczy 8 klatek (bardzo prawdopodobne, chyba że twoja gra jest prawdziwym przyciskiem masher). Wykorzystałem tę technikę w grze wyścigowej, aby skompresować 8 minut danych wejściowych od 2 graczy podczas wyścigu po torze do zaledwie kilkuset bajtów.

Jeśli chodzi o uczynienie takiego systemu wielokrotnego użytku, sprawiłem, że system powtórek zajmuje się ogólnymi strumieniami wejściowymi, ale zapewniam również haki, które pozwalają logice specyficznej dla gry na wprowadzenie klawiatury / klawiatury / myszy do tych strumieni.

Jeśli chcesz szybko przewijać do tyłu lub poszukiwać losowo, możesz zapisać punkt kontrolny (pełną gamestate) co N klatek. N należy wybrać, aby zminimalizować rozmiar pliku powtórki, a także upewnić się, że czas oczekiwania odtwarzacza jest rozsądny, gdy stan jest odtwarzany do wybranego punktu. Jednym ze sposobów obejścia tego jest zapewnienie losowych poszukiwań tylko w tych dokładnych lokalizacjach punktów kontrolnych. Przewijanie polega na ustawieniu stanu gry na punkt kontrolny bezpośrednio przed ramką, o której mowa, a następnie odtwarzaniu danych wejściowych, aż dojdziesz do bieżącej klatki. Jeśli jednak N jest zbyt duże, możesz zaczepiać co kilka klatek. Jednym ze sposobów na wygładzenie tych zaczepów jest asynchroniczne wstępne buforowanie ramek między poprzednimi 2 punktami kontrolnymi podczas odtwarzania buforowanej ramki z bieżącego regionu punktu kontrolnego.


jeśli w grę wchodzi RNG, dołącz wyniki wspomnianego RNG do strumieni
maniak ratchet

1
@ratchet freak: Dzięki deterministycznemu wykorzystaniu PRNG możesz sobie poradzić przechowując tylko jego nasiona podczas punktów kontrolnych.
NonNumeric

22

Oprócz rozwiązania „upewnij się, że naciśnięcia klawiszy są odtwarzalne”, co może być zaskakująco trudne, możesz po prostu zapisać cały stan gry na każdej klatce. Przy odrobinie sprytnej kompresji możesz go znacznie ściśnąć. W ten sposób Braid obsługuje swój kod przewijania czasu i działa całkiem dobrze.

Ponieważ i tak potrzebujesz do tego celu punktu kontrolnego, możesz spróbować wdrożyć go w prosty sposób przed skomplikowaniem rzeczy.


2
+1 Za pomocą sprytnej kompresji możesz naprawdę zmniejszyć ilość danych, które musisz przechowywać (na przykład nie przechowuj stanu, jeśli nie zmienił się w porównaniu z ostatnim stanem, który zapisałeś dla bieżącego obiektu) . Próbowałem już tego z fizyką i działa naprawdę dobrze. Jeśli nie masz fizyki i nie chcesz przewijać całej gry, wybrałbym rozwiązanie Joe tylko dlatego, że stworzy najmniejsze możliwe pliki, w takim przypadku, jeśli chcesz również przewinąć do tyłu, możesz zapisać tylko ostatnie nsekundy gra.
Samaursa,

@Samaursa - Jeśli używasz standardowych bibliotek kompresji (np. Gzip), otrzymasz taką samą (prawdopodobnie lepszą) kompresję bez konieczności ręcznego wykonywania takich czynności, jak sprawdzanie, czy stan się zmienił, czy nie.
Justin

2
@Kragen: Niezupełnie prawda. Standardowe biblioteki kompresji są z pewnością dobre, ale często nie będą mogły skorzystać z wiedzy specyficznej dla domeny. Jeśli możesz im trochę pomóc, umieszczając obok siebie podobne dane i usuwając rzeczy, które tak naprawdę się nie zmieniły, możesz znacznie je zniszczyć.
ZorbaTHut,

1
@ZorbaTHut Teoretycznie tak, ale czy w praktyce naprawdę warto?
Justin

4
To, czy warto, zależy od wysiłku, zależy całkowicie od ilości posiadanych danych. Jeśli masz RTS z setkami lub tysiącami jednostek, prawdopodobnie ma to znaczenie. Jeśli chcesz zapisać powtórki w pamięci, np. Warkocz, prawdopodobnie ma to znaczenie.

21

Możesz zobaczyć swój system tak, jakby składał się z szeregu stanów i funkcji, gdzie funkcja f[j]z wejściem x[j]zmienia stan systemu s[j]w stan s[j+1], tak jak:

s[j+1] = f[j](s[j], x[j])

Stan jest wyjaśnieniem całego twojego świata. Lokalizacje gracza, lokalizacja wroga, wynik, pozostała amunicja itp. Wszystko, czego potrzebujesz, aby narysować ramkę gry.

Funkcja to wszystko, co może wpłynąć na świat. Zmiana ramki, naciśnięcie klawisza, pakiet sieciowy.

Dane wejściowe to dane pobierane przez funkcję. Zmiana ramki może zająć trochę czasu od ostatniego przejścia, naciśnięcie klawisza może obejmować faktyczny naciśnięty klawisz, a także to, czy naciśnięto klawisz Shift.

Ze względu na to wyjaśnienie przyjmuję następujące założenia:

Założenie 1:

Ilość stanów dla danego przebiegu gry jest znacznie większa niż liczba funkcji. Prawdopodobnie masz setki tysięcy stanów, ale tylko kilkadziesiąt funkcji (zmiana ramki, naciśnięcie klawisza, pakiet sieciowy itp.). Oczywiście ilość danych wejściowych musi być równa liczbie stanów minus jeden.

Założenie 2:

Koszt przestrzenny (pamięć, dysk) przechowywania pojedynczego stanu jest znacznie większy niż koszt przechowywania funkcji i jej danych wejściowych.

Założenie 3:

Czasowy koszt (czas) przedstawienia stanu jest podobny lub tylko o jeden lub dwa rzędy wielkości dłuższy niż koszt obliczenia funkcji w stanie.

W zależności od wymagań twojego systemu powtórek istnieje kilka sposobów na wdrożenie systemu powtórek, więc możemy zacząć od najprostszego. Zrobię też mały przykład, używając gry w szachy, zapisanej na kawałkach papieru.

Metoda 1:

Store s[0]...s[n]. To jest bardzo proste, bardzo proste. Z powodu założenia 2 koszt przestrzenny jest dość wysoki.

W przypadku szachów można to osiągnąć poprzez narysowanie całej planszy dla każdego ruchu.

Metoda 2:

Jeśli potrzebujesz tylko powtórki do przodu, możesz po prostu zapisać s[0], a następnie zapisać f[0]...f[n-1](pamiętaj, że jest to tylko nazwa identyfikatora funkcji) i x[0]...x[n-1](jakie były dane wejściowe dla każdej z tych funkcji). Aby odtworzyć, po prostu zacznij od s[0]i oblicz

s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])

i tak dalej...

Chcę tutaj zrobić małą adnotację. Kilku innych komentatorów powiedziało, że gra „musi być deterministyczna”. Każdy, kto to powie, musi ponownie wziąć Computer Science 101, ponieważ chyba że twoja gra ma być uruchamiana na komputerach kwantowych, WSZYSTKIE PROGRAMY KOMPUTEROWE SĄ DETERMINISTYCZNE¹. Właśnie dlatego komputery są tak niesamowite.

Ponieważ jednak Twój program najprawdopodobniej zależy od programów zewnętrznych, od bibliotek po faktyczną implementację procesora, upewnienie się, że funkcje zachowują się tak samo między platformami, może być dość trudne.

Jeśli używasz liczb pseudolosowych, możesz albo zapisać wygenerowane liczby jako część danych wejściowych x, albo zapisać stan funkcji prng jako część swojego stanu si jej implementację jako część funkcji f.

W przypadku szachów można to osiągnąć poprzez narysowanie początkowej planszy (która jest znana), a następnie opisanie każdego ruchu, mówiąc, który kawałek poszedł gdzie. Tak przy okazji, tak właśnie to robią.

Metoda 3:

Teraz najprawdopodobniej chcesz móc zagrać w swoją powtórkę. Oznacza to, że obliczyć s[n]dla dowolnego n. Korzystając z metody 2, musisz obliczyć, s[0]...s[n-1]zanim będziesz mógł obliczyć s[n], co zgodnie z założeniem 2 może być dość wolne.

Aby to zaimplementować, metoda 3 jest uogólnieniem metod 1 i 2: zapisz f[0]...f[n-1]i x[0]...x[n-1]podobnie jak metoda 2, ale także zapisz s[j]dla wszystkich j % Q == 0dla danej stałej Q. Mówiąc prościej, oznacza to, że przechowujesz zakładkę w jednym z każdego Qstanu. Na przykład Q == 100przechowujeszs[0], s[100], s[200]...

Aby obliczyć s[n]arbitralnie n, najpierw ładujesz poprzednio zapisane s[floor(n/Q)], a następnie oblicz wszystkie funkcje od floor(n/Q)do n. Co najwyżej będziesz obliczał Qfunkcje. Mniejsze wartości Qsą szybsze do obliczenia, ale zajmują znacznie więcej miejsca, podczas gdy większe wartości Qzużywają mniej miejsca, ale obliczanie trwa dłużej.

Metoda 3 z Q==1jest taka sama jak metoda 1, podczas gdy metoda 3 z Q==infjest taka sama jak metoda 2.

W przypadku szachów można to osiągnąć poprzez losowanie każdego ruchu, a także jednego na każde 10 plansz (dla Q==10).

Metoda 4:

Jeśli chcesz, aby odwrócić powtórka, można zrobić małą odmianą metody 3. Załóżmy Q==100, a chcesz obliczyć s[150]poprzez s[90]odwrotnie. W przypadku niezmodyfikowanej metody 3 konieczne będzie wykonanie 50 obliczeń, aby uzyskać, s[150]a następnie 49 dodatkowych obliczeń, aby uzyskać s[149]itd. Ale ponieważ już obliczono, s[149]aby uzyskać s[150], możesz utworzyć pamięć podręczną s[100]...s[150]przy s[150]pierwszym obliczeniu , a następnie jesteś już s[149]w pamięci podręcznej, gdy musisz ją wyświetlić.

Musisz tylko zregenerować pamięć podręczną za każdym razem, gdy musisz obliczyć s[j], j==(k*Q)-1dla dowolnego z nich k. Tym razem zwiększenie Qspowoduje mniejszy rozmiar (tylko dla pamięci podręcznej), ale dłuższe czasy (tylko do odtworzenia pamięci podręcznej). Optymalną wartość Qmożna obliczyć, jeśli znasz rozmiary i czasy wymagane do obliczenia stanów i funkcji.

W przypadku szachów można to osiągnąć poprzez narysowanie każdego ruchu, a także jednego na każde 10 plansz (dla Q==10), ale wymagałoby to również narysowania osobnego kawałka papieru, 10 ostatnich obliczonych przez ciebie plansz.

Metoda 5:

Jeśli stany po prostu zajmują zbyt dużo miejsca lub funkcje zajmują zbyt dużo czasu, możesz stworzyć rozwiązanie, które faktycznie implementuje (a nie podróbki) odwrotne odtwarzanie. Aby to zrobić, musisz utworzyć funkcje odwrotne dla każdej z posiadanych funkcji. Wymaga to jednak, aby każda z twoich funkcji była zastrzykiem. Jeśli jest to wykonalne, to dla f'oznaczenia odwrotności funkcji fobliczanie s[j-1]jest tak proste, jak

s[j-1] = f'[j-1](s[j], x[j-1])

Zauważ, że tutaj zarówno funkcja, jak i dane wejściowe j-1nie są j. Ta sama funkcja i dane wejściowe byłyby tymi, których użyłbyś podczas obliczeń

s[j] = f[j-1](s[j-1], x[j-1])

Tworzenie odwrotności tych funkcji jest trudną częścią. Jednak zwykle nie można, ponieważ niektóre dane stanu są zwykle tracone po każdej funkcji w grze.

Ta metoda, jak jest, może odwrócić obliczenia s[j-1], ale tylko jeśli masz s[j]. Oznacza to, że możesz oglądać powtórkę tylko od tyłu, zaczynając od momentu, w którym postanowiłeś ją odtworzyć wstecz. Jeśli chcesz odtwarzać wstecz z dowolnego miejsca, musisz to połączyć z metodą 4.

W przypadku szachów nie można tego zrealizować, ponieważ przy danej planszy i poprzednim ruchu możesz wiedzieć, który pionek został przeniesiony, ale nie skąd.

Metoda 6:

Wreszcie, jeśli nie możesz zagwarantować, że wszystkie twoje funkcje są zastrzykami, możesz zrobić małą sztuczkę, aby to zrobić. Zamiast zwracania przez każdą funkcję tylko nowego stanu, możesz także zwrócić odrzucone dane:

s[j+1], r[j] = f[j](s[j], x[j])

Gdzie r[j]są odrzucone dane. A następnie utwórz funkcje odwrotne, aby pobierały odrzucone dane:

s[j] = f'[j](s[j+1], x[j], r[j])

Oprócz f[j]i x[j]musisz także przechowywać r[j]dla każdej funkcji. Jeszcze raz, jeśli chcesz móc szukać, musisz przechowywać zakładki, na przykład metodą 4.

W przypadku szachów byłaby to ta sama metoda, co metoda 2, ale w przeciwieństwie do metody 2, która mówi tylko, który kawałek idzie, gdzie trzeba, musisz również zapisać, skąd pochodzi każdy kawałek.

Realizacja:

Ponieważ działa to dla wszystkich rodzajów stanów, z wszystkimi rodzajami funkcji, dla konkretnej gry, możesz przyjąć kilka założeń, które ułatwią wdrożenie. W rzeczywistości, jeśli zaimplementujesz metodę 6 z całym stanem gry, nie tylko będziesz mógł odtworzyć dane, ale także cofnąć się w czasie i wznowić odtwarzanie od dowolnego momentu. To byłoby całkiem niesamowite.

Zamiast przechowywać cały stan gry, możesz po prostu przechowywać absolutne minimum wymagane do narysowania danego stanu i serializować te dane co ustalony czas. Twoje stany będą serializacjami, a twój wkład będzie teraz różnicą między dwiema serializacjami. Kluczem do tego jest to, że serializacja powinna się nieznacznie zmienić, jeśli stan świata również się zmieni. Ta różnica jest całkowicie odwracalna, więc wdrożenie metody 5 z zakładkami jest bardzo możliwe.

Widziałem to zaimplementowane w niektórych głównych grach, głównie do natychmiastowego odtwarzania ostatnich danych, gdy wystąpi zdarzenie (frag w FPS lub wynik w grach sportowych).

Mam nadzieję, że to wyjaśnienie nie było zbyt nudne.

¹ Nie oznacza to, że niektóre programy zachowują się tak, jakby były niedeterministyczne (takie jak MS Windows ^^). Poważnie, jeśli możesz stworzyć niedeterministyczny program na deterministycznym komputerze, możesz być całkiem pewien, że jednocześnie zdobędziesz medal Fields, nagrodę Turinga, a prawdopodobnie nawet Oscara i Grammy za wszystko, co jest warte.


W sekcji „WSZYSTKIE PROGRAMY KOMPUTEROWE SĄ DETERMINISTYCZNE”, zaniedbujesz rozważanie programów opartych na wątkach. Podczas gdy wątkowanie jest najczęściej używane do ładowania zasobów lub do oddzielania pętli renderowania, istnieją wyjątki od tego, i w tym momencie możesz nie być w stanie dłużej twierdzić, że jest to prawdziwy determinizm, chyba że przestrzegasz zasad ścisłego egzekwowania determinizmu. Same mechanizmy blokujące nie wystarczą. Nie byłoby możliwe udostępnianie ŻADNYCH zmiennych danych bez dodatkowej pracy. W wielu scenariuszach gra nie wymaga takiego poziomu ścisłości ze względu na siebie, ale może mieć na przykład powtórki.
krdluzni

1
@krdluzni Wątek, równoległość i liczby losowe z prawdziwych źródeł losowych nie powodują, że programy nie są deterministyczne. Czasy wątków, zakleszczenia, niezainicjowana pamięć, a nawet warunki wyścigu to tylko dodatkowe dane, które pobiera Twój program. Twój wybór, aby odrzucić te dane wejściowe lub nawet ich nie brać pod uwagę (z jakiegokolwiek powodu) nie wpłynie na fakt, że twój program wykona dokładnie to samo, biorąc pod uwagę te same dane wejściowe. „niedeterministyczny” jest bardzo precyzyjnym terminem informatyki, więc unikaj go, jeśli nie wiesz, co to znaczy.

@oscar. program sam lub w pełni kontrolowany przez programistę. Ponadto program, który nie jest deterministyczny, różni się znacznie od tego, że jest niedeterministyczny (w sensie automatu stanów). Rozumiem znaczenie tego terminu. Chciałbym, żeby wybrali coś innego niż przeciążanie wcześniejszego terminu.
krdluzni

@krdluzni Moim celem przy projektowaniu systemów powtórek z nieprzewidywalnymi elementami, takimi jak czasy wątków (jeśli wpływają one na twoją zdolność do dokładnego obliczenia powtórki), jest traktowanie ich jak każde inne źródło wejściowe, tak jak wkład użytkownika. Nie widzę, aby ktokolwiek narzekał, że program jest „niedeterministyczny”, ponieważ wymaga całkowicie nieprzewidywalnego wkładu użytkownika. Jeśli chodzi o termin, jest to niedokładne i mylące. Wolałbym, żeby używali czegoś w rodzaju „praktycznie nieprzewidywalnego” lub czegoś takiego. I nie, nie jest to niemożliwe, sprawdź debugowanie powtórek VMWare.

9

Jedną z rzeczy, których inne odpowiedzi jeszcze nie ujęły, jest niebezpieczeństwo pływaków. Nie można tworzyć w pełni deterministycznych aplikacji przy użyciu liczb zmiennoprzecinkowych.

Używając pływaków, możesz mieć całkowicie deterministyczny system, ale tylko jeśli:

  • Używanie dokładnie tego samego pliku binarnego
  • Używanie dokładnie tego samego procesora

Jest tak, ponieważ wewnętrzna reprezentacja liczb zmiennoprzecinkowych różni się w zależności od procesora - najbardziej dramatycznie między procesorami AMD i Intel. Dopóki wartości znajdują się w rejestrach FPU, są one bardziej dokładne niż wyglądają od strony C, więc wszelkie pośrednie obliczenia są wykonywane z większą precyzją.

To całkiem oczywiste, jak wpłynie to na bit AMD vs Intel - powiedzmy, że jeden używa 80-bitowych liczb zmiennoprzecinkowych, a drugi 64, na przykład - ale dlaczego ten sam wymóg binarny?

Jak powiedziałem, stosowana jest wyższa precyzja, o ile wartości znajdują się w rejestrach FPU . Oznacza to, że przy każdej ponownej kompilacji optymalizacja kompilatora może zamieniać wartości z rejestrów FPU i z nich, powodując nieznacznie różne wyniki.

Możesz w tym pomóc, ustawiając flagi _control87 () / _ controlfp () tak, aby używały najniższej możliwej precyzji. Jednak niektóre biblioteki również mogą tego dotykać (przynajmniej niektóre wersje d3d zrobiły to).


3
Za pomocą GCC można użyć opcji -ffloat-store, aby wymusić wartości z rejestrów i skrócić do 32/64 bitów precyzji, bez potrzeby martwienia się o inne biblioteki zepsute flagami kontrolnymi. Oczywiście wpłynie to negatywnie na twoją prędkość (ale podobnie jak każda inna kwantyzacja).

8

Zapisz stan początkowy generatorów liczb losowych. Następnie zapisz, oznaczone datą, każde wejście (mysz, klawiatura, sieć, cokolwiek). Jeśli masz grę sieciową, prawdopodobnie masz to wszystko na swoim miejscu.

Ponownie ustaw RNG i odtwórz wejście. Otóż ​​to.

To nie rozwiązuje ponownego uzwojenia, dla którego nie ma ogólnego rozwiązania, poza odtwarzaniem od początku tak szybko, jak to możliwe. Możesz poprawić wydajność w tym celu, sprawdzając cały stan gry co X sekund, wtedy będziesz musiał odtworzyć tylko tyle, ale cały stan gry może być zbyt kosztowny.

Szczegóły formatu pliku nie mają znaczenia, ale większość silników ma już sposób na serializację poleceń i określanie stanu - do pracy w sieci, zapisywania itp. Po prostu użyj tego.


4

Głosowałbym przeciwko deterministycznemu odtwarzaniu. Zapisuje stan każdego bytu co 1/5 sekundy, jest DALEJ prostszy i DOLNIE podatny na błędy.

Zapisz tylko to, co chcesz pokazać podczas odtwarzania - jeśli to tylko pozycja i kurs, w porządku, jeśli chcesz również pokazać statystyki, zapisz to również, ale ogólnie zapisz jak najmniej.

Popraw kodowanie. Używaj jak najmniej bitów do wszystkiego. Powtórka nie musi być idealna, o ile wygląda wystarczająco dobrze. Nawet jeśli używasz liczb zmiennoprzecinkowych do, powiedzmy, nagłówka, możesz zapisać go w bajcie i uzyskać 256 możliwych wartości (precyzja 1,4º). To może wystarczyć lub nawet za dużo dla twojego konkretnego problemu.

Użyj kodowania delta. O ile twoje byty nie teleportują się (a jeśli tak, potraktuj sprawę osobno), koduj pozycje jako różnicę między nową pozycją a starą pozycją - w przypadku krótkich ruchów możesz uzyskać znacznie mniej bitów, niż potrzebujesz pełnych pozycji .

Jeśli chcesz łatwo przewijać do tyłu, dodaj klatki kluczowe (pełne dane, bez delt) co N ramek. W ten sposób można uniknąć mniejszej precyzji dla delt i innych wartości, błędy zaokrąglania nie będą tak problematyczne, jeśli okresowo resetujesz do „prawdziwych” wartości.

Na koniec zapakuj całość :)


1
Zależy to jednak trochę od rodzaju gry.
Jari Komppa

Byłbym bardzo ostrożny z tym stwierdzeniem. Szczególnie w przypadku większych projektów z zależnościami stron trzecich ratowanie stanu może być niemożliwe. Podczas resetowania i odtwarzania wejście jest zawsze możliwe.
TomSmartBishop

2

To trudne. Przede wszystkim przede wszystkim przeczytaj odpowiedzi Jari Komppy.

Powtórka wykonana na moim komputerze może nie działać na tym komputerze, ponieważ wynik zmiennoprzecinkowy jest nieznacznie inny. To ważna sprawa.

Ale potem, jeśli masz losowe liczby, należy zapisać wartość początkową w powtórce. Następnie załaduj wszystkie stany domyślne i ustaw liczbę losową na to ziarno. Stamtąd można po prostu zapisać bieżący stan klawisza / myszy i czas, przez jaki był w ten sposób. Następnie uruchom wszystkie zdarzenia, używając tego jako danych wejściowych.

Aby skakać po plikach (co jest znacznie trudniejsze), musisz zrzucić THE MEMORY. Tak jak tam, gdzie jest każda jednostka, pieniądze, czas mija, cały stan gry. Następnie szybkie przewijanie do przodu, ale odtwarzanie wszystkiego oprócz pomijania renderowania, dźwięku itp., Aż dojdziesz do żądanego miejsca docelowego. Może się to zdarzać co minutę lub 5 minut w zależności od tego, jak szybko jest do przodu.

Główne punkty to: - Radzenie sobie z przypadkowymi liczbami - Kopiowanie danych wejściowych (odtwarzacza (-ów) i odtwarzacza (-ów) zdalnego) - Stan zrzutu do przeskakiwania plików i ...


2

Jestem nieco zaskoczony, że nikt nie wspomniał o tej opcji, ale jeśli twoja gra ma składnik do gry wieloosobowej, być może wykonałeś już dużo ciężkiej pracy niezbędnej do tej funkcji. W końcu czym jest gra wieloosobowa, ale próba ponownego odtworzenia ruchów innej osoby w (nieco) innym czasie na twoim komputerze?

Daje to również korzyści wynikające z mniejszego rozmiaru pliku jako efektu ubocznego, ponownie zakładając, że pracujesz nad kodem sieci przyjaznym dla przepustowości.

Na wiele sposobów łączy zarówno opcje „bądź bardzo deterministyczny”, jak i „rejestruj wszystko”. Nadal będziesz potrzebować determinizmu - jeśli twoja gra jest zasadniczo botami ponownie grającymi w dokładnie taki sam sposób, w jaki grałeś, niezależnie od tego, jakie podejmą działania, które mogą przynieść losowe wyniki, muszą mieć ten sam wynik.

Format danych może być tak prosty jak zrzut ruchu sieciowego, choć wyobrażam sobie, że nie zaszkodzi go trochę wyczyścić (w końcu nie musisz się martwić opóźnieniem przy ponownym odtwarzaniu). Możesz odtworzyć tylko część gry, korzystając z mechanizmu punktu kontrolnego, o którym wspominali inni ludzie - zazwyczaj gra dla wielu graczy i tak co jakiś czas wysyła pełny stan aktualizacji gry, więc znowu mogłeś już wykonać tę pracę.


0

Aby uzyskać możliwie najmniejszy plik powtórki, upewnij się, że gra jest deterministyczna. Zwykle polega to na spojrzeniu na generator liczb losowych i sprawdzeniu, gdzie jest on wykorzystywany w logice gry.

Najprawdopodobniej będziesz musiał mieć logikę gry RNG i wszystko inne RNG do takich rzeczy jak GUI, efekty cząsteczkowe, dźwięki. Gdy to zrobisz, musisz zapisać stan początkowy logiki gry RNG, a następnie polecenia gry wszystkich graczy w każdej klatce.

W wielu grach istnieje poziom abstrakcji między wejściem a logiką gry, w której wejście zamienia się w polecenia. Na przykład naciśnięcie przycisku A na kontrolerze powoduje, że polecenie cyfrowe „skok” jest ustawione na true, a logika gry reaguje na polecenia bez bezpośredniego sprawdzania kontrolera. W ten sposób wystarczy nagrać polecenia wpływające na logikę gry (nie trzeba rejestrować polecenia „Wstrzymaj”) i najprawdopodobniej dane te będą mniejsze niż dane kontrolera. Nie musisz się też martwić rejestrowaniem stanu schematu sterowania, na wypadek gdyby gracz zdecydował się zmienić przypisanie przycisków.

Przewijanie do tyłu jest trudnym problemem przy użyciu metody deterministycznej i innym niż użycie migawki stanu gry i szybkiego przewijania do momentu, w którym chcesz spojrzeć, nie ma wiele do zrobienia poza rejestrowaniem całego stanu gry w każdej klatce.

Z drugiej strony szybkie przewijanie do przodu jest z pewnością wykonalne. Dopóki twoja logika gry nie zależy od renderowania, możesz uruchomić logikę gry tyle razy, ile chcesz, przed renderowaniem nowej ramy gry. Szybkość szybkiego przewijania będzie po prostu ograniczona przez maszynę. Jeśli chcesz przeskakiwać do przodu w dużych przyrostach, musisz użyć tej samej metody migawki, co w przypadku przewijania.

Być może najważniejszą częścią pisania systemu powtórek opartego na determinizmie jest zapisanie strumienia danych debugowania. Ten strumień debugowania zawiera migawkę jak największej ilości informacji w każdej ramce (nasiona RNG, transformacje encji, animacje itp.) I jest w stanie przetestować zarejestrowany strumień debugowania pod kątem stanu gry podczas powtórek. Umożliwi to szybkie powiadomienie o niezgodnościach na końcu dowolnej ramki. Pozwoli to zaoszczędzić niezliczone godziny na ciągnięciu włosów z nieznanych niedeterministycznych błędów. Coś tak prostego jak niezainicjowana zmienna zepsuje wszystko o jedenastej godzinie.

UWAGA: Jeśli gra wymaga dynamicznego przesyłania strumieniowego treści lub logiki gry dotyczy wielu wątków lub różnych rdzeni ... powodzenia.


0

Aby włączyć zarówno nagrywanie, jak i przewijanie, zapisz wszystkie zdarzenia (wygenerowane przez użytkownika, wygenerowane przez timer, wygenerowane przez komunikację itp.)

Dla każdego zdarzenia rejestruj czas zdarzenia, co zostało zmienione, poprzednie wartości, nowe wartości.

Obliczonych wartości nie trzeba rejestrować, chyba że obliczenia są losowe
(w takich przypadkach można albo zapisać obliczone wartości, albo zapisać zmiany w ziarnie po każdym losowym obliczeniu).

Zapisane dane to lista zmian.
Zmiany można zapisać w różnych formatach (binarny, xml, ...).
Zmiana składa się z identyfikatora jednostki, nazwy właściwości, starej wartości, nowej wartości.

Upewnij się, że Twój system może odtworzyć te zmiany (uzyskać dostęp do pożądanego elementu, zmienić pożądaną właściwość do nowego stanu lub do poprzedniego stanu).

Przykład:

  • czas od początku = t1, byt = gracz 1, właściwość = pozycja, zmieniono z a na b
  • czas od początku = t1, byt = system, właściwość = tryb gry, zmieniono z c na d
  • czas od początku = t2, byt = gracz 2, właściwość = stan, zmieniono z e na f
  • Aby umożliwić szybsze przewijanie do tyłu / szybkie przewijanie lub nagrywanie tylko niektórych zakresów
    czasowych, konieczne są klatki kluczowe - jeśli nagrywasz cały czas, co jakiś czas zapisuj cały stan gry.
    Jeśli nagrywasz tylko określone przedziały czasowe, na początku zapisz stan początkowy.


    -1

    Jeśli potrzebujesz pomysłów na wdrożenie swojego systemu powtórek, wyszukaj w Google, jak zaimplementować cofanie / ponawianie w aplikacji. Dla niektórych może być oczywiste, ale może nie dla wszystkich, że cofanie / ponawianie jest koncepcyjnie tym samym, co odtwarzanie gier. Jest to tylko szczególny przypadek, w którym można przewijać do tyłu i, w zależności od aplikacji, szukać określonego momentu w czasie.

    Przekonasz się, że nikt implementujący cofanie / ponawianie nie skarży się na deterministyczne / niedeterministyczne, zmienne zmiennoprzecinkowe lub określone procesory.


    Cofanie / ponawianie ma miejsce w aplikacjach, które same w sobie są zasadniczo deterministyczne, sterowane zdarzeniami i sygnalizują stan (np. Stanem edytora tekstu jest wyłącznie tekst i zaznaczenie, a nie cały układ, który można ponownie obliczyć).

    To oczywiste, że nigdy nie korzystałeś z aplikacji CAD / CAM, oprogramowania do projektowania obwodów, oprogramowania do śledzenia ruchu ani żadnej aplikacji z cofaniem / ponawianiem bardziej zaawansowanym niż edytor tekstu. Nie mówię, że kod cofania / ponawiania można skopiować w celu odtworzenia w grze, tylko że jest on koncepcyjnie taki sam (zapisz stany i odtwórz je później). Jednak główna struktura danych to nie kolejka, ale stos.
    Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
    Licensed under cc by-sa 3.0 with attribution required.