Informationen zum Rendering in Spielschleifen

Eine sehr beliebte Methode zum Implementieren einer Spielschleife sieht so aus:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

Dabei gibt es einige Probleme. Das Grundlegende ist die Idee, definieren, was ein „Frame“ ist. Die Aktualisierung erfolgt auf unterschiedlichen Displays. Diese Rate kann sich im Laufe der Zeit ändern. Wenn Sie Frames schneller generieren als die angezeigt wird, müssen Sie es gelegentlich entfernen. Wenn Sie zu langsam ist, findet SurfaceFlinger in regelmäßigen Abständen keinen neuen Puffer, und der vorherige Frame wieder angezeigt wird. Beide Situationen können sichtbare Störungen verursachen.

Du musst nur die Framerate des Displays anpassen, um zum Spielstatus zu gelangen je nachdem, wie viel Zeit seit dem vorherigen Frame vergangen ist. Es gibt mehrere Vorgehensweise:

  • Android Frame Pacing-Bibliothek verwenden (empfohlen)
  • Die BufferQueue voll füllen und auf die „Swap-Puffer“ zurückgreifen Rückdruck
  • Choreographer verwenden (API 16+)

Android Frame Pacing-Bibliothek

Weitere Informationen finden Sie unter Richtige Frametaktung erreichen. zur Verwendung dieser Bibliothek.

Überflüssige Warteschlangen

Die Implementierung ist ganz einfach: Tauschen Sie die Zwischenspeicher so schnell wie möglich aus. Anfang könnte dies eine Strafe nach sich ziehen, SurfaceView#lockCanvas() würde dich 100 ms lang in den Ruhemodus versetzen. Jetzt wird er von der BufferQueue getaktet und die BufferQueue so schnell wie möglich geleert SurfaceFlinger kann das.

Ein Beispiel für diesen Ansatz findest du im Android Breakout. Es GLSurfaceView, das in einer Schleife ausgeführt wird, die den Aufruf onDrawFrame() und tauscht dann den Zwischenspeicher aus. Wenn die BufferQueue voll ist, Der eglSwapBuffers()-Aufruf wartet, bis ein Zwischenspeicher verfügbar ist. Puffer sind verfügbar, sobald sie von SurfaceFlinger veröffentlicht werden. eine neue Kampagne für das Displaynetzwerk kaufen. Da dies in VSYNC passiert, Timing mit der Aktualisierungsrate übereinstimmt. Meistens.

Bei diesem Ansatz gibt es einige Probleme. Zunächst ist die App SurfaceFlinger-Aktivität, die unterschiedlich lange dauern wird je nachdem, wie viel Arbeit zu erledigen ist und ob es um die CPU-Zeit geht, mit anderen Prozessen. Da sich dein Spielstatus im Laufe der Zeit zwischen Pufferaustauschen hin und her wechseln, wird Ihre Animation nicht regelmäßig aktualisiert. Wann? mit 60 fps läuft und die Inkonsistenzen im Laufe der Zeit jedoch durchschnittlich ausfallen, werden Sie die Erhebungen wahrscheinlich nicht bemerken.

Die ersten Puffer werden sehr schnell gewechselt. da die BufferQueue noch nicht voll ist. Die berechnete Zeitspanne zwischen den Frames bei null liegen, sodass das Spiel einige Frames generiert, in denen nichts passiert. In einem Spiel wie Breakout, bei dem der Bildschirm bei jeder Aktualisierung aktualisiert wird, ist die Warteschlange immer voll, außer wenn ein Spiel neu gestartet oder nicht pausiert wird. nicht auffällig ist. Ein Spiel, bei dem die Animation gelegentlich pausiert wird und dann wieder so schnell wie möglich, kann es zu seltsamen Problemen kommen.

Choreographer

Mit Choreographer können Sie einen Callback festlegen, der beim nächsten VSYNC ausgelöst wird. Die Die tatsächliche VSYNC-Zeit wird als Argument übergeben. Auch wenn die App nicht aktiviert wird, wenn die Anzeige sofort aktualisiert wird, Periode begonnen. Wenn Sie diesen Wert anstelle der aktuellen Uhrzeit verwenden, erhalten Sie eine eine konsistente Zeitquelle für die Logik der Aktualisierung des Spielstatus zu verwenden.

