Mechanizm odwrotnego czasu w grach

10

Zastanawiam się, jak zwykle zaprojektowane są mechanizmy manipulacji czasem w grach. Szczególnie interesuje mnie cofanie czasu (podobnie jak w najnowszym SSX lub Prince of Persia).

Ta gra to strzelanka 2D z góry.

Mechanizm, który próbuję zaprojektować / wdrożyć, ma następujące wymagania:

1) Działania podmiotów poza postacią gracza są całkowicie deterministyczne.

  • Działanie podejmowane przez byt jest oparte na postępach klatek od początku poziomu i / lub pozycji gracza na ekranie
  • Istoty odradzają się w ustalonym czasie na poziomie.

2) Odwrócenie czasu działa poprzez cofanie w czasie rzeczywistym.

  • Działania gracza również są odwrócone, odtwarza w odwrotnej kolejności to, co zrobił gracz. Gracz nie ma kontroli podczas odwrotnego czasu.
  • Czas cofania nie jest ograniczony, w razie potrzeby możemy cofnąć się do początku poziomu.

Jako przykład:

Klatki 0-50: Gracz porusza się w tym czasie o 20 jednostek Wróg 1 odradza się w klatce 20 Wróg 1 porusza się w lewo o 10 jednostek podczas klatki 30-40 Gracz strzela kulą w klatkę 45 Kula przemieszcza się 5 w przód (45-50) i zabija wroga 1 w rama 50

Cofnięcie tego odtwarzałoby się w czasie rzeczywistym: Gracz porusza się w tym czasie o 20 jednostek Wróg 1 odradza się w klatce 50 Kula pojawia się ponownie w klatce 50 Kula porusza się do tyłu 5 i znika (50-45) Wróg porusza się w lewo 10 (40-30) Wróg usunięty przy ramka 20.

Patrząc na ruch, miałem kilka pomysłów, jak to osiągnąć, pomyślałem o interfejsie, który zmieniałby zachowanie, gdy czas się zbliżał lub cofał. Zamiast robić coś takiego:

void update()
{
    movement += new Vector(0,5);
}

Zrobiłbym coś takiego:

public interface movement()
{
    public void move(Vector v, Entity e);
}

public class advance() implements movement
{
    public void move(Vector v, Entity e)
    {
            e.location += v;
    }
}


public class reverse() implements movement
{
    public void move(Vector v, Entity e)
    { 
        e.location -= v;
    }
}

public void update()
{
    moveLogic.move(new vector(5,0));
}

Jednak zdałem sobie sprawę, że nie byłoby to optymalne pod względem wydajności i szybko skomplikowałoby się w przypadku bardziej zaawansowanych działań (takich jak płynny ruch po zakrzywionych ścieżkach itp.).

Jkh2
źródło
1
Nie obejrzałem tego wszystkiego (chwiejna kamera na YouTube, 1,5 godziny) , ale być może jest kilka pomysłów, że Jonathan Blow pracował nad tym w swojej grze Braid.
MichaelHouse
Możliwy duplikat gamedev.stackexchange.com/questions/15251/…
Hackworth

Odpowiedzi:

9

Możesz rzucić okiem na wzór polecenia .

Zasadniczo każda odwracalna akcja podejmowana przez twoje jednostki jest implementowana jako obiekt polecenia. Wszystkie te obiekty implementują co najmniej 2 metody: Execute () i Undo (), a także wszystko, czego potrzebujesz, na przykład właściwość znacznika czasu dla poprawnego pomiaru czasu.

Ilekroć twoja jednostka wykonuje odwracalną akcję, najpierw tworzysz odpowiedni obiekt polecenia. Zapisujesz go na stosie Cofnij, a następnie dodajesz do silnika gry i uruchamiasz go. Kiedy chcesz cofnąć czas, wysuwasz akcje z góry stosu i wywołujesz ich metodę Undo (), co działa odwrotnie niż w przypadku metody Execute (). Na przykład w przypadku skoku z punktu A do punktu B wykonujesz skok z B do A.

Po kliknięciu akcji zapisz ją na stosie Ponów, jeśli chcesz iść do przodu i do tyłu do woli, podobnie jak funkcja cofania / powtarzania w edytorze tekstu lub programie do malowania. Oczywiście twoje animacje muszą również obsługiwać tryb „przewijania do tyłu”, aby odtwarzać je wstecz.

Aby uzyskać więcej shenaniganów w projektowaniu gier, pozwól każdemu bytowi przechowywać swoje działania na swoim stosie, abyś mógł je cofać / ponawiać niezależnie od siebie.

Wzorzec poleceń ma inne zalety: na przykład zbudowanie rejestratora powtórek jest bardzo proste, ponieważ wystarczy zapisać wszystkie pliki na stosach do pliku, a podczas odtwarzania po prostu wprowadzić je do silnika gry jeden po drugim jeden.

