Обещанный мой SoundManager

В посте о SoundManager камрада jarofed я рассказал какие требования к этом классу у меня:

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

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

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

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

Плюс еще всегда хорошо для однотипных часто повторяющихся звуков (выстрелов, попаданий итп) — иметь некоторый набор звуков. Иначе в ухо начинает резать. Соответственно, в коде запуск такого звука — это вызов метода с передачей типа «попадание_в_плоть». А уже менеджер рандомно пустит что-то из набора.

Несколько камрадов высказали желание глянуть на мою реализацию. Я минимально прокомментировал код и выкладываю.

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

Задавайте вопросы если чо.



package core {
        import flash.events.Event;
        import flash.media.Sound;
        import flash.media.SoundChannel;
        import flash.media.SoundTransform;
        
        /**
         * @author Alexander Porechnov
         */
        public class SoundManager {

                // Из всего количества каналов выделяем сколько-то под взрывы
                public static const BOOM_CHANNEL_MAX : int = 10;
                // Для замещения или объединения звуков взрывов вводим группы/приоритеты
                // Взрывы многоразовых мин, их много, манипуляции будут малозаметны
                public static const BOOM_MINE_PRI : int = 0;
                // Взрывы мобильной техники
                public static const BOOM_UNIT_PRI : int = 1;
                // Большие, редкие, хорошо выделяющиеся взрывы спец. юнитов или зданий,
                // замещать их плохо - будет заметно
                public static const BOOM_MEGA_PRI : int = 2;
                // Ядерный взрыв, замещать нельзя
                public static const BOOM_NUKE_PRI : int = 3;

                // Регулятор громкости взрыва юнитов для подстройки
                public static const UNIT_BOOM_VOLUME : Number = 0.75;

                // Начальное расстояние, на котором громкость движка юнита нарастает
                // (иначе при появлении на экране звук включается резко, неестественно с артефактами/щелчками)
                public static const START_DIST : Number = 40.0;
                public static const REV_START_DIST : Number = 1.0 / START_DIST;

                // максимальное возможное количество дорог, по которым едут юниты в игре
                public static const PATH_MAX : int = 18;
                
                //Индексы одиночных (не зацикленных звуков)
                public static const SINGLE_SOUND_SHOT : int = 0;
                public static const SINGLE_SOUND_CANNON_SHOT : int = 1;
                public static const SINGLE_SOUND_MISSILE : int = 2;
                public static const SINGLE_SOUND_VULCAN_SHORT_SHOT : int = 3;
                public static const SINGLE_SOUND_MINE_BOOM : int = 4;
                public static const SINGLE_SOUND_CANNON_HIT : int = 5;

                public static const SINGLE_SOUND_VULCAN_HIT : int = 6;
                public static const SINGLE_SOUND_VULCAN_SHORT_HIT : int = 7;

                public static const SINGLE_SOUND_UNIT_BOOM : int = 8;

                public static const SINGLE_SOUND_ZZZ : int = 9;
                public static const SINGLE_SOUND_INCOME : int = 10;
                public static const SINGLE_SOUND_INCOME_MINE : int = 11;
                public static const SINGLE_SOUND_FROST_POOH : int = 12;

                public static const SINGLE_SOUND_BOOMZ : int = 13;
                public static const SINGLE_SOUND_SILO_LAUNCH : int = 14;

                public static const SINGLE_SOUND_SHUTTLE_LAND : int = 15;

                public static const SINGLE_SOUND_WASP : int = 16;
                public static const SINGLE_SOUND_WASP_POOH : int = 17;

                public static const SINGLE_SOUND_SHUTTLE_BOOM : int = 18;

                public static const SINGLE_SOUND_NUKE : int = 19;
                public static const SINGLE_SOUND_MEGABOOM : int = 20;
                
                protected static const SINGLE_SOUND_MAX : int = 21;

                // Вся техника и зацикленные звуки разбиты на группы по звучанию
                // Гусеничная
                public static const MOBILE_UNIT_SOUND_TANK_MOVING : int = 0;
                // Машины
                public static const MOBILE_UNIT_SOUND_CAR_MOVING : int = 1;
                // На воздушной подушке
                public static const MOBILE_UNIT_SOUND_HOVER_FLY : int = 2;
                // Полет шаттла
                public static const MOBILE_UNIT_SOUND_SHUTTLE_FLY : int = 3;
                // Горение
                public static const MOBILE_UNIT_SOUND_FIRE_TEL : int = 4;
                protected static const MOBILE_UNIT_SOUND_MAX : int = 5;
                
                // Библиотека Sound для незацикленных звуков, лежат по индексам SINGLE_SOUND_*
                // Дополнительно здесь хранятся настройки для каждого звука, ибо в библиотеке
                // все звуки нормализованы, подстройку по громкости и стереобазе проводим
                // прямо тут
                protected static var singleSoundBase : Array;
                // Булеан пометка для скорости кода. Пометка о том, является ли звук в библиотеке
                // выше - набором. В случае часто повторяющего звука игрок хорошо слышит искуственность
                // ситуации, необходимо набрать несколько разных и выбирать из них случайным образом
                protected static var singleSoundBaseGroupMark : Array;

                // Каналы сейчас звучащих звуков
                protected static var singleSoundChannels : Array;
                // Соотв. Sound-ы для этих каналов. Их надо помнить, чтобы при unpause восстанавливать
                protected static var singleSounds : Array;
                // Объекты, вызвавшие звук. В основном это юниты. Помнить их нужно, чтобы проапдейтить стереобазу
                // звука при изменении позиции или убрать звук при уничтожении юнита
                protected static var singleCallers : Array;

                // Аналогично для зацикленных звуков, однако юнитов на экране очень много,
                // всем каналов не хватит, кроме того будет зашкаливание, поэтому юниты группируются
                // по типам техники и по дорогам. Если по дороге едет больше трех юнитов одного типа,
                // то играется все равно три звука, для игрока на слух это все равно толпа
                protected static var mobileUnitSoundBase : Array;
                protected static var mobileUnitPathChannels : Array;
                protected static var mobileUnitPathSounds : Array;
                protected static var mobileUnitPathPositions : Array;
                protected static var mobileUnitPathCount : Array;

                protected static var mobileUnitTypePathCount : Array;


                // Так как на взрывчики выделено только 10 каналов - необходим механизм, который
                // объединяет или незаметно вытесняет звуки по приоритетам и группам
                protected static var boomChannels : Array;
                protected static var boomSounds : Array;
                protected static var boomPositions : Array;
                protected static var boomPriorities : Array;
                protected static var boomExecutes : Array;
                
                // Временный SoundTransform, чтобы не пересоздавать постоянно
                protected static var tempST : SoundTransform;

                protected static var paused : Boolean;

                public static function getUsedChannelsCount() : int {
                        var res : int = singleSoundChannels.length;
                        for (var j : int = 0; j < PATH_MAX; j++) {
                                for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                        if (mobileUnitPathChannels[i][j] != null) {
                                                res++;
                                        }
                                }
                        }                       
                        for (i = 0; i < BOOM_CHANNEL_MAX; i++) {
                                if (boomChannels[i] != null) {
                                        res++;
                                }
                        }
                        return res;
                }

                public static function init() : void {
                        tempST = new SoundTransform();
                        paused = false;

                        // single sounds                        
                        singleSoundBase = new Array(SINGLE_SOUND_MAX); 
                        singleSoundBaseGroupMark = new Array(SINGLE_SOUND_MAX); 
                        
                        // Библиотека незацикленных звуков, первым идет Sound, затем настроенный SoundTransform
                        // и потом стартовое время (некоторые звуки можно реюзать, скажем есть взрыв с эхо, в другом месте используем только эхо)
                        singleSoundBase[SINGLE_SOUND_SHOT] =            [       new VulcanSound(),              new SoundTransform(1, 0),       0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_SHOT] = false;
                        
                        singleSoundBase[SINGLE_SOUND_VULCAN_SHORT_SHOT] = [     new VulcanShortSound(),         new SoundTransform(1, 0),       0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_VULCAN_SHORT_SHOT] = false;

                        singleSoundBase[SINGLE_SOUND_VULCAN_HIT] =              [       new VulcanHitSound(),           new SoundTransform(1, 0),       0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_VULCAN_HIT] = false;

                        singleSoundBase[SINGLE_SOUND_VULCAN_SHORT_HIT] =                [       new VulcanShortHitSound(),              new SoundTransform(1, 0),       0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_VULCAN_SHORT_HIT] = false;

                        
                        singleSoundBase[SINGLE_SOUND_CANNON_SHOT] = [   new CannonMissSound(),  new SoundTransform(0.5, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_CANNON_SHOT] = false;
                        
                        singleSoundBase[SINGLE_SOUND_CANNON_HIT] = [    new CannonHitSound(),   new SoundTransform(0.5, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_CANNON_HIT] = false;
                        
                        singleSoundBase[SINGLE_SOUND_MISSILE] = [       new Missile2Sound(),    new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_MISSILE] = false;

                        var mineSound : Sound = new Mine2Sound();

                        singleSoundBase[SINGLE_SOUND_MINE_BOOM] = [     mineSound,      new SoundTransform(0.5, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_MINE_BOOM] = false;
                        
                        singleSoundBase[SINGLE_SOUND_WASP_POOH] = [     mineSound,      new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_WASP_POOH] = false;


                        singleSoundBase[SINGLE_SOUND_UNIT_BOOM] = [     new UnitBoom9Sound(),   new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_UNIT_BOOM] = false;

                        singleSoundBase[SINGLE_SOUND_ZZZ] = [   new ZzzSound(), new SoundTransform(0.1, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_ZZZ] = false;

                        var incomeSound : Sound = new IncomeSound();
                        singleSoundBase[SINGLE_SOUND_INCOME] = [        incomeSound,    new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_INCOME] = false;

                        singleSoundBase[SINGLE_SOUND_INCOME_MINE] = [   incomeSound,    new SoundTransform(0.7, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_INCOME_MINE] = false;

                        singleSoundBase[SINGLE_SOUND_FROST_POOH] = [    new FrostPoohSound(),   new SoundTransform(0.1, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_FROST_POOH] = false;

                        singleSoundBase[SINGLE_SOUND_BOOMZ] = [ new BoomZSound(),       new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_BOOMZ] = false;
                        
                        singleSoundBase[SINGLE_SOUND_SILO_LAUNCH] = [   new SiloLaunchSound(),  new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_SILO_LAUNCH] = false;

                        singleSoundBase[SINGLE_SOUND_SHUTTLE_LAND] = [  new ShuttleLandSound(), new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_SHUTTLE_LAND] = false;

                        singleSoundBase[SINGLE_SOUND_SHUTTLE_BOOM] = [  new ShuttleBoomSound(), new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_SHUTTLE_BOOM] = false;

                        singleSoundBase[SINGLE_SOUND_WASP] = [  new WaspSound(),        new SoundTransform(0.8, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_WASP] = false;

                        singleSoundBase[SINGLE_SOUND_NUKE] = [  new NukeSound(),        new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_NUKE] = false;

                        singleSoundBase[SINGLE_SOUND_MEGABOOM] = [      new MegaBoomSound(),    new SoundTransform(1.0, 0),     0       ];
                        singleSoundBaseGroupMark[SINGLE_SOUND_MEGABOOM] = false;

                        // Пример звука с вариантами и пометкой об этом
//                      singleSoundBase[SINGLE_SOUND_TEST] = [
//                              [new UnitBoom9Sound(),          new SoundTransform(1, 0),       0],     
//                              [new UnitBoom8Sound(),          new SoundTransform(1, 0),       0],     
//                      ];
//                      singleSoundBaseGroupMark[SINGLE_SOUND_TEST] = true;

                        singleSoundChannels = new Array();
                        singleSounds = new Array();
                        singleCallers = new Array();
                        
                        // mobile units
                        mobileUnitSoundBase = new Array(MOBILE_UNIT_SOUND_MAX); 

                        mobileUnitSoundBase[MOBILE_UNIT_SOUND_TANK_MOVING] = [
                                [new Track1Sound(),             new SoundTransform(1, 0),       0, 0.3],        
                        ];

                        mobileUnitSoundBase[MOBILE_UNIT_SOUND_CAR_MOVING] = [
                                [new Tank1Sound(),              new SoundTransform(1, 0),       0, 0.45],       
                        ];

                        mobileUnitSoundBase[MOBILE_UNIT_SOUND_HOVER_FLY] = [
                                [new HoverFly1Sound(),  new SoundTransform(1, 0),       0, 0.3],        
                                [new HoverFly2Sound(),  new SoundTransform(1, 0),       0, 0.3],        
                                [new HoverFly3Sound(),  new SoundTransform(1, 0),       0, 0.3],        
                        ];

                        mobileUnitSoundBase[MOBILE_UNIT_SOUND_SHUTTLE_FLY] = [
                                [new ShuttleMoveSound(),        new SoundTransform(1, 0),       0, 1.0],        
                        ];

                        mobileUnitSoundBase[MOBILE_UNIT_SOUND_FIRE_TEL] = [
                                [new FireTELSound(),    new SoundTransform(1, 0),       0, 1.0],        
                        ];
                        
                        mobileUnitPathChannels = new Array(MOBILE_UNIT_SOUND_MAX);
                        mobileUnitPathSounds = new Array(MOBILE_UNIT_SOUND_MAX);
                        mobileUnitPathPositions = new Array(MOBILE_UNIT_SOUND_MAX);
                        mobileUnitPathCount = new Array(MOBILE_UNIT_SOUND_MAX);
                        mobileUnitTypePathCount = new Array(MOBILE_UNIT_SOUND_MAX);
                        
                        for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                mobileUnitPathChannels[i] = new Array(PATH_MAX);
                                mobileUnitPathSounds[i] = new Array(PATH_MAX);
                                mobileUnitPathPositions[i] = new Array(PATH_MAX);
                                mobileUnitPathCount[i] = new Array(PATH_MAX);
                                for (var j : int = 0; j < PATH_MAX; j++) {
                                        mobileUnitPathChannels[i][j] = null;
                                        mobileUnitPathSounds[i][j] = null;
                                }
                        }

                        prepareMobileUnitLoopedSounds();
                        

                        boomChannels = new Array(BOOM_CHANNEL_MAX);
                        boomSounds = new Array(BOOM_CHANNEL_MAX);
                        boomPositions = new Array(BOOM_CHANNEL_MAX);
                        boomPriorities = new Array(BOOM_CHANNEL_MAX);
                        boomExecutes = new Array(BOOM_CHANNEL_MAX);
                        for (j = 0; j < BOOM_CHANNEL_MAX; j++) {
                                boomChannels[j] = null;
                                boomSounds[j] = null;
                                boomPositions[j] = 0;
                                boomPriorities[j] = 0;
                                boomExecutes[j] = 0;
                        }

                }


                // Boom section
                
                // При появлении нового взрыва, он сразу не играется, а информация накапливается в течении
                // логического кванта
                public static function addBoomSoundByPos(soundID : int, pos : Number, pri : int) : void {
                        var freeIdx : int = getFreeIdxBoom();
                        if (freeIdx >= 0) {
                                // Если из 10 каналов под взрывы есть свободный - запоминаем
                                boomSounds[freeIdx] = getSingleSoundBaseBySoundID(soundID);
                                boomPositions[freeIdx] = pos;
                                boomPriorities[freeIdx] = pri;
                        } else {
                                // Если нет свободного канала, выискиваем ближайший по приоритету
                                // и ближайший по расстоянию
                                var nearestIdx : int = -1;
                                switch (pri) {
                                        case BOOM_NUKE_PRI:
                                                {
                                                        nearestIdx = getNearestIdxBoomByPri(pos, BOOM_NUKE_PRI);
                                                }
                                                break;
                                        case BOOM_MEGA_PRI:
                                                {
                                                        nearestIdx = getNearestIdxBoomByPri(pos, BOOM_UNIT_PRI);
                                                        if (nearestIdx < 0) {
                                                                nearestIdx = getNearestIdxBoomByPri(pos, BOOM_MEGA_PRI);
                                                        }
                                                }
                                                break;
                                        case BOOM_UNIT_PRI:
                                                {
                                                        nearestIdx = getNearestIdxBoomByPri(pos, BOOM_UNIT_PRI);
                                                }
                                                break;
                                        case BOOM_MINE_PRI:
                                                {
                                                        nearestIdx = getNearestIdxBoomByPri(pos, BOOM_MINE_PRI);
                                                }
                                                break;
                                }
                                // Если ближайший найден, то производим замещение (останавливаем старый, запоминаем новый)
                                if (nearestIdx >= 0) {
                                        //stop old channel
                                        var channelToStop : SoundChannel = boomChannels[nearestIdx];
                                        if (channelToStop != null) {
                                                channelToStop.stop();
                                                channelToStop.removeEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);
                                        }
                                        
                                        boomChannels[nearestIdx] = null;
                                        boomSounds[nearestIdx] = getSingleSoundBaseBySoundID(soundID);
                                        boomPositions[nearestIdx] = pos;
                                        boomPriorities[nearestIdx] = pri;
                                }
                                // Если ближайшего так и не нашли, значит по приоритету ему не звучать,
                                // сейчас уже звучат более приоритетные (то-есть заметные немассовые звуки)
                        }
                }

                // В конце логического кванта Сессия вызовет этот метод, чтобы
                // разобраться с запомненными звуками взрывов и проанализировав запустить
                public static function playPendingBoomSounds() : void {
                        var soundBase : Array = null;

                        // Для взрывов юнитов алгоритм похитрее, взрываться юниты могут в большом
                        // количестве и в разных частях экрана одновременно. Поэтому для экономии каналов
                        // и предотвращения какофонии приходится эти звуки сливать подрегулируя громкость
                        // и стереобазу
                        var newUnitBoomCount : int = 0;
                        
                        for (var j : int = 0; j < BOOM_CHANNEL_MAX; j++) {
                                if (
                                
                                                boomSounds[j] != null
                                                && boomPriorities[j] == BOOM_UNIT_PRI
                                                && (boomChannels[j] == null || boomExecutes[j] < 2)
                                                
                                        ) {
                                                
                                        newUnitBoomCount++;
                                        
                                }
                        }
                        
                        var newUnitBoomVolume : Number = 1.0 * UNIT_BOOM_VOLUME;
                        
                        if (newUnitBoomCount > 2) { 
                                newUnitBoomVolume = 2.0 / newUnitBoomCount * UNIT_BOOM_VOLUME;
                        }
                                
                        for (var i : int = 0; i < BOOM_CHANNEL_MAX; i++) {
                                soundBase = boomSounds[i];
                                if (soundBase != null) {
                                        if (boomChannels[i] == null) {
                                                var startTime : Number = soundBase[2];
                                                var sound : Sound = soundBase[0];
                                                
                                                var soundTrans : SoundTransform = soundBase[1];
                                                prepareSTPan(soundTrans, boomPositions[i]);
                                                
                                                if (boomPriorities[i] == BOOM_UNIT_PRI) {
                                                        soundTrans.volume = newUnitBoomVolume; 
                                                }
        
                                                var soundChannel : SoundChannel = sound.play(startTime, 0, soundTrans);
        
                                                if (soundChannel != null) {
                                                        soundChannel.addEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);
                                                        boomChannels[i] = soundChannel;                 
                                                        boomExecutes[i] = 1;
                                                }
                                                
                                        } else if (boomPriorities[i] == BOOM_UNIT_PRI) {
                                                if (boomExecutes[i] < 2) {
                                                        tempST = boomChannels[i].soundTransform;
                                                        tempST.volume = newUnitBoomVolume;
                                                        boomChannels[i].soundTransform = tempST;
                                                }
                                                boomExecutes[i] += 1;
                                        }
                        

                                }
                        }
                }
                
                // Листенер по окончанию звука - когда взрыв отгремел - убирает его
                protected static function boomSoundCompleteListener(event : Event) : void {
                        if (!paused) {
                                for (var i : int = 0; i < BOOM_CHANNEL_MAX; i++) {
                                        if (boomChannels[i] == event.target) {
                                                event.target.removeEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);
                                                boomChannels[i] = null;         
                                                boomSounds[i] = null;
                                                break;                          
                                        }
                                }
                        }
                        
                }

                // Выдача звука из библиотеки по индексу, или случайного звука из набора, если есть пометка
                protected static function getSingleSoundBaseBySoundID(soundID : int) : Array {
                        var soundBase : Array = null;
                        if (singleSoundBaseGroupMark[soundID]) {
                                var idx : int = Math.floor(Math.random() * singleSoundBase[soundID].length);
                                return singleSoundBase[soundID][idx];
                        } else {
                                return singleSoundBase[soundID];
                        }
                }

                // Получение индекса свободного канала для взрывов, количество которых ограничено
                protected static function getFreeIdxBoom() : int {
                        for (var i : int = 0; i < BOOM_CHANNEL_MAX; i++) {
                                if (boomSounds[i] == null) {
                                        return i;
                                }
                        }
                        return -1;
                }

                // Поиск ближаешего по расстоянию взрыва
                protected static function getNearestIdxBoomByPri(pos : Number, pri : int) : int {
                        var currDist : Number = -1;
                        
                        var nearestIdx : int = -1;
                        var dist : Number = 1000000;
                        
                        for (var i : int = 0; i < BOOM_CHANNEL_MAX; i++) {
                                if (boomPriorities[i] <= pri) {
                                        currDist = Math.abs(boomPositions[i] - pos);
                                        if (currDist < dist) {
                                                dist = currDist;
                                                nearestIdx = i;
                                        }
                                }
                        }
                        return nearestIdx;
                }


                // Запуск незацикленного звука, имеющего позицию на экране: выстрелы, промахи, посадка шаттла, открытие шахты итд, взрывы шаттла, сирена при въезде юнита в шахту
                public static function playSingleSoundByPos(soundID : int, pos : Number, caller : Object = null) : void {
                        var soundBase : Array = getSingleSoundBaseBySoundID(soundID);
                        
                        var startTime : Number = soundBase[2];
                        var sound : Sound = soundBase[0];
                        
                        var soundTrans : SoundTransform = soundBase[1];
                        // Рассчет стереобазы в зависимости от положения звука на экране
                        prepareSTPan(soundTrans, pos);
                        
                        var soundChannel : SoundChannel = sound.play(startTime, 0, soundTrans);
                        if (soundChannel != null) {
                                soundChannel.addEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);               
                                singleSoundChannels.push(soundChannel);
                                singleSounds.push(sound);
                                singleCallers.push(caller);
                        }
                }
                
                // Рассчет стереобазы в зависимости от положения звука на экране
                protected static function prepareSTPan(st : SoundTransform, pos : Number) : void {
                        st.pan = (pos / 350.0 - 1.0) * 0.5;
                }


                //  Запуск незацикленных звуков без конкретной позиции на экране: интерфейсные звуки, попадание в станцию
                public static function playSingleSound(soundID : int, caller : Object = null) : void {
                        var soundBase : Array = getSingleSoundBaseBySoundID(soundID);
                        
                        var startTime : Number = soundBase[2];
                        var sound : Sound = soundBase[0];
                        
                        var soundTrans : SoundTransform = soundBase[1];
                        soundTrans.pan = 0;
                        
                        var soundChannel : SoundChannel = sound.play(startTime, 0, soundTrans);
                        if (soundChannel != null) {
                                soundChannel.addEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);               
                                singleSoundChannels.push(soundChannel);
                                singleSounds.push(sound);
                                singleCallers.push(caller);
                        }
                        
                }

                // Объект просит проапдейтить его звук, поскольку он сменил позицию
                public static function updateSingleSoundByPos(pos : Number, caller : Object) : void {
                        var singleSoundChannelsLength : int = singleSoundChannels.length;
                        var channel : SoundChannel = null;
                        for (var i : int = 0; i < singleSoundChannelsLength; i++) {
                                channel = singleSoundChannels[i];
                                if (channel != null && singleCallers[i] == caller) {
                                        tempST = channel.soundTransform; 
                                        prepareSTPan(tempST, pos);
                                        channel.soundTransform = tempST; 
                                        return;
                                }               
                        }
                        
                }

                // Объект просит прекратить его звук, поскольку он умер
                public static function stopSingleSound(caller : Object) : void {
                        var singleSoundChannelsLength : int = singleSoundChannels.length;
                        var channel : SoundChannel = null;
                        for (var i : int = 0; i < singleSoundChannelsLength; i++) {
                                channel = singleSoundChannels[i];
                                if (channel != null && singleCallers[i] == caller) {
                                        channel.stop();
                                        channel.removeEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);
                                        singleSoundChannels.splice(i, 1);
                                        singleSounds.splice(i, 1);
                                        singleCallers.splice(i, 1);
                                        return;
                                }               
                        }
                }
                
                // Листенер для зачистки звука который отрыграл
                protected static function singleSoundCompleteListener(event : Event) : void {
                        if (!paused) {
                                var singleSoundChannelsLength : int = singleSoundChannels.length;
                                for (var i : int = 0; i < singleSoundChannelsLength; i++) {
                                        if (singleSoundChannels[i] == event.target) {
                                                event.target.removeEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);            
                                                singleSoundChannels.splice(i, 1);
                                                singleSounds.splice(i, 1);
                                                singleCallers.splice(i, 1);
                                                break;                          
                                        }
                                }
                        }
                }

                // Секция зацикленных звуков

                // Каждый юнит себя регистрирует - по какой дороге едет, какая позиция, какой тип техники, его крупность итд
                public static function addMobileUnitLoopedSound(
                                                                                                                pathID : int,
                                                                                                                mobUnitMoveSoundID : int,
                                                                                                                mobUnitWeight : Number,
                                                                                                                mobUnitPassedDist : Number,
                                                                                                                mobUnitX : Number
                                                                                                                ) : void {
                        if (pathID >= PATH_MAX) {
                                pathID -= PATH_MAX;
                        }
                        var groupID : int = mobUnitMoveSoundID; 
                        if (groupID >= 0) {
                                var weight : Number = mobUnitWeight; 
                                // Чтобы звук не начался внезапно - нарастаем постепенно при выезде
                                if (mobUnitPassedDist < START_DIST) { 
                                        weight *= mobUnitPassedDist * REV_START_DIST; 
                                }
                                mobileUnitPathPositions[groupID][pathID] += mobUnitX * weight; 
                                mobileUnitPathCount[groupID][pathID] += weight;
                        }
                }

                // В конце каждого логического кванта Сессия вызывает этот метод,
                // чтобы проанализировать, объединить и запустить/проапдейтить звуки движков техники
                // Алгоритм довольно специфичен. Результат - игрок хорошо слышит тип техники, с какой
                // стороны она подъезжает и ее количество (1-2-3-4-много)
                public static function processMobileUnitLoopedSounds() : void {
                        for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                checkMobileUnitLoopedSounds(i);
                        }
                        
                        for (var j : int = 0; j < PATH_MAX; j++) {
                                for (i = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                        processPath(i, j);
                                }
                        }
                }
                
                protected static function checkMobileUnitLoopedSounds(groupID : int) : void {
                        var firstPathIDX : int = -1;
                        for (var i : int = 0; i < PATH_MAX; i++) {
                                if (mobileUnitPathCount[groupID][i] > 0) {
                                        mobileUnitTypePathCount[groupID] += 1;
                                        if (firstPathIDX < 0) {
                                                firstPathIDX = i;
                                        }       
                                        if (mobileUnitTypePathCount[groupID] > 4) {
                                                mobileUnitPathCount[groupID][firstPathIDX] += mobileUnitPathCount[groupID][i];
                                                mobileUnitPathPositions[groupID][firstPathIDX] += mobileUnitPathPositions[groupID][i];
                                                mobileUnitPathCount[groupID][i] = 0;
                                                mobileUnitPathPositions[groupID][i] = 0;
                                        }
                                }
                        }
                }

                protected static function processPath(groupID : int, pathID : int) : void {
                        var pos : Number = mobileUnitPathPositions[groupID][pathID]; 
                        var count : Number = mobileUnitPathCount[groupID][pathID];
                        if (count > 0) {
                                pos /= count;
                        }
                        if (count > 5) {
                                count = 5;
                        }
                        var channel : SoundChannel = mobileUnitPathChannels[groupID][pathID]; 
                        if (channel != null) {
                                if (count > 0) {
                                        var st : SoundTransform = channel.soundTransform;
                                        prepareSTPan(st, pos);
                                        if (count < 1) {
                                                st.volume = count * 0.4 * mobileUnitPathSounds[groupID][pathID][3];
                                        } else {
                                                st.volume = (0.25 + count * 0.15) * mobileUnitPathSounds[groupID][pathID][3];
                                        }
                                        channel.soundTransform = st;
                                } else {
                                        channel.stop();
                                        mobileUnitPathChannels[groupID][pathID] = null;
                                        mobileUnitPathSounds[groupID][pathID] = null;
                                }
                        } else if (count > 0) {
                                var idx : int = Math.floor(Math.random() * mobileUnitSoundBase[groupID].length);
                                var soundBase : Array = mobileUnitSoundBase[groupID][idx];
                                
                                var sound : Sound = soundBase[0];
                                var startTime : Number = soundBase[2];
                        
                                var soundTrans : SoundTransform = soundBase[1];
                                prepareSTPan(soundTrans, pos);
                                if (count < 1) {
                                        soundTrans.volume = count * 0.4 * soundBase[3];
                                } else {
                                        soundTrans.volume = (0.25 + count * 0.15) * soundBase[3];
                                }
                        
                                var soundChannel : SoundChannel = sound.play(startTime, 1000000, soundTrans);
                                if (soundChannel != null) {
                                        mobileUnitPathChannels[groupID][pathID] = soundChannel;
                                        mobileUnitPathSounds[groupID][pathID] = soundBase;
                                }
                        }
                }

                

                // Секция паузы, остановки и зачистки

                // Пауза требует временного срезания листенеров окончания, иначе звуки вместо паузы завершатся
                // Кроме того, приходится для незацикленных звуков спрашивать из позицию, не запоминая ее
                // чтобы она не обнулилась при stop и тогда можно при снятии с паузы запустить новый канал
                // с позиции останова
                public static function setPaused(pausedMode : Boolean) : void {
                        paused = pausedMode;
                        var singleSoundChannelsLength : int = singleSoundChannels.length;
                        var channelToStop : SoundChannel = null;
                        var stopPos : Number = 0.0;
                        if (paused) {
                                for (var j : int = 0; j < singleSoundChannelsLength; j++) {
                                        channelToStop = singleSoundChannels[j];
                                        if (channelToStop != null) {
                                                stopPos = channelToStop.position;
                                                channelToStop.stop();
                                                channelToStop.removeEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);
                                        }               
                                }
                                for (j = 0; j < PATH_MAX; j++) {
                                        for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                                var channel : SoundChannel = mobileUnitPathChannels[i][j]; 
                                                if (channel != null) {
                                                        channel.stop();
                                                }
                                        }
                                }
                                for (j = 0; j < BOOM_CHANNEL_MAX; j++) {
                                        channelToStop = boomChannels[j];
                                        if (channelToStop != null) {
                                                stopPos = channelToStop.position;
                                                channelToStop.stop();
                                                channelToStop.removeEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);
                                        }               
                                }
                        } else {
                                var oldChannel : SoundChannel = null;
                                var newChannel : SoundChannel = null;
                                for (i = 0; i < singleSoundChannelsLength; i++) {
                                        oldChannel = singleSoundChannels[i];
                                        newChannel = singleSounds[i].play(oldChannel.position, 0, oldChannel.soundTransform);
                                        if (newChannel != null) { 
                                                singleSoundChannels[i] = newChannel;
                                                newChannel.addEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);
                                        } else {
                                                singleSoundChannels[i] = null;
                                                singleCallers[i] = null;
                                        }               
                                }
                                for (j = 0; j < PATH_MAX; j++) {
                                        for (i = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                                var oldPathChannel : SoundChannel = mobileUnitPathChannels[i][j]; 
                                                if (oldPathChannel != null) {
                                                        mobileUnitPathChannels[i][j] = mobileUnitPathSounds[i][j][0].play(0, 1000000, oldPathChannel.soundTransform);
                                                }
                                        }
                                }                       
                                for (i = 0; i < BOOM_CHANNEL_MAX; i++) {
                                        oldChannel = boomChannels[i];
                                        if (oldChannel != null) {
                                                newChannel = boomSounds[i][0].play(oldChannel.position, 0, oldChannel.soundTransform);
                                                if (newChannel != null) { 
                                                        boomChannels[i] = newChannel;
                                                        newChannel.addEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);
                                                } else {
                                                        boomChannels[i] = null;         
                                                        boomSounds[i] = null;
                                                }
                                        }               
                                }
                        }
                }

                // Зачистка всего, ибо миссия закончена или прервана
                // Зачистка отличается для ситуаций "были на паузе" или нет
                public static function stopMissionSounds() : void {
                        var singleSoundChannelsLength : int = singleSoundChannels.length;
                        if (!paused) {
                                paused = true;
                                var channelToStop : SoundChannel = null;
                                for (var j : int = 0; j < singleSoundChannelsLength; j++) {
                                        channelToStop = singleSoundChannels[j];
                                        channelToStop.stop();
                                        channelToStop.removeEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);           
                                }
                                singleSoundChannels = new Array();
                                singleSounds = new Array();
                                singleCallers = new Array();
                                
                                for (j = 0; j < PATH_MAX; j++) {
                                        for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                                var channel : SoundChannel = mobileUnitPathChannels[i][j]; 
                                                if (channel != null) {
                                                        channel.stop();
                                                        mobileUnitPathChannels[i][j] = null;
                                                        mobileUnitPathSounds[i][j] = null;
                                                }
                                        }
                                }                       
                                for (j = 0; j < BOOM_CHANNEL_MAX; j++) {
                                        channelToStop = boomChannels[j];
                                        if (channelToStop != null) {
                                                channelToStop.stop();
                                                channelToStop.removeEventListener(Event.SOUND_COMPLETE, boomSoundCompleteListener);             
                                        }
                                        boomChannels[j] = null;         
                                        boomSounds[j] = null;
                                }
                        } else {
                                for (i = 0; i < singleSoundChannelsLength; i++) {
                                        singleSoundChannels[i].removeEventListener(Event.SOUND_COMPLETE, singleSoundCompleteListener);          
                                }
                                singleSoundChannels = new Array();
                                singleSounds = new Array();
                                singleCallers = new Array();
                                
                                for (j = 0; j < PATH_MAX; j++) {
                                        for (i = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                                mobileUnitPathChannels[i][j] = null;
                                                mobileUnitPathSounds[i][j] = null;
                                        }
                                }                       
                                for (j = 0; j < BOOM_CHANNEL_MAX; j++) {
                                        boomChannels[j] = null;         
                                        boomSounds[j] = null;
                                }
                        }
                        
                        paused = false;
                }
                

                // Зачистка массивов для тарахтения многих юнитов
                public static function prepareMobileUnitLoopedSounds() : void {
                        for (var j : int = 0; j < PATH_MAX; j++) {
                                for (var i : int = 0; i < MOBILE_UNIT_SOUND_MAX; i++) {
                                        mobileUnitTypePathCount[i] = 0;
                                        mobileUnitPathPositions[i][j] = 0.0;
                                        mobileUnitPathCount[i][j] = 0.0;
                                }
                        }
                }
                
        }
        
}
  • +26

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

