Предыдущие части: Жизни и модальные окна, Подсчёт очков, Главное меню, Геймплей, Есть ли у него душа, Организация ввода, View, Визуализация поля, Загрузка уровня, INI-файл, Пишем Питона на Питоне!
Код для этой части находится в ветке sound на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
Конкретно в нашем случае для звука разрабатывать особо нечего, так как всё уже есть. В игровой библиотеке pygame, которую мы уже используем для графики и обработки ввода, есть методы и для воспроизведения звука.
За это отвечает компонент pygame.mixer и класс pygame.mixer.Sound. Всё, что требуется – это загрузить звуковую дорожку из файла типа .wav или .ogg.
У микшера есть несколько каналов. В каждом канале может воспроизводиться свой собственный звук, независимо от других.
Если мы создаём звуковой объект и даём ему команду играть, он начнёт играть в одном из каналов. Канал будет выбран автоматически, но нам это не подходит, так как проигрывание звука в случайном канале может не сработать вообще, если все каналы заняты.
Поэтому мы определяем количество каналов, которое нам нужно. Это пока что всего два канала: один для музыки и один для звуковых эффектов.
В канале для музыки будет играться, собственно, музыка, а в канале для звуковых эффектов будут играться звуки, когда питон ест яблоко, и когда он натыкается на препятствие. Если один из звуков перекроет другой, это не страшно. В дальнейшем список звуков можно будет расширить, но пока хватит.
Чтобы управлять каналами напрямую, нужно получить объект класса "канал" (pygame.mixer.Channel) и уже ему передавать объект класса "звук" (pygame.mixer.Sound), который мы хотим играть. Таким образом, мы всегда будем играть нужный звук в нужном канале.
Самих каналов нам нужно только два, поэтому установим их количество с помощью
pygame.mixer.set_num_channels(2)
Думаю, такое ограничение имеет смысл, потому что каналов по умолчанию 8, а убрав лишние, мы по идее облегчаем работу микшера.
Внимание! Непростой момент!
Размер звукового буфера в pygame.mixer по умолчанию равен 4096 байт. Работает он примерно так: звук, который надо воспроизвести, нарезается кусками по 4096 байт и первый кусок кладётся в буфер. У буфера есть внутренний указатель воспроизведения, который движется от начала к концу буфера. Когда указатель достигает конца буфера, в буфер кладётся следующий кусок и указатель переходит на начало и т.д.
На практике это приводит к задержке в воспроизведении звука:
- Мы вызываем метод Channel.play() или Sound.play()
- Звук начинает играть не сразу, а с некоторой задержкой.
Точную причину я не знаю, но моё понимание такое: любой новый звук начинает играть только тогда, когда текущий остаток буфера доиграет до конца и можно будет загружать новый кусок. Вот это ожидание, пока буфер освободится, и создаёт задержку.
Чтобы уменьшить эту задержку, надо уменьшить размер буфера. Чем меньше буфер, тем меньше придётся ждать, пока он доиграет до конца. Размер буфера должен быть кратен степени двойки, т.е. 16, 32, 64, 128 и т.д. В то же время слишком маленький размер буфера ставить нельзя. Это приведёт к очень частым его обновлениям, что сильно нагрузит процессор и звук начнёт хрипеть. Чем больше буфер – тем лучше для процессора и качества звука.
Путём экспериментов я выяснил, что размер буфера 2048 байт не приводит к ощутимой задержке в данной игре. Поэтому мы проинициализируем pygame.mixer так:
pygame.mixer.pre_init(44100, -16, 2, 2048)
pygame.mixer.init()
Здесь мы установили параметры: частота семплирования 44100 Гц, формат данных – 16-битный со знаком, число каналов 2 (не в смысле каналов воспроизведения, а в смысле стерео), и наконец размер буфера 2048. Порядок инициализации ВАЖЕН. Кроме того, эту инициализацию нужно сделать ДО общей pygame.init(), которая у нас находится в классе Graphics. Иначе параметры звука не установятся!
Затем получим каналы в виде отдельных объектов, с которыми будем работать:
channel_music = pygame.mixer.Channel(0)
channel_sfx = pygame.mixer.Channel(1)
Звуки нужно загрузить из файлов, но мы не собираемся каждый раз, когда надо играть звук, грузить его из файла. Поэтому создадим их заранее, и эти объекты будут существовать в течение всей игры.
music = pygame.mixer.Sound('data/audio/music.wav')
sfx_apple = pygame.mixer.Sound('data/audio/apple.wav')
sfx_death = pygame.mixer.Sound('data/audio/death.wav')
Теперь, чтобы сыграть музыку, мы вызовем
channel_music.play(music, -1)
Первым параметром мы передали объект класса Sound, который надо играть в канале. Второй параметр это количество повторений. Музыка будет в фоне играть бесконечно, поэтому ставим -1 (бесконечно).
Чтобы сыграть звук съедаемого яблока, мы вызовем
channel_sfx.play(sfx_apple)
Разница с музыкой в том, что здесь мы не указываем количество повторений.
Все эти функции я обернул в класс Audio, который, так же как и Graphics, инкапсулирует все методы pygame, чтобы не создавать лишних зависимостей.
Теперь осталось доработать контроллер GameController. Когда он определяет, что питон съел яблоко или врезался, он играет соответствующий звук:
game.audio.play_sfx('apple') или game.audio.play_sfx('death')
Также в качестве теста я добавил в контроллер MainMenuController проигрывание звука яблока на нажатиях стрелок вверх и вниз.
Вы можете заменить файлы *.wav на другие, чтобы играла другая музыка и другие звуки.
Теперь, когда в игре есть звук, необходимо доработать меню, чтобым этим звуком можно было управлять.
Читайте дальше: Остановите музыку
Читайте также:
- ООП в Python: особенности реализации