Subtitles for Randomized Dialogue Events?

I’m working on making the closed caption system for my game in Unity (2021.3.20f1), and have it mostly figured out. But the one thing I can’t figure out is in relation to randomized dialogue. For example, for certain repetitive interaction events (ie trying to flip a light switch if the power is off, trying to open a locked door, etc.), I have several dialogue clips that I have FMOD (2.02.13) randomly choose from, to make it less repetitive. But how do I make FMOD communicate to my closed caption script which clip it is playing, so that the script knows which subtitle to queue?

Hi,

There’s a number of ways to handle this, depending on how you want to set up your clips in FMOD Studio, and how much you want to handle at run-time in Unity vs at design-time in Studio.

The simplest way to handle this would likely be to handle the randomization in your own code. If you set up an event with a labeled parameter, and assign each of your audio clips to a corresponding parameter value:

At runtime, you can pick a random parameter value to use for the event, and using that value:

1 Like

Mmmkay, so I’ve been playing around with this, and adapting this guide I’ve compiled the following code (edited down to the relevant parts of the code):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using FMOD.Studio;
using FMODUnity;

public class CollectKeys : Interactable
{
    public NarratorController narratorController;
    public EventReference collectedDialogue;
    private int randomDiaTrig;
    private FMOD.Studio.PARAMETER_ID randomDialogueID;
    public FMOD.Studio.EventInstance dialogueInstance;
    private string Clip1, Clip2, Clip3, Clip4, Clip5;