Leider ist die Tatsache, dass Sie nach jedem VSYNC einen Callback erhalten, dass Ihr Callback rechtzeitig ausgeführt wird schnell genug reagieren können. Ihre App muss Folgendes erkennen: und Frames manuell fallen lassen.

Die App „Record GL“ Aktivität in Grafika liefert ein Beispiel dafür. Auf einigen (z.B. Nexus 4 und Nexus 5) verwendet wird, werden Frames eingeblendet, wenn einfach nur sitzen und zuschauen. Das GL-Rendering ist simpel, aber gelegentlich werden neu gezeichnet und der Measure/Layout-Pass kann sehr lange dauern, Das Gerät befindet sich in einem Modus mit geringerer Leistung. (Laut Systrace dauert unter Android 4.4 28 ms statt 6 ms, wenn die Uhr langsam ist. Beim Ziehen Finger auf dem Bildschirm bewegt, erkennt es, dass Sie mit der Aktivität interagieren, damit die Taktgeschwindigkeit hoch bleibt und kein Frame verloren geht.)

Die einfache Lösung bestand darin, einen Frame im Choreographer-Callback zu löschen, wenn der aktuelle liegt mehr als N Millisekunden nach der VSYNC-Zeit. Im Idealfall ist der Wert von N anhand der zuvor beobachteten VSYNC-Intervalle ermittelt. Wenn zum Beispiel der Parameter der Aktualisierungszeitraum bei 16,7 ms (60 fps) liegt.Möglicherweise wird ein Frame weggelassen, als 15 ms später.

Bei Wiedergabe von „Record GL app“ sehen Sie den Zähler für die abgelegten und es ist sogar ein roter Blitz im Rand zu sehen, wenn die Frames heruntergelassen werden. Es sei denn, Ihre Augen sind sehr gut, aber die Animation bleibt ruckelig. Bei 60 fps, kann die App den gelegentlichen Frame auslassen, ohne dass es zu merken ist, solange der bewegt sich die Animation konstant voran. Wie viel du entkommen kannst hängt zum Teil davon ab, was Sie zeichnen, Display und wie gut die Person, die die App verwendet, Verzögerungen erkennt.

Thread-Verwaltung

Grundsätzlich gilt, wenn Sie in SurfaceView, GLSurfaceView oder TextureView soll das Rendering in einem speziellen Thread erfolgen. Niemals "Schwere Heben" oder irgendetwas, das unbestimmte Zeit in Anspruch nimmt, UI-Thread. Erstellen Sie stattdessen zwei Threads für das Spiel: einen Spiel-Thread. und einem Rendering-Thread. Siehe Spielleistung verbessern .

Breakout und „Record GL App“ dedizierte Renderer-Threads verwenden Animationsstatus für diesen Thread aktualisieren. Dies ist ein vernünftiger Ansatz, solange schnell aktualisiert werden.

Bei anderen Spielen werden Spiellogik und Rendering vollständig getrennt. Wenn Sie eine bei dem nur alle 100 ms ein Block verschoben wurde, Diskussions-Thread, der gerade dies getan hat:

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(Möglicherweise möchtest du für die Schlafzeit eine feste Uhr verwenden, um eine Verlagerung zu vermeiden -- Sleep() ist nicht hundertprozentig konsistent, und MoveBlock() nimmt eine Zahl ungleich null aber Sie verstehen schon.)

Wenn der Zeichencode aktiviert wird, greift er einfach am Schloss und ruft die aktuelle Position ab. des Blocks, löst die Sperre auf und zeichnet. Anstelle von Bruchwerten basierend auf den Delta-Zeiten zwischen den Frames haben, haben Sie nur einen Thread, der sich und einem weiteren Thread, der die Dinge zieht, wo immer sie sich gerade befinden wenn die Zeichnung beginnt.

Für eine Szene von beliebiger Komplexität bietet es sich an, eine Liste der bevorstehenden Ereignisse zu erstellen. sortiert nach Aufwachzeit und Schlaf bis zum nächsten Termin. Idee.