Воспроизведение и управление звуками в Unity 3D (Sound complete event, Play in edit mode)

    К этой статье будет приложен небольшой, но полезный csharp скрипт, и показано как им пользоваться.
    Поводом для написания скрипта стало то, что появилась необходимость в настройке и тестировании звуковых эффектов не запуская сцены проекта. А так же в отслеживании основных событий воспроизведения.

    Скрипт работает одинаково как в PlayMode так и в EditMode, и позволяет:
    1. Воспроизвести звук с необходимой задержкой и отследить начало воспроизведения.
    2. Отследить окончание звука, в том числе каждый момент завершения зацикленного воспроизведения.
    3. Отследить незапланированное окончание воспроизведения звука.
    4. Использовать событие для отслеживания и изменения параметров в процессе воспроизведения.

    Для воспроизведения звука используются статические методы:

    public static SoundTrack PlaySound(AudioClip clip, float volume = 1, float pitch = 1, float loopTime = 0, float delayTime = 0);
    
    public static SoundTrack PlaySound(GameObject target, AudioClip clip, float volume = 1, float pitch = 1, float loopTime = 0, float delayTime = 0);
    

    Эти методы возвращают экземпляр класса SoundTrack, к которому в последствии можно прикрепить необходимые события. Первый метод создаёт на сцене GameObject, второй добавляет указанному GameObject компоненты SoundTrack и AudioSource.

    Параметры volume и pitch не нуждаются, наверное, в представлении.

    loopTime – можно использовать для задания времени в секундах, которое будет длиться цикл воспроизведения. При значении 0 звук проиграется только один раз, при значении float.PositiveInfinity звук будет проигрываться бесконечно.
    delayTime – это задержка перед воспроизведением звука в секундах.
    Примечание:
    По прошествии loopTime, первый метод удалит созданный для звука GameObject, второй метод удалит только созданные для звука компоненты.

    Итак, самый простой пример кода для воспроизведения звука в игре.

    public AudioClip testSound;
    
    void Start () {
    	SoundTrack.PlaySound (testSound);
    }
    

    И для воспроизведения звука в редакторе.

    [MenuItem("MyMenu/TestSound #F1")]
    static void TestSound(){
    	AudioClip[] clips=Resources.FindObjectsOfTypeAll<AudioClip>();
    	if(clips==null||clips.Length==0){
    		Debug.LogError("No clips in the resources!");
    		return;
    	}
    	SoundTrack.PlaySound(clips[0]);
    }
    


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

    Данный пример запускается сочетанием клавиш SHIFT+F1, и показывает как использовать события.

    using UnityEngine;
    using UnityEditor;
    using System.Collections;
    
    public class SoundTrackTest : Editor {
    
    	[MenuItem("MyMenu/TestSound #F1")]
    	static void TestSound(){
    		clips=Resources.FindObjectsOfTypeAll<AudioClip>();
    		Debug.Log("clips " + clips.Length);
    		if(clips==null||clips.Length==0){
    			Debug.LogError("No clips in the resources!");
    			return;
    		}
    		rePlayCount = 0;
    		StartNextSound (0);
    	}
    	
    	static AudioClip[] clips;
    	static int rePlayCount=0;
    	static void StartNextSound(float timePosition){
    		SoundTrack track=SoundTrack.PlaySound(clips[Random.Range(0,clips.Length-1)]);
    		rePlayCount++;
    
    		// событие начала звука
    		track.start_action += soundStartEvent;
    
    		// событие срабатывает каждый кадр
    		track.processing += soundProcessEvent;
    
    		// это событие подходит только для зацикленного воспроизведения
    		// оно срабатывает каждый раз, когда звук завершается
    		track.complete_action += soundCompleteEvent;
    
    		// событие срабатывает при далении звука
    		track.destroy_action += soundDestroyEvent;
    
    		if(timePosition>0){
    			// перематывает точку воспроизведения на определённый момент, в секундах
    			track.setTimePosition (timePosition);
    		}
    	}
    		
    	static void soundStartEvent(SoundTrack track){
    		Debug.Log("Sound Start event!");
    	}
    	
    	static void soundProcessEvent(SoundTrack track){
    		track.volume = track.playing_time % 1f;
    	}
    	
    	static void soundCompleteEvent(SoundTrack track, float offset){
    		Debug.Log("Sound Complete event! "+offset);
    	}
    	
    	static void soundDestroyEvent(SoundTrack track, bool atEndOfSound, float offset){
    		Debug.Log("Sound Destroy event! "+offset);
    
    		// проверяет было ли уничтожение инициировано из за окончания воспроизведения
    		// atEndOfSound будет равен false в случае если воспроизведения было прервано по другим причинам
    		// к примеру при удалении звука со сцены вручную и т.д.
    		if(atEndOfSound){
    			// данный подход демонстрирует как можно плавно соединить несколько последовательных звуков
    			// offset - это неизбежная задержка вызова события 
    			// можно её компенсировать используя track.setTimePosition для следующего звука
    
    			// это сделано чтобы звуки в редакторе не воспроизводились бесконечно
    			// так как если звук очень короткий, это будет весьма неприятный тест
    			if(rePlayCount<4){
    				StartNextSound (offset);
    			}
    		}
    	}
    
    }
    

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

    void Start(){
        clips=Resources.FindObjectsOfTypeAll<AudioClip>();
        Debug.Log("clips " + clips.Length);
        if(clips==null||clips.Length==0){
            Debug.LogError("No clips in the resources!");
            return;
        }
        rePlayCount = 0;
        StartNextSound (0);
    }
    

    Это событие срабатывает непосредственно при начале воспроизведения звука.

    track.start_action += soundStartEvent; // событие начала звука
    

    Т.е. в случае, если delayTime > 0, оно сработает не при создании звука, а при старте воспроизведения.

    Это событие может быть полезно в ряде случаев.

    track.processing += soundProcessEvent; // событие срабатывает каждый кадр
    

    К примеру можно музыке и эффектам задать разные события, и одной переменной регулировать громкость всей музыки в игре, а другой всех эффектов. А потом добавить и эффектам и музыке ещё одно событие, в котором уже регулировать скорость их воспроизведения, если в игре есть эффект Slow Motion.
    В данном примере звук должен в течении каждой секунды менять громкость.

    track.complete_action += soundCompleteEvent; // событие срабатывает каждый раз, когда звук завершается
    

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

    track.destroy_action += soundDestroyEvent; // событие срабатывает при удалении звука
    

    Это событие сработает в 2-х случаях. Первый случай, это когда воспроизведение звука завершено, по истечении loopTime или при одноразовом воспроизведении. Второй случай, это никак не связанное с логикой скрипта удаление звука со сцены (например вручную удалив объект из иерархии сцены). Отличить один случай от другого можно с помощью параметра atEndOfSound, если он равен false, то это как раз второй случай.

    Во втором случае мы уже не будем иметь доступ к GameObject'у и компонентам звука.

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

    Помимо этого в скрипте есть параметры которые могут понадобиться:
    • time_position — позиция воспроизведения аудиофайла, от 0 до длины файла;
    • life_time — время с начала воспроизведения звука;
    • playing_time — время затраченное на воспроизведения звука, не учитывает паузы;
    • loop_time — время, которое звук будет проигрываться;
    • delay_time — время задержки перед воспроизведением;
    • startTime — момент начала воспроизведения звука;
    • created_time — момент создания звука (startTime-delay_time).


    Всё время расчитывается относительно Time.realtimeSinceStartup.

    Здесь можно скачать сам скрипт и приведённый выше пример.

    Если в примере не работает сочетание клавиш, можно найти пункт TestSound в меню.

    Если Unity не находит звук в ресурсах, надо его просто выделить (посмотреть настройки аудиофайла).
    • +2
    • 22,4k
    • 3
    Поделиться публикацией
    Ммм, длинные выходные!
    Самое время просмотреть заказы на Фрилансим.
    Мне повезёт!
    Реклама
    Комментарии 3
    • 0
      Всё время расчитывается относительно Time.realtimeSinceStartup.

      Т.е. если я изменю Time.timeScale — все поломается?
      time_position, life_time, playing_time, loop_time, delay_time, startTime, created_time

      Вы бы хоть сами определились бы со стилем именования, прежде чем в публику выкладывать… Почему time_position и все остальное через нижний слеш, но startTime и весь остальной код внезапно camelCase?..
      • 0
        нет, с изменением Time.timeScale ничего не поломается, на realtimeSinceStartup этот параметр не влияет.
      • 0
        Со стилем именования косяк, согласен, не стал причёсывать код, а с момента написания скрипта, до его последних изменений стиль у меня изменился
        А на счёт Time.realtimeSinceStartup я специально оставил ссылку, Time.timeScale не влияет на расчёты времени.
        realtimeSinceStartup returns the time since startup, not affected by Time.timeScale

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.