Pętla gry, jak raz sprawdzić warunki, zrób coś, a potem nie rób tego ponownie

13

Na przykład mam klasę gier, która prowadzi intśledzenie życia gracza. Mam warunkowe

if ( mLives < 1 ) {
    // Do some work.
}

Jednak ten warunek trwa nadal i praca jest wykonywana wielokrotnie. Na przykład chcę ustawić minutnik, aby wyjść z gry za 5 sekund. Obecnie ustawia go na 5 sekund na każdą klatkę, a gra nigdy się nie kończy.

To tylko jeden przykład i mam ten sam problem w kilku obszarach mojej gry. Chcę sprawdzić jakiś warunek, a następnie zrobić coś raz i tylko raz, a następnie ponownie nie sprawdzać ani nie wykonywać kodu w instrukcji if. Niektóre z możliwych rozwiązań, które przychodzą na myśl, obejmują ustawienie booldla każdego warunku i ustawienie czasu booluruchomienia warunku. Jednak w praktyce robi się to bardzo nieładnie bools, ponieważ muszą być przechowywane jako pola klasy lub statycznie w samej metodzie.

Jakie jest właściwe rozwiązanie tego problemu (nie dla mojego przykładu, ale dla domeny problemowej)? Jak byś to zrobił w grze?

EddieV223
źródło
Dzięki za odpowiedzi, moje rozwiązanie jest teraz kombinacją odpowiedzi ktodisco i crancran.
EddieV223,

Odpowiedzi:

13

Myślę, że możesz rozwiązać ten problem, po prostu zachowując ostrożniejszą kontrolę nad możliwymi ścieżkami kodu. Na przykład, jeśli sprawdzasz, czy liczba żyć gracza spadła poniżej jednego, dlaczego nie sprawdzić tylko wtedy, gdy gracz traci życie, zamiast każdej klatki?

void subtractPlayerLife() {
    // Generic life losing stuff, such as mLives -= 1
    if (mLives < 1) {
        // Do specific stuff.
    }
}

To z kolei zakłada, że subtractPlayerLifebędzie wywoływane tylko w określonych przypadkach, co być może wynikałoby z warunków, w których musisz sprawdzić każdą klatkę (być może kolizje).

Starannie kontrolując sposób działania kodu, można uniknąć niechlujnych rozwiązań, takich jak statyczne booleany, a także stopniowo zmniejszać ilość kodu wykonywanego w pojedynczej ramce.

Jeśli jest coś, co po prostu wydaje się niemożliwe do refaktoryzacji, a naprawdę musisz sprawdzić tylko raz, to zaznacz (jak enum) lub statyczne booleany. Aby uniknąć samodzielnego deklarowania statyki, możesz użyć następującej sztuczki:

