Wie mache ich eine Spiel-Tick-Methode?

7

Ich habe in einigen anderen einfachen 2D-Spielen gesehen, dass eine "Tick" -Methode verwendet wird, um Spielelogik und Grafik-Rendering zu synchronisieren. Mein Hauptgrund für die Verwendung ist eine Fehlfunktion meiner Kollisionserkennung, da die Player-Entität vollständig durch feste Objekte läuft, es sei denn, ich tippe fast pixelweise auf die Steuerelemente, um die Rechtecke zu kollidieren.

Ich gehe davon aus, dass eine Tick-Methode die schnelle Aktualisierung der Spiele so verlangsamt, dass diese leichten Kollisionserkennungen erfasst werden.

Wie erstelle ich eine solche Methode? Ich habe den Code ausprobiert, den Sie unten sehen können. Er führt jedoch dazu, dass das gesamte Spiel einfriert:

Die Aktualisierungsmethode:

 @Override
    public void update(float deltaTime) {
        List<TouchEvent> touchEvents = game.getInput().getTouchEvents();
            long lastTime = 0;
            long currentTime = 0;

        if (state == GameState.Ready) {
            updateReady(touchEvents);
        }
        while (state == GameState.Running) {
                lastTime = currentTime;
                currentTime = System.currentTimeMillis();
                deltaTime = currentTime - lastTime;

                player.update(touchEvents, deltaTime);
                repaint(deltaTime);
        }
        if (state == GameState.Paused) {
            updatePaused(touchEvents);
        }
        if (state == GameState.GameOver) {
            updateGameOver(touchEvents);
        }
    }

Die Repaint-Methode, die aktualisiert wird, während die reguläre Paint-Methode nichts bewirkt. Ich habe die while-Schleife mit beiden ausprobiert, scheint nicht aufzuhören zu frieren:

public void repaint(float deltaTime) {
        Graphics g = game.getGraphics();

        if(deltaTime >= 60) {
            g.drawRect(0, 0, 805, 485, color.black);
            map.loadMap(getXScroll(), getYScroll(), g, game);
            map.loadEntities(getXScroll(), getYScroll(), g, game, map);
            g.drawImage(Assets.bitSoldier, 448, 256);
            g.drawImage(Assets.dpad, 0, 280);

            // Secondly, draw the UI above the game elements.
            if (state == GameState.Ready)
                drawReadyUI();
            if (state == GameState.Running)
                drawRunningUI();
            if (state == GameState.Paused)
                drawPausedUI();
            if (state == GameState.GameOver)
                drawGameOverUI();
        }
    }
A13X
quelle
Sie schlafen zwischen den Simulationen 100 ms lang. Aus persönlicher Erfahrung würde ich empfehlen, dass Sie maximal 40 ms schlafen sollten.
Shaun Wild
1
Idealerweise sollten Sie überhaupt nicht schlafen - aktualisieren Sie Ihre Logik so oft wie möglich (kleinere Zeitschritte verringern auch die Wahrscheinlichkeit solcher Geisterbilder). Angenommen, Sie schlafen absichtlich 40 ms lang, das Rendern / Aktualisieren dauert 10 ms - Sie haben 50 ms pro Zyklus, was Sie ohne wesentlichen Grund auf 20 Bilder pro Sekunde beschränken könnte.
Mario
Dies ist ein bekanntes Gebiet, über das Robert Nystrom in seinem Buch "Game Programming Patterns" (siehe gameprogrammingpatterns.com/game-loop.html ) und Glenn Fiedler ( gafferongames.com/post/fix_your_timestep ) zwei IMNSHO-Diskussionen darüber geführt hat ).
No-Bugs Hare
Zwischen den Antworten / verwandten Links / Büchern und der Erfahrung eines erfahrenen Spielentwicklers ist klar, dass ich und andere jetzt die Ressourcen haben, um dies richtig zu machen. Danke für die Hilfe!
A13X

Antworten:

12

Im Allgemeinen gibt es zwei Möglichkeiten, die Spielelogik zum "Ticken" zu bringen:

  • Feste Zeitschritte werden eine feste Anzahl von Malen pro Sekunde ausgeführt, beispielsweise 100 Mal oder vielleicht sogar 1000 Mal.
  • Dynamische Zeitschritte führen die Spiellogik nach Möglichkeit aus, wobei die seit dem letzten Update verstrichene Zeit berücksichtigt wird.

Beide Ansätze haben ihre Vor- und Nachteile, je nachdem, was Sie erreichen möchten.

