Доволі інтенсивна робота над грою“Turtle Dreams to Fly” дозволила мені досить швидко засвоїти основні принципи програмування на AS3. І хоча до справжнього розуміння концепціїООП (об’єктно-орієнтованого програмування) мені ще дуже далеко, все ж виходить, що я вже переріс рівень абсолютно “початкових” уроків, які зазвичай публікую на цьому блозі.

Саме тому сьогодні я вирішив поділитися з вами досвідом розробки, який уже досить складно назвати “початковим” рівнем. Сподіваюся, він буде цікавий розробникам, які вже освоїли ази AS3 і працюють над втіленням своїх перших повноцінних флеш-проектів.

Звісно, базові уроки залишатимуться основою блога і я продовжуватиму публікувати їх. Але відтепер тут час від часу з’являтимуться і статті для людей зі знанням “азів ActionScript”. Сподіваюся, це не відштовхне новачків, а навпаки – допоможе їм швидше розібратися з програмуванням у Flash.

Тож сьогодні ми поговоримо про створення глобальної системи звуків для ігрового флеш-проекту.

Чужий та власний код

ActionScript 3 пропонує доволі багатий набір інструментів для роботи зі звуком. Вбудовані класи Sound і SoundChannel дозволяють імпортувати звукові файли безпосередньо у fla-файл і керувати ними методами ActionScript. Втім, якщо ви хочете створити по-справжньому зручну і гнучку звукову систему – то тут не обійтися без зовнішнього рішення.

Можна скористатися вже готовими класами, такими якSoundManager. Але особисто мені не дуже подобається ідея використання чужого коду у своїх проектах. В першу чергу через неповне розуміння того, як працюють такі класи.

 

Набагато логічнішим (хоч і більш часовитратним) виглядає створення власного класу управління звуками у флеш-проекті:

  • по-перше, ви точно знатимете “що” і “як” відбувається у вашому коді;
  • по-друге, власний клас завжди ідеально підходитиме під потреби конкретного проекту (адже він пишеться спеціально для цього) і не міститиме зайвих методів;
  • нарешті, написання власної системи звуків – це ще один шанс збагатити свої знання і краще зрозуміти внутрішню структуру мови програмування.

Звісно, не варто повністю відмовлятися від зовнішніх (чужих) класів. Їх корисно вивчати на предмет наявності цікавих прийомів та “хитрощів”, які ви потім зможете використовувати у своїй роботі. Основний принцип – ви повинні повністю розуміти, як працює чужий код і вміти оптимізувати його під свої потреби.

Навіщо потрібна глобальна система звуків (Sound Manager)

Середньостатистична гра складається із численних класів. Це можуть бути менеджери, ігрові об’єкти, допоміжні класи, ігрові рівні тощо. Часто розробнику доводиться прив’язувати ті чи інші звукові ефекти до різних об’єктів. Наприклад, якщо у грі-стратегії є кілька типів юнітів, то кожен з них може мати власні звуки руху та вистрілів.

Хорошим тоном вважається реалізація у грі можливості вимкнути всі звуки одним кліком (розміщення в інтерфейсі mute button). Для гравця це дуже зручно, наприклад, коли в розпалі “битви” йому по скайпу зателефонував товариш і потрібно вимкнути зайвий “шум”.

Мета глобальної системи звуків – дати розробникові інструменти для керування всіма звуками не залежно від того, в яких місцях, в яких класах та до яких об’єктів вони прив’язані. Натиснувши на кнопку “Mute” ви повинні вимкнути звук “руху” та “вистрілів” всіх ігрових об’єктів, не залежно від того, наскільки складною є структура самої гри. А це доволі складно (якщо не неможливо) зробити, якщо кожен звуковий ефект окремо прив’язаний до відповідного об’єкту.

Щоб вирішити цю проблему – потрібно створити окремий клас керування звуками, який міститиме у собі всю логіку роботи і передаватиме об’єктам посилання на звуки. Щоб було простіше це зрозуміти – тепер ви тримаєте звуки не в кожному об’єкті, а в одному класі, де ними легко керувати. А для того, щоб ігрові об’єкти (наприклад, юніти) мали до них доступ – передаєте посилання на ці звуки відповідним класам.

