Akcje gry, które wymagają wykonania wielu klatek

20

Nigdy wcześniej tak naprawdę nie zajmowałem się programowaniem gier, dość proste pytanie.

Wyobraź sobie, że buduję grę Tetris, w której główna pętla wygląda mniej więcej tak.

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            remove all complete rows
            move rows down so there are no gaps
            if we can spawn a new block
                spawn a new current block
            else
                game over

Jak do tej pory wszystko w grze dzieje się natychmiast - rzeczy są odradzane natychmiast, rzędy są usuwane natychmiast itp. Ale co, jeśli nie chcę, aby coś się stało natychmiast (tj. Animacja)?

for every frame
    handle input
    if it's time to make the current block move down a row
        if we can move the block
            move the block
        else
            ?? animate complete rows disappearing (somehow, wait over multiple frames until the animation is done)
            ?? animate rows moving downwards (and again, wait over multiple frames)
            if we can spawn a new block
                spawn a new current block
            else
                game over

W moim klonie Pong nie było to problemem, ponieważ każdą klatkę po prostu poruszałem piłką i sprawdzałem kolizje.

Jak mogę owinąć głowę tym problemem? Z pewnością większość gier wymaga akcji wymagającej więcej niż klatki, a inne rzeczy zatrzymują się do momentu wykonania akcji.


źródło

Odpowiedzi:

11

Tradycyjnym rozwiązaniem tego jest skończona maszyna stanowa, co sugeruje kilka komentarzy.

Nienawidzę skończonych maszyn stanowych.

Oczywiście są proste, są obsługiwane w każdym języku, ale praca z nimi jest niesamowita. Każda manipulacja wymaga mnóstwa podatnego na błędy kopiowania i wklejania kodu, a drobna poprawa efektu może być ogromną zmianą w kodzie.

Jeśli możesz użyć języka, który je obsługuje, polecam coroutines. Pozwalają Ci pisać kod, który wygląda następująco:

function TetrisPieceExplosion()
  for brightness = 0, 1, 0.2 do
    SetExplosionBrightness(brightness)
    coroutine.yield()
  end

  AllowNewBlockToFall()

  SpawnABunchOfParticles()

  RemoveBlockPhysics()

  for transparency = 0, 1, 0.5 do
    SetBlockTransparency(transparency)
    coroutine.yield()
  end

  RemoveBlockGraphics()
end

Oczywiście raczej pseudocodey, ale powinno być jasne, że jest to nie tylko prosty liniowy opis efektu specjalnego, ale z łatwością pozwala upuścić nowy blok, gdy animacja wciąż się kończy . Osiągnięcie tego za pomocą automatu stanowego będzie na ogół upiorne.

Według mojej najlepszej wiedzy, ta funkcja nie jest łatwo dostępna w C, C ++, C #, Objective C lub Java. To jeden z głównych powodów, dla których używam Lua do całej mojej logiki gry :)

ZorbaTHut
źródło
Możesz również zaimplementować coś w tym stylu w innych językach OOP. Wyobraź sobie jakąś Actionklasę i kolejkę działań do wykonania. Po zakończeniu akcji usuń ją z kolejki i wykonaj następną akcję itp. O wiele bardziej elastyczna niż automat stanów.
bummzack
3
To działa, ale wtedy patrzysz na czerpanie z Akcji dla każdej unikalnej akcji. Zakłada również, że proces ładnie pasuje do kolejki - jeśli chcesz rozgałęzić się lub zapętlić z nieokreślonymi warunkami końcowymi, rozwiązanie kolejki szybko się psuje. Jest to z pewnością czystsze niż podejście do automatu państwowego, ale myślę, że coroutines nadal atutem pod względem czytelności :)
ZorbaTHut
To prawda, ale dla przykładu Tetris powinno wystarczyć :)
bummzack
Wspólne rutyny - ale Lua jako język jest do bani na wiele innych sposobów, po prostu nie mogę tego polecić.
DeadMG,
Tak długo, jak musisz ustąpić tylko na najwyższym poziomie (a nie z zagnieżdżonego wywołania funkcji), możesz osiągnąć to samo w C #, ale tak, Lua coroutines rock.
hojny
8