Hackworth
źródło
2
Zauważ, że odwracalność działań w grze może być bardzo drażliwa, ze względu na problemy z precyzją zmiennoprzecinkową, zmienne kroki czasowe itp .; o wiele bezpieczniej jest zapisać ten stan niż odbudować go w większości sytuacji.
Steven Stadnicki
@StevenStadnicki Może, ale na pewno jest to możliwe. Z mojej głowy C&C Generals robi to w ten sposób. Ma godzinne powtórki z udziałem maksymalnie 8 graczy, w najgorszym przypadku o wartości kilkuset kB, i tak myślę, że większość, jeśli nie wszystkie gry RTS działają w trybie dla wielu graczy: Po prostu nie można przekazać pełnego stanu gry potencjalnie setkami jednostek w każdej ramce, musisz pozwolić silnikowi na aktualizację. Więc tak, to zdecydowanie opłacalne.
Hackworth
3
Powtórzenie jest czymś zupełnie innym niż przewijanie do tyłu, ponieważ operacje, które są konsekwentnie odtwarzalne do przodu (na przykład znalezienie pozycji w klatce n, x_n, zaczynając od x_0 = 0 i dodanie delta v_n dla każdego kroku) niekoniecznie są odtwarzalne do tyłu ; (x + v_n) -v_n nie zawsze jest równy x w matematyce zmiennoprzecinkowej. Łatwo jest powiedzieć „obejdź to”, ale mówisz o potencjalnym całkowitym remoncie, w tym niemożności korzystania z wielu bibliotek zewnętrznych.
Steven Stadnicki
1
W przypadku niektórych gier podejście może być wykonalne, ale większość gier AFAIK, które wykorzystują odwrócenie czasu jako mechanikę, używa czegoś bliższego podejściu OriginalDaemon Memento, w którym odpowiedni stan jest zapisywany dla każdej klatki.
Steven Stadnicki
2
Co powiesz na przewijanie przez ponowne obliczanie kroków, ale zapisywanie klatki kluczowej co kilka sekund? Błędy zmiennoprzecinkowe prawdopodobnie nie będą miały znaczącej różnicy w ciągu zaledwie kilku sekund (oczywiście w zależności od złożoności). Wykazano również, że działa w kompresji wideo: P
Tharwen
1

Możesz rzucić okiem na Wzorzec Memento; jego głównym celem jest zaimplementowanie operacji cofania / ponawiania poprzez cofanie stanu obiektu, ale w przypadku niektórych rodzajów gier powinno wystarczyć.

W przypadku gry w pętli czasu rzeczywistego każdą klatkę operacji można uznać za zmianę stanu i zapisać ją. Jest to proste podejście do wdrożenia. Alternatywą jest pułapka, gdy zmienia się stan obiektu. Na przykład wykrywanie zmiany sił działających na sztywny korpus. Jeśli używasz właściwości do pobierania i ustawiania zmiennych, może to być również stosunkowo prosta implementacja, trudną częścią jest określenie, kiedy cofnąć stan, ponieważ nie będzie to ten sam czas dla każdego obiektu (możesz przechowywać czas cofania jako liczba klatek od początku systemu).

OriginalDaemon
źródło
0

W konkretnym przypadku obsługa cofania przez przewijanie ruchu powinna działać poprawnie. Jeśli używasz dowolnej formy wyszukiwania ścieżek z jednostkami AI, pamiętaj, aby ponownie ją obliczyć po wycofaniu, aby uniknąć nakładania się jednostek.

Problem polega na tym, jak radzisz sobie z samym ruchem: przyzwoity silnik fizyczny (strzelanka 2D z góry na dół będzie bardzo dobra z bardzo prostym), który śledzi informacje o przeszłych krokach (w tym pozycję, siłę netto itp.) solidna podstawa. Następnie, decydując o maksymalnym wycofaniu i szczegółowości kroków wycofywania, powinieneś uzyskać pożądany wynik.

Darkwings
źródło
0

Chociaż jest to ciekawy pomysł. Odradzałbym to.

Odtwarzanie gry do przodu działa dobrze, ponieważ operacja zawsze będzie miała taki sam wpływ na stan gry. Nie oznacza to, że operacja odwrotna daje pierwotny stan. Na przykład oceń poniższe wyrażenie w dowolnym języku programowania (wyłącz optymalizację)

(1.1 + 3 - 3) == 1.1

Przynajmniej w C i C ++ zwraca false. Chociaż różnica może być niewielka, wyobraź sobie, ile błędów może się kumulować przy 60 klatkach na sekundę w ciągu 10 sekund. Zdarzają się przypadki, w których gracz po prostu coś nie trafi, ale trafi go, gdy gra będzie odtwarzana wstecz.

Polecam przechowywanie klatek kluczowych co pół sekundy. To nie zajmie zbyt dużo pamięci. Następnie możesz albo interpolować między klatkami kluczowymi, albo jeszcze lepiej, symulować czas między dwiema klatkami kluczowymi, a następnie odtwarzać go wstecz.

Jeśli Twoja gra nie jest zbyt skomplikowana, po prostu przechowuj klatki kluczowe stanu gry 30 razy na sekundę i odtwarzaj ją wstecz. Gdybyś miał 15 obiektów z pozycją 2D, zajęłoby to dobre 1,5 minuty, aby uzyskać MB, bez kompresji. Komputery mają gigabajty pamięci.

Nie komplikuj go zbytnio, nie będzie łatwo odtworzyć gry wstecz i spowoduje wiele błędów.

Hannesh
źródło