Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

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 den 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 gemixt (nicht kontinuierlich), abhängig von der Größe der verwendeten Audiopuffer (überprüfen Sie die Latenz in den Projekteinstellungen).

  • Gemixte Audio-Blö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 Mixen von Sounds erheblich mehr CPU erforderlich ist. Dies erhöht das Risiko von Skipping (ein Knacks im Sound, weil ein Mix-Callback verloren gegangen ist).

Dies ist ein häufiger Kompromiss, daher wird Godot mit vernünftigen Default-Einstellungen 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.

Synchronisierung mit der Systemuhr

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 = Time.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 = (Time.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)

Da die Uhr der Sound-Hardware jedoch nie genau mit der Systemuhr übereinstimmt, werden die Zeitinformationen auf lange Sicht abdriften.

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

Die Verwendung von AudioStreamPlayer.get_playback_position(), um die aktuelle Position für den Song zu erhalten, hört sich ideal an, ist aber in der jetzigen Form nicht sehr nützlich. Dieser Wert wird stückweise inkrementiert (jedes Mal, wenn der Audio-Callback einen Soundblock mixt), so dass viele Aufrufe denselben Wert zurückgeben können. Hinzu kommt, dass der Wert aus den bereits erwähnten Gründen nicht mit den Lautsprechern synchronisiert ist.

Um die Ausgabe in Stücken zu kompensieren, gibt es eine Funktion, die helfen kann: 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 Mixen 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 zuvor, aber mit 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)