#define ONE_TIME_IF(condition, statement) \
    static bool once##__LINE__##__FILE__; \
    if(!once##__LINE__##__FILE__ && condition) \
    { \
        once##__LINE__##__FILE__ = true; \
        statement \
    }

co dałoby następujący kod:

while (true) {
    ONE_TIME_IF(1 > 0,
        printf("something\n");
        printf("something else\n");
    )
}

wydrukuj somethingi something elsetylko raz. Nie daje najlepiej wyglądającego kodu, ale działa. Jestem też całkiem pewien, że istnieją sposoby na jego poprawę, być może poprzez lepsze zapewnienie unikalnych nazw zmiennych. Działa #defineto tylko raz podczas działania. Nie ma sposobu, aby go zresetować i nie ma sposobu, aby go zapisać.

Pomimo tej sztuczki zdecydowanie zalecamy lepszą kontrolę przepływu kodu w pierwszej kolejności i używanie go #definew ostateczności lub wyłącznie do celów debugowania.

kevintodisco
źródło
+1 ładne raz, jeśli rozwiązanie. Bałagan, ale fajnie.
kender
1
jest jeden duży problem z Twoim ONE_TIME_IF, nie możesz go powtórzyć. Na przykład gracz wraca do menu głównego, a później wraca do sceny gry. To oświadczenie nie zadziała ponownie. są też warunki, które możesz potrzebować między uruchomieniami aplikacji, na przykład lista już zdobytych trofeów. Twoja metoda nagrodzi trofeum dwukrotnie, jeśli gracz zdobędzie je w dwóch różnych uruchomieniach aplikacji.
Ali1S232
1
@Gajoo To prawda, jest problem, jeśli warunek wymaga zresetowania lub zapisania. Zredagowałem to, aby to wyjaśnić. Dlatego zaleciłem to jako ostateczność lub po prostu do debugowania.
kevintodisco
Czy mogę zasugerować id {do} while (0)? Wiem, że osobiście coś spieprzyłem i wstawiłem średnik tam, gdzie nie powinienem. Fajne makro, brawo.
michael.bartnett
5

To brzmi jak klasyczny przypadek użycia wzorca stanu. W jednym stanie sprawdzasz swój stan życia <1. Jeśli warunek ten się spełni, przejdziesz w stan opóźnienia. Ten stan opóźnienia czeka określony czas, a następnie przechodzi do stanu wyjścia.

W najbardziej trywialnym podejściu:

switch(mCurrentState) {
  case LIVES_GREATER_THAN_0:
    if(lives < 1) mCurrentState = LIVES_LESS_THAN_1;
    break;
  case LIVES_LESS_THAN_1:
    timer.expires(5000); // expire in 5 seconds
    mCurrentState = TIMER_WAIT;
    break;
  case TIMER_WAIT:
    if(timer.expired()) mCurrentState = EXIT;
    break;
  case EXIT:
    mQuit = true;
    break;
}

Można rozważyć użycie klasy State wraz z StateManager / StateMachine do obsługi zmiany / wypychania / przejścia między stanami zamiast instrukcji switch.

Możesz sprawić, by Twoje rozwiązanie do zarządzania stanami było tak wyrafinowane, jak chcesz, w tym wiele stanów aktywnych, pojedynczy aktywny stan nadrzędny z wieloma aktywnymi stanami potomnymi przy użyciu pewnej hierarchii itp. Oczywiście używaj tego, co ma dla ciebie sens, ale naprawdę uważam, że Rozwiązanie stanu wzorca sprawi, że kod będzie znacznie łatwiejszy do ponownego użycia oraz łatwiejszy w utrzymaniu i przestrzeganiu.

Naros
źródło
1

Jeśli martwisz się wydajnością, wydajność wielokrotnego sprawdzania zwykle nie jest znacząca; to samo dotyczy warunków boolowych.

Jeśli interesuje Cię coś innego, możesz użyć czegoś podobnego do wzorca projektowego o wartości zerowej, aby rozwiązać ten problem.

W takim przypadku załóżmy, że chcesz wywołać metodę, foojeśli gracz nie ma już życia. Zaimplementowałbyś to jako dwie klasy, jedną wywołującą fooi drugą, która nic nie robi. Zadzwońmy do nich DoNothingi CallFootak:

class SomeLevel {
  int numLives = 3;
  FooClass behaviour = new DoNothing();
  ...
  void tick() { 
    if (numLives < 1) {
      behaviour = new CallFoo();
    }

    behaviour.execute();
  }
}

To jest trochę „surowy” przykład; kluczowe punkty to:

  • Śledzenie niektórych przypadków, FooClassktóre mogą być DoNothinglub CallFoo. Może to być klasa podstawowa, klasa abstrakcyjna lub interfejs.
  • Zawsze wywołuj executemetodę tej klasy.
  • Domyślnie klasa nic nie robi ( DoNothinginstancja).
  • Kiedy życie się skończy, instancja jest zamieniana na instancję, która faktycznie coś robi ( CallFooinstancję).

Zasadniczo jest to połączenie strategii i zerowych wzorców.

ashes999
źródło
Ale będzie nadal tworzyć nowe CallFoo () dla każdej ramki, które należy usunąć przed nową, a także będzie się działo, co nie rozwiązuje problemu. Na przykład, gdybym wywołał metodę CallFoo (), która ustawia licznik czasu na wykonanie w ciągu kilku sekund, licznik czasu byłby ustawiony na taki sam czas dla każdej ramki. O ile mogę stwierdzić, twoje rozwiązanie jest takie samo, jak gdyby (numLives <1) {// do stuff} else {// empty}
EddieV223
Hmm, rozumiem co mówisz. Rozwiązaniem może być przeniesienie ifczeku do FooClasssamej instancji, więc jest ona wykonywana tylko raz i to samo w celu utworzenia instancji CallFoo.
ashes999