Глобальная система звуков. Создание собственного класса управления звуками с помощью паттерна Singleton

Эта публикация является переводом моей статьи о создании собственного Sound Manager-а (на украинском).

SoundЗанимаясь разработкой игры "Turtle Dreams to Fly" (сейчас доступна на FGL), я столкнулся с необходимостью управления звуками. Требовался обычный функционал: кнопки включить/выключить все звуки и музыку в игровом интерфейсе. Сложность ситуации заключалась в том, что разные звуки были привязаны к разным игровым объектам, и управлять ими с помощью единого интерфейса (кнопок mute music и mute soundeffects) представлялось невозможным.

Конечно, сегодня существует множество готовых решений (например, класс SoundManager), но их использование показалось мне не слишком удобным в первую очередь из-за неполного понимания чужого кода и нежелания «связываться» с его оптимизацией/отлавливанием багов и т. д… Поэтому было решено создавать свой собственный класс управления звуками, в котором будет только нужный функционал и который будет идеально вписываться в структуру игры.

Представленный ниже класс достаточно простой. По сути в нем есть только базовый функционал: библиотека звуков и реализация возможности включать/отключать звуковые эффекты и фоновою музыку. Предусмотрено так же определенное количество звуковых каналов для одновременного проигрывания нескольких звуковых эффектов. Для музыки используется один звуковой канал. Но имея такую основу, очень легко встраивать туда любой нужный вам функционал. Например, эффекты fade in и fade out, изменение громкости звуков и т. п.

Этот урок будет интересен в первую очередь начинающим разработчикам, которые, как и я в прошлом, пребывают в поисках простой, удобной и главное – понятной – системы управления звуками в игре. Но надеюсь, что более опытные разработчики так же найдут для себя что-то полезное, или хотя бы поделятся собственным опытом создания подобных классов в комментариях.

Так же хочу поблагодарить ребят из форума FlashGameDev, которые помогали искать решение в этой ветке.

Почему Singleton?

Уверен, что большинство читателей этого блога знают, что такое паттерн синглтон и используют его в своих проектах. Для остальных объясню:

Singleton – это по сути что-то среднее между статическим и динамическим классом. Можно создать только один экземпляр класса синглтон и всегда иметь доступ к нему из любого класса вашего игрового проекта через метод getInstance(). При этом, в отличии от статического класса, синглтон не противоречит принципам ООП и не плодит множество глобальных переменных и методов.

Единственный экземпляр класса синглтон создается не через ключевое слово new, а через метод getInstance(). Попытка создать экземпляр с помощью new НазваниеКласса() из любого другого класса проекта заканчивается ошибкой. При этом метод getInstance() создает экземпляр всего один раз, после чего при использовании просто возвращает ссылку на этот экземпляр.
Главное преимущество синглтона заключается в том, что теперь стоит только импортировать его в любой класс вашего проекта – и можно обращаться к его публичным переменным/методам через конструкцию НазваниеКласса.getInstance().НазваниеМетода()

Собственно код класса управления звуками

В моей игре класс имел название TurtleSounds, поэтому в коде я оставил это название. Так же по возможности я пытался прокомментировать все важные моменты в коде. Если не понятно, зачем используется или как работает тот или иной фрагмент кода – задавайте вопросы в комментариях.

