Search Unity

[RELEASED] Dialogue System for Unity - easy conversations, quests, and more!

Discussion in 'Assets and Asset Store' started by TonyLi, Oct 12, 2013.

  1. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Check out Silent Streets Mockingbird on Apple App Store

    Funbakers' Silent Streets, made with the Dialogue System (and making good use of i2 Localization integration), is now available on the Apple App Store, and will be available for Android on April 1.



    Silent Streets is an innovative augmented reality detective game that takes place in the foggy streets of Victorian England. If you like Pokemon Go or Fallen London, check it out!
     
    AGregori likes this.
  2. AGregori

    AGregori

    Joined:
    Dec 11, 2014
    Posts:
    527
    Hi Tony, your Master Audio integration has a splendid depth to it, I'm just having trouble hooking it up with your Menu Framework add-on. Is there any help available on how to link the Options/Music volume slider with the Master Audio playlist volume? Thanks.
     
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @Gregorik - I recommend hooking it up outside of the Dialogue System integration.The Menu Framework saves the volume setting to PlayerPrefs. Just use that PlayerPrefs value for Master Audio's volume. It will require a little modification of the script.
     
    AGregori likes this.
  4. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    You may not have seen my thread yet, but someone mentioned skipping dialogue.

    For the new system, however, this would require having the timers update when you skip text.

    Seems fairly straightforward--have it update the response timers whenever the "skip" button is pressed.

    So I guess does the DS already have this functionality--skipping a line--and where is it if so?

    Edit: I'm silly. Just discovered that it was "Escape" when trying to pause. Just need to find out where that is so I can make skipping a different button, like spacebar...

    Edit: Nvm most of that, it was simply on the Dialogue Manager GO. Simple enough. But I need to go into the code to force it to update the timer when it cancels the current line. Unfortunately however pressing "edit script" on the Dialogue System Manager component doesn't open it up. Any tips for that?

    Additionally, there's something else I'd like to do. When reaching the end of the response list, I would like to move back to the beginning if I press "down" again. Currently it doesn't do that.
     
    Last edited: Mar 2, 2018
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @EternalAmbiguity - The timer is handled in your dialogue UI script. You can add an OnConversationLineCancelled method to the script. The Dialogue System will call this method when you cancel the current line.
    Code (csharp):
    1. void OnConversationLineCancelled(Subtitle subtitle)
    2. {
    3.     // Your code here to update timer.
    4. }
    I just added a 'Loop Explicit Navigation' checkbox to the Unity UI Dialogue UI component. You can download the latest Unity UI support package on the Dialogue System Extras page and tick this checkbox.
     
    EternalAmbiguity likes this.
  6. AGregori

    AGregori

    Joined:
    Dec 11, 2014
    Posts:
    527
    Tony, the Menu Framework's Pause Menu Resume button doesn't work for some reason when I press it runtime. The code seems fine though. Maybe it's just me, but it's not responsive, whatever I do.
     
  7. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @Gregorik - Make sure the Resume button's OnClick() event still points to the PausePanel's SelectablePanel.Close method. If that doesn't fix it, please feel free to send a reproduction project to tony (at) pixelcrushers.com. I'll be happy to take a look.
     
    AGregori likes this.
  8. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    When you say "dialogue UI script," what script are you referring to? I tried adding it to the InterjectableDialogueUI script, but it doesn't seem to be working.

    Before I go into all that, just want to say that I'm making things a bit more complex because I'm trying to update the timer rather than restart it anew. A restarted timer won't give the user the same sense of how much "time" they've lost for the response as a definite jump in the slider will.

    Anyway, my new method in the InterjectableDialogueUI script:

    Code (csharp):
    1. void OnConversationLineCancelled(Subtitle subtitle)
    2.     {
    3.         Debug.Log("Firing Timer Update Method...");
    4.         Response[] responses = DialogueManager.CurrentConversationState.pcResponses;
    5.         for (int i = 0; i < responses.Length; i++)
    6.         {
    7.             var response = responses[I];
    8.             var button = dialogue.responseMenu.instantiatedButtons[I];
    9.             var responseTimeout = Field.LookupFloat(response.destinationEntry.fields, "Timeout");
    10.             if (Mathf.Approximately(0, responseTimeout))
    11.             {
    12.                 // If Timeout is zero or doesn't exist, hide the slider if it exists:
    13.                 var slider = button.GetComponentInChildren<UnityEngine.UI.Slider>();
    14.                 if (slider != null) slider.gameObject.SetActive(false);
    15.             }
    16.             else if (responseTimeout > 0)
    17.             {
    18.                 // If response has a positive Timeout field, set up the timer with Timeout's value:
    19.                 UpdateResponseTimer(button, responseTimeout);
    20.             }
    21.             else if (responseTimeout < 0)
    22.             {
    23.                 // If response has a negative Timeout field, compute the timer value from the dialogue tree:
    24.                 UpdateResponseTimer(button, ComputeDurationUntilHideResponseWithID(response.destinationEntry.id));
    25.             }
    26.         }
    27.     }
    The UpdateResponseTimer method is extremely similar to the StartTimer method used before.

    Code (csharp):
    1. private void UpdateResponseTimer(GameObject button, float timeout)
    2.     {
    3.         var timedResponseButton = button.GetComponent<TimedResponseButton>();
    4.         if (timedResponseButton == null) timedResponseButton = button.gameObject.AddComponent<TimedResponseButton>();
    5.         timedResponseButton.UpdateTimer(timeout, OnHideResponse);
    6.     }
    The UpdateTimer method is a new one I added to the TimedResponseButton script. As before it's very similar to the StartTimer method.

    Code (csharp):
    1. public void UpdateTimer(float duration, GameObjectTimeoutDelegate timeoutHandler)
    2.     {
    3.         var timer = GetComponentInChildren<UnityUITimer>();
    4.         if (timer == null)
    5.         {
    6.             // Make sure we have a timer, on a slider if one exists:
    7.             var slider = GetComponentInChildren<UnityEngine.UI.Slider>();
    8.             if (slider != null)
    9.             {
    10.                 timer = slider.gameObject.AddComponent<UnityUITimer>();
    11.             }
    12.             else
    13.             {
    14.                 timer = this.gameObject.AddComponent<UnityUITimer>();
    15.             }
    16.         }
    17.         if (timer != null)
    18.         {
    19.             // Start the timer:
    20.             timer.UpdateTimeLeft(duration);
    21.         }
    22.         this.timeoutHandler = timeoutHandler;
    23.     }
    UpdateTimeLeft was already in the UnityUITimer script, so you should know that one better than I do--I didn't change any part of it.

    See anything that could be amiss? That Debug.Log line at the beginning does fire, so I don't know what's up.[/I][/I]
     
  9. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    @EternalAmbiguity - Your OnConversationLineCancelled method is being called, as proved by the Debug.Log message. I haven't stepped through the code line by line, but I suspect the issue is in
    Code (csharp):
    1. timer.UpdateTimeLeft(duration);
    The duration should be a normalized value in the range 0 to 1, where 1 indicate a full timer bar, 0.5 is half-full, and 0 is empty.

    Try adding some more debug lines -- for example, reporting the value of duration before you call timer.UpdateTimeLeft -- or step through with the debugger.
     
  10. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay. There are a number of issues here. The first one to address...

    I changed the beginning of the Line Cancel method to this:

    Code (csharp):
    1. void OnConversationLineCancelled(Subtitle subtitle)
    2.     {
    3.         Debug.Log("Firing Line Cancel Method...");
    4.         Response[] responses = DialogueManager.CurrentConversationState.pcResponses;
    5.         Debug.Log("Response Length: " + responses.Length);
    6.         for (int i = 0; i < responses.Length; i++)
    7.         {
    I found that the second time I skip, there were no responses in pcResponses. These are apparently getting deleted somewhere after the first skip. Do you have any idea where that might be?
     
  11. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    What is the state of the conversation the second time you skip? It's possible that, at that point, the current node doesn't link to any PC responses.

    In your custom InterjectableDialogueUI script, you're also holding onto older responses that don't directly link from the current node. You can check if the variable showingInterjectableResponses is true. If so, also loop through the responses in dialogue.responseMenu.instantiatedButtons. This is a list of GameObjects, so you'll want to get the UnityUIResponseButton component on each, and reference its response property to get the actual Response.
     
    EternalAmbiguity likes this.
  12. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Thanks, this works.

    The main problem is the way the UnityUITimer is set up.

    Code (csharp):
    1. private IEnumerator Countdown(float duration, System.Action timeoutHandler) {
    2.    float startTime = DialogueTime.time;
    3.             trueStartTime = startTime;
    4.    float endTime = startTime + duration;
    5.    while (DialogueTime.time < endTime) {
    6.     float elapsed = DialogueTime.time - startTime;
    7.                 UpdateTimeLeft(Mathf.Clamp(1 - (elapsed / duration), 0, 1));
    8.     yield return null;
    9.    }
    10.    if (timeoutHandler != null) timeoutHandler();
    11.   }
    As you can see, it's using time to determine how the slider decreases. However, skipping forward in dialogue is obviously not going to make time itself skip forward.

    I'm not sure, but I think that to "fix" this I'm going to need to do something like make "elapsed" public, determine the amount of time we'd naturally have "spent" on the skipped entry, then subtract that from "elapsed."

    If you have any better ideas feel free to share, but I'll try this out.

    Edit: That didn't work, but I got it working.

    Reason that didn't work was because it was continually resetting elapsed in the coroutine.

    I got it working by making a new coroutine that takes the initial duration and incrementally decreases it while doing a "waitforseconds." Then lowered that value outside in the TimedResponseButton script. See below:

    Code (csharp):
    1.  
    2.         private float fullTime;
    3.         public float duration;
    4.         /// <summary>
    5.         /// Called by the response menu. Starts the timer. Each tick, the UpdateTimeLeft
    6.         /// method is called.
    7.         /// </summary>
    8.         /// <param name="duration">Duration in seconds.</param>
    9.         /// <param name="timeoutHandler">Handler to invoke if the timer reaches zero.</param>
    10.         public virtual void StartCountdown(float incduration, System.Action timeoutHandler) {
    11.             duration = incduration;
    12.             fullTime = incduration;
    13.             StartCoroutine(TimedCountdown(timeoutHandler));
    14.   }
    15.         private IEnumerator TimedCountdown(System.Action timeoutHandler)
    16.         {
    17.             while(Mathf.Approximately(0, duration) == false)
    18.             {
    19.                 Debug.Log("timeremaining: " + Mathf.Clamp((duration / fullTime), 0, 1));
    20.                 UpdateTimeLeft(Mathf.Clamp((duration / fullTime), 0, 1));
    21.                 duration -= 0.01f;
    22.                 yield return new WaitForSecondsRealtime(0.01f);
    23.             }
    24.                 if (timeoutHandler != null) timeoutHandler();
    25.         }
    And then in the TimedResponseButton script:

    Code (csharp):
    1. public void UpdateTimer(float skip, GameObjectTimeoutDelegate timeoutHandler)
    2.     {
    3.         var timer = GetComponentInChildren<UnityUITimer>();
    4.         if (timer == null)
    5.         {
    6.             // Make sure we have a timer, on a slider if one exists:
    7.             var slider = GetComponentInChildren<UnityEngine.UI.Slider>();
    8.             if (slider != null)
    9.             {
    10.                 timer = slider.gameObject.AddComponent<UnityUITimer>();
    11.             }
    12.             else
    13.             {
    14.                 timer = this.gameObject.AddComponent<UnityUITimer>();
    15.             }
    16.         }
    17.         if (timer != null)
    18.         {
    19.             timer.duration = (timer.duration - skip);
    20.         }
    21.         this.timeoutHandler = timeoutHandler;
    22.     }
    I suspect that behind the scenes this is a little less accurate because the coroutine will take some amount of time to run, which isn't accounted for in the blanket "duration -= 0.01f," but I doubt the difference will be super significant.
     
    Last edited: Mar 3, 2018
  13. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    @EternalAmbiguity - Glad you got it working. I was going to offer to add something to UnityUITimer, but at this point since your solution works we should leave it at that.
     
  14. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Well, it seems I spoke too soon. For some reason this only goes to like half the slider length. I apologize cause in my mind this just feels like a jumbled mess, but...

    Code (csharp):
    1.  
    2. private IEnumerator TimedCountdown(System.Action timeoutHandler)
    3.         {
    4.             float prevTime = DialogueTime.time;
    5.             while(Mathf.Approximately(0, duration) == false)
    6.             {
    7.                 float nowTime = DialogueTime.time;
    8.                 float timeDiff = nowTime - prevTime;
    9.                 //duration -= timeDiff;
    10.                 duration -= 0.01f;
    11.                 Debug.Log("Full: " + fullTime + " Current: " + duration);
    12.                 prevTime = nowTime;
    13.                 UpdateTimeLeft(Mathf.Clamp((duration / fullTime), 0, 1));
    14.                 yield return new WaitForSeconds(0.01f);
    15.             }
    16.                 if (timeoutHandler != null) timeoutHandler();
    17.         }
    This is what I've put in now. For the first dialogue (with no skipping) this ends with a Console line of: "Full: 19.16667 Current: 10.01646." I have not the slightest clue why it isn't going the full length. Or rather, why it seems that the "ComputeDuration" method over in the Interjectable script is giving me double the amount of time it would take.

    If I comment out that duration "decrementor" and go with the "timeDiff" version, I finish with a Console line of "Full: 19.16667 Current: 3.859394," aka some crazy number I truly have no idea how it came up with.

    So it's not working correctly. Sorry, but if you have any ideas I'd love to hear them.
     
  15. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    @EternalAmbiguity - Tomorrow morning, I'll add an UpdateTimeLeft method or something similar. That should make it a lot easier. You should just be able to call that method, and the timer should jump down accordingly.
     
    EternalAmbiguity likes this.
  16. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Many thanks. Appreciate the help.
     
  17. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    @EternalAmbiguity - I added a SkipTime(float) method to TimedResponseButton, which just calls the UnityUITimer's SkipTime(float) method, so you can call either one. For example, to drop button 0's timer by 2 seconds:

    Code (csharp):
    1. dialogue.responseMenu.instantiatedButtons[0].GetComponent<TimedResponseButton>().SkipTime(2);
    Here's the updated package:

    InterjectableResponsesExample_2018-03-04.unitypackage

    And these are the scripts:

    TimedResponseButton.cs
    Code (csharp):
    1. using UnityEngine;
    2. using PixelCrushers.DialogueSystem;
    3.  
    4. // InterjectableDialogueUI automatically adds this script to
    5. // response buttons that have non-zero Timeout fields. On
    6. // timeout, it hides the button and calls a handler method.
    7. public class TimedResponseButton : MonoBehaviour
    8. {
    9.  
    10.     private System.Action hideHandler; // Call this method if timeout duration is reached.
    11.  
    12.     private UnityUITimer timer;
    13.  
    14.     public void StartTimer(float duration, System.Action hideHandler)
    15.     {
    16.         timer = GetComponentInChildren<UnityUITimer>();
    17.         if (timer == null)
    18.         {
    19.             // Make sure we have a timer, on a slider if one exists:
    20.             var slider = GetComponentInChildren<UnityEngine.UI.Slider>();
    21.             if (slider != null)
    22.             {
    23.                 timer = slider.gameObject.AddComponent<UnityUITimer>();
    24.             }
    25.             else
    26.             {
    27.                 timer = this.gameObject.AddComponent<UnityUITimer>();
    28.             }
    29.         }
    30.         if (timer != null)
    31.         {
    32.             // Start the timer:
    33.             timer.StartCountdown(duration, OnTimeout);
    34.         }
    35.         this.hideHandler = hideHandler;
    36.     }
    37.  
    38.     public void SkipTime(float amountToSkip)
    39.     {
    40.         if (timer != null) timer.SkipTime(amountToSkip);
    41.     }
    42.  
    43.     private void OnTimeout()
    44.     {
    45.         // Hide the button and call the hideHandler:
    46.         this.gameObject.SetActive(false);
    47.         if (hideHandler != null) hideHandler();
    48.     }
    49. }


    UnityUITimer.cs
    Code (csharp):
    1. #if !(UNITY_4_3 || UNITY_4_5)
    2. using UnityEngine;
    3. using System;
    4. using System.Collections;
    5.  
    6. namespace PixelCrushers.DialogueSystem
    7. {
    8.  
    9.     /// <summary>
    10.     /// Basic slider-based timer for response menus.
    11.     /// </summary>
    12.     [AddComponentMenu("Dialogue System/UI/Unity UI/Dialogue/Unity UI Timer")]
    13.     public class UnityUITimer : MonoBehaviour
    14.     {
    15.  
    16.         private UnityEngine.UI.Slider slider = null;
    17.  
    18.         private float startTime; // When the timer started.
    19.  
    20.         public virtual void Awake()
    21.         {
    22.             slider = GetComponent<UnityEngine.UI.Slider>();
    23.         }
    24.  
    25.         /// <summary>
    26.         /// Called by the response menu. Starts the timer. Each tick, the UpdateTimeLeft
    27.         /// method is called.
    28.         /// </summary>
    29.         /// <param name="duration">Duration in seconds.</param>
    30.         /// <param name="timeoutHandler">Handler to invoke if the timer reaches zero.</param>
    31.         public virtual void StartCountdown(float duration, System.Action timeoutHandler)
    32.         {
    33.             StartCoroutine(Countdown(duration, timeoutHandler));
    34.         }
    35.  
    36.         private IEnumerator Countdown(float duration, System.Action timeoutHandler)
    37.         {
    38.             startTime = DialogueTime.time;
    39.             float endTime = startTime + duration;
    40.             while (DialogueTime.time < endTime)
    41.             {
    42.                 float elapsed = DialogueTime.time - startTime;
    43.                 UpdateTimeLeft(Mathf.Clamp(1 - (elapsed / duration), 0, 1));
    44.                 yield return null;
    45.             }
    46.             if (timeoutHandler != null) timeoutHandler();
    47.         }
    48.  
    49.         /// <summary>
    50.         /// Adjusts the amount of time left.
    51.         /// </summary>
    52.         /// <param name="amountToSkip">Seconds to fast-forward the timer (or rewind the timer if negative).</param>
    53.         public void SkipTime(float amountToSkip)
    54.         {
    55.             startTime -= amountToSkip;
    56.         }
    57.  
    58.         /// <summary>
    59.         /// Called each tick to update the timer display. The default method updates a UI slider.
    60.         /// </summary>
    61.         /// <param name="normalizedTimeLeft">1 at the start, 0 when the timer times out.</param>
    62.         public virtual void UpdateTimeLeft(float normalizedTimeLeft)
    63.         {
    64.             if (slider == null) return;
    65.             slider.value = normalizedTimeLeft;
    66.         }
    67.  
    68.         public virtual void OnDisable()
    69.         {
    70.             StopAllCoroutines();
    71.         }
    72.  
    73.     }
    74. }#endif
     
    EternalAmbiguity likes this.
  18. PicturesInDark

    PicturesInDark

    Joined:
    Jun 13, 2013
    Posts:
    89
    Hi everybody: I present you my videogame with Dialogue System for Dialogs and for save with UFPS

    In the first video you can see Dialogue System working at 3:11



    and in the other videos showing alerts


     
    TonyLi likes this.
  19. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144

    I'm a little loathe to migrate completely, given all the unique stuff I have in my InterjectableDialogue script, so I just copied these two scripts over to replace the old ones. However, it seems there are differences in the InterjectableDialogue script.

    Unrelated to the above: it looks like your package still uses the old UnityUITimer script. Made a new project, imported the 1_7_7_1 eval DS, then this package, and I got an error in TimedResponseButton.cs about UnityUITimer.cs not having the appropriate method. Checked it and it's the old one, doesn't have your SkipTime method.

    Returning to where I was before, the Interjectable Dialogue stuff is strange. One thing I noticed is that while my version has this method which gets called in the StartResponseTimer method:

    Code (csharp):
    1. public void OnHideResponse(GameObject button)
    2.     {
    3.         if (dialogue.responseMenu.instantiatedButtons.Contains(button))
    4.         {
    5.             // If this is the last button, hide the response menu.
    6.             if (dialogue.responseMenu.instantiatedButtons.Count == 1)
    7.             {
    8.                 HideAllResponses();
    9.             }
    10.             else
    11.             {
    12.                 var wasSelected = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject == button;
    13.                 // Remove and destroy the button:
    14.                 dialogue.responseMenu.instantiatedButtons.Remove(button);
    15.                 Destroy(button);
    16.                 // If this was the selected button, select another one:
    17.                 if (wasSelected)
    18.                 {
    19.                     UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(dialogue.responseMenu.instantiatedButtons[0]);
    20.                 }
    21.                 // Redo navigation:
    22.                 SetupResponseButtonNavigation();
    23.             }
    24.         }
    25.     }
    the new version instead has this method:

    Code (csharp):
    1. public void OnHidResponse()
    2.     {
    3.         // If a response button just disappeared, update the count.
    4.         // If none are left, hide the response menu.
    5.         numVisibleResponses--;
    6.         if (numVisibleResponses <= 0)
    7.         {
    8.             showingInterjectableResponses = false;
    9.             HideResponses();
    10.         }
    11.     }
    I've switched to using the new version. I'm unsure of what differences there would be between them. There don't seem to be any in my use thus far.

    I found the source of my slider problem, however. I think I mentioned a little while ago that I added a float to the Interjectable Dialogue script to be used to increase the duration of subtitles. Well I stopped using that and went with your suggestion of changing subtitle speed, but I neglected to remove the variable for some reason. It was set to 1. So it seems an additional second was being left on the slider when the responses were removed. So that was my fault. Removed the variable, and the slider's working properly now. That aside, I'm glad I have an "official" solution for skipping ahead rather than my jury-rigged version. Thanks.


    Edit: Sorry, but more questions. More minor stuff I think.

    When I pause the game, the dialogue system does indeed pause. However, the dialogue UI is still visible. I tried modifying the CanvasGroup on the Dialogue Panel GameObject, but that merely makes it un-interactable--the subtitle and any responses are still visible.

    Any idea how to hide those completely?
     
    Last edited: Mar 7, 2018
  20. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    It may depend on your dialogue UI. The default generic dialogue UI's Dialogue Panel has an Animator. This Animator controls the CanvasGroup's alpha value. You'll want to change its state when pausing. Something like:

    Code (csharp):
    1. // Pause:
    2. var animator = (DialogueManager.DialogueUI as UnityUIDialogueUI).dialogue.panel.GetComponent<Animator>();
    3. animator.Play("Hide");
    4.  
    5. // Resume:
    6. animator.Play("Show");
     
    EternalAmbiguity likes this.
  21. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Good to know. Thanks.
     
  22. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Someone asked how to set up "hub" style conversations as seen in games like the Ultima series. The requirements are:
    1. Return to a main hub, while still showing the NPC's previous text.
    2. Show a summary in the player response menu, and expand it when showing the PC's full text.
    So it might look like this:

    (The background and portrait graphics are from Ultima 7 and are used only as an example of how to set up this kind of dialogue. Pixel Crushers has no copyright or ownership of them.)

    The summary menu text is easy. Put the summary in the Menu Text field and the full text in the Dialogue Text field.

    To set up a hub node, create an empty node and tick the Group checkbox. Then link the ends of your dialogue branches back to this hub node, as shown here:


    Here's a playable example scene: UltimaVIIStyleExample_2018-03-07.unitypackage
    (Please note once again that the background and portrait graphics are from Ultima 7 and are used only as an example of how to set up this kind of dialogue. Pixel Crushers has no copyright or ownership of them.)
     
    TeagansDad likes this.
  23. TeagansDad

    TeagansDad

    Joined:
    Nov 17, 2012
    Posts:
    957
    Thanks, @TonyLi for the usual amazing support!
     
  24. Alic

    Alic

    Joined:
    Aug 6, 2013
    Posts:
    137
    Hey Tony, The Dialogue system is awesome! I ran into a small annoyance where Run3DRaycast on the selector class can't be usefully overridden because there is no way to invoke SelectedUsableObject from a subclass, which is done within the Ray casting methods on the base selector class. A quick fix to this would be to add to a protected method to the selector class which simply invokes the event if it's not null:

    Code (CSharp):
    1.        protected void OnSelectedUsableObject(Usable usable)
    2.         {
    3.             if (SelectedUsableObject != null) SelectedUsableObject(usable);
    4.         }
    (Or optionally don't include a usable parameter.)

    I was trying to implement a top-down RPG selector which works like the proximity selector but only on usable targets you're currently facing, and I can't see any way to do this without branching the code currently.
     
  25. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @Alic - You can count on this method in the next release:
    Code (csharp):
    1. protected void OnSelectedUsableObject(Usable usable)
    2. {
    3.     if (SelectedUsableObject != null) SelectedUsableObject(usable);
    4.     onSelectedUsable.Invoke(usable);
    5. }
    And you can download the updated Selector.cs right now here: Selector_2018-03-08.unitypackage

    Alternatively, you could override the entire Run3DRaycast() method.

    Or you could use ProximitySelector. Instead of putting the trigger collider on the usable target (e.g., NPC), add the trigger collider as a child of the player. Position it in front of the player. Then add a ProximitySelector, and assign the main player GameObject to the Actor Transform field.
     
    TeagansDad likes this.
  26. Alic

    Alic

    Joined:
    Aug 6, 2013
    Posts:
    137
    Ha, I had a feeling there was a simple solution I wasn't thinking of. I'll probably try out the proximity selector solution and see how the feeling it gives compares to what I just built. Thanks for your help Tony!

    Quick note, overriding the entire Run3DRaycast() method Is exactly what I wanted to do, but I can't see any way to invoke SelectedUsableObject in a subclass unless you wrap it in a method. (C sharp doesn't allow subclasses to invoke events from their base class, has something to do with the underlying delegate being private I think.) Am I misunderstanding what you meant by override the entire method?
     
  27. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I took a look through the documentation, but I couldn't really find anything on adding audio. How do I do that? Do I need to put the path and filename in the "Audio Files" field? Or is there a simple button or drag and drop somewhere?
     
  28. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    No, you make a good point. If you were to override the entire method, you'd have to introduce a new event, which would not be ideal. That's why I took your suggestion and put the invocation in a separate protected method. :)

    Put the audio file in a Resources folder, or in a subfolder nested inside a Resources folder. (You can also put it in an AssetBundle if you prefer.)

    If you're using patch 1.7.7.2-p20180301 available on the customer download page, you can simply drag the audio clip onto the dialogue entry node's Sequence field. This feature will also be in the upcoming 1.7.7.3.

    If you're using 1.7.7.2 (no patch) or earlier, you'll need to manually type the AudioWait() sequencer command:

    AudioWait(myClip)

    There are alternative commands, too, such as Audio() (which plays without waiting for the clip to finish), Voice() (plays an animation in conjunction), SALSA() (plays through Crazy Minnow Studios' SALSA asset), etc.
     
    EternalAmbiguity likes this.
  29. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Does the patch work with the evaluation version? That's what I'm using for the "Dialogue Test" project.

    Additionally, how does this interface with the "subtitle speed" in terms of length of time the response will be visible? If the length of time for the audio is shorter than the subtitle speed, which determines how long the response will be visible? What about the opposite? And if it's the latter, with audio taking longer and thereby making the response stay longer, how will this affect the timer (which as I recall is based only on subtitle length & speed)?
     
  30. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi,
    No, not with the current evaluation version. But the feature will be in the upcoming 1.7.7.3 evaluation version. It's a new feature request that someone suggested after 1.7.7.2 was released. It's really just a convenience so you don't have to manually type "AudioWait(clipName)".

    This is the default behavior: The subtitle stays onscreen for the duration of the dialogue entry node's Sequence. The Sequence lasts until all sequencer commands are done.

    The default sequence (defined on the Dialogue Manager) is Delay({{end}}). This references a special keyword {{end}} whose value is based on the subtitle text length. So this command runs for a duration based on the text length.

    The AudioWait() command runs for the length of the audio clip.

    Since you're using a timer based on the text length, try this Sequence:

    Delay({{end}});
    AudioWait(clipName)

    It will wait for the text length or the audio clip, whichever is longer.

    This part of your question is tougher:

    Sequences can last an unspecified duration. For example, say this dialogue entry is part of a tutorial in an RTS:
    • Dialogue Text: "Build a supply depot."
    • Sequence: WaitForMessage(BuiltDepot)
    This entry's sequence uses the WaitForMessage() sequencer command. This command waits until it receives a sequencer message, typically when a script calls the Sequencer.Method() function. We can't know ahead of time how long the player will take to build a supply depot, so we can't determine the sequence duration.

    It's a little easier in your project, if you know you'll only use the Delay and AudioWait commands above. Your timer code already computes the value of {{end}}. You'll just need to extend it to grab the clip name from the AudioWait command. (This will require a little string parsing of subtitle.sequence.) Then load that audio clip using Resources.Load(), and check its length. Set your timer to the maximum between {{end}} and the audio clip length.
     
  31. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay thanks, I'll take a look at this later.
     
  32. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    FYI - Gamasutra Q&A on Hiring, Casting, and Directing Voice Actors

    Gamasutra recently published an excellent Q&A titled "An indie dev's guide to hiring, casting, and directing voice actors" with indie developer Ashe Thurman of Pixels and Pins.

    It covers much more than hiring, casting, and directing, though. She provides some great advice for planning how much voice acting to add to your game, how to budget it, how to market it to players, how to prepare scripts, and much more. It's really solid advice. She also provide good resources where you can find quality voice actors. If you're contemplating adding voice acting to your game, it's well worth your time to read.

    In the Dialogue System, you can add voice acting to a line using the AudioWait() sequencer command or similar third party commands such as SALSA(), LipSync(), or FaceFX(). In the upcoming version 1.7.7.3, you can just drag and drop audio files onto the Sequence field to automatically generate the command for you. If you have a lot of audio, you can use entrytags to simplify management.

    If you haven't recorded audio yet, you can use RT-Voice support to automatically generate text-to-speech as a stand-in. It's a nice way to get a feel for the timing of your conversations.
     
    EternalAmbiguity and flashframe like this.
  33. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Dialogue System for Unity 1.7.7.3 Released!

    Version 1.7.7.3 is now available on the Unity Asset Store!

    This update adds some handy usability enhancements to the Dialogue Editor, among other miscellaneous improvements. In particular, I want to point out:
    • Drag and drop audio files onto Sequence fields to automatically set up Audio(), AudioWait(), and/or SALSA() commands without any typing. (If you have a lot of audio, I still recommend entrytags.)
    • Copy and paste selections of dialogue entry nodes across different conversations.
    • Filter actors, quests, variables, etc., the same way as Unity's Project and Hierarchy views.
    • Monitor the state of all quests at runtime on the Watches tab by selecting Menu > Add All Quests.


    Release Notes

    Core
    • Dialogue Editor:
      • Added drag-and-drop of audio files to Sequence fields.
      • Added node copy/paste.
      • Added zoom at cursor.
      • Improved snap to grid behavior.
      • Added filters to Actor, Items/Quests, Locations, and Variables tabs.
      • Added quest & quest entry dropdowns to Watches.
      • Fixed Watches bug that reported invalid cached info.
      • Fixed node editor context menu placement when canvas is zoomed.
    • Changed: Dialogue Manager’s Show Alerts During Conversations checkbox is now respected by Alert Trigger and ShowAlert() function, not just Variable[“Alert”].
    • Added: DialogueTime dropdown to Dialogue Manager.
    • Added: Sequence field to Condition Observer.
    • Added: [Actor] popup attribute for scripts.
    • Added: UnityUIDialogueUI option to bind autonumber keys to numpad.
    • Improved: QuestStateListeners now automatically add QuestStateDispatcher to Dialogue Manager if missing.
    • Fixed: Unity UI Typewriter Effect now handles uppercase rich text codes.
    • Fixed: Override Actor Name’s Unique ID button wasn’t marking scene dirty.
    Third Party Support
    • Adventure Creator: Added Action mode dropdown to Conversation action to Start Conversation (default) or Stop Conversation.
    • articy:draft: Added support for converting strips as subtables.
    • NGUI: Quest tracker now manually calls Reposition in case table doesn’t auto-reposition.
    • plyGame: Updated for plyGame 3.1.4c. ConversationController now has option to set NPC to Idle-Stay instead of disabling plyGame control. Updated save/load system integration.
    • TextMesh Pro Support: Fixed typewriter effect to allow typing to start even if it hasn’t run its Awake method yet; fixed bug in quest tracker to respect value of Show Completed Entry Text checkbox.
    • Third Person Controller (Opsive): Added AbilityStartEvent.
     
  34. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Patch 1.7.7.3-p20180321 Available

    A small patch is available on the Pixel Crushers customer download site. It addresses two issues:
    • Fixed: Conversation override for Input Settings > Always Force Response Menu checkbox is now observed.
    • Fixed: Articy Converter now handles empty strips.
    If you're not affected by those issues, there's no need to import the patch. If you need access to the customer download site, please PM me your Asset Store invoice number.
     
  35. DevStack

    DevStack

    Joined:
    Oct 7, 2014
    Posts:
    4
    Hello TonyLi,

    I'm still reading up on this asset, but I had a quick question for you (or anyone who knows).

    Let's say I'm building a 2D turn-based game and at the start of each turn I want to programatically bring up a dialog box with choices from a randomly selected persona (so basically build out NPC personas with dialog possibilities). Will this asset help me with this?

    Thanks!
     
  36. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @DevStack - Yes. Turn-based games like Brave the Lonely do this. To programmatically bring up a dialogue box, you'd call DialogueManager.StartConversation(). You could pass the name of a prewritten conversation (which could use variables to display dynamic content) or create a conversation at runtime and pass its name. If you have any questions about setting that up, just let me know.
     
    DevStack likes this.
  37. DevStack

    DevStack

    Joined:
    Oct 7, 2014
    Posts:
    4
    Thanks for the reply @TonyLi . That sounds pretty good. Thinking I'll get this soon as I have some time to start messing with it. :)
     
  38. Alic

    Alic

    Joined:
    Aug 6, 2013
    Posts:
    137
    Hey Tony, did I read somewhere that you have a version 2.0 of the dialogue system planned in the near future? I may have just imagined it. If it's true, have you posted a description anywhere of the main changes/Improvements planned?
     
  39. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @Alic - Yes, it's planned for late spring/early summer. Version 2.0 won't add new features except for a "Standard Dialogue UI" that will supercede Unity UI Dialogue UI and Text Mesh Pro Dialogue UI. Instead, it will streamline and simplify the workflow. I'll maintain a public Trello board so you can see what's planned and what's implemented. I haven't made the Trello public yet, but I will soon.
     
    hopeful and TeagansDad like this.
  40. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    ^ I'm always open to suggestions! In particular, for version 2.0, what are your pain points? What took too long to understand? What workflows aren't as efficient as they could be?

    Dialogue UIs are definitely at or near the top. The Dialogue System's UI functionality has grown over the years to handle everything from cinematic FPSes, to turn-based RPGs, to visual novels, to audio-only games, and more. The Standard Dialogue UI will wrap all that functionality into a more intuitive package that should be easier to comprehend and customize.

    Another common point of confusion is the way actor GameObjects are associated with conversations. Unlike previous versions, version 2.0 will introduce some changes to the way things work. By default, in version 2.0 the dialogue UI will show the actor's name in the dialogue database, not the name of the GameObject passed to the conversation trigger. This is the opposite of the way it currently works, but I think it will be more intuitive.
     
    TeagansDad likes this.
  41. MrG

    MrG

    Joined:
    Oct 6, 2012
    Posts:
    368
    I admit I haven't dug in too deep, but wondered if this sort of dialog was possible, where certain words in the text as shown in blue (ignore the red underlines) can link to additional content. I always thought this was a nice way to present a "TL;DR" while giving the player access to more detail if they need/want it.

     
  42. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Hi @MrG - Back in 2016, someone asked the same question and coincidentally posted the exact same Morrowind screenshot. :) I posted an example scene made in the Dialogue System, and using TextMesh Pro for the clickable link functionality, in this post. I haven't touched it since 2016, so if you notice any issues (e.g., compatibility with the latest version of Unity), just let me know.
     
    TeagansDad likes this.
  43. MrG

    MrG

    Joined:
    Oct 6, 2012
    Posts:
    368
    HA! That's hilarious! One question...I see the key words are also in the menu on the right...is that required, or would the two mechanics work independently?
     
  44. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    They can work independently. The right-hand keywords were part of the request back in 2016, so I included it in the example scene, but you can certainly skip them if you want.
     
    MrG likes this.
  45. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Updated RT-Voice Support Package Available

    The latest version of RT-Voice has an API change that requires an updated support package for the Dialogue System. If you use RT-Voice or RT-Voice Pro, you can download the updated package from the Dialogue System Extras page. The updated support package will also be included in the next full release.
     
  46. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I really do have GDADD. Just spent two weeks on a totally new project ignoring this one...

    Anyway. Reporting that this works. I changed the sequence for the Dialogue Manager. Then modified the InterjectableDialogueUI script like so (first part is within the ComputeDurationUntilHideResponseWithID method):

    Code (csharp):
    1. while (!done && safeguard < 999)
    2.         {
    3.             float subtitleDuration = 0;
    4.             float audioDuration = 0;
    5.             var text = state.subtitle.formattedText.text;
    and

    Code (csharp):
    1. if (!done)
    2.             {
    3.                 // Otherwise add the node's duration and progress to the next node:
    4.                 subtitleDuration = GetDefaultSubtitleDuration(text);
    5.                 audioDuration = GetAudioDuration(state);
    6.                 duration += Mathf.Max(subtitleDuration, audioDuration);
    7.                 state = model.GetState(state.FirstNPCResponse.destinationEntry);
    8.                 //Debug.Log("Moving down a node...");
    9.             }
    Then the new method:

    Code (csharp):
    1. private float GetAudioDuration(ConversationState state)
    2.     {
    3.         string audioString = state.subtitle.dialogueEntry.Sequence;
    4.         string [] strings = audioString.Split(',');
    5.         string relevantString = strings[0].Remove(0, 10);
    6.         AudioClip audioFile = Resources.Load(relevantString) as AudioClip;
    7.         //Debug.Log("Length is: " + audioFile.length);
    8.         return audioFile.length;
    9.     }
    Works perfectly, lengthening the timer duration and ending it at the appropriate time (maybe like a 10th of a second faster than I expected, but I'll double-check through the whole conversation and see if it's a problem; I don't think it is).

    If you see any place there for optimization, please let me know.
     
  47. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Looks fine to me. I don't see any major performance gotchas. The GetAudioDuration method does make the assumption that the Sequence is in a specific format, but as long as you can guarantee that format I don't see a problem with it.
     
    EternalAmbiguity likes this.
  48. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I added a couple if statements to double-check that we've got anything in the sequencer field, then if we're actually finding audio files. For instance, a couple of times I have just "..." as the subtitle, and I didn't make audio for that. I get an error "Dialogue System: Sequencer: AudioWait() command: Clip 'clipName' wasn't found." and I imagine that's why (or is it literally because I don't have anything named clipName? I only have "AudioWait(clipName)" on the Dialogue Manager GameObject, all dialogue entries have exact names and give no such errors), but in such a case the GetAudioDuration method returns 0 and exits, so it goes with the subtitle/sequence length.

    Edit:
    Code (csharp):
    1. private float GetAudioDuration(ConversationState state)
    2.     {
    3.         string audioString = state.subtitle.dialogueEntry.Sequence;
    4.         if(audioString.Length > 0)
    5.         {
    6.             string[] strings = audioString.Split(',');
    7.             string relevantString = strings[0].Remove(0, 10);
    8.             if (Resources.Load(relevantString) == null)
    9.             {
    10.                 return 0;
    11.             }
    12.             else
    13.             {
    14.                 AudioClip audioFile = Resources.Load(relevantString) as AudioClip;
    15.                 //Debug.Log("Length is: " + audioFile.length);
    16.                 return audioFile.length;
    17.             }
    18.         }
    19.         else
    20.         {
    21.             return 0;
    22.         }
    23.     }
    I could probably extend it to where if it doesn't find a file with the specific name, it jumps to one of the other possible entry formats, only quitting if it can't find the file over all (three?) sequencer field Audio prompts. but it works for now and I really don't see why I would change what I'm entering in the sequencer field (so it wouldn't encounter these other situations), so I'll leave it as is and tackle that later if it comes up.

    Many, many thanks again for all of your help with this. I'll bounce over to the other side, and if others have suggestions for improvement I may be back about this specific implementation, but outside of that I can't think of anything else I really need to improve right now.

    Have not wound up doing the Lua or whatever check for if the dialogue entry has been spoken yet, as opposed to all of my booleans, but that's mainly because I was mostly done adding the booleans when I mentioned it. When (if? Nothin' like GDADD...) I return to this concept for a new project I'll tackle that. But thanks again.
     
    Last edited: Mar 31, 2018
  49. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    @EternalAmbiguity - Glad to help! It was fun working on this with you. It gave me a lot to think about regarding innovation in interactive dialogue.
     
    EternalAmbiguity likes this.
  50. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,694
    Dialogue System 1.7.7.3 Patch 20180406 Available

    Patch 20180406 is available on the Pixel Crushers customer download site. If you need access, please PM me your Asset Store invoice number. The improvements in this patch will also be in the upcoming version 1.8.0. They include:
    • [04/06] Dialogue Editor:
      • Conditions wizard: Added "between" option to require that a value is between min and max values.
      • Improved positioning of pasted nodes & zooming with mouse wheel.
      • Added horizontal option when adding new nodes & auto-arranging.
    • [04/06] Unity UI: Updated animated portraits to support override controls.
    • [04/06] articy:draft: Fixed handling of empty strips.
    • [04/06] RT-Voice: Updated for RT-Voice 2.9.6.
    • [03/26] Improved: Added checkbox (default is ticked) to initialize variables when loading saved games that weren't present when game was saved.
    • [03/21] Fixed: Now observes conversation override for Input Settings > Always Force Response Menu checkbox.
    • [03/21] Fixed: Articy Converter now handles empty strips.