    void Awake()
    {
                dialogueInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/Chapter1/Collections/CollectedKey");
        randomDiaTrig = Random.Range(1, 5);
        FMOD.Studio.EventDescription eventDescription;
        dialogueInstance.getDescription(out eventDescription);
        FMOD.Studio.PARAMETER_DESCRIPTION parameterDescription;
        eventDescription.getParameterDescriptionByName("RandomDialogue", out parameterDescription);
        randomDialogueID = parameterDescription.id;

        if (randomDiaTrig == 1) dialogueInstance.setParameterByID(randomDialogueID, Clip1);
        if (randomDiaTrig == 2) dialogueInstance.setParameterByID(randomDialogueID, Clip2);
        if (randomDiaTrig == 3) dialogueInstance.setParameterByID(randomDialogueID, Clip3);
        if (randomDiaTrig == 4) dialogueInstance.setParameterByID(randomDialogueID, Clip4);
        if (randomDiaTrig == 5) dialogueInstance.setParameterByID(randomDialogueID, Clip5);
    }

And here’s my FMOD setup for the event example:

And the parameter setup:

I’m very nearly there, I think, but I’m getting a compile error from the script, saying "Argument 2: cannot convert from ‘string’ to ‘float’. I’ve tried putting each of the clips into quotations (e.g: if (randomDiaTrig == 1) dialogueInstance.setParameterByID(randomDialogueID, “Clip1”); ) but that generates the same error. I also just tried copying the exact code provided in the example link above, but that generated the same error, so not sure what I’m doing wrong…

Studio::EventInstance::setParameterByID expects a float value, not a parameter value label - if you wish to set a parameter using a label, you can use Studio::EventInstance::setParameterByIDWithLabel.

1 Like

Mmkay, that got the console error to clear, but I’m still not getting the parameter to communicate from the script to FMOD. The closed captions are responding to the randomDiaTrig integer, but the FMOD event is not. Here’s the current FMOD setup for the event and parameter:


Oh yes, one modification I made to the script in troubleshooting was that I removed the series of If Statements out of the Awake() since I realized that as the script is attached to multiple objects, that should be called on at the time of interaction instead of the awake.

I also tried again with alternating between putting the Clip# strings in quotations or not - not having the quotations didn’t generate any console errors, but it crashes Unity when I play the level and pickup one of the keys. Having the quotations doesn’t crash it, but it doesn’t switch the parameter, either.

The crashing issue I mentioned in my last post appears to have resulted from a corruption in the .cs file. I started the script over from scratch, and that fixed it, but it’s still not communicating the parameter change from Unity into FMOD.

I also tried using the integer in place of the strings:

dialogueInstance.setParameterByID(randomDialogueID, randomDiaTrig);

But that didn’t yield any different result.

Here’s the (abbreviated) current version of the (relevant) script (I currently have the bits for the string version //commented out):

public class CollectKeys : Interactable
{      
    public NarratorController narratorController;
   
    public TMP_Text CCTXT;

    public EventReference collectedDialogue;
    public int randomDiaTrig;
    private FMOD.Studio.PARAMETER_ID randomDialogueID;
    public FMOD.Studio.EventInstance dialogueInstance;

    private bool ccTimerOn;
    private float ccTimer;

    //private string Clip1, Clip2, Clip3, Clip4, Clip5;

    void Awake()
    {
        dialogueInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/Chapter1/Collections/CollectedKey");
        randomDiaTrig = Random.Range(0, 4);
        FMOD.Studio.EventDescription eventDescription;
        dialogueInstance.getDescription(out eventDescription);
        FMOD.Studio.PARAMETER_DESCRIPTION parameterDescription;
        eventDescription.getParameterDescriptionByName("RandomDialogue", out parameterDescription);
        randomDialogueID = parameterDescription.id;

        ccTimerOn = false;
        ccTimer = 0.0f;
    }

    
    void Update()
    {        
        if (ccTimerOn)
        {
            ccTimer = ccTimer + Time.deltaTime;
            print("CC Timer On");

            if (randomDiaTrig == 1 && ccTimer >= 3 || randomDiaTrig == 5 && ccTimer >= 3)
            {
                CCTXT.text = "";
                ccTimerOn = false;
                ccTimer = 0.0f;
                print("CC Timer Off");
            }

            else if (randomDiaTrig == 2 && ccTimer >= 1 || randomDiaTrig == 3 && ccTimer >= 1)
            {
                CCTXT.text = "";
                ccTimerOn = false;
                ccTimer = 0.0f;
                print("CC Timer Off");
            }

            else if (randomDiaTrig == 4 && ccTimer >= 2)
            {
                CCTXT.text = "";
                ccTimerOn = false;
                ccTimer = 0.0f;
                print("CC Timer Off");
            }
        }
    }

    public override void OnInteract()
    {
            CollectFunction();
    }

    void CollectFunction()
    {
        StartCoroutine(CollectedDialogueDelay());
        print("COLLECTED " + gameObject.name);

        dialogueInstance.setParameterByID(randomDialogueID, randomDiaTrig);
        print("Random Dialogue Trigger = " + randomDiaTrig + " / Random Dialogue ID = " + randomDialogueID);

        /*if (randomDiaTrig == 1) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip1);
        if (randomDiaTrig == 2) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip2);
        if (randomDiaTrig == 3) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip3);
        if (randomDiaTrig == 4) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip4);
        if (randomDiaTrig == 5) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip5);*/
    }

    IEnumerator CollectedDialogueDelay()
    {
        yield return new WaitForSeconds(1);

        ccTimerOn = true;
        ccTimer = 0.0f;

        print("Collected Dialogue Delay");
        narratorController.Play(collectedDialogue, SFXConcurrencyStrategy.Wait);

        if (randomDiaTrig == 1) CCTXT.text = "Got it! Now...what does it unlock?";
        else if (randomDiaTrig == 2) CCTXT.text = "Got it!";
        else if (randomDiaTrig == 3) CCTXT.text = "Found it!";
        else if (randomDiaTrig == 4) CCTXT.text = "Wonder what this unlocks...";
        else if (randomDiaTrig == 5) CCTXT.text = "Another key, another lock to find.";
    }
}

Unfortunately, I’ve been unable to reproduce the issue you’re describing with your script - I’ve simplified it to the following, and it functions as expected:

public class CollectKeys : MonoBehaviour
{
    public int randomDiaTrig;
    private FMOD.Studio.PARAMETER_ID randomDialogueID;
    public FMOD.Studio.EventInstance dialogueInstance;

    //private string Clip1, Clip2, Clip3, Clip4, Clip5;

    void Awake()
    {
        dialogueInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/Chapter1/Collections/CollectedKey");
        randomDiaTrig = Random.Range(0, 4);
        FMOD.Studio.EventDescription eventDescription;
        dialogueInstance.getDescription(out eventDescription);
        FMOD.Studio.PARAMETER_DESCRIPTION parameterDescription;
        eventDescription.getParameterDescriptionByName("RandomDialogue", out parameterDescription);
        randomDialogueID = parameterDescription.id;
    }


    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            CollectFunction();
        }
    }

    void CollectFunction()
    {
        //StartCoroutine(CollectedDialogueDelay());
        //print("COLLECTED " + gameObject.name);

        dialogueInstance.setParameterByID(randomDialogueID, randomDiaTrig);
        dialogueInstance.start();
        print("Random Dialogue Trigger = " + randomDiaTrig + " / Random Dialogue ID = " + randomDialogueID);

        /*if (randomDiaTrig == 1) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip1);
        if (randomDiaTrig == 2) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip2);
        if (randomDiaTrig == 3) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip3);
        if (randomDiaTrig == 4) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip4);
        if (randomDiaTrig == 5) dialogueInstance.setParameterByIDWithLabel(randomDialogueID, Clip5);*/
    }

    private void OnDestroy()
    {
        dialogueInstance.release();
    }
}

There’s a couple of reasons that your code might be failing. To help diagnose this, can you check the following?

  • Your code (though abbreviated) doesn’t contain dialogueInstance.start(). I’ve added it after setting the parameter in my script above, but make sure that you start your event instance, otherwise the event instance won’t play, and your parameter change won’t matter
  • Do you receive any warnings or errors from FMOD?
  • If you log the results of your FMOD calls in Awake(), do they return anything other than 0 or FMOD_OK? For example:
