Звук и музыка в игре
Цыплёнок уже бегает и прыгает молча — пора, чтобы прыжок «пукал», монетка звенела, а на фоне крутился бодрый трек.
Спрайт рисует, как игра выглядит, а звук решает, как она ощущается. Один и тот же прыжок с весёлым «бойнг» и без него — это две разные игры.
Закрой глаза и вспомни любую игру, в которую ты залипал. Mario без «дзынь» от монетки? Geometry Dash без музыки, под которую ты подгадываешь прыжки? Among Us без того самого тревожного звука репорта? Звук — это половина кайфа, хотя его почти никогда не замечаешь специально. Замечаешь только когда он пропал: выключи звук в любимой игре и она сразу станет какой-то картонной, будто немое кино.
В прошлый раз ты добивал платформер: цыплёнок собирал монетки и убегал от врагов — мы делали это в уроке про предметы и врагов. Всё двигалось, всё сталкивалось, но было гробовое молчание. Сегодня мы дадим цыплёнку голос: научимся проигрывать короткие звуковые эффекты на прыжок и сбор монетки, запустим зацикленную фоновую музыку и сделаем кнопку «выключить звук», чтобы никого не бесить ночью в наушниках.
Что мы получим в конце
К концу урока у тебя будет вот такая картина: цыплёнок прыгает — слышен короткий «бойнг»; влетает в монетку — звенит «дзынь»; на фоне без остановки крутится трек, который начинается не сразу при загрузке, а после первого клика игрока. И будет кнопка с иконкой динамика, которая глушит вообще всё. Это полный звуковой набор для маленькой аркады.
Хорошая новость: звук в браузере устроен проще, чем рисование на canvas. Никаких контекстов и циклов — буквально «создал объект, сказал play()». Плохая новость: у браузера есть одно вредное правило, из-за которого музыка молчит у новичков. Про него отдельно поговорим, потому что на нём спотыкаются вообще все.
Звук как плеер в кармане
Представь, что у каждого звука в твоей игре есть свой маленький MP3-плеер. Ты заранее загрузил в него один трек, и теперь можешь в любой момент нажать «play» — плеер мгновенно проиграет свой звук. Хочешь, чтобы прыжок звучал — заводишь плеер с файлом «boing.mp3». Хочешь монетку — отдельный плеер с «coin.mp3». В JavaScript такой плеер называется объект Audio.
Создаётся он буквально одной строкой:
const jumpSound = new Audio('sounds/jump.mp3');
jumpSound.play();Результат: браузер подгружает файл sounds/jump.mp3 и сразу его проигрывает — ты слышишь короткий звук прыжка из динамиков.
Вот и весь базовый секрет. new Audio('путь') — это «купить плеер и вставить в него трек», а .play() — «нажать кнопку воспроизведения». Файлы лучше брать в формате .mp3 или .ogg — их понимают все браузеры. Короткие эффекты можно нарезать самому в любом аудиоредакторе или взять бесплатные с сайтов вроде freesound — главное, чтобы эффект был коротким, на доли секунды, иначе игра превратится в кашу из звуков.
Полезно сразу понять, в чём разница между двумя типами звука, которые мы будем делать. Есть эффекты — короткие, резкие, привязанные к конкретному событию: прыжок, монетка, удар. Они длятся доли секунды и срабатывают столько раз, сколько ты прыгнул. И есть фоновая музыка — длинный трек, который играет всё время и крутится по кругу. Технически и то и другое — один и тот же объект Audio, просто настроенный по-разному. Эффект ты дёргаешь по событию и перематываешь в начало, а музыку запускаешь один раз и зацикливаешь. Держи это различие в голове — оно объясняет, почему дальше мы пишем для них немного разный код.
Ещё одна важная привычка: все плееры создавай один раз, в самом начале программы, рядом с остальными переменными. Создание new Audio() — это поход в файл за дорожкой, операция не бесплатная. Если делать её каждый раз заново, игра будет дёргаться. Создал плееры на старте — и потом весь урок только дёргаешь у них play(), pause() и крутишь свойства. Это та же логика, что и со спрайтами: картинку героя ты тоже грузишь один раз, а не перед каждым кадром.
Пример 1. Звук прыжка цыплёнка
У нас уже есть цыплёнок, который прыгает: при нажатии пробела мы добавляли ему вертикальную скорость против гравитации. Найдём то самое место в коде, где начинается прыжок, и подвесим туда звук. Сначала где-то наверху, рядом с остальными переменными, заведём наш плеер:
const chicken = { x: 60, y: 200, vx: 0, vy: 0, onGround: true };
// плееры со звуками — создаём один раз при старте
const jumpSound = new Audio('sounds/jump.mp3');
function jump() {
if (chicken.onGround) {
chicken.vy = -12; // толкаем цыплёнка вверх
chicken.onGround = false;
jumpSound.currentTime = 0; // перематываем звук в начало
jumpSound.play(); // и проигрываем
}
}Результат: каждый раз, когда цыплёнок отрывается от земли, ты слышишь короткий «бойнг». Если зажать пробел и быстро прыгать, звук исправно срабатывает на каждый прыжок.
Зачем строка currentTime = 0
Это самая хитрая деталь, и без неё новички ломают голову. Объект Audio — это один плеер. Если ты нажал play(), а звук ещё доигрывает прошлый прыжок, второй play() просто ничего не сделает — плеер уже занят. Поэтому перед каждым проигрыванием мы вручную перематываем дорожку в самое начало через jumpSound.currentTime = 0. Это как нажать «стоп, в начало, play» одним движением — звук всегда стартует заново, даже если ты дубасишь по пробелу как сумасшедший.
Пример 2. Звон монетки
С монеткой та же история, но звук вешаем в момент коллизии. Помнишь проверку AABB из платформера — когда прямоугольник цыплёнка пересекается с прямоугольником монетки? Вот прямо туда:
const coinSound = new Audio('sounds/coin.mp3');
function collectCoins() {
for (const coin of coins) {
if (!coin.collected && hitAABB(chicken, coin)) {
coin.collected = true; // монетка исчезает
score += 1; // плюс очко
coinSound.currentTime = 0;
coinSound.play(); // дзынь!
}
}
}Результат: цыплёнок влетает в монетку — она пропадает, счёт растёт на единицу, и одновременно звенит «дзынь». Собираешь пять монеток подряд — слышишь пять чётких звоночков.
Обрати внимание: звук стоит ровно в той же ветке if, что и начисление очка. Это правильно — звук должен совпадать с тем, что видит игрок. Если повесить coinSound.play() снаружи проверки, он будет пиликать каждый кадр без остановки, 60 раз в секунду. Звук — это реакция на событие, а не фоновый шум.
Пример 3. Фоновая музыка по кругу
Эффекты — это короткие «дзынь» на событие. А музыка должна играть постоянно и сама перезапускаться, когда трек кончился. Для этого у объекта Audio есть два полезных свойства: loop и volume.
const music = new Audio('sounds/theme.mp3');
music.loop = true; // зациклить: кончился трек — играет заново
music.volume = 0.4; // громкость от 0 (тишина) до 1 (на всю)
function startMusic() {
music.play();
}Результат: как только вызывается startMusic(), трек начинает играть на 40% громкости и крутится по кругу без пауз — дошёл до конца и тут же стартует с начала.
loop = true — это галочка «повторять» в любом плеере. volume = 0.4 — ползунок громкости, где 1 это максимум, а 0 — полная тишина. Музыку почти всегда стоит делать тише эффектов: если трек гремит на единице, он заглушит «дзынь» от монетки, и игрок перестанет слышать обратную связь. Сорок процентов — комфортный старт, дальше подкрутишь на слух.
Главная грабля: браузер не даёт играть до клика
А теперь самое важное, из-за чего музыка молчит у каждого второго новичка. Ты пишешь music.play() прямо при загрузке страницы, открываешь — и тишина. В консоли вылезает что-то вроде play() failed because the user didn't interact with the document first. Кода нет ошибки — это сам браузер тебя блокирует.
Браузеры специально запрещают сайтам запускать звук до того, как игрок что-нибудь нажал на странице.
Логика простая и, если честно, добрая. Представь, что любой сайт мог бы заорать музыкой, едва ты его открыл — ты бы поседел, открывая 20 вкладок в тишине ночью. Поэтому есть правило: автозапуск звука со страницы запрещён, пока пользователь сам не кликнул, не нажал клавишу или не тапнул по экрану. Только после первого «контакта» браузер считает, что игрок реально пришёл в игру, и разрешает звук.
Решение — не запускать музыку сразу, а дождаться первого действия игрока. Обычно это и так происходит: в игре есть кнопка «Старт» или клик по холсту. Вешаем запуск музыки туда:
const music = new Audio('sounds/theme.mp3');
music.loop = true;
music.volume = 0.4;
let musicStarted = false;
// запускаем музыку при первом клике по странице
document.addEventListener('click', () => {
if (!musicStarted) {
music.play();
musicStarted = true; // больше не запускаем
}
});Результат: при загрузке тихо, но как только игрок щёлкнул мышкой хоть раз — фоновый трек заводится и дальше крутится по кругу. Флаг musicStarted следит, чтобы при каждом следующем клике музыка не запускалась заново поверх самой себя.
Разберём, что тут происходит, по шагам. addEventListener('click', ...) — это «подпишись на каждый клик по странице и каждый раз вызывай вот эту функцию». Внутри функции мы первым делом проверяем флаг: если музыка ещё не запускалась (!musicStarted), запускаем её и сразу ставим флаг в true. Со второго клика условие уже не сработает, и музыка спокойно играет себе дальше. Без этого флага каждый твой клик дёргал бы play() заново — и хотя браузер не плодит копий одного плеера, выглядит это неаккуратно, а на эффектах такой подход вообще привёл бы к наложению звуков.
Кстати, эта же первая «разблокировка» открывает дорогу всем звукам, а не только музыке. Браузер запоминает, что игрок уже взаимодействовал со страницей, и дальше эффекты прыжка и монетки проигрываются без проблем. Поэтому если ты вешаешь звуки на клавиши (как прыжок на пробел), они обычно работают сразу — нажатие клавиши само по себе считается взаимодействием. А вот музыка, которую ты хочешь завести до любого действия игрока, упрётся в запрет. Отсюда вывод: спрячь старт музыки за первый клик или за кнопку «Играть», и проблема исчезнет навсегда.
Пример 4. Кнопка «выключить звук»
Любой нормальный игрок рано или поздно захочет приглушить игру — он на уроке, рядом спит кот или он просто слушает свой плейлист. У объекта Audio для этого есть свойство muted. Заведём одну переменную-флаг и будем переключать её, а заодно глушить все наши плееры разом:
let soundOn = true;
const allSounds = [jumpSound, coinSound, music];
function toggleSound() {
soundOn = !soundOn; // переключаем вкл/выкл
for (const s of allSounds) {
s.muted = !soundOn; // muted = true — тишина
}
}
// рисуем иконку динамика в углу холста
function drawSoundIcon(ctx) {
ctx.font = '28px sans-serif';
ctx.fillText(soundOn ? '\uD83D\uDD0A' : '\uD83D\uDD07', 460, 36);
}Результат: в углу холста рисуется иконка динамика. По вызову toggleSound() все звуки разом замолкают, а иконка меняется на перечёркнутый динамик; повторный вызов возвращает звук обратно.
Мы собрали все плееры в массив allSounds и пробегаем по нему циклом — так не нужно вручную трогать каждый звук, а если завтра добавишь звук врага, просто допишешь его в массив. muted = true заглушает плеер мгновенно, при этом музыка не останавливается, а продолжает крутиться «беззвучно» — снимешь mute, и трек подхватится с того места, где играл бы.
Иконку мы рисуем прямо на canvas через ctx.fillText, используя символы-эмодзи динамика. Тернарный оператор soundOn ? '...' : '...' читается как «если звук включён — нарисуй обычный динамик, иначе — перечёркнутый». Это короткая запись для «выбрать одно из двух в зависимости от условия». Чтобы кнопка реагировала на клик, тебе останется в обработчике клика проверить, попал ли курсор в правый верхний угол холста, где нарисована иконка, и если да — вызвать toggleSound(). Координаты клика по холсту мы уже умели считать в уроке про управление мышью, так что это знакомая задача.
Почему именно muted, а не volume = 0? Можно и так, разница тонкая, но важная: muted — это отдельный выключатель, который не трогает выставленную громкость. Заглушил, потом снял mute — и музыка снова на своих 40%, как ты и настраивал. А если глушить через volume = 0, тебе придётся где-то запоминать прежнее значение, чтобы вернуть его обратно. Для кнопки «вкл/выкл звук» muted просто удобнее.
Частые ошибки и подводные камни
- Музыка молчит, в консоли ошибка про autoplay. Ты запускаешь звук до клика игрока. Перенеси
play()в обработчик клика или нажатия кнопки «Старт» — это правило браузера, обойти его «в лоб» нельзя. - Один и тот же звук не срабатывает при быстром повторе. Плеер ещё доигрывает прошлый раз. Добавь
звук.currentTime = 0передplay(), чтобы перематывать дорожку в начало. - Звук пиликает 60 раз в секунду. Ты поставил
play()внутри игрового цикла или снаружи проверкиif. Звук должен срабатывать на событие (прыжок, коллизия), а не каждый кадр. - Создаёшь
new Audio(...)внутри функции, которая зовётся каждый кадр. Тогда каждый кадр рождается новый плеер, память забивается, игра тормозит. Создавай объектыAudioодин раз при старте, рядом с остальными переменными, и переиспользуй. - Тишина и в консоли пусто. Проверь путь к файлу:
'sounds/jump.mp3'должен реально существовать рядом со страницей. Открой вкладку Network в инструментах разработчика — если файл красный с кодом 404, путь неверный.
Мини-практика: озвучь свою аркаду
Теперь твоя очередь. Возьми платформер с прошлого урока и доведи звук до ума:
- Добавь звук на приземление цыплёнка — короткий «туп», когда
onGroundснова становитсяtrueпосле полёта. - Сделай отдельный звук столкновения с врагом — что-нибудь тревожное, чтобы игрок сразу понял: что-то пошло не так.
- Добавь второй трек — музыку проигрыша. Когда игра заканчивается, останови фоновую музыку через
music.pause()и проиграй грустный джингл один раз (безloop). - Сделай так, чтобы кнопка звука реагировала на клавишу, например
M— повесьtoggleSound()на нажатие этой клавиши.
Подсказка для пункта 3: чтобы переключиться с фоновой музыки на джингл, сначала music.pause(), потом loseSound.play(). Не забудь, что джингл проигрыша зацикливать не надо — loop у него оставь по умолчанию false.
Итоги
Ты дал цыплёнку голос. Теперь ты умеешь главное про звук в браузерных играх: создавать плеер через new Audio() и проигрывать его на событие, перематывать дорожку через currentTime = 0 для быстрых повторов, зацикливать музыку через loop и крутить громкость через volume. И, что важнее всего, ты знаешь про вредное правило браузера — звук стартует только после первого клика игрока, и теперь это правило тебя не застанет врасплох.
Эти куски кода — отдельные плееры, флаг soundOn, иконка динамика — мы заберём с собой дальше. В следующих уроках раздела графики мы займёмся тем, чтобы игра не только звучала, но и красиво вспыхивала: добавим частицы — облачка пыли под ногами цыплёнка и брызги искр, когда он хватает монетку. Звук плюс частицы — и твоя простая аркада начнёт ощущаться как настоящая игра.