Biorę to z Game Coding Complete autorstwa Mike'a McShaffry'ego.

Mówi o „menedżerze procesów”, który sprowadza się do listy zadań, które należy wykonać. Na przykład proces kontrolowałby animację rysowania miecza (AnimProcess), otwierania drzwi lub, w twoim przypadku, powodowania zniknięcia rzędu.

Proces zostałby dodany do listy menedżera procesów, która byłaby iterowana w każdej ramce i wywołanej Update () dla każdej z nich. Tak bardzo podobne byty, ale do działań. Po zakończeniu tej operacji pojawi się flaga „zabicia”.

Inną fajną rzeczą w nich jest to, w jaki sposób mogą się połączyć, mając wskaźnik do następnego procesu. W ten sposób animowany proces wiersza może faktycznie składać się z:

  • Proces animacji dla znikającego wiersza
  • Proces Ruchu, aby usunąć elementy
  • ScoreProcess, aby dodać punkty do wyniku

(Ponieważ procesy mogą być jednorazowe, warunkowo tam lub tam przez X czasu)

Jeśli chcesz więcej szczegółów, zapytaj.

Kaczka komunistyczna
źródło
3

Możesz użyć priorytetowej kolejki działań. Wrzucasz akcję i czas. W każdej klatce dostajesz czas i odsuwasz wszystkie akcje, które mają określony czas jak poprzednio, i je wykonujesz. Bonus: Podejdź ładnie do paraleli, a tak naprawdę możesz zaimplementować prawie całą logikę gry.

DeadMG
źródło
1

Zawsze musisz znać różnicę czasu między poprzednią i bieżącą ramką, a następnie musisz zrobić dwie rzeczy.

- Zdecyduj, kiedy zaktualizować swój model: np. w tetris, gdy zaczyna się usuwanie wiersza, nie chcesz, aby rzeczy już kolidowały z wierszem, więc usuwasz wiersz z „modelu” aplikacji.

-Musisz następnie obsłużyć obiekt będący w stanie przejściowym do osobnej klasy, która rozwiąże animację / zdarzenie w pewnym okresie czasu. W przykładzie tetris wiersz powoli zanikałby (nieco zmieniaj nieprzezroczystość każdej klatki). Po nieprzezroczystości wynoszącej 0 przenosisz wszystkie bloki na rząd wiersza jeden w dół.

Na początku może się to wydawać nieco skomplikowane, ale zrozumiesz to, po prostu upewnij się, że dużo streszczasz w różnych klasach, to ułatwi ci to. Upewnij się także, że zdarzenia, które wymagają czasu, takie jak usunięcie wiersza w tetris, są tego rodzaju „Fire and Forget”, po prostu stwórz nowy obiekt, który obsługuje wszystko, co należy zrobić automatycznie, a gdy wszystko jest zrobione, usuwa się ze scenariusza.

Roy T.
źródło
Ponadto w niektórych przypadkach ciężkie obliczenia mogą przekraczać dozwolony czas na jeden etap czasowy fizyki (np. Wykrywanie kolizji i planowanie ścieżki). W takich przypadkach możesz wyskoczyć z obliczeń, gdy wykorzystany został przydzielony czas i kontynuować obliczenia w następnej klatce.
Nailer
0

Musisz myśleć o grze jako o „skończonej maszynie stanów”. Gra może być w jednym z kilku stanów: w twoim przypadku „oczekuje wejścia”, „kawałek przesuwa się w dół”, „eksploduje rząd”.

Robisz różne rzeczy w zależności od stanu. Na przykład podczas „przesuwania elementu w dół” ignorujesz dane gracza i zamiast tego animujesz kawałek z bieżącego wiersza do następnego. Coś takiego:

if state == ACCEPTING_INPUT:
    if player presses any key:
        handle input
    row_timer = row_timer - time_since_last_frame
    if row_timer < 0:
        state = MOVING_PIECE_DOWN
elif state == MOVING_PIECE_DOWN:
    piece.y = piece.y + piece.speed*time_since_last_frame
    if piece.y >= target_piece_y:
        piece.y = target_piece_y
        state = ACCEPTING_INPUT
ggambett
źródło