Für Bewegungen und dergleichen sind dynamische Zeitschritte möglicherweise besser, aber für Dinge wie das Animieren von Pixelgrafiken sind feste Zeitschritte möglicherweise interessanter. Sie können beide nach Belieben kombinieren.

Im Allgemeinen sollten Sie, wie in den Kommentaren erwähnt, Ihr Programm niemals in der Spieleschleife schlafen lassen. Dies verlangsamt das Spiel ohne Grund und kann sogar zu Stottern führen, obwohl der Computer leistungsstark genug ist, um das Spiel tatsächlich ohne Verlangsamung auszuführen.

Ihre generische Spieleschleife sollte ungefähr so ​​aussehen (Pseudocode):

function mainloop() {
    double time_passed = 0;
    double delta_time = 0;

    while (running) { // keep running

        // update game logic based on time passed
        updateDynamicStep(delta_time);

        // update game logic once for every tick passed
        while (time_passed >= time_per_timestep) {
            updateFixedStep();
            time_passed -= time_per_timestep;
            // You might limit the number of iterations here (like 10)
            // to not get stuck due to processing taking too long (and time adding up)
        }

        // draw screen content
        drawStuff(delta_time);

        // update timing
        delta_time = getTimePassedAndResetTimer();
        time_passed += delta_time;
    }
}

Dies zu tun - egal ob Sie feste Zeitschritte verwenden oder nicht - hat den Vorteil, dass Ihr Spiel immer mit der gleichen Geschwindigkeit läuft, egal ob Sie mit 60 fps, 100 fps, 5000 fps oder nur 15 fps laufen fps.

Der einzige Grund, den Schlaf zu erzwingen, könnte darin bestehen, Batterie zu sparen (z. B. Handyspiele), aber ich würde auch hier nicht für eine feste Zeit schlafen, sondern nur basierend auf der Zeit, die diese Iteration dauerte, und der Zeit, die Sie für einen Frame geplant haben. Wenn das Aktualisieren Ihres Spiels und Zeichnens beispielsweise 10 ms dauert und Sie mit 60 Bildern pro Sekunde arbeiten möchten, schlafen Sie 5 ms (insgesamt 15 ms; die perfekte Zeit wäre 16,67 ms, aber Sie lassen etwas Platz für andere Dinge wie den Kontext Schalter und dergleichen).

Mario
quelle
Ich mochte besonders das bisschen über Handyspiele, da ich ein Android-App-Spiel entwerfe. Ich bin jedoch nicht der Beste, wenn es darum geht, Pseudocode zu entschlüsseln. Woher würde die Variable time_per_timestep stammen?
A13X
Das definieren Sie, im Wesentlichen hängt es von Ihrer Timer-Auflösung ab, dh es ist die Zeit zwischen Aktualisierungsaufrufen. Angenommen, der Timer repräsentiert Millisekunden und Sie möchten 100 Aktualisierungen pro Sekunde, die Sie verwenden würden time_per_timestep = 1000.0 / 100;.
Mario
Hier ist eine weitere Zwangslage ... Die while-Schleife scheint das Spiel einzufrieren und malt den Bildschirm nicht neu oder erlaubt keine Interaktion. Vielleicht hat es mit der mobilen Plattform und der Art und Weise zu tun, wie ich die Aktivität verwende, oder mit etwas, das wichtige Prozesse blockiert, die immer während der App ausgeführt werden?
A13X
2
Sie haben den Rahmenaufruf update()und falsch verstanden repaint(). Diese funktionieren im Wesentlichen bereits so, wie ich es in meinem Beispiel beschrieben habe (dies sind im Wesentlichen updateDynamicStep()und drawStuff()). Sie versuchen im Wesentlichen, eine Hauptschleife in einer Hauptschleife zu verschachteln, was nicht funktioniert. Sie müssen update()das Framework verlassen, um Ereignisse neu zu zeichnen und zu akzeptieren.
Mario
1
Theoretisch sind alle variablen Zeitschritte in Ordnung und gut. In der Praxis warnen jedoch sowohl Robert Nystrom in seinem wegweisenden Buch "Game Programming Patterns" ( gameprogrammingpatterns.com/game-loop.html ) als auch Glenn Fiedler selbst ( gafferongames.com/post/fix_your_timestep ) vor Instabilität und erheblichen Risiken variabler Zeitschritte (Halbfeste Zeitschritte sind jedoch eine andere Geschichte).
No-Bugs Hare