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.

Синхронізація гри зі звуком та музикою

Вступ

У будь-якому додатку, або грі, відтворення звуку і музики матиме невелику затримку. Для ігор ця затримка часто настільки мала, що немає значення. Звукові ефекти з'являться через кілька мілісекунд після виклику будь-якої функції play(). Для музики це не має значення, оскільки в більшості ігор вона не взаємодіє з ігровим процесом.

Тим не менш, для деяких ігор (в основному, ритм-ігор) може знадобитися синхронізація дій гравця з чимось, що відбувається в пісні (зазвичай синхронізовано з BPM). Для цього корисно мати точнішу інформацію про час позиції відтворення.

Досягти дуже малої різниці часу важко. Це пов'язано з тим, що під час відтворення звуку відтворюється ще багато факторів:

  • Звук змішується в шматки (не безперервно), в залежності від розміру використовуваних аудіо буферів (перевірте затримки в параметрах проекту).

  • Змішані фрагменти звуку відтворюються не відразу.

  • Графічні API відображаються із запізненням на два-три кадри.

  • При відтворенні на телевізорах може бути додана деяка затримка через обробку зображень.

Найпоширенішим способом зменшення затримки є зменшення звукових буферів (знову ж таки, шляхом редагування параметра затримки в налаштуваннях проекту). Проблема в тому, що коли затримка занадто мала, змішування звуку зажадає значно потужнішого процесора. Це збільшує ризик пропуску (тріщини в звучанні, тому що зворотний виклик міксу був втрачений).

Це загальний компроміс, тому Godot поставляється з розумними дефолтами, які не треба міняти.

Проблема, врешті-решт, не в цій невеликій затримці, а в синхронізації графіки та звуку в іграх, які цього вимагають. Починаючи з Godot 3.2, були додані деякі помічники, для отримування точнішого часу відтворення.

Використання системного годинника для синхронізації

Як згадувалося раніше, Якщо ви викликаєте AudioStreamPlayer.play(), звук почнеться не відразу, а коли звуковий потік обробить наступний фрагмент.

Цієї затримки не уникнути, але її можна вирахувати викликавши AudioServer.get_time_to_next_mix().

Затримку виводу (що відбувається після змішування) також можна вирахувати викликавши AudioServer.get_output_latency().

Додайте ці дві функції, і зможете майже точно вгадати, коли звук, або музика, почнуть відтворюватися в динаміках під час виконання _process():

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)

У довгостроковій перспективі, однак, оскільки звуковий апаратний годинник ніколи точно не синхронізується з системним годинником, інформація про хронометраж повільно відхиляється.

Для ритм-гри, де пісня починається і закінчується через кілька хвилин, цей підхід прекрасний (і це рекомендований підхід). Для гри, де відтворення може тривати набагато довше, гра в кінцевому підсумку вийде з синхронізації, тому потрібен інший підхід.

Використання звукового апаратного годинника для синхронізації

Використання AudioStreamPlayer.get_playback_position() для отримання поточної позиції відтворення пісні здається ідеальним, але це не зовсім так. Це значення буде збільшуватися в фрагментах (кожен раз, при обробці нового блоку звуку), тому багато викликів можуть повертати одне і те ж значення. Окрім цього, значення також буде не синхронізоване з динаміками через раніше згадані причини.

Компенсувати "фрагментований" вивід може допомогти функція: AudioServer.get_time_since_last_mix().

Додавання повернутого значення з цієї функції до get_playback_position() збільшує точність:

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

Щоб збільшити точність, відніміть інформацію про затримку (скільки потрібно для того, щоб звук був почутий після його змішування):

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

Результат може бути трохи тремтячим через те, як працюють кілька потоків. Просто переконайтеся, що значення не менше, ніж у попередньому кадрі (відкиньте його, якщо так). Це також менш точний підхід, ніж раніше, але він буде працювати для пісень будь-якої довжини, або синхронізації чого-небудь (звукові ефекти, як приклад) з музикою.

Ось той же код, що і до використання такого підходу:

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)