Any way to trigger function/emotion at specific parts of conversation?

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
HappyBot
Posts: 3
Joined: Sun Nov 15, 2020 6:35 pm

Any way to trigger function/emotion at specific parts of conversation?

Post by HappyBot »

I have a script attached to my NPCs to change their facial textures to display emotion during a conversation. I'd like to use some type of trigger in a conversation ("Oh hey PLAYER. Wait...[Excited]you're actually here?!"). I know how I'd handle most everything else except setting up some way to trigger said function at a specific part of a conversation.

I've read responses in the past that people could using sequences to wait x amount of time until doing said emote, but that seems like it would take a lot of trial and error to get working at the perfect moment and it also seems prone to error.
Is there no way to trigger a function at the exact moment a keyword appears in a conversation?
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Any way to trigger function/emotion at specific parts of conversation?

Post by Tony Li »

Hi,

Yes. You can add a script with an OnConversationLine() method to the character or the Dialogue Manager GameObject. Find the position of the emotion in the subtitle text. Then call your emotion function based on how far into the subtitle text it is. You could add a sequencer command to do it, or kick off a coroutine. Here's an example:

Code: Select all

public class HandleEmotionsInSubtitles : MonoBehaviour
{
    void OnConversationLine(Subtitle subtitle)
    {
        string subtitleText = UITools.StripTextMeshProTags(subtitle.formattedText.text); // Ignore rich text codes and TMP tags.
        int excitedPos = subtitleText.IndexOf("[Excited]");
        if (excitedPos >= 0) // Subtitle contains [Excited], so handle it: (for simplicity in example, assumes only 1 occurrence)
        {
            subtitle.formattedText.text = subtitle.formattedText.text.Replace("[Excited]", string.Empty);
            {
                // Identify which subtitle panel we'll be using:
                DialogueActor dialogueActor;
                var panel = (DialogueManager.dialogueUI as StandardDialogueUI).conversationUIElements.standardSubtitleControls.GetPanel(subtitle, out dialogueActor);
                // Get typewriter speed (assumes panel has typewriter effect) and figure out when emotion should change:
                float charsPerSecond = panel.GetTypewriter().charactersPerSecond;
                float emotionTime = excitedPos / charsPerSecond;
                // Insert a hypothetical custom sequencer command Emotion() to play the emotion:
                subtitle.sequence = $"Emotion(Excited)@{emotionTime}; {subtitle.sequence};
            }
        }
    }
}
HappyBot
Posts: 3
Joined: Sun Nov 15, 2020 6:35 pm

Re: Any way to trigger function/emotion at specific parts of conversation?

Post by HappyBot »

This actually works better than I thought. I might use the RPGMaker delays, but I have an idea how to account for those. The only issue I'm having right now is that the emote only plays on time if the player allows the dialogue to run without speeding up to the end. I have used "required" which ensures the sequence plays when the node ends, but is there a way to ensure the node instantly plays if the player speeds up the text to the end without continuing to the next node?

Also thank you a lot for the help Tony. I notice you've been doing this stuff for people on a daily basis and I'm sure it gets old after awhile, but I think I speak for a lot of us when I say it's very much appreciated.
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Any way to trigger function/emotion at specific parts of conversation?

Post by Tony Li »

I'm glad to help! It's a privilege for me to be able to provide something of value for so many creative game devs.

If you only need to play the sequencer command at the end of the typewriter effect, you can add "@Message(Typed)" to the command:

Code: Select all

required SetEmotion(Excited)@Message(Typed)
This should work even if you're adding SetEmotion() commands programmatically in an OnConversationLine() method. If the programmatic SetEmotion() doesn't kick off, the "@Message(Typed)" one will act as a failsafe.

However, if you need character-precise timing in all scenarios, there's another option. It's a little more complex, so it might not be worth the extra effort. Instead of the OnConversationLine() method, make a subclass of UnityUITypewriterEffect or TextMeshProTypewriterEffect (whichever one you're using). Assign your subclass to the subtitle text GameObject instead of the regular typewriter effect.

Override the Play(). In Play(), pull out all of your emotion tags, and make of note of how many characters into the string they are. Also assign a method to the onCharacter() event. This event will be called when each character is typed, so you'll know when to set emotions. Here's a rough example that only handles one [Excited]:

Code: Select all

public class CustomTypewriterEffect : UnityUITypewriterEffect
{
    private int excitedPosition;
    private int numCharsTyped;
    
    public override IEnumerator Play(int fromIndex = 0)
    {
        // Extract [Excited] tag. This simplified example assumes text has only zero or one [Excited] tags:
        string s = UITools.StripRPGMakerCodes(UITools.StripTextMeshProTags(control.text));
        numCharsTyped = 0;
        excitedPosition = s.IndexOf("[Excited]");
        control.text = s.Replace("[Excited]", string.Empty);
        
        // Hook into character event:
        onCharacter.AddListener(TypedCharacter);
        
        // Run the regular typewriter code:
        yield return StartCoroutine(base.Play(fromIndex));
        
        // Unhook from character event:
        onCharacter.RemoveListener(TypedCharacter);
    }
    
    void TypedCharacter()
    {
        if (numCharsTyped == excitedPosition)
        {
            // Your code here to set emotion.
        }
        numCharsTyped++;
    }
}
Post Reply