Як реалізувати глобальну систему звуків: Клас Singleton

Класи у AS3 бувають двох типів: статичні та динамічні. Динамічні класи можуть мати скільки завгодно екземплярів, але прямий доступ до них матимуть лише “батьки” та класи, яким безпосередньо передаватиметься посилання на них. Статичні класи є абстрактними. Ви не можете створювати екземпляри статичних класів, але будь-який інший клас матиме доступ до статичних змінних та методів.

Здавалося б – використання статичного класу – ідеальний вихід в ситуації із глобальною системою звуків. Адже нам саме це і потрібно – щоб доступ до змінних (звуків) та методів (управління звуками) мали всі ігрові класи. Але використання статичних класів для створення глобальних змінних/методів вважається поганим стилем в ActionScript, тому що воно повністю руйнує принципи ООП.

Тому для глобальної системи звуків найкраще підійде своєрідний гібрид динамічного та статичного класів – клас Singleton.

Сінглтону (одиничному класу) цілком можна присвятити окрему велику статтю, але оскільки сьогодні ми вирішили зосередитися на глобальній системі звуків, я постараюся максимально коротко описати цей клас і продемонструвати принцип його роботи. Тим більше, що Singleton точно знадобиться вам у подальшій роботі і не тільки при створенні глобальної системи звуків.

Клас Singleton – це штучно обмежений динамічний клас, який програмно створений так, що може мати тільки один екземпляр. Цей клас має свій цілком специфічний синтаксис. І основний “фокус” полягає в тому, що новий екземпляр створюється не з допомогою ключового слова new, а з допомогою спеціального методу getInstance(). Така структура дозволяє гарантувати, що екземпляр класу буде створений тільки один раз, а після цього при зверненні до getInstance() передаватиметься посилання на цей екземпляр. Спроба ж створити екземпляр через ключове слово new НазваКласу() видаватиме помилку.

Зручність сінглтона полягає в тому, що тепер варто лише імпортувати його у будь-який клас вашого проекту – і можна звертатися до його публічних змінних/методів через конструкціюНазваКласу.getInstance().НазваМетоду().

Тепер до практики:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package your.package
{
  public class TurtleSounds
  {
    //У класі Singleton завжди використовуються всього дві статичні змінні
    public static var _instance:TurtleSounds;
    public static var _allowInstance:Boolean = false;   
    //Зверність увагу, що ми не створюємо екземпляр класу, якщо змінна _allowInstance == false
    public function TurtleSounds ()
    {
      if (!_allowInstance)
      {
        throw new Error("Error: Use TurtleSounds.getInstance() instead of the new keyword.");
      }
      initSounds(); //Одноразове створення екземпляра класу TurtleSounds
    }
    //Звернутися до екземпляру (чи створити його, якщо він ще не створений) можна тільки через метод getInstance()
    public static function getInstance():TurtleSounds
    {
      if (_instance == null) //Якщо екземпляр ще не створений
      {
        _allowInstance = true; //Встановлюємо _allowInstance = true
        _instance = new TurtleSounds(); //Створюємо екземпляр класу
        _allowInstance = false; //Повертаємо _allowInstance = false. Більше ця змінна ніколи не буде true, а отже створити ще один екземпляр фізично неможливо
      }
      return _instance; // Якщо екземпляр класу вже існує, повертаємо посилання на цей екземпляр
    }
  }
}

Вітаю, ви створили клас Singleton, тепер переходимо безпосередньо до звукового функціоналу.

Створення глобальної системи звуків (Sound Manger)

