Звук и таймеры
Звук оживляет игру, а таймеры заставляют события происходить по расписанию — оба добавляются буквально парой узлов.
Суть: 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 до порога. Используй таймеры для спавна и перезарядки, ограничивай число объектов.