Spiel-Schleife, wie man einmal nach Bedingungen sucht, etwas macht und es dann nicht noch einmal macht

13

Zum Beispiel habe ich eine Spielklasse int, in der das Leben des Spielers aufgezeichnet wird. Ich habe eine Bedingung

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

Diese Bedingung wird jedoch weiterhin ausgelöst und die Arbeit wird wiederholt ausgeführt. Zum Beispiel möchte ich einen Timer einstellen, um das Spiel in 5 Sekunden zu beenden. Gegenwärtig wird es in jedem Frame auf 5 Sekunden eingestellt und das Spiel endet nie.

Dies ist nur ein Beispiel und ich habe das gleiche Problem in mehreren Bereichen meines Spiels. Ich möchte nach einer Bedingung suchen und dann einmal und nur einmal etwas tun und dann den Code in der if-Anweisung nicht erneut überprüfen oder ausführen. Einige mögliche Lösungen, die in den Sinn kommen, sind ein boolfür jede Bedingung und das Festlegen, boolwann die Bedingung ausgelöst wird. In der Praxis wird dies jedoch sehr unübersichtlich bools, da sie als Klassenfelder oder statisch innerhalb der Methode selbst gespeichert werden müssen.

Was ist die richtige Lösung dafür (nicht für mein Beispiel, sondern für die Problemdomäne)? Wie würdest du das in einem Spiel machen?

EddieV223
quelle
Vielen Dank für die Antworten, meine Lösung ist jetzt eine Kombination aus ktodisco- und crancran-Antworten.
EddieV223

Antworten:

13

Ich denke, dass Sie dieses Problem einfach lösen können, indem Sie eine genauere Kontrolle über Ihre möglichen Codepfade ausüben. Wenn Sie beispielsweise überprüfen, ob die Anzahl der Leben des Spielers unter eins gesunken ist, prüfen Sie dann nur, wenn der Spieler ein Leben verliert, und nicht jedes Bild.

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

Dies setzt wiederum voraus, dass subtractPlayerLifedies nur zu bestimmten Anlässen aufgerufen wird, was möglicherweise auf Bedingungen zurückzuführen ist, die Sie für jeden Frame überprüfen müssen (möglicherweise Kollisionen).

Durch sorgfältige Kontrolle der Codeausführung können Sie unordentliche Lösungen wie statische Boolesche Werte vermeiden und außerdem Stück für Stück die Ausführung von Code in einem einzelnen Frame reduzieren.

Wenn es etwas gibt, das sich einfach nicht umgestalten lässt und das Sie wirklich nur einmal überprüfen müssen, sind state (wie ein enum) oder statische Boolesche Werte der richtige Weg. Um zu vermeiden, dass Sie die Statik selbst deklarieren, können Sie den folgenden Trick anwenden:

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

das würde den folgenden Code machen:

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

drucken somethingund something elsenur einmal. Es liefert nicht den am besten aussehenden Code, aber es funktioniert. Ich bin mir auch ziemlich sicher, dass es Möglichkeiten gibt, dies zu verbessern, indem Sie möglicherweise eindeutige Variablennamen sicherstellen. Dies #definefunktioniert jedoch nur einmal zur Laufzeit. Es gibt keine Möglichkeit, es zurückzusetzen, und es gibt keine Möglichkeit, es zu speichern.

Trotz des Tricks empfehle ich dringend, zuerst den Code-Fluss besser zu steuern und dies #defineals letzten Ausweg oder nur für Debug-Zwecke zu verwenden.

kevintodisco
quelle
+1 schön einmal wenn Lösung. Chaotisch, aber cool.
Kender
1
Es gibt ein großes Problem mit Ihrem ONE_TIME_IF. Sie können es nicht wieder in Gang bringen. Der Spieler kehrt zum Beispiel zum Hauptmenü zurück und kehrt später zur Spielszene zurück. Diese Aussage wird nicht wieder ausgelöst. und es gibt Bedingungen, die Sie möglicherweise zwischen Anwendungsläufen einhalten müssen, z. B. eine Liste bereits erworbener Trophäen. Ihre Methode belohnt die Trophäe zweimal, wenn der Spieler sie in zwei verschiedenen Anwendungsläufen erhält.
Ali1S232
1
@ Gajoo Das stimmt, es ist ein Problem, wenn die Bedingung zurückgesetzt oder gespeichert werden muss. Ich habe bearbeitet, um das zu klären. Deshalb habe ich es als allerletzten Ausweg oder nur zum Debuggen empfohlen.
kevintodisco
Kann ich das do {} while (0) -Idiom vorschlagen? Ich weiß, ich würde persönlich etwas vermasseln und ein Semikolon dort anbringen, wo ich es nicht sollte. Cooles Makro, Bravo.
michael.bartnett
5