package ваш.пакет
{
  //Импортируем все нужные классы
  import flash.events.MouseEvent;
  import flash.events.Event;
  import flash.media.Sound;
  import flash.media.SoundChannel;
  import flash.media.SoundTransform;

  public class TurtleSounds 
  {
    //В классе синглтон используется всего две статические переменные
    public static var _instance:TurtleSounds;
    public static var _allowInstance:Boolean = false;   

    public var allowSounds:Boolean; //Переменная отвечает за разрешение проигрывать музыку 
    public var allowSFX:Boolean; //Переменная отвечает за разрешение проигрывать звуковые эффекты
    var sound:Object; //Звуковые эффекты
    var music:Sound; //Музыка
    private var mSoundChannels:Array; //Массив для звуковых эффектов
    private var mMusicChannel:SoundChannel; //Единственный звуковой канал для музыки
    private const MAX_SOUND_CHANNELS:int = 8; //Количество звуков, которые могут проигрываться одновременно
                 
    //Обратите внимание, что мы не создаем экземпляр класса, если _allowInstance == false
    public function TurtleSounds ()     
    {
      if (!_allowInstance)
      {
        throw new Error("Error: Use TurtleSounds.getInstance() instead of the new keyword."); //Ошибка при попытке создать экземпляр класса, если _allowInstance == false
      }
      initSounds(); //Только при _allowInstance == true создаем экземпляр класса
    }
                
    //Обратиться к экземпляру или создать его (если он еще не создан) можно только через метод getInstance()
    public static function getInstance():TurtleSounds
    {
      if (_instance == null) //Если экземпляр еще не создан
      {
        _allowInstance = true; //Устанавливаем _allowInstance = true (иначе конструктор класса не позволит создать экземпляр)
        _instance = new TurtleSounds(); //Создаем экземпляр класса
        _allowInstance = false; //И возвращаем _allowInstance = false. Больше эта переменная никогда не станет true, а значит создать еще один экземпляр физически невозможно
      }
      return _instance; //Если экземпляр уже существует, возвращаем ссылку на него
    }


    private function initSounds (): void
    {
      allowSounds = true;
      allowSFX = true;
                        
      mSoundChannels = [];
                        
      sound = new Object();
                        
      //Добавляем все нужные звуковые эффекты
      sound['sound1'] = new sound1();
      sound['sound2'] = new sound2();
      sound['sound3'] = new sound3();
                        
      //Добавляем саундтрек
      music = new backgroundMusic();
    }
                
    //Метод, который отвечает за проигрывание звукового эффекта
    public function playSFX (sound_name:String):void
    {
      if (!allowSFX) return; //Если allowSFX == false – не проигрываем эффект и останавливаем исполнение метода
      var thisSound:Sound = sound[sound_name];
      if (!sound) return; //Останавливаем метод, если указанного звука не существует
                        
      //Если количество звуков в массиве больше максимально допустимого (в нашем случае 8 каналов) - "убиваем" лишний звук
      if (mSoundChannels.length >= MAX_SOUND_CHANNELS)
      {
        var unluckySound:SoundChannel = mSoundChannels.shift();
        unluckySound.stop();
      }
                        
      var sndChannel:SoundChannel = thisSound.play(); //Включаем звук в канале
      mSoundChannels.push(sndChannel); //Добавляем канал к масиву
      sndChannel.addEventListener(Event.SOUND_COMPLETE, OnSFXComplete); //Добавляем слушатель события, который проверяет, не закончилось ли проигрывание звука. Если закончилось - вызывается метод OnSFXComplete
    }   
                
    //Этот метод удаляет звук с масива после окончания проигрывания
    private function OnSFXComplete(e:Event):void
    {
      var thisSoundChannel:SoundChannel = e.currentTarget as SoundChannel;
      thisSoundChannel.removeEventListener(Event.SOUND_COMPLETE, OnSFXComplete);
      var idx:int = mSoundChannels.indexOf(thisSoundChannel);
      mSoundChannels.splice(idx, 1);
    }
                
    //Этот метод отвечает за проигрывание фоновой музыки
    public function playMusic():void
    {
      var soundTransform:SoundTransform = new SoundTransform(1); //Громкость на максимум

      if (!allowSounds) soundTransform.volume = 0; //Громкость на "0", если звуки отключены
                        
      if (!mMusicChannel) 
      {
        mMusicChannel = music.play(0, 999, soundTransform); //Чтобы достичь бесперерывного звучания, количество повторов - 999 (при желании можно и больше)
      } else {
        mMusicChannel.soundTransform = soundTransform;
      }
    }
  } 
}


Теперь можно использовать класс TurtleSounds в любом месте своей игры, импортировав его туда:

import ваш.пакет.TurtleSounds;


В любом месте, где вам понадобится проиграть звуковой эффект, вставляем:

TurtleSounds.getInstance().playSFX('НазваниеЭффекта');


Музыка включается так:

TurtleSounds.getInstance().playMusic();


Для реализации кнопок включения/выключения звуков/музыки переключаем переменные allowSFX или allowSounds с true на false и обратно. При этом изменения будут применятся ко всем звукам вне зависимости от их привязки к тем или иным объектам.

Надеюсь, статья была для вас полезной. Буду благодарен за критику и особенно – возможности усовершенствования данного класса.

Комментарии (20)

0
Для аналогичного класса в своем проекте мне показалось удобным еще добавить метод playSingleForClass (не позволяет одновременно воспроизводить одинаковый звук экземплярам одного класа) и playSingleForInstance (не позволяет одновременно играть одинаковый звук одному объекту). Достаточно типичная ситуация когда однотипные объекты воспроизводят одинаковые звуки, и чтобы они на накладывались можно воспользоваться таким вот ходом.
Плюс как по мне удобней давать доступ к экземпляру синглтона таким вот образом:
public static function get instance():TurtleSounds

ИМХО вызов
TurtleSounds.instance.playSFX()
выглядит эстетичней
TurtleSounds.getInstance().playSfx()
0
Для аналогичного класса в своем проекте мне показалось удобным еще добавить метод playSingleForClass (не позволяет одновременно воспроизводить одинаковый звук экземплярам одного класа) и playSingleForInstance (не позволяет одновременно играть одинаковый звук одному объекту).

