Using Unity 2021.3.20f1 and FMOD 2.02.13. I have several mini dialogue events that correspond with player interactions in my game (eg, player picks up a key and says, “Found it!”) The problem I’m running into is that if multiple interactions happen within a short period of each other, there is a bit of overlap in the dialogue, which obviously sounds unnatural. I’ve already added a 1-2 second delay to most of the events anyway, since there are UI sound effects involved with most of the interactions that I didn’t want the dialogue clashing with, and that has minimized this problem a tad, but not completely. Is there a way to tell FMOD via script to check if a line of dialogue is currently playing and not to start the next line until the current one has stopped? (PS - I’m very new to scripting and game sound design in general, so the more specific details the better, lol).
Hi,
I had the same issue and build a component to handle this. What it does is that it keeps track of the last event it plays and it gives you the means to define what should happen when you start another one while it is still playing. I call that the concurrency strategy.
It can abort the current one, ignore the new one, wait until the current one is finished, or just play anyway.
In order to use this, you would have to have one object with this script attached and other scripts could call the Play method and pass an EventReference as well as the concurrency strategy.
Feel free to use and modify this in any way you like.
using System.Collections;
using FMOD.Studio;
using FMODUnity;
using UnityEngine;
public enum SFXConcurrencyStrategy
{
Cancel,
Wait,
AbortCurrent,
PlayAnyway
}
public class NarratorController : MonoBehaviour
{
private EventInstance _instance;
public bool IsPlaying
{
get
{
if (!_instance.isValid()) return false;
_instance.getPlaybackState(out PLAYBACK_STATE state);
return state == PLAYBACK_STATE.PLAYING || state == PLAYBACK_STATE.STARTING;
}
}
public void Play(EventReference eventReference,
SFXConcurrencyStrategy sfxConcurrencyStrategy = SFXConcurrencyStrategy.Cancel)
{
if (eventReference.IsNull)
{
return;
}
if (!IsPlaying)
{
PlayNow(eventReference);
}
else
{
switch (sfxConcurrencyStrategy)
{
case SFXConcurrencyStrategy.Cancel:
_instance.getDescription(out EventDescription eventDescription);
eventDescription.getPath(out string currentEventPath);
Debug.LogWarning(
$"Will not play narrator comment {eventReference}, because {currentEventPath} is already playing");
break;
case SFXConcurrencyStrategy.Wait:
StartCoroutine(PlayAfterCurrent(eventReference));
break;
case SFXConcurrencyStrategy.AbortCurrent:
_instance.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
PlayNow(eventReference);
break;
case SFXConcurrencyStrategy.PlayAnyway:
PlayNow(eventReference);
break;
}
}
}
private void PlayNow(EventReference eventReference)
{
_instance = RuntimeManager.CreateInstance(eventReference);
_instance.start();
_instance.release();
}
private IEnumerator PlayAfterCurrent(EventReference eventReference)
{
while (enabled)
{
_instance.getPlaybackState(out PLAYBACK_STATE state);
if (state == PLAYBACK_STATE.PLAYING || state == PLAYBACK_STATE.STARTING) yield return null;
else break;
}
PlayNow(eventReference);
}
}
Awesome, thanks!!!
Mmkay, so I have the basic idea down, but the implementation I’m a bit hazy on. As suggested, I have an empty game object (NarratorController) which I’ve attached the script to. Then in my CollectObjects script I call on the IsPlaying bool with:
NarratorController narratorController = NarratorControllerOB.GetComponent();
IsPlaying = narratorController.IsPlaying;
After that, is where I’m currently unsure of what to do next.
Do I need to add the:
public enum SFXConcurrencyStrategy
{
Cancel,
Wait,
AbortCurrent,
PlayAnyway
}
to the start of my CollectObjects script, or is that unnecessary?
And not sure how to proceed with assigning whether to Cancel, Wait, AbortCurrent, or PlayAnyway within the CollectObjects script.
For example, here are two coroutines that I currently have set up to play two interaction dialogues (one for when he picks up a toolbelt, and another for picking up a tape player).
IEnumerator ToolBeltDialogue()
{
yield return new WaitForSeconds(1);
FMODUnity.RuntimeManager.PlayOneShot(“event:/Dialogue/Chapter1/Collections/CollectedToolBelt”);
}
IEnumerator TapePlayerDialogue()
{
yield return new WaitForSeconds(1);
FMODUnity.RuntimeManager.PlayOneShot("event:/Dialogue/Chapter1/Collections/CollectedTapePlayer");
}
If I understand correctly, I would want to add an if(IsPlaying) and an if(!IsPlaying) to each coroutine, and within the if(IsPlaying) I would call on the Wait function of the NarratorController script, and in the if(!IsPlaying) I would just play the event (and I presume I would need to toggle the !IsPlaying bool to true - and setup another coroutine to toggle it back to false after the clip is done). Just not sure how to call upon the Wait function from within the CollectObjects script.
You are trying to implement all the things that my script is supposed to do for you
Your script needs two public fields. One that references the NarratorController and one of type EventReference.
Then instead of doing RuntimeManager.PlayOneShot you call NarratorController.Play()
As the first argument you pass your event reference and as the second one you can pass the concurrency strategy.
Hope that’ll help
Sorry - like I said, total newb, lol! (I started teaching myself all of this in February, so they don’t come much greener than me, lol!)
So at the start of the class, I would put something along the lines of:
public GameObject ConcurrencyManagerOB;
public FMOD.Studio.EventInstance dialogueCues;
Then in the Coroutine, using the example I included above, I’d put:
yield return new WaitForSeconds(1);
NarratorController.Play();
{
FMODUnity.RuntimeManager.PlayOneShot("event:/Dialogue/Chapter1/Collections/CollectedToolBelt");
Wait;
}
I’m getting errors on the last part, so I gather there’s something I’m missing, lol.
Not quite. You don’t call PlayOneShot
yourself. That happens in the narrator controller.
Your class would look something like this
// assign these fields in the inspector
public EventReference dialogueCue;
public NarratorController narratorController;
private IEnumerator PlaySoundDelayedCoroutine()
{
// wait for one second
yield return new WaitForSeconds(1);
// tell the narrator controller to play the event as soon as whatever it is currently playing has finished
narratorController.Play(dialogueCue, SFXConcurrencyStrategy.Wait);
}
It’ll take time to get used to coding stuff like this. Keep up your efforts!
Got it! Cheers, that’s a really useful tool to have on hand!
Follow up question: if I need to pause one of these event references (e.g., if the player activates the pause menu in the middle of the audio clip), how would you go about that? For most other events using EventInstance, I use the code:
“if (pauseMenuOn) narrationInstance.setPaused(true);”
but that doesn’t seem to work on EventReference. Is there a similar code to use for EventInstance? If not, I guess it’s not that critical if these short dialogue cues bleed over into the pause menu for a few seconds, so I can let my OCD live with it, lol, but if there is an easy fix, I’d like to employ it.
EventInstance does have a setPaused() function.
General (somewhat patronizing) advice: you will need a code editor that offers you code completion so you can quickly find what methods an object offers. Also you will have to get used to reading documentation. You won’t be able to get answers through forums for these issues.
But also, pausing short dialogue cues might not be something you actually want to do. Might be better to just let them finish.
I have the same problem using the code provided by FMOD. Is it possible to implement your code on the example provided by FMOD?:
sing System;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
class ScriptUsageProgrammerSounds : MonoBehaviour
{
FMOD.Studio.EVENT_CALLBACK dialogueCallback;
public FMODUnity.EventReference EventName;
#if UNITY_EDITOR
void Reset()
{
EventName = FMODUnity.EventReference.Find("event:/Character/Radio/Command");
}
#endif
void Start()
{
// Explicitly create the delegate object and assign it to a member so it doesn't get freed
// by the garbage collected while it's being used
dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);
}
void PlayDialogue(string key)
{
var dialogueInstance = FMODUnity.RuntimeManager.CreateInstance(EventName);
// Pin the key string in memory and pass a pointer through the user data
GCHandle stringHandle = GCHandle.Alloc(key);
dialogueInstance.setUserData(GCHandle.ToIntPtr(stringHandle));
dialogueInstance.setCallback(dialogueCallback);
dialogueInstance.start();
dialogueInstance.release();
}
[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);
// Retrieve the user data
IntPtr stringPtr;
instance.getUserData(out stringPtr);
// Get the string object
GCHandle stringHandle = GCHandle.FromIntPtr(stringPtr);
String key = stringHandle.Target as String;
switch (type)
{
case FMOD.Studio.EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
{
FMOD.MODE soundMode = FMOD.MODE.LOOP_NORMAL | FMOD.MODE.CREATECOMPRESSEDSAMPLE | FMOD.MODE.NONBLOCKING;
var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));
if (key.Contains("."))
{
FMOD.Sound dialogueSound;
var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(Application.streamingAssetsPath + "/" + key, soundMode, out dialogueSound);
if (soundResult == FMOD.RESULT.OK)
{
parameter.sound = dialogueSound.handle;
parameter.subsoundIndex = -1;
Marshal.StructureToPtr(parameter, parameterPtr, false);
}
}
else
{
FMOD.Studio.SOUND_INFO dialogueSoundInfo;
var keyResult = FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(key, out dialogueSoundInfo);
if (keyResult != FMOD.RESULT.OK)
{
break;
}
FMOD.Sound dialogueSound;
var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data, soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound);
if (soundResult == FMOD.RESULT.OK)
{
parameter.sound = dialogueSound.handle;
parameter.subsoundIndex = dialogueSoundInfo.subsoundindex;
Marshal.StructureToPtr(parameter, parameterPtr, false);
}
}
break;
}
case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND:
{
var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));
var sound = new FMOD.Sound(parameter.sound);
sound.release();
break;
}
case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROYED:
{
// Now the event has been destroyed, unpin the string memory so it can be garbage collected
stringHandle.Free();
break;
}
}
return FMOD.RESULT.OK;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
PlayDialogue("640148main_APU Shutdown");
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
PlayDialogue("640165main_Lookin At It");
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
PlayDialogue("640169main_Press to ATO");
}
}
}
Hi,
Would it be possible to elaborate on your desired behavior? Could I also please get your FMOD and Unity versions?
Thank you for answering,
I am developing a videogame which has a interactive dialog between the player and the npc (which -in short- works like a quest giver). You can speed up the dialog by not reading the texts and click fast on the right questions / answers. But, by doing that, it´s a chaos because the sounds overlap.
My desired behaviour would be as soon as you click on a new question / answer, the previous sound would be stopped in order to play the new one and avoid the overlapping.
My versions:
Unity: 2022.3.55f1
FMOD: 2.03.06
Thank you in advance.
Thank you for the explanation.
Depending on your setup, if you are using the same event instance (FMOD Engine | Studio API Reference - Event Instance) for all dialog, restarting the event with the new dialog key should stop its previous dialog. Alternatively, you could use FMOD Engine | Studio API Reference - Bus::stopAllEvents to stopthe dialog events routed into the a bus. Would it be possible to get a profiler (FMOD Studio | Profliling) session recorded while triggering the dialog events and post a screenshot of the event lifespans?
Thank you for your reply.
Here is the screenshot of the profile while triggering the dialogue event. I hope i did correctly. Thank you.
With the programmer sound example, you could modify it to stop the old event before starting the new one, which would just be a matter of making dialogueInstance
a member and stopping it before recreating it. i.e
+ FMOD.Studio.EventInstance dialogueInstance = new FMOD.Studio.EventInstance(IntPtr.Zero); // make member
void PlayDialogue(string key)
{
+ if (dialogueInstance.isValid())
+ {
+ dialogueInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE); // Stop if we are already playing something
+ }
+ dialogueInstance = FMODUnity.RuntimeManager.CreateInstance(EventName);
// Pin the key string in memory and pass a pointer through the user data
GCHandle stringHandle = GCHandle.Alloc(key);
dialogueInstance.setUserData(GCHandle.ToIntPtr(stringHandle));
dialogueInstance.setCallback(dialogueCallback);
dialogueInstance.start();
dialogueInstance.release();
}