Обещанный мой 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
Спасибо.
Можно еще добавить уменьшение громкости при удалении от центра (или при выходе за экран при большой карте).
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.