Gameplay mit Audio und Musik synchronisieren

Einführung

In jeder Anwendung oder jedem Spiel verzögert sich die Ton- und Musikwiedergabe geringfügig. Bei Spielen ist diese Verzögerung oft so gering, dass sie vernachlässigbar ist. Soundeffekte werden einige Millisekunden nach dem Aufrufen einer play() Funktion ausgegeben. Für Musik spielt dies keine Rolle, da sie in den meisten Spielen nicht mit dem Gameplay interagiert.

Bei einigen Spielen (hauptsächlich Rhythmus-Spielen) kann es jedoch erforderlich sein, Spieleraktionen mit etwas zu synchronisieren, das in einem Song passiert (normalerweise synchron mit dem BPM). Zu diesem Zweck ist es nützlich, genauere Timing-Informationen für eine genaue Wiedergabeposition zu haben.

Es ist schwierig, eine sehr geringe Zeitgenauigkeit der Wiedergabe zu erreichen. Dies liegt daran, dass während der Audiowiedergabe viele Faktoren eine Rolle spielen:

  • Audio wird in Blöcken gemischt (nicht kontinuierlich), abhängig von der Größe der verwendeten Audiopuffer (überprüfen Sie die Latenz in den Projekteinstellungen).

  • Gemischte Audiostücke werden nicht sofort abgespielt.

  • Grafik-APIs zeigen zwei oder drei Frames zu spät an.

  • Bei der Wiedergabe auf Fernsehgeräten kann es aufgrund der Bildverarbeitung zu Verzögerungen kommen.

Die häufigste Methode zum Reduzieren der Latenz besteht darin, die Audiopuffer zu verkleinern (wiederum durch Bearbeiten der Latenzzeiteinstellung in den Projekteinstellungen). Das Problem ist, dass bei zu geringer Latenz für das Mischen von Sounds erheblich mehr CPU erforderlich ist. Dies erhöht das Risiko des Überspringens (ein Riss im Sound, weil ein Mix-Rückruf verloren gegangen ist).

Dies ist ein häufiger Kompromiss, daher wird Godot mit vernünftigen Standardeinstellungen geliefert, die nicht geändert werden müssen.

Das Problem ist letztendlich nicht diese leichte Verzögerung, sondern die Synchronisierung von Grafik und Audio für Spiele, die dies erfordern. Beginnend mit Godot 3.2 wurden einige Hilfen hinzugefügt, um ein genaueres Wiedergabe-Timing zu erhalten.

Nutze die Systemzeit zum synchronisieren

Wie bereits erwähnt, beginnt die Audiowiedergabe nicht sofort nach dem Aufrufen von AudioStreamPlayer.play(), sondern erst, nachdem der Audio-Prozess den nächsten Chunk verarbeitet hat.

Diese Verzögerung kann nicht vermieden werden, kann jedoch durch Aufrufen von AudioServer.get_time_to_next_mix() geschätzt werden.

Die Ausgabe-Latenz (nach dem Mischen) kann also durch das Aufrufen von AudioServer.get_output_latency() abgeschätzt werden.

Wenn Sie diese beiden hinzufügen, können Sie fast genau erraten, wann während _process() Ton oder Musik in den Lautsprechern abgespielt werden:

var time_begin
var time_delay


func _ready():
    time_begin = OS.get_ticks_usec()
    time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
    $Player.play()


func _process(delta):
    # Obtain from ticks.
    var time = (OS.get_ticks_usec() - time_begin) / 1000000.0
    # Compensate for latency.
    time -= time_delay
    # May be below 0 (did not begin yet).
    time = max(0, time)
    print("Time is: ", time)

Auf lange Sicht werden die Timing-Informationen jedoch langsam verschwinden, da die Sound-Hardware-Uhr nie genau mit der Systemuhr synchronisiert ist.

Für ein Rhythmus-Spiel, bei dem ein Song nach einigen Minuten beginnt und endet, ist dieser Ansatz in Ordnung (und auch empfohlen). Für ein Spiel, bei dem die Wiedergabe viel länger dauern kann, ist das Spiel möglicherweise nicht mehr synchron und es ist ein anderer Ansatz erforderlich.

Verwenden der Sound-Hardware-Uhr zum Synchronisieren

Verwende AudioStreamPlayer.get_playback_position() um die aktuelle Position des Songs zu erhalten, bei der das Lied ideal klingt. Dies ist aber nicht so nützlich wie es sich anhört. Dieser Wert wird in Blöcken erhöht (jedes Mal, wenn der Audio-Rückruf einen Tonblock mischt), sodass viele Aufrufe denselben Wert zurückgeben können. Hinzu kommt, dass der Wert aus den zuvor genannten Gründen auch nicht mit den Lautsprechern synchron ist.

To compensate for the "chunked" output, there is a function that can help: AudioServer.get_time_since_last_mix().

Das Hinzufügen des Rückgabewerts dieser Funktion zu get_playback_position() erhöht die Genauigkeit:

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()

Um die Genauigkeit zu erhöhen, subtrahieren Sie die Latenzinformationen (wie viel Zeit benötigt wird, um das Audio nach dem Mischen zu hören):

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()

Das Ergebnis kann aufgrund der Funktionsweise mehrerer Threads etwas verzerrt sein. Überprüfen Sie einfach, ob der Wert nicht kleiner als im vorherigen Frame ist (verwerfen Sie ihn gegebenenfalls). Dies ist auch ein weniger präziser Ansatz als der vorherige, funktioniert jedoch für Songs beliebiger Länge oder zum Synchronisieren von Elementen (z.B. Soundeffekte) mit Musik.

Hier ist derselbe Code wie vor diesem Ansatz:

func _ready():
    $Player.play()


func _process(delta):
    var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
    # Compensate for output latency.
    time -= AudioServer.get_output_latency()
    print("Time is: ", time)