Синхронизация игры со звуками и музыкой

Введение

В приложении или игре воспроизведения звуков и музыки имеют небольшую задержку. Для игр эта задержка часто настолько мала, что ей пренебрегают. Звуковые эффекты появятся через несколько миллисекунд после вызова функции play (). Для музыки это не имеет значения, поскольку в большинстве игр она не влияет на игровой процесс.

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

Достичь низкой задержки воспроизведения трудно. Это происходит потому, что во время воспроизведения аудио работают много других процессов :

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

  • Микшированные фрагменты звука воспроизводятся не сразу.

  • API графики имеют задержку на два или три кадра.

  • При воспроизведении на телевизоре может добавляться некоторая задержка из-за передачи изображения.

Самый распространенный способ уменьшить задержку - уменьшить аудиобуфер (опять же, отредактировав настройку задержки в настройках проекта). Проблема в том, что когда задержка слишком мала, для микширования звука требуется значительно больше процессорного времени. Это увеличивает риск пропуска звука из-за того, что процессор не успел подготовить микс.

Это распространенный компромисс, поэтому Godot поставляется с настройками по умолчанию, которые не требуется изменять.

Проблема, в конце концов, не в этой небольшой задержке, а в синхронизации графики и звука для игр, в которых это требуется. Начиная с Godot 3.2, были добавлены некоторые помощники для получения более точного времени воспроизведения.

Использование системных часов для синхронизации

Как упоминалось ранее, если вы вызовете: ref: AudioStreamPlayer.play () <class_AudioStreamPlayer_method_play>, звук начнется не сразу, а когда процессор обработает фрагмент.

Этой задержки нельзя избежать, но ее можно оценить, вызвав: ref: AudioServer.get_time_to_next_mix () <class_AudioServer_method_get_time_to_next_mix>.

Выходную задержку (ту, что происходит после микширования) также можно оценить, вызвав: ref: AudioServer.get_output_latency () <class_AudioServer_method_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));
}

В долгосрочной перспективе, звуковые аппаратные часы никогда не синхронизируются точно с системными часами, информация о времени будет постепенно расстраиваться.

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

Использование звуковых аппаратных часов для синхронизации

Использование: ref: AudioStreamPlayer.get_playback_position () <class_AudioStreamPlayer_method_get_playback_position> для получения текущей позиции воспроизведения кажется идеально, но это не так. Это значение будет увеличиваться по частям (каждый раз, когда обрабатывается очередной блок звука), поэтому многие вызовы могут возвращать одно и то же значение. Кроме того, значение будет не синхронизировано с динамиками по ранее упомянутым причинам.

Чтобы компенсировать "фрагментированный" вывод, есть функция, которая может помочь:: ref: AudioServer.get_time_since_last_mix () <class_AudioServer_method_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));
}