Звук и таймеры

Звук оживляет игру, а таймеры заставляют события происходить по расписанию — оба добавляются буквально парой узлов.

Суть: AudioStreamPlayer проигрывает звук по команде play(), а Timer шлёт сигнал timeout через заданное время.

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

extends Node

@onready var jump_sound: AudioStreamPlayer = $JumpSound

func jump() -> void:
    jump_sound.play()

Вторая героиня урока — Timer: узел-будильник. Ты задаёшь ему интервал (например, 2 секунды), запускаешь, и через это время он шлёт сигнал timeout. Таймеры используют для всего, что должно происходить по расписанию: спавн врагов каждые несколько секунд, перезарядка оружия, обратный отсчёт.

Поток таймера спавна:

  Timer (wait_time = 2с) --start--> тикает...
        |
        v  каждые 2 секунды
  timeout ----> _on_timeout() ----> создать врага

Стоит уловить, что таймер — это способ думать о времени декларативно, а не вручную. Можно было бы в _process каждый кадр прибавлять delta к собственной переменной и сравнивать с порогом — но это шумно и легко ошибиться. Узел Timer прячет эту бухгалтерию внутри себя: ты просто говоришь «разбуди меня через две секунды», и он будит. Точно так же звук — это декларация «сыграй вот это», а не ручное складывание звуковых волн. Движок берёт рутину на себя, а ты описываешь намерение. Чем больше такой рутины ты доверяешь движку, тем чище и короче твой собственный код.

Как работает под капотом

Timer каждый кадр уменьшает свой внутренний счётчик на delta. Когда счётчик дошёл до нуля, узел эмитит сигнал timeout. Если таймер зациклен (one_shot = false), счётчик сбрасывается и отсчёт идёт снова — так получается равномерный поток событий. Это тот же приём с накоплением delta, что и в движении, только тут мы ждём порога.

wait_time = 2.0   # секунды между событиями
elapsed = 0.0
enemies = 0
delta = 0.5       # шаг "кадра"

for frame in range(1, 13):
    elapsed += delta
    if elapsed >= wait_time:
        elapsed = 0.0          # сброс, таймер зациклен
        enemies += 1
        print(f"Кадр {frame:2}: TIMEOUT -> спавн врага #{enemies}")

print("Всего заспавнено врагов:", enemies)

Та же логика на Python ▶. Таймер копит время, и при достижении порога шлёт «timeout» и сбрасывается. Так из равномерных кадров рождается ритмичный спавн врагов раз в две секунды.

Полезно подумать и про культуру звука в игре. Слишком громкие или часто повторяющиеся эффекты быстро утомляют, а полное молчание делает мир мёртвым. Хорошие разработчики дают игроку отдельные регуляторы громкости для музыки и эффектов и подбирают звуки так, чтобы они подсказывали, а не раздражали: тихий щелчок монеты, отчётливый, но не визгливый звук урона, ненавязчивая фоновая музыка. Звук — это половина ощущения от игры, хотя его и не видно. Уделив ему хотя бы немного внимания, ты заметно поднимешь впечатление от своего проекта почти без усилий.

Частые ошибки

Первая ошибка со звуком — переиспользовать один плеер для всего: если звук уже играет, повторный play() его обрывает; для частых эффектов лучше отдельные плееры или их пул. Вторая — забыть запустить таймер (start()) или подключить его сигнал timeout: тогда «будильник» молчит. Третья — забыть про флаг one_shot: одноразовый таймер срабатывает один раз, зацикленный — снова и снова; легко перепутать и удивляться, что враги не спавнятся. Четвёртая — спавнить врага по таймеру и не ограничивать их число: экран забивается, игра тормозит.

Best practices

Короткие эффекты (прыжок, монета) — на отдельных AudioStreamPlayer, чтобы не обрывали друг друга. Музыку — на один зацикленный плеер. Для повторяющихся событий используй зацикленный Timer и реагируй на его сигнал timeout, а не считай время вручную в _process. Выноси интервал таймера в @export, чтобы крутить темп игры из инспектора. Ограничивай число заспавненных объектов.

Итоги: AudioStreamPlayer проигрывает звук методом play(); короткие эффекты — на отдельных плеерах, музыка — на зацикленном. Timer — будильник: задаёшь интервал, ловишь сигнал timeout, зацикленный таймер шлёт его снова и снова. Под капотом таймер копит delta до порога. Используй таймеры для спавна и перезарядки, ограничивай число объектов.

Проверьте себя
1. Какой сигнал шлёт узел Timer, когда заданное время истекло?
Afinished
Btimeout
Cdone
Dbody_entered
2. Почему короткие звуковые эффекты лучше держать на отдельных AudioStreamPlayer?
AТак звук громче
BОдин плеер обрывает текущий звук при повторном play(), отдельные плееры не мешают друг другу
CИначе звук не запустится
DОтдельные плееры экономят память