
Обещанный мой 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
- scmorr
Комментарии (16)
Теперь у меня будет новый SoundManager :)
Для флешек такой функционал зачастую избыточен (многие вообще без звука играют), но камраду Scmorr однозначный респект.
Так, через год-два у коммьюнити появится полноценный фреймворк)
И, как я сказал, зная как все устроено, плюс прозрачная реализация — в нашем инди программировании в 100 раз быстрее просто взять в новой игре класс за основу и подхачить. Если сейчас ломать голову и пытаться универсализовать менеджер… в принципе можно, надо вычленять типы звучания, группы, алгоритмы. Писат расширяемую архитектуру. И все равно — в итоге может оказаться что для новой игры в другом жанре нужно что-то еще этакое. Или подхачить. В итоге потерянное время что на этапе универсализации, что на этапе кодинга новой игры.
Я работал долго в большой гейминдустрии. Там на больших проектах, движках итд — там универсализация оправдана.
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) или так и было задумано?
Можно еще добавить уменьшение громкости при удалении от центра (или при выходе за экран при большой карте).