Как мне кажется, это зависит от задумки разработчика. В игре, для которой писался этот клас, как-раз многократное воспроизведение одинаковых звуков не является проблемой. Там главный герой собирает много монет и звуки «ding» могут накладываться, создавая ощущения подобные к тому, как сыпятся монеты с игрового автомата — то есть звук приятный для игрока.

ИМХО вызов

TurtleSounds.instance.playSFX()


выглядит эстетичней

TurtleSounds.getInstance().playSfx()

Согласен, возьму на вооружение в будущих проектах!
+1
Вообще говоря, если программист не может написать такой простой класс как в посте, то ему надо учиться и учиться. А основные сложности и возня у разработчика возникают тогда, когда надо организовать честную паузу, следить за количеством каналов и менеджить их по приоритетам (а не просто глушить несчастливый канал, например у меня одновременно идет перестрелка, взрывчики и один ярко выделяющийся на фоне старт баллистической ракеты — если число каналов переполнилось, то убрав один из выстрелов я мало что пореяю, а вот если заглохнет резко один выделяющийся страт, то это fail).

Потом, а что делать с зацикленными спецэффектами? Например едет танчик, пока он жив — крутится по циклу жужжание мотора и гусениц. Надо запоминать того кто вызвал спецэффек и уметь принудительно его стопить.

Кроме того, тюнить громкость и стереобазу каждого эффекта в аудиоредакторе это неверно (да и долго), поэтому каждый саунд должен импортироваться нормализованным и в коде в библиотеке должен иметь связанный с ним саундтрансформ.

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

Плюс еще всегда хорошо для однотипных часто повторяющихся звуков (выстрелов, попаданий итп) — иметь некоторый набор звуков. Иначе в ухо начинает резать. Соответственно, в коде запуск такого звука — это вызов метода с передачей типа «попадание_в_плоть». А уже менеджер рандомно пустит что-то из набора.
0
По моему флеш загнётся, если реализовать всё вышеперечисленное. Но я уже хочу это. (:
0
Всмысле загнется? По перфомансу? У меня все это реализовано и работает на ура. Код кушает доли процента, весь перфоманс отжирает внутренний рендер флеша. Даже при том, что у меня почти нет векторной графики.

Если найду силы причесать код моего СаундМенеджера, то выложу. Не уверен, комменты надо написать, убрать специфику. Там просто я запихал много специфичных для игры вещей. Типа конкретных алгоритмов оптимизации звуков движения юнитов, ибо у меня их на экране может быть много, поэтому они анализируются по позиции, путям, типам итд.

Так как я работаю один, то я не заморачиваюсь архитектурными задумками абстрагировать классы. Написаны они довольно прозрачно, поэтому для другой игры просто беру нужный класс и адаптирую.
+7
Пожалуйста, найди в себе силы хотя бы выложить этот класс куда-нибудь — очень интересно поизучать.
0
соглашусь
+1
минимально причешу, напишу хоть пару комментов-пояснений и выложу
0
Присоединяюсь.
+1
Ого, спасибо за такой интересный набор возможных спецэффектов и функций! Это у вас уже получается настоящий монстр «на все случаи жизни». :)

Касательно статьи — она больше предназначена для новичков. По крайней мере я не нашел ничего подобного, когда «шерстил» интернет в поисках информации о создании собственного класса управляющего звуками. Очень надеюсь, что кому-то пригодится.
0
Можете еще тут идеи черпнуть)
0
Интересная статья, спасибо.
0
Давно собирался спросить:

//Если количество звуков в массиве больше максимально допустимого (в нашем случае 8 каналов) — «убиваем» лишний звук

Какая может быть опасность, если такового не делать? И вообще не создавать массив для каналов, а оставлять удаление остановившихся каналов на совести Garbage Collector'a?

В данном случае ведь тоже после
unluckySound.stop();
наступает работа для GC?
0
как бы, если уже играет максимально допустимое(аппаратно) кол-во каналов, то при попытке проиграть ещё один — будет фейл(thisSound.play() вернёт null).
0
О, тогда понятно, спасибо!
0
У меня вопрос: и все же, почему Singleton? а не статичный класс?
0
0
спасибо, но вопрос остался открытым.
не что такое Singleton,
а почему именно Singleton,
а не статичный класс.
0
Там и комментарии есть
0
Я использую свой класс практически с такими же возможностями, но синглтон у меня отдельно и я так же вызываю свой звуковой класс где хочу через главный класс игры. Короче говоря звуковой класс это одно, а синглтон тут не причем. А вот в звуковом классе своем реализовал такие методы как погашение громкости музыки во время проигрывания звука, погашение громкости на время, (все плавное с параметрами). ну и стандартные вкл/выкл тоже плавные с параметрами. В будущем хочу добавить разные фишки на контроль повтора звука, часто с физикой колдовать приходится.
  • Plov
  • Plov
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.