Синхронізація гри зі звуком та музикою¶
Вступ¶
У будь-якому додатку, або грі, відтворення звуку і музики матиме невелику затримку. Для ігор ця затримка часто настільки мала, що немає значення. Звукові ефекти з'являться через кілька мілісекунд після виклику будь-якої функції 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 = 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)
private double _timeBegin;
private double _timeDelay;
public override void _Ready()
{
_timeBegin = OS.GetTicksUsec();
_timeDelay = AudioServer.GetTimeToNextMix() + AudioServer.GetOutputLatency();
GetNode<AudioStreamPlayer>("Player").Play();
}
public override void _Process(float _delta)
{
double time = (OS.GetTicksUsec() - _timeBegin) / 1000000.0d;
time = Math.Max(0.0d, time - _timeDelay);
GD.Print(string.Format("Time is: {0}", 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()
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
Щоб збільшити точність, відніміть інформацію про затримку (скільки потрібно для того, щоб звук був почутий після його змішування):
var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - AudioServer.GetOutputLatency();
Результат може бути трохи тремтячим через те, як працюють кілька потоків. Просто переконайтеся, що значення не менше, ніж у попередньому кадрі (відкиньте його, якщо так). Це також менш точний підхід, ніж раніше, але він буде працювати для пісень будь-якої довжини, або синхронізації чого-небудь (звукові ефекти, як приклад) з музикою.
Ось той же код, що і до використання такого підходу:
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)
public override void _Ready()
{
GetNode<AudioStreamPlayer>("Player").Play();
}
public override void _Process(float _delta)
{
double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
// Compensate for output latency.
time -= AudioServer.GetOutputLatency();
GD.Print(string.Format("Time is: {0}", time));
}