+3
ахринеть. ну и размерчик.
+3
чуть-чуть не дотянул до 1000 строк =)
+2
В некоторых играх кода меньше ))
+3
С первого взгляда понял мало, но однозначно круто!
+2
+1 Спасибище!
Теперь у меня будет новый SoundManager :)
+2
Супер, мне свой все лень было написать, теперь точно не напишу :).
+1
Посмотрел на свой куцый звуковой класс. А что, зато удобно располагается — целиком на одном экране :)

Для флешек такой функционал зачастую избыточен (многие вообще без звука играют), но камраду Scmorr однозначный респект.
+2
Шикарно. Спасибо :)
Так, через год-два у коммьюнити появится полноценный фреймворк)
0
кстати, эта игра вышла, нет? тыщу лет назад видел ролик на ютубе, тогда мне понравилось, хотелось бы поиграть )
0
Не вышла, хотя продана 8 месяцев назад. Спонсор маринует все это время. Но вот вроде окуклились. Надеюсь в ближайшее время будет.
+3
Зря ты игрозависимые ресурсы внес внутрь менеджера, а так прикольно :)
0
А смысл? Обрати внимание, что в классе куча игрозависимых алгоритмов и методов. Звуки взрывов, вытесняющие друг друга в отдельной группе каналов по особому алгоритму. Зацикленные звуки движения сильно завязаны на то, что у меня есть дороги, цепочки юнитов итд. Таким образом было бы глупо выносить что-то из менеджера. Именно что логично держать все в одном месте.