Dies klingt wie der klassische Fall der Verwendung des Statusmusters. In einem Zustand überprüfen Sie Ihren Zustand auf Leben <1. Wenn dieser Zustand eintritt, wechseln Sie in einen Verzögerungszustand. Dieser Verzögerungsstatus wartet auf die angegebene Dauer und wechselt dann in den Beendigungsstatus.

Im einfachsten Ansatz:

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;
}

Sie können die Verwendung einer State-Klasse zusammen mit einem StateManager / StateMachine in Betracht ziehen, um das Ändern / Verschieben / den Übergang zwischen Zuständen anstelle einer switch-Anweisung zu handhaben.

Sie können Ihre Zustandsverwaltungslösung so komplex gestalten, wie Sie möchten, einschließlich mehrerer aktiver Zustände, eines einzelnen aktiven übergeordneten Zustands mit vielen aktiven untergeordneten Zuständen unter Verwendung einer Hierarchie usw. Verwenden Sie das, was für Sie jetzt Sinn macht, aber ich denke wirklich a state pattern solution macht Ihren Code viel wiederverwendbarer und einfacher zu warten und zu befolgen.

Naros
quelle
1

Wenn Sie sich Sorgen über die Leistung machen, ist die Leistung der mehrmaligen Überprüfung normalerweise nicht signifikant. Das Gleiche gilt für boole Bedingungen.

Wenn Sie an etwas anderem interessiert sind, können Sie etwas verwenden, das dem Null-Entwurfsmuster ähnelt, um dieses Problem zu lösen.

Nehmen wir in diesem Fall an, Sie möchten eine Methode aufrufen, foowenn der Spieler keine Leben mehr hat. Sie würden dies als zwei Klassen implementieren, eine, die aufruft foo, und eine, die nichts tut. Nennen wir sie DoNothingund CallFoowie folgt:

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

    behaviour.execute();
  }
}

Dies ist ein bisschen ein "rohes" Beispiel; Die wichtigsten Punkte sind:

  • Verfolgen Sie eine Instanz, von FooClassder DoNothingoder sein kann CallFoo. Dies kann eine Basisklasse, eine abstrakte Klasse oder eine Schnittstelle sein.
  • Rufen Sie immer die executeMethode dieser Klasse auf.
  • Standardmäßig macht die Klasse nichts ( DoNothingInstanz).
  • Wenn Leben ausgehen, wird die Instanz gegen eine Instanz ausgetauscht, die tatsächlich etwas tut ( CallFooInstanz).

Dies ist im Wesentlichen wie eine Mischung aus Strategie- und Nullmustern.

ashes999
quelle
Aber es wird weiterhin für jeden Frame ein neues CallFoo () erstellt, das Sie löschen müssen, bevor Sie ein neues Frame erstellen, und es wird weiterhin ausgeführt, was das Problem nicht behebt. Wenn ich zum Beispiel CallFoo () hatte, eine Methode aufzurufen, die einen Timer so einstellt, dass er in wenigen Sekunden ausgeführt wird, würde dieser Timer für jeden Frame auf die gleiche Zeit eingestellt. Soweit ich weiß, ist Ihre Lösung die gleiche, als ob (numLives <1) {// Sachen machen} else {// leer}
EddieV223
Hmm, ich verstehe, was du sagst. Die Lösung könnte darin bestehen, den ifScheck in die FooClassInstanz selbst zu verschieben, damit er nur einmal ausgeführt wird, und das Gleiche gilt für die Instanziierung CallFoo.
Asche999