1
2
3
4
5
6
7
8
9
public var allowSounds:Boolean; //Відповідає за дозвіл програвання музики (звукового фону)
public var allowSFX:Boolean; //Відповідає за дозвіл програвання звуків (ефектів)
var sound:Object; //Власне, самі звукові файли (всі вони знаходитимуться у глобальній системі управління звуками)
var music:Sound; //А це змінна, яка відповідає за фонову музику
private var mSoundChannels:Array; //Оскільки звукових ефектів може бути зразу декілька, ми створюємо для них масив. Наприклад, звуки кількох вистрілів можуть лунати одночасно. У грі Turtle Dreams to Fly черепаха може одночасно зібрати кілька монет
private var mMusicChannel:SoundChannel; //Фонова музика у нас в одному екземплярі, тому для неї вистачить одного звукового каналу
private const MAX_SOUND_CHANNELS:int = 8; //А ця константа відповідає за кількість звуків, які можуть програватися одночасно. Не раджу робити її занадто великою
import your.package.TurtleSounds;

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

TurtleSounds.getInstance().playSFX('НазваЕфекту');

Музика вмикається стрічкою:

TurtleSounds.getInstance().playMusic();

Відповідно, для того, щоб реалізувати кнопки ввімкнення/вимкнення звуків чи музики потрібно всього лише перемикнути змінні allowSFX та allowSounds. Зміни при цьому будуть застосовані до всіх звуків у проекті!

P.S.: По суті наведений вище код – це один з найпростіших способів реалізації системи звуків у флеш-проекті. Він передбачає тільки можливість ввімкнення та вимкнення звуків і музики. Тут не використовуються численні можливості згасання, наростання, зміни гучності тощо. Але маючи перед собою основу, набагато простіше реалізувати всі необхідні вам можливості.

  1. Створюємо клас, який в майбутньому стане нашою глобальною системою звуків. Назвати її можна як завгодно. У грі Turtle Dreams to Fly цей клас має назвуTurtleSounds.as, тому в рамках цієї статті він буде називатися саме так. Але ви можете (і повинні) використовувати свою назву.
  2. Пишемо код сінглтона (я намагався повністю прокоментувати, що відбувається в коді. Якщо залишилися запитання – задавайте у коментарях):
  3. При створенні екземпляра класу ми ініціалізуємо звукову систему (зверніть увагу на стрічку initSounds();). Нам знадобляться нові змінні:
  4. Після того, як всі необхідні змінні внесено, давайте розглянемо остаточний код готового класу управління звуками з коментарями до нових стрічок в коді.
    001
    002
    003
    004
    005
    006
    007
    008
    009
    010
    011
    012
    013
    014
    015
    016
    017
    018
    019
    020
    021
    022
    023
    024
    025
    026
    027
    028
    029
    030
    031
    032
    033
    034
    035
    036
    037
    038
    039
    040
    041
    042
    043
    044
    045
    046
    047
    048
    049
    050
    051
    052
    053
    054
    055
    056
    057
    058
    059
    060
    061
    062
    063
    064
    065
    066
    067
    068
    069
    070
    071
    072
    073
    074
    075
    076
    077
    078
    079
    080
    081
    082
    083
    084
    085
    086
    087
    088
    089
    090
    091
    092
    093
    094
    095
    096
    097
    098
    099
    100
    101
    102
    103
    104
    105
    package your.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;     
        public function TurtleSounds ()
        {
          if (!_allowInstance)
          {
            throw new Error("Error: Use TurtleSounds.getInstance() instead of the new keyword.");
          }
          initSounds();
        }
        public static function getInstance():TurtleSounds
        {
          if (_instance == null)
          {
            _allowInstance = true;
            _instance = new TurtleSounds();
            _allowInstance = false;
          }
          return _instance;
        }
        private function initSounds (): void
        {
          allowSounds = true;
          allowSFX = true;
          mSoundChannels = [];
          sound = new Object();
          //Додаємо всі необхідні звукові ефекти попередньо імпортовані в бібліотеку fla-файла
          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. Приміром, якщо ваш звуковий фон має тривалість одну хвилину, то музика гратиме безперервно протягом 999 хвилин, тобто близько 17 годин. А можна поставити ще більшу кількість повторів.
          } else {
            mMusicChannel.soundTransform = soundTransform;
          }
        }
      }
    }
  5. Тепер ви можете використовувати клас TurtleSounds в будь-якому класі своєї гри, імпортувавши його туди