void Awake()
{
        FMOD.RESULT result;
        //...
        result = dialogueInstance.getDescription(out eventDescription);
        Debug.Log("getDescription = " + result);
        FMOD.Studio.PARAMETER_DESCRIPTION parameterDescription;
        result = eventDescription.getParameterDescriptionByName("LabeledParam", out parameterDescription);
        Debug.Log("getParameterDescription = " + result);
        //...
}
  • If you retrieve the parameter value from the event with Studio.EventInstance.getParameterByID after setting it, does it return the value you set it to? The value you set the parameter to does get set asynchronously (this won’t have any bearing on the event playing correctly, just that you’ll retrieve the old value until the system updates), so you’ll need to put the getParameterByID() call in Update() to make sure you’re correctly getting the updated value.
  • Are your banks up to date with your FMOD Studio project?
  • Are all of the banks needed to play the event loaded (i.e. master bank, master string bank, bank containing the event, etc.)?

Ah, ok. I think I know what’s the discrepancy here. First, let me back up a bit.

In a separate forum thread (Overlapping Dialogue Interactions), I worked out with another user a technique he had developed for preventing interactive dialogue cues from overlapping unnaturally. This technique has the dialogue clips run as an EventReference instead of an EventInstance (the code narratorController.Play(collectedDialogue, SFXConcurrencyStrategy.Wait); in the last script I shared) which runs it through a concurrency script to determine if another dialogue clip is currently running, and if so how it should be handled (cancel, wait, stop current, etc.).

So, I gather that since the event is being triggered by an EventReference instead of an EventInstance, that’s why the parameter isn’t being triggered (I ran into a similar issue with trying to pause the EventReference cues in the case of the Pause Menu being loaded mid-dialogue, but ultimately decided it wasn’t worth wasting my time on figuring a way around that for only a few seconds of dialogue).

I just attempted to see if I could start the EventInstance and the EventReference simultaneously, but that of course just resulted in the clip playing twice.

I tested taking it off of the EventReference, and passed it through as an EventInstance instead, and that did work, so this definitely seems to be the issue. I also tried starting the EventInstance and immediately stopping it, then letting the EventReference play, but that didn’t work either - the clip played, and didn’t double up, but the parameter switch didn’t hold over into the Event Reference (didn’t think it would, but it was worth a shot, lol).

Is there a way to pass the parameter trigger through to an EventReference, as you would with an EventInstance, or do I need to essentially choose between having my dialogue overlap and having the captions synch up?

What you’ve described would definitely explain the behavior you’re observing.

The short answer is “no”.

The long answer is that it appears you may be confused about what exactly an EventReference is, what it “triggers”, etc., as well as the scope on which parameters exist. I suspect some of this confusion is caused by the code that actually plays an event being in NarratorController instead of directly in CollectKeys.

An EventInstance is an instance of an event you’ve authored in FMOD Studio that has been created by the FMOD System. it can be played, paused, stopped, have parameter values set on it, etc.

An EventReference, on the other hand, is a convenience class in the Unity plugin that essentially just stores the path and GUID of an event to make it easier for you to create instances of the event. You can view the entirety of the source code for it at ./Assets/Plugins/FMOD/src/EventReference.cs.

It is not possible to set the parameters of an EventReference, because an EventReference has no parameters. It is possible to set parameters of an EventInstance. However, unless your parameter is “global”, it exists on a per-EventInstance basis, so setting the value of a parameter on one EventInstance will not affect another EventInstance.

No, it’s entirely possible to do both. From what I can tell from the thread you’ve linked, NarratorController would require a couple of tweaks to handle it. The simplest way to do this would be to have a member of NarratorController that represents the desired parameter value, and then make sure to set the parameter to it in NarratorController.PlayNow() before calling _instance.start(). For example:

using System.Collections;
using FMOD.Studio;
using FMODUnity;
using UnityEngine;

public enum SFXConcurrencyStrategy
{
    Cancel,
    Wait,
    AbortCurrent,
    PlayAnyway
}

public class NarratorController : MonoBehaviour
{
    private EventInstance _instance;
    private string _parameterLabel;

    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.setParameterByNameWithLabel("RandomDialogue", _parameterLabel);
        _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);
    }

    public void SetParameter(string label){
        _parameterLabel = label;
    }
}

You can then call narratorController.SetParameter() in CollectKeys.CollectedDialogueDelay() before narratorController.Play(collectedDialogue, SFXConcurrencyStrategy.Wait).

This does assume that you’re only playing one specific event and setting one parameter with NarratorController, but it should serve as a good example of what you might want to do.

Thank you so much! That got it working - and yes, there are unfortunately different events aside from the keys that I will be doing this with, but I think I can figure out my way from here - I think I’ll be running the captions for most dialogue interactions (random or otherwise) through the NarratorController going forward, as that seems to make more sense with the delays and whatnot.

And also thank you for explaining everything so thoroughly and simply - as a completely self-taught noob, the online documentation sometimes goes over my head, so I really appreciate it when someone takes the effort to help me understand!

1 Like