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)
architecture
data-structure
file-format
game-recording
skalowalny
źródło
źródło
Odpowiedzi:
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:
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.
źródło
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.
źródło
n
sekundy gra.Możesz zobaczyć swój system tak, jakby składał się z szeregu stanów i funkcji, gdzie funkcja
f[j]
z wejściemx[j]
zmienia stan systemus[j]
w stans[j+1]
, tak jak: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) ix[0]...x[n-1]
(jakie były dane wejściowe dla każdej z tych funkcji). Aby odtworzyć, po prostu zacznij ods[0]
i obliczi 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 stanus
i jej implementację jako część funkcjif
.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 dowolnegon
. 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]
ix[0]...x[n-1]
podobnie jak metoda 2, ale także zapiszs[j]
dla wszystkichj % Q == 0
dla danej stałejQ
. Mówiąc prościej, oznacza to, że przechowujesz zakładkę w jednym z każdegoQ
stanu. Na przykładQ == 100
przechowujeszs[0], s[100], s[200]...
Aby obliczyć
s[n]
arbitralnien
, najpierw ładujesz poprzednio zapisanes[floor(n/Q)]
, a następnie oblicz wszystkie funkcje odfloor(n/Q)
don
. Co najwyżej będziesz obliczałQ
funkcje. Mniejsze wartościQ
są szybsze do obliczenia, ale zajmują znacznie więcej miejsca, podczas gdy większe wartościQ
zużywają mniej miejsca, ale obliczanie trwa dłużej.Metoda 3 z
Q==1
jest taka sama jak metoda 1, podczas gdy metoda 3 zQ==inf
jest 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]
poprzezs[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]
przys[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)-1
dla dowolnego z nichk
. Tym razem zwiększenieQ
spowoduje mniejszy rozmiar (tylko dla pamięci podręcznej), ale dłuższe czasy (tylko do odtworzenia pamięci podręcznej). Optymalną wartośćQ
moż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 funkcjif
obliczanies[j-1]
jest tak proste, jakZauważ, że tutaj zarówno funkcja, jak i dane wejściowe
j-1
nie sąj
. Ta sama funkcja i dane wejściowe byłyby tymi, których użyłbyś podczas obliczeń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 maszs[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:
Gdzie
r[j]
są odrzucone dane. A następnie utwórz funkcje odwrotne, aby pobierały odrzucone dane:Oprócz
f[j]
ix[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.
źródło
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:
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).
źródło
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.
źródło
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ść :)
źródło
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 ...
źródło
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ę.
źródło
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.
źródło
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:
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.
źródło
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.
źródło