И, как я сказал, зная как все устроено, плюс прозрачная реализация — в нашем инди программировании в 100 раз быстрее просто взять в новой игре класс за основу и подхачить. Если сейчас ломать голову и пытаться универсализовать менеджер… в принципе можно, надо вычленять типы звучания, группы, алгоритмы. Писат расширяемую архитектуру. И все равно — в итоге может оказаться что для новой игры в другом жанре нужно что-то еще этакое. Или подхачить. В итоге потерянное время что на этапе универсализации, что на этапе кодинга новой игры.

Я работал долго в большой гейминдустрии. Там на больших проектах, движках итд — там универсализация оправдана.
0
Плюсую однозначно! Многие вещи можно взять себе в «копилку». Спасибо, что выложил клас.
0
// Рассчет стереобазы в зависимости от положения звука на экране
protected static function prepareSTPan(st: SoundTransform, pos: Number): void {
st.pan = (pos / 350.0 — 1.0) * 0.5; }
Зачем *0,5? Тогда pan будет от -0,5 до 0,5 (при видимой области 700) или так и было задумано?
+2
Да, 700 ширина. Просто крайние значения pan это жесть для слуха. Если у тебя голый взрыв или посадка на грани правого края — то пан 1.0 приведет к тому что в правом ухе только будет звучать. Это неестественно звучит. Особенно для моей игры, где ощущения аналогичны тому, что ты стоишь на вершине горы и смортишь сверху на долину/поляну. Если на ее правом краю что-то взорвется, то к левому уху все равно звук прилетит. Это только если у тебя прямо рядом с правым ухом из пистолета жахнут, тогда может +1.0 pan оправдан.
0
Спасибо.
Можно еще добавить уменьшение громкости при удалении от центра (или при выходе за экран при большой карте).
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.