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,697
    Yes, it's feasible. When discussing ideas like this, I try to consider whether they can be generalized and incorporated into the Dialogue System for the benefit of other users, too. In this first pass at least, I'll leave this one to you.

    No. You might even be able to use Visual Studio's refactoring features to automate a lot of the changes from Response[] to List<Response>. Switching from DLL to source code is a bit of a pain, however. It's something that will be addressed in Dialogue System 2.0.

    It might be easier to simply maintain a separate List<Response> in your dialogue UI subclass, and convert it to an array when calling ShowResponses.
     
    EternalAmbiguity likes this.
  2. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    The DLLs were something I was wondering about. I haven't worked with them much at all, and I opened one with Notepad++ to be greeted by gibberish. I'll take a look and see about doing this.

    So in this case, I maaay be able to take care of this problem without you having to make that example for me. I may be forgetting some other problem I highlighted (or you may know of another one I haven't addressed yet), if that's the case let me know, but for now don't worry about it and I'll work on making these changes.
     
    Last edited: Dec 12, 2017
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Sounds good.

    Should you decide to replace the precompiled DLLs with the raw source code, the instructions and caveats are here. The newer assets, like Love/Hate and Quest Machine, don't have the same source/DLL GUID restriction that the Dialogue System currently does. In them, you can switch back and forth fairly effortlessly. With the Dialogue System, other improvements have taken priority, so that's been pushed back to the version 2.0 roadmap.
     
    EternalAmbiguity likes this.
  4. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Apologies for more comments (feel free to disregard until you have free time). But I've never really messed with DLLs before, so I don't want to mess something up here.


    Okay, I've been poring through the DialogueSystem DLL. Decompiled it with dotPeek, then moved it to a VS solution to look at and edit.

    I've tentatively identified the relevant array as pcResponses in the ConversationState class. Examining all references, it looks like responses get added to it in the class ConversationModel, in the method EvaluateLinksAtPriority (technically it's in UpdateResponses, but a chain of method calls within that leads to the E.L.A.P. method).

    This seems to be the key reason why responses get reset with each new PC response entry (and why the [Hide Responses] method doesn't actually remove the responses from the array, which I noticed recently) - the group of methods in ConversationModel actually creates a new [list, not array, which is interesting--seems it's already using a list in the backend at one key point], populates that with currently available responses, then reassigns the referenced array to that list.

    So it seems the backend resets the data structure holding the responses when a new response appears--and completely ignores the data structure when clicking or hiding responses.

    Things to do:

    Replace the array with a list. Probably going to remove the separate list in ConversationModel and just have it affect the new list from ConversationState (or create a list, pull from ConversationState, modify, change ConversationState list to the modified one. Whatever).

    Rework the "hide" or "click" messages to remove elements from the list instead of automatically (and only) hiding the dialogue box.


    Please let me know if this seems reasonable.
     
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    I'm sorry I didn't make this clear before: The complete source code is included in Assets / Dialogue System / Scripts / SourceCode.unitypackage. You don't have to decompile the DLL. The original source code contains detailed comments, too, which you miss if you decompile the DLL.

    What you describe seems reasonable. However, I'd suggest taking a different approach, using the OnConversationLine and OnConversationResponseMenu script messages, or override methods in UnityUIDialogueUI, if possible. This way you won't lose your edits if you update the Dialogue System. Most changes to the Dialogue System these days affect the editor windows and third party support. But, as you'll see in my next post, occasional improvements are still sometimes made to the core engine code, too.
     
  6. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Dialogue System Patch 1.7.6_p20171211 Available

    Version 1.7.7 is just around the corner, but changes in the recently-released articy:draft 3.1.8 and Unity 2017.3b require immediate updates. Those updates are available on the Pixel Crushers customer download site in patch 20171211. If you need access, please PM me your Asset Store invoice number.

    The patch contents are below. Note that the articy converter window will be receiving additional improvements in version 1.7.7, but I haven't pulled those into the main release branch yet. This patch just adds articy:draft 3.1.8 support.

    Patch 20171211 Release Notes:
    • Updated for compatibility with Unity 2017.3.
    • Dialogue Editor improvement:
      • Moved “Link To:” dropdown items “Another conversation” & “New entry” to top of menu.
      • Added option to show node ID on nodes.
      • Removing field from template now gives option to remove field from all assets in database.
      • Conversation node editor now highlights incoming link arrows in a different color.
      • Releasing MMB no longer deselects node.
      • Fixed auto backup error message.
    • Localized text table editor: Added search bar.
    • Improved: Unity UI Dialogue UI uses Unity UI Typewriter Effect more effectively by calling it directly instead of relying on OnEnable.
    • Fixed: UnityUISelectorDisplay & OverrideUnityUIControls continue button weren’t syncing with Dialogue Manager after load game.
    • Fixed: Unity UI Quest Log Window slowdown when mashing Track toggle button.
    • Improved: UnityUITypewriterEffect now automatically adds audio source if necessary.
    • Changed: Unity UI JRPG prefab now shows player image and name with response menu.
    • Fixed: DialogueSystemController acting as singleton now calls Destroy instead of DestroyImmediate.
    • Added: QuestLog.StringToState delegate hook.
    • Fixed: Timeline() sequencer command no longer destroys GameObject unless it was instantiated for the command.
    • Improved: The {{end}} keyword is now available in bark sequences.
    • Fixed: Calling PersistentDataManager.ApplyData() with an empty string and SimStatus enabled now keeps SimStatus intact.
    • Fixed: Persistent Data Manager SimX bug.
    • Improved: Added option to save all conversation fields.
    • Improved: Persistent Active Data now has an option to check the condition on start, not just when applying persistent data.
    • Selector / ProximitySelector: Added UnityEvents.
    • NLua:
      • Optimized DialogueLua.Get/SetTableField.
      • Implemented compressed SimStatus in PersistentDataManager to match default LuaInterpreter.
      • Fixed Get/SetTableField and Get/SetVariable handle blank table element names (e.g., blank actor names) more gracefully.
    • [Third Party Support] articy:draft: Updated for articy:draft 3.1.8 compatibility.
    • [Third Party Support] CSV: Added additional error reporting to CSV Converter.
    • [Third Party Support] Corgi Platformer Engine: Updated for Corgi 1.4 & Inventory Engine support.
    • [Third Party Support] Customizable SciFi Holo Interface: Added support.
    • [Third Party Support] I2 Localization: Added support.
    • [Third Party Support] Ink: Updated for Unity 2017.2 compatibility.
    • [Third Party Support] Inventory Engine: Added support.
    • [Third Party Support] PlayMaker: Added actions to sync variables between PlayMaker and Lua.
    • [Third Party Support] Realistic FPS Prefab: Updated for RFPS 1.44.
    • [Third Party Support] RPG Kit: Updated for RPG Kit 3.1.8.
    • [Third Party Support] Rog: Added ReplaceActorSprite() Lua function.
    • [Third Party Support] TextMesh Pro: Improved dialogue UI animation transitions. Added TextMeshProSelectorDisplay component.
     
    BackwoodsGaming likes this.
  7. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I did notice that the source code is there. However, I shied away from unpacking it for use there because I didn't want to deal with having to reassign script references (weirdly lazy I know) (Edit: looks like I'll have to modify it there, because all of the Unity references in a standalone "solution" make Visual Studio very grumpy) (Double Edit: just realized the evaluation version doesn't contain source code. Going to pull it from a full version of the asset, just for testing purposes here).

    And keep in mind that I'm using the evaluation version, which doesn't seem to have changed recently, so it's not like (for this version at least) there are new features being added.

    I've changed all of the references thus far, so I'm about halfway there. I'll work on the click/hide methods next. If I can get it to work, I'll take a look at making everything more high-level (this may seem strange, but I feel like the more high-level abstract way will take me longer than editing the source code).

    (Triple Edit: import of source code from the full version of the asset has a number of errors, stuff like "Dialogue Manager does not contain a definition for GetButtonDown." I may go for the script message as mentioned...don't mind me, I'll figure it out)
     
    Last edited: Dec 12, 2017
  8. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I've got it working. Project is here.

    I returned to the InterjectableDialogueUI script. Basically made everything depend on a list of responses in that script - I assign the values to the "pcResponses" array. Added a couple if statements, one for adding dialogue responses and one for replacing responses (which removes old ones and starts anew). I also changed the single "hide response" statement to refer to ID. The previous method depended on a response's location in the "responseList." However, if the user selects one response or another, that gets removed and the other response may change its location in the list, meaning the previous way wouldn't work. Thus using ID.

    Next step is to incorporate a way to remove a group of responses (yet not all responses) at a time. Shouldn't be difficult.

    Question: Is it possible for the IDs to randomly change? What would be the best way to do this if the IDs are volatile in any way?


    Once that's working, I'd be curious if there's a way to get a timer for each specific dialogue response. That way the user can see how much time is left for each one. I imagine there's a timer somewhere in each dialogue entry (including the PC ones) that determines the length of time it'll be on screen (based on amount of text & text "reading" speed). So it's just adding those up. Might be cumbersome, though, to have it look forward to determine that time. Would likely belong in the statements for adding a response, perhaps with a separate UpdateRemainingTime method or something (to account for situations where the user selects one available option, going off on a side branch and thereby likely increasing the available time).

    Ah, but that's kind of a side issue. Important for the other thread but not as much so here.

    If you think I should change anything let me know.

    Edit: a couple things. First, having some difficulty with the variables. I have a condition that looks for "playerMorals" less than 3 (I changed it slightly from the uploaded project--the automatic number is now 1, and the condition is "less" 3 so Variable["playerMorals"] < 3). Unfortunately, it does not work. Not sure what's wrong. Something to do with it being the evaluation version, perhaps with a few bugs?

    Second: if you look at the project you'll notice that I have a bunch of bools, and frequently multiple checks related to different ways to get to that node. Makes me wonder...what about putting the conditions on the links themselves? That way a specific pathway has its conditions, while another has its own. They don't interact at all.
     
    Last edited: Dec 12, 2017
  9. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    I suspect this has to do with switching between different versions or something like that. As long as everything is version 1.7.6, it shouldn't report any errors.

    IDs remain static if you're using the Dialogue System's built-in Dialogue Editor window, Chat Mapper, or CSV import/export. However, if you import from articy:draft, Talkit, or Neverwinter Nights, it's possible that the IDs could change, since they get assigned at the time of import.

    When the Dialogue System plays a dialogue entry, it sets the sequencer keyword "{{end}}" to a value based on the length of the dialogue text and the Dialogue Manager's Subtitle Settings.

    But this only applies to the current dialogue entry. For multiple timers, you'd have to compute this yourself, or for even finer control you could add a custom Number field (e.g., "Timeout Duration") to each dialogue entry and set it to whatever value you want.

    There shouldn't be any bugs. (Famous last words, right? Up there with "hold my beer.") But it's the exact same code. The only difference is that when compiling the evaluation version a bit of extra code is compiled via "#if EVALUATION_VERSION" that adds a watermark.

    Here are a few suggestions to debug this:
    • At runtime, click on the Dialogue Editor window's Watches tab. Add a watch for playerMorals, and tell it to auto-update.
    • Or add a Lua Console to your scene. To check the value, press ~+L to open the Lua Console, and enter:
      return Variable["playerMorals"]
    • Or temporarily set the Dialogue Manager's Debug Level to Info. This will log a lot of info to the Unity editor's Console window, including whether each dialogue entry's Conditions evaluated true or false.

    You make a good point. But on the flip side you could see how someone might want to set a condition once on the dialogue entry, and then assume all links to that dialogue entry should check the condition on the entry (i.e., the way it works right now). However, if you want to accomplish something like what you describe, you can use group nodes. Insert a node between the other two, tick its Is Group checkbox, and add a condition.
     
    EternalAmbiguity likes this.
  10. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Good to know, thanks.

    Alright. What part of the Subtitle Settings? I'm looking at the class Subtitle and I don't really see anything that could be used for that.

    To do this I imagine I'll need to first check if a given dialogue text contains the keywords to close a response ([HideResponseID X] / [Hide Responses]). If so stop, if not move on. Then I'll need to apply any condition changes (this being "imaginary" somehow or otherwise not affecting the real conditions in the real dialogue traversal) during that jump. Then it should look at all outgoing links, check their conditions, then check if those are true. First one that's true, we move to that node, grab the time, add it to a time variable, and run the function again (recursive function? I just learned how to use 'em two days ago).

    Given all that, I'll need to parse the condition text field. I'll look through the code again to see where that's mentioned, but if you see this sometime soon, can you tell me where that might be/how it's done?

    Ah, I was linking to the other node first in the "Links To" section. It's working now.

    Got it, thanks. I'll take a look at that if this becomes untenable.
     
  11. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    {{end}} = Max( SubtitleSettings.MinSubtitleSeconds,
    Length(DialogueText) / SubtitleSettings.SubtitleCharsPerSecond )​

    To access a dialogue entry's Conditions and Script values: DialogueEntry.conditionsString and DialogueEntry.userScript. For example:
    Code (csharp):
    1. public class YourCustomDialogueUI : UnityUIDialogueUI
    2. {
    3.     public override void ShowSubtitle(Subtitle subtitle)
    4.     {
    5.         Debug.Log("Subtitle has these Conditions: " + subtitle.dialogueEntry.conditionsString);
    6.         base.ShowSubtitle(subtitle);
    7.     }
    8. }
     
    EternalAmbiguity likes this.
  12. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Good to know. I'll take a look at that next. The main thing I need with this is a specific example of parsing the conditions string, but I'll look for that in the code I guess.

    Having a very peculiar issue here. I've implemented the removal of multiple IDs at once, but when I get to that point in the dialogue "tree" Unity freezes, every time.

    Here's the script:

    Code (csharp):
    1. using UnityEngine;
    2. using PixelCrushers.DialogueSystem;
    3. using System.Collections.Generic;
    4.  
    5. /// <summary>
    6. /// Overrides UnityUIDialogueUI with handling for response menus that can occur while
    7. /// the NPC is talking (called interjectable responses in this example).
    8. /// </summary>
    9. public class InterjectableDialogueUI : UnityUIDialogueUI
    10. {
    11.  
    12.     // Temporary measure to allow for a global "subtitles" variable because I don't know how to reference it properly for my new method...
    13.     Subtitle newSub;
    14.     List<Response> responseList = new List<Response>();
    15.  
    16.     // Are we showing interjectable responses?
    17.     private bool showingInterjectableResponses = false;
    18.  
    19.     public override void Open()
    20.     {
    21.         base.Open();
    22.         showingInterjectableResponses = false; // Initialize.
    23.     }
    24.  
    25.     public override void ShowSubtitle(Subtitle subtitle)
    26.     {
    27.         //for (int i = 0; i < DialogueManager.CurrentConversationState.pcResponses.Length; i++)
    28.         //{
    29.         //    responseList.Add(DialogueManager.CurrentConversationState.pcResponses[i]);
    30.         //}
    31.         newSub = subtitle;
    32.  
    33.         // Add the newest group of responses to the full list of responses
    34.         if (subtitle.formattedText.text.Contains("[Add Responses]"))
    35.         {
    36.             for(int i = 0; i < subtitle.dialogueEntry.outgoingLinks.Count; i++)
    37.             {
    38.                 int currentID = subtitle.dialogueEntry.outgoingLinks[i].destinationDialogueID;
    39.                 DialogueEntry thisEntry = DialogueManager.MasterDatabase.conversations[subtitle.dialogueEntry.conversationID - 1].GetDialogueEntry(currentID);
    40.                 if(thisEntry.ActorID == 1)
    41.                 {
    42.                     FormattedText thisText = new FormattedText();
    43.                     thisText.text = thisEntry.DialogueText;
    44.                     responseList.Add(new Response(thisText, thisEntry, enabled));
    45.                 }
    46.             }
    47.          
    48.             subtitle.formattedText.text = subtitle.formattedText.text.Replace("[Add Responses]", string.Empty);
    49.          
    50.             showingInterjectableResponses = true;
    51.         }
    52.  
    53.         // Clear the list of responses, and add the new ones
    54.         if (subtitle.formattedText.text.Contains("[Replace Responses]"))
    55.         {
    56.             responseList.Clear();
    57.             for (int i = 0; i < subtitle.dialogueEntry.outgoingLinks.Count; i++)
    58.             {
    59.                 int currentID = subtitle.dialogueEntry.outgoingLinks[i].destinationDialogueID;
    60.                 DialogueEntry thisEntry = DialogueManager.MasterDatabase.conversations[subtitle.dialogueEntry.conversationID - 1].GetDialogueEntry(currentID);
    61.                 if (thisEntry.ActorID == 1)
    62.                 {
    63.                     FormattedText thisText = new FormattedText();
    64.                     thisText.text = thisEntry.DialogueText;
    65.                     responseList.Add(new Response(thisText, thisEntry, enabled));
    66.                 }
    67.             }
    68.             subtitle.formattedText.text = subtitle.formattedText.text.Replace("[Replace Response]", string.Empty);
    69.  
    70.             showingInterjectableResponses = true;
    71.         }
    72.  
    73.         // If subtitle contains special tag [HideResponses], stop showing interjectable responses:
    74.         if (subtitle.formattedText.text.Contains("[Hide Responses]"))
    75.         {
    76.             subtitle.formattedText.text = subtitle.formattedText.text.Replace("[Hide Responses]", string.Empty);
    77.             HideResponses();
    78.             showingInterjectableResponses = false;
    79.         }
    80.      
    81.         if (subtitle.formattedText.text.Contains("[HideResponseID ") && subtitle.formattedText.text.Contains("]"))
    82.         {
    83.             Debug.Log("Starting method");
    84.             // Can these two be made more rigorous?
    85.             int startIndex = subtitle.formattedText.text.IndexOf("[");
    86.             int endIndex = subtitle.formattedText.text.IndexOf("]");
    87.             string newText = subtitle.formattedText.text.Remove(startIndex, ((endIndex - startIndex) + 1));
    88.             string tempText = subtitle.formattedText.text.Remove(0, startIndex);
    89.             List<int> ids = new List<int>();
    90.  
    91.             subtitle.formattedText.text = newText;
    92.             tempText = tempText.Remove(0, 16);
    93.             int tempTextEnd = tempText.IndexOf("]");
    94.             tempText = tempText.Remove(tempTextEnd, 1);
    95.             while (tempText.Contains("&"))
    96.             {
    97.                 int parser = tempText.LastIndexOf("&");
    98.                 string tempID = tempText.Remove(0, (parser + 1));
    99.                 int result;
    100.                 if (int.TryParse(tempID, out result) == true)
    101.                 {
    102.                     ids.Add(int.Parse(tempID));
    103.                 }
    104.                 tempText.Remove(parser, (tempText.Length - parser));
    105.             }
    106.             int remainingResult;
    107.             if (int.TryParse(tempText, out remainingResult) == true)
    108.             {
    109.                 ids.Add(int.Parse(tempText));
    110.             }
    111.             for (int i = 0; i < ids.Count; i++)
    112.             {
    113.                 int removalID = ids[i];
    114.                 for (int j = 0; j < responseList.Count; j++)
    115.                 {
    116.                     int tempID = responseList[j].destinationEntry.id;
    117.                     if(tempID == removalID)
    118.                     {
    119.                         responseList.RemoveAt(j);
    120.                     }
    121.                 }
    122.                 if (responseList.Count == 0)
    123.                 {
    124.                     HideResponses();
    125.                     showingInterjectableResponses = false;
    126.                 }
    127.             }
    128.         }
    129.  
    130.         // Show the subtitle:
    131.         base.ShowSubtitle(subtitle);
    132.  
    133.         // If the NPC is speaking but the state also has valid PC responses, show them as interjectable responses:
    134.         if (subtitle.speakerInfo.IsNPC)
    135.         {
    136.             DialogueManager.CurrentConversationState.pcResponses = responseList.ToArray();
    137.             var hasInterjectableResponses = DialogueManager.CurrentConversationState.HasNPCResponse && DialogueManager.CurrentConversationState.HasPCResponses;
    138.             if (hasInterjectableResponses)
    139.             {
    140.                 ShowResponses(subtitle, DialogueManager.CurrentConversationState.pcResponses, 0);
    141.                 showingInterjectableResponses = true;
    142.             }
    143.         }
    144.     }
    145.  
    146.     public override void HideResponses()
    147.     {
    148.         // Only hide if we're not showing interjectable responses:
    149.         if (!showingInterjectableResponses)
    150.         {
    151.             responseList.Clear();
    152.             base.HideResponses();
    153.         }
    154.     }
    155.  
    156.     public override void OnClick(object data)
    157.     {
    158.         // After clicking, we're no longer showing interjectable responses:
    159.         //Well, if other replies still exist, they should be available.
    160.         // Both for replying later in the conversation, and to interrupt ourselves if so desired.
    161.         //showingInterjectableResponses = false;
    162.  
    163.         base.OnClick(data);
    164.     }
    165. }
    166.  
    You can just import the "Interjectable Responses" unity package you had on the previous page, change the script to that, then make a node that has [HideResponseID X&Y] at the end (of course, there need to be two responses available before then, so at least two player replies before then with [Add Responses] at the end).

    Freezes every time. Weird thing is that if you change it to only [HideResponseID X] or [HideResponseID Y], it works absolutely fine. Additionally, you may notice on line 83 in the code above I have a printout to the console. That printout doesn't fire when doing the multiple IDs at once, so it's like it can't even reach that if statement.

    Have no idea what this could be. Any clue?
     
    Last edited: Dec 12, 2017
  13. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Why do you need to parse the conditions string? Can you simply evaluate it to see if it's true or false? To do that, check Lua.IsTrue(string). For example:
    Code (csharp):
    1. bool result = Lua.IsTrue(dialogueEntry.conditionsString);
    In this loop:
    Code (csharp):
    1.             while (tempText.Contains("&"))
    2.             {
    3.                 int parser = tempText.LastIndexOf("&");
    4.                 string tempID = tempText.Remove(0, (parser + 1));
    5.                 int result;
    6.                 if (int.TryParse(tempID, out result) == true)
    7.                 {
    8.                     ids.Add(int.Parse(tempID));
    9.                 }
    10.                 tempText.Remove(parser, (tempText.Length - parser));
    11.             }
    tempText isn't removing the number and ampersand, so it becomes an infinite loop. Stepping through with the Monodevelop debugger or Visual Studio debugger is really helpful in cases like this.

    As a general practice, you may have seen in the source code that I always add a safeguard condition in loops. For example:
    Code (csharp):
    1.              // Loop is guaranteed to never run more than 1000 iterations, so it won't freeze Unity:
    2.             int safeguard = 0;
    3.             while (tempText.Contains("&") && safeguard < 1000) // (example)
    4.             {
    5.                 safeguard++;
    6.                 int parser = tempText.LastIndexOf("&");
    7.                 ...
    8.             }
    I try to always add a safeguard even if I'm absolutely sure the loop won't go infinite -- because I've been "sure" plenty of times. ;-)

    However, in this case you could simplify your code by using string.Split:
    Code (csharp):
    1. string[] idStrings = tempText.Split('&');
    It's getting to that method. Unity queues up all its Debug.Log output until your code returns control. In this case, since your method never returns control, it just keeps queueing up Debug.Log lines and never displays anything.
     
  14. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I see. That should work for the conditions. However, if you saw my post in the other thread, I have a number of checks that change as you progress through the conversation. The reason for this is because of those player response side sections--they need to be slotted in to the appropriate place in the ongoing conversation (which requires links to each "section" of the main dialogue tree along with a bunch of checks to ensure that it returns to the proper section).

    Edit: see image:

    Unity_2017-12-12_22-03-46.png

    This means that when we finish talking about Haven, for example, the bool "Middle_talkedHaven" is changed to true.

    If any part of that is unclear, please let me know. What that means is that in the "script" section I change the variables, and this UpdateResponseTimer method needs to be able to traverse the tree, change those variables (or a facsimile of those variables, so we don't impact the real version of the tree the player is seeing), and check if those (and possibly others) are true.

    Does that make sense? So I can use the "isTrue" thing for conditions, but I'll still need to parse the scripts section to adjust (the replicated version of) those variables in real time.

    Excellent, thanks a ton for this. Really appreciate the help with my rudimentary programming skill.
     
  15. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    I'll admit that I haven't thought it out end-to-end (I'll mull it over a bit more tomorrow), but here are a couple technical features in the Dialogue System that could make the job easier:

    1. You can use levels in indirection. For example, say your dialogue database has these Boolean variables:

    Variable["Likes_Lower_Class_Neighborhood"]
    Variable["Likes_Middle_Class_Neighborhood"]
    Variable["Likes_Upper_Class_Neighborhood"]​

    And this Text variable:

    Variable["Current_Location"]​

    which could be set to "Lower_Class_Neighborhood", "Middle_Class_Neighborhood", or "Upper_Class_Neighborhood".

    If you want a dialogue entry's Conditions to require that the player likes the current location, you could use this:

    Variable[ "Likes_"..Variable["Current_Location"] ]


    2. You can also register your own C# methods with Lua. The Dialogue System's third party support packages use this feature. For example, the S-Inventory Support package registers a C# GetItemAmount() function that returns the quantity of an item in S-Inventory. Since it's registered with Lua, you can use this function in your dialogue entries' Conditions. It's a handy way to tie your C# code into the Dialogue System.
     
    Last edited: Dec 13, 2017
  16. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Actually, thinking about it a bit more, the isTrue won't work either, unless there's some custom built-in clause that automatically changes those variables to the temporary version. Because the whole time we're traversing the "virtual tree" I'll call it, as I mentioned before we can't be changing the actual variables the real tree uses. So we change the temporary ones. However, if we tried to check the real ones...obviously they wouldn't be changed.

    I'll have to take a look at this Lua business, don't know much about it.
     
  17. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    That was my thinking with this example:
    Code (csharp):
    1. Variable[ "Likes_"..Variable["Current_Location"] ]
    You can change Variable["Current_Location"] before calling IsTrue().

    There's not much to it. It's like an extremely simplified version of C#, with a few syntax changes (for example, ~= instead of != to test for inequality). But it can be very useful for logic stuff like this.
     
  18. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Alright, thank you for all the help. I'll have to pause this for a day or so, but I should be picking it back up in ~36 hours. I'll change that splitting the string part, then start on incorporating the timer then. If I have any trouble (or upon successful completion) I'll post here.
     
    TonyLi likes this.
  19. Sigma266

    Sigma266

    Joined:
    Mar 25, 2017
    Posts:
    99
    Hey, Tony. So, is it possible to make the Dialogue System's variables NOT global? I don't want my characters to repeat the same lines over and over, so I want to put a bool that tells them they already said a certain line. But this bool is global so it works for everybody! What's a good way to fix this?
     
  20. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Hi @Sigma266 - Variables are global, but many devs incorporate "scope" into their variable names like this:
    • Global.TimeOfDay
    • Global.Temperature
    • Adam.SaidHello
    • Bob.SaidHello
    • Bob.ConfessedLove
    • Charlie.SaidHello
    In version 1.7.8 (which I will try to release soon after 1.7.7 comes out), the Variables section of the Dialogue Editor will group variables into foldouts based on the part before the dot (e.g., Global, Adam, Bob, and Charlie). So in effect they'll look like local variable groups. But their full variable names will still be "Adam.SaidHello", etc.
     
    Sigma266 likes this.
  21. Sigma266

    Sigma266

    Joined:
    Mar 25, 2017
    Posts:
    99
    Oh yeah, that makes sense. However, i'm afraid that if there are many characters it'll become a mess full of variables. Anyway, thank you for helping!
     
  22. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    The Menu > Sort feature can help keep them organized.
     
    Sigma266 likes this.
  23. Sigma266

    Sigma266

    Joined:
    Mar 25, 2017
    Posts:
    99
    Ohhhh that's right. It's been a while and I forgot a lot things. Thanks again!
     
  24. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Noticed a script in the "Scripts" folder system called UnityUITimer. Might try using that. Any tips on how to use it? I briefly tried adding it to the Dialogue Manager GameObject but nothing happened.
     
  25. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    You could use it.

    Normally, this script is on the dialogue UI's Timer slider:



    (The dialogue UI will automatically add one at runtime if it's missing on the slider.)

    The Dialogue System uses it if Input Settings > Response Timeout is non-zero:



    In this case, the response menu activates the slider GameObject and calls UnityUITimer.StartCountdown(duration, timeoutHandler). The first parameter is the timeout length. The second parameter is a method to call if the duration is reached. To turn off the timer, the response menu deactivates the slider GameObject, which disables the script.

    If the UnityUITimer script doesn't find a UI slider on its GameObject, it will still run the timer countdown and skip updating a UI slider.

    I suppose you could add a UnityUITimer script to each response button. If the response is timed, call its UnityUITimer.StartCountdown method. In the timeout method, remove the response button from the menu. If you'd like an example, let me know. I can throw one together for you.
     
    EternalAmbiguity likes this.
  26. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I definitely would like an example, specifically of how to attach it to each response button. I think that's my best angle of attack here rather than trying to stick a number somewhere.
     
  27. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Here's an example:

    InterjectableResponsesExample_2017-12-16.unitypackage

    I directly modified InterjectableDialogueUI.cs. If you've customized this script, you may want to save your copy as a new script, or import this example into a different project. It also adds another script TimedResponseButton.cs. Both scripts have comments that explain what they're doing.

    Briefly, InterjectableDialogueUI checks each response for a Number field named "Timeout". If the field exists and has a non-zero value, it starts a timer by automatically adding a TimedResponseButton script to the response button. TimedResponseButton in turn will add a UnityUITimer to the button.

    The TimedResponseButton script will use a UI slider if the button has one. This is useful for debugging:



    If the response button template doesn't have a slider, it'll run the timer without a slider.
     
    EternalAmbiguity likes this.
  28. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I've been looking for this, but I have no idea. I searched the entire project with Visual Studio for "Max(" as well as for "Length(DialogueText)" but found neither. It looks like the "Max" method or whatever is not here.

    I'm basically trying to replace "{{end}} =" with (it's a float) "timer +=."



    And I'm sorry, but I'm really struggling to see how you're doing the variables. For the changing of variables based on the "script" field, I did something like this:

    When a response is created, add a temporary database:
    Code (csharp):
    1. DialogueDatabase tempDatabase = DialogueManager.MasterDatabase;
    (I realize writing this out that this won't work. If the timer ever needs to be updated, that database will not still be there. May be able to create the database in the method below)

    Then, in the method where I actually update, I find and compare the variable like this:

    Code (csharp):
    1. public void UpdateTimer(DialogueEntry thisEntry, float timer, DialogueDatabase tempDatabase)
    2.     {
    3.         // Add current thisEntry's "subtitle time" to timer
    4.  
    5.         // Check if there's a "[HideResponseID]" with thisEntry's ID, or for a "HideResponses"
    6.         // If so, end method
    7.      
    8.         // Check for "scripts" field changing any variables. If so, change them.
    9.         string script = thisEntry.userScript;
    10.         if(script.Length > 0)
    11.         {
    12.             string[] pseudoVariables = script.Split(';');
    13.             for(int i = 0; i < pseudoVariables.Length; i++)
    14.             {
    15.                 int endIndex = pseudoVariables[i].IndexOf("]");
    16.                 string variableName = pseudoVariables[i].Remove((endIndex - 1), 2);
    17.                 int startIndex = variableName.IndexOf("[");
    18.                 int otherStartIndex = pseudoVariables[i].IndexOf("=");
    19.                 variableName = variableName.Remove(0, (startIndex + 2));
    20.                 string variableValue = pseudoVariables[i].Remove(0, (otherStartIndex + 1));
    21.                 // Change to find by specific value, by searching list of variables for this specific string (can this be done?)
    22.                 for (int j = 0; j < tempDatabase.variables.Count; j++)
    23.                 {
    24.                     if(variableName == tempDatabase.variables[j].Name)
    25.                     {
    26.                         // Note! Check if this variable, initial value, is used for the value regardless of the type of variable.
    27.                         tempDatabase.variables[j].InitialValue = variableValue;
    28.                     }
    29.                 }
    30.             }
    31.  
    32.             // Search DialogueEntry in each outgoing link for conditionString
    33.             // If length > 0, parse like above, then compare to the variables in tempDatabase much like above
    34.             // If one found where all variable values are same, call function again (from within function) with new DialogueEntry:
    35.             UpdateTimer(newEntry, timer, tempDatabase);
    36.         }
    37.     }
    Currently the method is not working (just to be clear, where all the comments are here there are other parts of the code. I just didn't show them to keep focus on this area). Not sure why, but I haven't done any detailed analysis yet. I intend to, but I wanted to share this in case I'm making it way more difficult than I have to.

    Is that an acceptable way to use recursion? To give an easier, not-325-lines-long example:

    Code (csharp):
    1. int timer = 0;
    2. RecursiveFunction(timer);
    3.  
    4. public void RecursiveFunction(int timer)
    5. {
    6.     if(timer > 50)
    7.     return;
    8.     timer += 5;
    9.     RecursiveFunction(timer);
    10. }
    Is the above acceptable? If so I may double-check my code and then if it's not working post the full thing.
     
  29. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Sorry, I wasn't providing actual C# code. This is just the mathematical idea:

    {{end}} = Max( SubtitleSettings.MinSubtitleSeconds,
    Length(DialogueText) / SubtitleSettings.SubtitleCharsPerSecond )​

    The actual C# code is:
    Code (csharp):
    1. int numCharacters = string.IsNullOrEmpty(text) ? 0 : Tools.StripRichTextCodes(text).Length;
    2. return Mathf.Max(displaySettings.GetMinSubtitleSeconds(), numCharacters / Mathf.Max(1, displaySettings.GetSubtitleCharsPerSecond()));
    What's the end goal? You want to dynamically extend the duration that a subtitle is onscreen?

    This may just be a side note, but when working with variables at runtime, use DialogueLua.GetVariable and DialogueLua.SetVariable. It's much easier, and these method will automatically create the variable if it doesn't already exist.

    Would you please remind me why you want to change the Script field? I have a suspicion that there's an easier approach.

    Yup. If you'll forgive me for nitpicking, I'd like to suggest this change:
    Code (csharp):
    1. public void RecursiveFunction(int timer)
    2. {
    3.     if(timer > 50) return;
    4.     RecursiveFunction(timer + 5);
    5. }
    Try not to change the values of pass-by-value parameters. If you need another value, declare a new variable.

    It's a safety thing. In your example, if you accidentally commented out "timer += 5;", you'd get an infinite loop. But if you're passing "timer + 5" to RecursiveFunction, it's a bit clearer what value is being passed.

    I'm finishing up work for the day, but I'll check back in the morning. Just one more post to make here for tonight...
     
    EternalAmbiguity likes this.
  30. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Dialogue System 1.7.7 release candidate 1

    The Pixel Crushers customer download site has a release candidate for version 1.7.7 that contains significant changes to the articy:draft converter. If you're using articy:draft, I'd be very grateful if you have the time to test it with your project.

    If you need access to the customer download page, please PM me your Asset Store invoice number or email it to tony (at) pixelcrushers.com.

    Full release notes.

    p.s. - Note that there's now a Unity 2017.x-specific version for compatibility with API changes being introduced in Unity 2017.3.
     
  31. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I'll respond to everything else later, but maybe this will make it more clear.

    Okay, so the whole idea of all this is to add a timer for each dialogue response that displays how much longer it will be available to the user. To do this, for each response I'll need to travel through the dialogue tree to reach the location where the response is removed. I was going to provide a picture of one of my current trees, but the 'arrange nodes" option makes it look super confusing and not really that helpful here. See this image made in XMind:

    DialogueExample.png

    Imagine that, at subtopic 1 of main topic 1, a response is introduced. At the beginning of main topic 3 it gets removed. When the response is added, starting from subtopic 1 the method traverses the tree--subtopic 1, subtopic 2, subtopic 3, and finally main topic 2. As it traverses, it finds the amount of time that node (whichever it's currently on) is meant to be displayed, and adds that to the timer. once it arrives at the final node where the response is stopped, it ends the method, and thus gives a time for the slider which should match the time until the "code" to remove that response is called.

    Traversing the tree won't work on its own, though, because we have all of those variables we have to check. So, when traversing the tree, we need to change any variables that we come across. That way when we arrive at a node with conditions we'll pass (if we have the correct variables at the correct values).

    However, if we change any actual variables, this would totally screw up with the real conversation. Remember this is all temporary. We're only doing this for the timer for each response. So A) it's not "real," and B) it gets done multiple times. So we need to have a set of "virtual" variables for our "virtual" traversal of the tree.

    I tried to do this by having a temporary database formed from the real database when the update timer method is called. That way it has the current status of any variables. Then when coming to a node, I check the "script" field for any changes to variables. if there are any, I apply them to the temp database. Later, when searching the outgoing links for a valid path, I first set a bool called "allTrue" to true. I then check the conditions field of that current outgoing link (or the dialogue entry at that link) for any variable prerequisites. If there are any, I compare them to the variables in the temp database. If any fail, I set "allTrue" to false.

    If it looks through a link and "allTrue" is still true, I call the update timer method again. It runs like that until it find the node that ends the currently looked at response.

    Does that make sense?
     
  32. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Yes. It's complicated, but it makes sense. My example scene doesn't do that. In it, the timeout values are hard-coded. Each response has a Timeout field that specifies the timer duration in seconds.

    Could you skirt around the issue by starting the countdown only on the last node in which the response option will be available? This would let the player know that a response option is going to disappear soon. In fact, it might be cool if, instead of using a timer, the response button flashed to get the player's attention and then gradually faded away over the duration of this node's subtitle.

    If that's not possible, and you really need to simulate the full tree traversal with variable checks and changes, I can provide details on how to do it. (In brief, you'd take a snapshot of the variables prior to the traversal, and then restore that snapshot when you're done.)
     
  33. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    The way I was doing it was getting a timer value, doing all of the calculations, then passing that value in for the "Timeout" value. So in effect it works the same way. It doesn't really matter that the Timeout value is hardcoded (I don't think).

    I'm not completely certain about how the timer design should work. Might be something like a green/yellow/red thing based on the time left. however, I don't want to shy away from this because it's difficult.

    I may not have made this super clear, but I actually do have a method that does (should do) most of this. The only thing it was missing was the beginning part, the value calculated through the subtitles (where previously you showed me the {{end}} sequence formula). I sidestepped that by just giving a single value, timer += 5 or something, but the method isn't working. I intend to analyze it for mistakes, but I wanted to check that my presumed recursion in the method was correct.

    I'll post the code for it below. This first part is in the ShowSubtitle method of the InterjectableDialogueUI script.

    Code (csharp):
    1. // Add the newest group of responses to the full list of responses
    2.         if (subtitle.formattedText.text.Contains("[Add Responses]"))
    3.         {
    4.             for (int i = 0; i < subtitle.dialogueEntry.outgoingLinks.Count; i++)
    5.             {
    6.                 int currentID = subtitle.dialogueEntry.outgoingLinks[i].destinationDialogueID;
    7.                 DialogueEntry thisEntry = DialogueManager.MasterDatabase.conversations[subtitle.dialogueEntry.conversationID - 1].GetDialogueEntry(currentID);
    8.                 if (thisEntry.ActorID == 1)
    9.                 {
    10.                     FormattedText thisText = new FormattedText();
    11.                     thisText.text = thisEntry.DialogueText;
    12.                     float timer = 0;
    13.                     DialogueDatabase tempDatabase = DialogueManager.MasterDatabase;
    14.                     UpdateTimer(thisEntry, timer, tempDatabase);
    15.                     Field timerField = new Field("Timeout", timer.ToString(), FieldType.Number);
    16.                     thisEntry.fields.Add(timerField);
    17.                     responseList.Add(new Response(thisText, thisEntry, enabled));
    18.                 }
    19.             }
    20.             subtitle.formattedText.text = subtitle.formattedText.text.Replace("[Add Responses]", string.Empty);
    21.             showingInterjectableResponses = true;
    22.         }
    This next part is the UpdateTimer method.

    Code (csharp):
    1. public void UpdateTimer(DialogueEntry thisEntry, float timer, DialogueDatabase tempDatabase)
    2.     {
    3.         // Add this dialogue entry's timer amount
    4.         // Check if this contais the Timeout field
    5.         // Needs to be from the special subtitle button
    6.         //timer += Max(DisplaySettings.SubtitleSettings.minSubtitleSeconds, Length(thisEntry.DialogueText) / DisplaySettings.SubtitleSettings.subtitleCharsPerSecond);
    7.         timer += 5;
    8.  
    9.         // If subtitle ends this response being shown, stop the search
    10.         {
    11.             string thisSubtitle = thisEntry.SubtitleText;
    12.             if (thisSubtitle.Contains("[HideResponses]"))
    13.             {
    14.                 return; // Stop method
    15.             }
    16.             if (thisSubtitle.Contains("[HideResponseID ") && thisSubtitle.Contains("]"))
    17.             {
    18.                 int startIndex = thisSubtitle.IndexOf("[");
    19.                 string tempText = thisSubtitle.Remove(0, startIndex);
    20.                 tempText = tempText.Remove(0, 16);
    21.                 int tempTextEnd = tempText.IndexOf("]");
    22.                 tempText = tempText.Remove(tempTextEnd, 1);
    23.                 string[] ids = tempText.Split('&');
    24.                 for (int i = 0; i < ids.Length; i++)
    25.                 {
    26.                     int result;
    27.                     if (int.TryParse(ids[i], out result) == true)
    28.                     {
    29.                         int removalID = int.Parse(ids[i]);
    30.       // Note: this will not work! Need to have reference to original added response ID
    31.                         if (removalID == thisEntry.id)
    32.                         {
    33.                             return; // Stop method
    34.                         }
    35.                     }
    36.                 }
    37.             }
    38.         }
    39.         // Check for "scripts" field changing any variables. If so, change them.
    40.         string script = thisEntry.userScript;
    41.         if(script.Length > 0)
    42.         {
    43.             string[] pseudoVariables = script.Split(';');
    44.             for(int i = 0; i < pseudoVariables.Length; i++)
    45.             {
    46.                 int endIndex = pseudoVariables[i].IndexOf("]");
    47.                 string variableName = pseudoVariables[i].Remove((endIndex - 1), 2);
    48.                 int startIndex = variableName.IndexOf("[");
    49.                 int otherStartIndex = pseudoVariables[i].IndexOf("=");
    50.                 variableName = variableName.Remove(0, (startIndex + 2));
    51.                 string variableValue = pseudoVariables[i].Remove(0, (otherStartIndex + 1));
    52.                 // Change to find by specific value, by searching list of variables for this specific string
    53.                 for (int j = 0; j < tempDatabase.variables.Count; j++)
    54.                 {
    55.                     if(variableName == tempDatabase.variables[j].Name)
    56.                     {
    57.                         // Note! Check if this variable, initial value, is used regardless of the type of variable.
    58.                         tempDatabase.variables[j].InitialValue = variableValue;
    59.                     }
    60.                 }
    61.             }
    62.         }
    63.         //look through links
    64.         for (int i = 0; i < thisEntry.outgoingLinks.Count; i++)
    65.         {
    66.             int currentID = thisEntry.outgoingLinks[i].destinationDialogueID;
    67.             DialogueEntry newEntry = DialogueManager.MasterDatabase.conversations[thisEntry.conversationID - 1].GetDialogueEntry(currentID);
    68.             if ((thisEntry.ActorID == 1) == false)
    69.             {
    70.                 // Change to find by specific value, by searching list of variables for this specific string
    71.                 string condition = newEntry.conditionsString;
    72.                 if (condition.Length > 0)
    73.                 {
    74.                     string[] pseudoVariables = condition.Split(';');
    75.                     bool allTrue = true;
    76.                     for (int j = 0; j < pseudoVariables.Length; j++)
    77.                     {
    78.                         int endIndex = pseudoVariables[j].IndexOf("]");
    79.                         string variableName = pseudoVariables[j].Remove((endIndex - 1), 2);
    80.                         int startIndex = variableName.IndexOf("[");
    81.                         int otherStartIndex = pseudoVariables[j].IndexOf("=");
    82.                         variableName = variableName.Remove(0, (startIndex + 2));
    83.                         string variableValue = pseudoVariables[j].Remove(0, (otherStartIndex + 1));
    84.                         // Change to find by specific value, by searching list of variables for this specific string
    85.                         for (int k = 0; k < tempDatabase.variables.Count; k++)
    86.                         {
    87.                             if (variableName == tempDatabase.variables[k].Name)
    88.                             {
    89.                                 // Note! Check if this variable, initial value, is used regardless of the type of variable.
    90.                                 string databasevalue = tempDatabase.variables[k].LookupValue(tempDatabase.variables[k].Name);
    91.                                 if ((databasevalue == variableValue) == false)
    92.                                 {
    93.                                     allTrue = false;
    94.                                     continue;
    95.                                 }
    96.                             }
    97.                         }
    98.                     }
    99.                     if(allTrue == true)
    100.                     {
    101.                         UpdateTimer(newEntry, timer, tempDatabase);
    102.                     }
    103.                 }
    104.             }
    105.         }
    106.     }
    First, sorry for bastardizing your lovely code :p

    Anyway, this is a big bunch of stuff. The part I wanted to check with you on, as far as the variables are concerned, is lines (39-62), where I check the current DialogueEntry's script field for any values, split them by semicolon, get rid of everything but the variable name, check that against the temp database, then change the values for the matching variable names in the temp database to the values taken from the script field.

    I wanted to see if I was doing that properly (this first). I wanted to see if there was an easier way (this second).

    I also wanted to check if my recursion in the method was doing what it should (this last).
     
  34. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Instead of all this parsing, why not make a snapshot of all the variables' values, traverse your tree, and then restore the variables' values? Would that approach work for you?
     
    EternalAmbiguity likes this.
  35. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Definitely. No problem with a simpler method if you can show me.
     
  36. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    I'll put together an example today and post it here.
     
    EternalAmbiguity likes this.
  37. flashframe

    flashframe

    Joined:
    Feb 10, 2015
    Posts:
    798
    Hey Tony!

    Is there a way to get a reference to the last ConversationState / DialogueEntryID, or anything that would allow me to skip backwards one line in the conversation?

    I'd like to be able to move forwards/backwards in the conversation while it's running in the editor for quickly testing sequences.

    I'm presuming the best way is to call GotoState() on the ConversationController?

    Thanks!
     
  38. Galahad

    Galahad

    Joined:
    Feb 13, 2012
    Posts:
    72
    Do Dialogue System has an integration with Third Person Controller (by Opsive). because I'm having some issues using both of them:




     
  39. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Hi @flashframe - Yes, and there's a Backtracking Example on the Dialogue System Extras page with a script that does it. I'll include the script here, too:

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. namespace PixelCrushers.DialogueSystem
    5. {
    6.  
    7.     /// <summary>
    8.     /// This script adds the ability to backtrack conversations. To backtrack, call Backtrack(true).
    9.     /// The bool parameter specifies whether to backtrack to an NPC line, which is what you usually
    10.     /// want to do; otherwise if you're in a response menu you'll keep backtracking to the same
    11.     /// response menu instead of going back to a previous NPC line.
    12.     /// </summary>
    13.     public class Backtracker : MonoBehaviour
    14.     {
    15.  
    16.         public bool debug;
    17.  
    18.         private Stack<ConversationState> stack = new Stack<ConversationState>();
    19.  
    20.         public void OnConversationStart(Transform actor)
    21.         {
    22.             stack.Clear();
    23.             if (debug) Debug.Log("Backtracker: Starting a new conversation. Clearing stack.");
    24.         }
    25.  
    26.         public void OnConversationLine(Subtitle subtitle)
    27.         {
    28.             stack.Push(DialogueManager.CurrentConversationState);
    29.             if (debug) Debug.Log("Backtracker: Recording dialogue entry " + subtitle.dialogueEntry.conversationID + ":" + subtitle.dialogueEntry.id + " on stack: '" + subtitle.formattedText.text + "' (" + subtitle.speakerInfo.characterType + ").");
    30.         }
    31.  
    32.         // Call this method to go back:
    33.         public void Backtrack(bool toPreviousNPCLine)
    34.         {
    35.             if (stack.Count < 2) return;
    36.             stack.Pop(); // Pop current entry.
    37.             var destination = stack.Pop(); // Pop previous entry.
    38.             if (toPreviousNPCLine)
    39.             {
    40.                 while (!destination.subtitle.speakerInfo.IsNPC && stack.Count > 0)
    41.                 {
    42.                     destination = stack.Pop(); // Keep popping until we get an NPC line.
    43.                 }
    44.                 if (!destination.subtitle.speakerInfo.IsNPC) return;
    45.             }
    46.             if (debug) Debug.Log("Backtracker: Backtracking to " + destination.subtitle.dialogueEntry.conversationID + ":" + destination.subtitle.dialogueEntry.id + " on stack: '" + destination.subtitle.formattedText.text + "' (" + destination.subtitle.speakerInfo.characterType + ").");
    47.             DialogueManager.ConversationController.GotoState(destination);
    48.         }
    49.     }
    50. }

    Hi @Galahad - To integrate with Opsive's TPC, you don't need to use the wizards. You just need to use the steps in the Third Person Controller Support documentation. I'll summarize them here:
    1. Set up your scene with TPC. (No Dialogue System stuff yet.) Make sure it's working.
    2. Make sure your TPC player has the Interact ability. If not, see here.
    3. Select menu item Assets > Import Package > Custom Package... and select Assets / Dialogue System / Third Party Support / Third Person Controller Support.unitypackage.
      • At this point, you might want to play the example scene in Third Party Support / Third Person Controller Support. Then go back to your scene.
    4. Add the Dialogue Manager prefab (in Assets / Dialogue System / Prefabs) to your scene.
      • Assign a dialogue database. For a quick test, you can assign the Feature Demo database.
    5. Inspect your player.
      • Add a Dialogue System Third Person Controller Bridge. (Component > Dialogue System > Third Party Support > Third Person Controller > Dialogue System Third Person Controller Bridge)
      • Add a Show Cursor On Conversation.
    6. Inspect your NPC.
      • If it's TPC-controlled, add a Dialogue System Third Person Controller Bridge.
      • Add a standard TPC Interactable component. Make sure your NPC has a trigger collider so TPC can detect it.
      • Add a Dialogue System Interactable Target component. Assign this component the Interactable component's Target field.
      • Add a Conversation Trigger. Set the Trigger to OnUse. Select a conversation.
    7. Play the scene and interact with the NPC to start the conversation.

    If you prefer videos, here's a video tutorial:

     
    Galahad likes this.
  40. flashframe

    flashframe

    Joined:
    Feb 10, 2015
    Posts:
    798
    Ah great, thank you! Sorry for not checking to see if there was an example already!
     
  41. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Happy to help! I forgot to mention that the Backtracker script does not undo changes to variable values or quest states. It simply hops back to a previous dialogue entry node.
     
    flashframe likes this.
  42. Galahad

    Galahad

    Joined:
    Feb 13, 2012
    Posts:
    72
    Thats awesome @TonyLi ! Thank you very much!
     
  43. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Happy to help! If you have any questions about this, just let me know.
     
    Galahad likes this.
  44. Galahad

    Galahad

    Joined:
    Feb 13, 2012
    Posts:
    72
    Those steps worked wonders to integrate the Third Person Controller interaction system. However the camera bug still happening. Do you have any idea on which Dialogue System event could be affecting the camera system of TPC?
     
  45. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Hi @Galahad - Would it be possible for you to send your scene/project to tony (at) pixelcrushers.com? I'd be happy to take a look. If you could zip up your project's Assets and ProjectSettings folder, that would be best.

    Otherwise, does the Dialogue System's TPC Support example scene work correctly?

    Are you using any sequencer commands in your conversation?

    On the player's Dialogue System Third Person Controller Bridge, did you tick Deactivate During Conversations?

    Are there any other warnings or error messages in the Console window?

    If you tick the bridge's Debug checkbox and reproduce the problem, does it add info to the Console window that looks helpful?
     
  46. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Sorry, I forgot to post this yesterday!

    InterjectableResponsesExample_2017-12-19.unitypackage

    It's like the previous example, except if a response's Timeout field is -1 it will compute the timer duration in the way you described. (If the Timeout field is a positive value, it will use the value for the timer duration.) To do this, it:
    1. Saves the Dialogue System variable values using a small helper class.
    2. Creates a new conversation model (i.e., just the internal data part of a runtime conversation) and runs through it, adding up the durations to compute the timer value.
    3. Restores the Dialogue System variable values using the helper class.
    The "Let's see Upper" response's Timeout field is -1. To test it, click the Upper button to start the conversation. The "Let's see Upper" timer should auto-compute to the correct duration. In contrast, the "Let's see Middle" timer is hard-coded to a shorter value.

    Also, when the conversation gets to "Every convenience you could...", a [HideResponse 8] tag will make "Let's see Lower" disappear.

    The scripts in the package are also included below. They're commented, but if you have questions about any of it, just ask.
    InterjectableDialogueUI.cs
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using PixelCrushers.DialogueSystem;
    4.  
    5. /// <summary>
    6. /// Overrides UnityUIDialogueUI with handling for response menus that can occur while
    7. /// the NPC is talking (called interjectable responses in this example).
    8. /// </summary>
    9. public class InterjectableDialogueUI : UnityUIDialogueUI
    10. {
    11.  
    12.     // Are we showing interjectable responses?
    13.     private bool showingInterjectableResponses = false;
    14.  
    15.     // How many responses are currently visible?
    16.     private int numVisibleResponses;
    17.  
    18.     public override void Open()
    19.     {
    20.         base.Open();
    21.         showingInterjectableResponses = false; // When conversation starts, we're not showing any interjectable responses.
    22.         numVisibleResponses = 0;
    23.     }
    24.  
    25.     public override void ShowSubtitle(Subtitle subtitle)
    26.     {
    27.         if (subtitle.formattedText.text.Contains("[HideResponses]"))
    28.         {
    29.             // If subtitle contains special tag [HideResponses], stop showing all interjectable responses:
    30.             subtitle.formattedText.text = subtitle.formattedText.text.Replace("[HideResponses]", string.Empty);
    31.             HideAllResponses();
    32.         }
    33.         else if (subtitle.formattedText.text.Contains("[HideResponse "))
    34.         {
    35.             // If subtitle contains special tag [HideResponse #], stop showing a specific interjectable response:
    36.             var text = subtitle.formattedText.text;
    37.             var startIndex = text.IndexOf("[HideResponse ");
    38.             var length = text.Substring(startIndex).IndexOf("]") + 1;
    39.             var tagString = text.Substring(startIndex, length); // Entire [HideResponse #].
    40.             var idString = tagString.Remove(tagString.Length - 1).Remove(0, "[HideResponse ".Length); // Just #.
    41.             var id = Tools.StringToInt(idString);
    42.             subtitle.formattedText.text = text.Remove(startIndex, length);
    43.             HideResponseWithID(id);
    44.         }
    45.  
    46.         // Show the subtitle:
    47.         base.ShowSubtitle(subtitle);
    48.  
    49.         // If the NPC is speaking but the state also has valid PC responses, show them as interjectable responses:
    50.         if (subtitle.speakerInfo.IsNPC)
    51.         {
    52.             var hasInterjectableResponses = DialogueManager.CurrentConversationState.HasNPCResponse && DialogueManager.CurrentConversationState.HasPCResponses;
    53.             if (hasInterjectableResponses)
    54.             {
    55.                 ShowResponses(subtitle, DialogueManager.CurrentConversationState.pcResponses, 0);
    56.                 showingInterjectableResponses = true;
    57.             }
    58.         }
    59.     }
    60.  
    61.     public override void ShowResponses(Subtitle subtitle, Response[] responses, float timeout)
    62.     {
    63.         // If we're already showing interjectable responses, save them in a temporary list:
    64.         List<GameObject> existingButtons = null;
    65.         if (showingInterjectableResponses)
    66.         {
    67.             existingButtons = dialogue.responseMenu.instantiatedButtons;
    68.             dialogue.responseMenu.instantiatedButtons.Clear();
    69.         }
    70.  
    71.         // Set up the new response buttons:
    72.         base.ShowResponses(subtitle, responses, timeout);
    73.         numVisibleResponses = responses.Length;
    74.  
    75.         // Set response-specific timeouts:
    76.         for (int i = 0; i < responses.Length; i++)
    77.         {
    78.             var response = responses[i];
    79.             var button = dialogue.responseMenu.instantiatedButtons[i];
    80.             var responseTimeout = Field.LookupFloat(response.destinationEntry.fields, "Timeout");
    81.             if (Mathf.Approximately(0, responseTimeout))
    82.             {
    83.                 // If Timeout is zero or doesn't exist, hide the slider if it exists:
    84.                 var slider = button.GetComponentInChildren<UnityEngine.UI.Slider>();
    85.                 if (slider != null) slider.gameObject.SetActive(false);
    86.             }
    87.             else if (responseTimeout > 0)
    88.             {
    89.                 // If response has a positive Timeout field, set up the timer with Timeout's value:
    90.                 StartResponseTimer(button, responseTimeout);
    91.             }
    92.             else if (responseTimeout < 0)
    93.             {
    94.                 // If response has a negative Timeout field, compute the timer value from the dialogue tree:
    95.                 StartResponseTimer(button, ComputeDurationUntilHideResponseWithID(response.destinationEntry.id));
    96.             }
    97.         }
    98.  
    99.         // If we were already showign interjectable responses, re-add them to the button list:
    100.         if (showingInterjectableResponses)
    101.         {
    102.             dialogue.responseMenu.instantiatedButtons.AddRange(existingButtons);
    103.         }
    104.     }
    105.  
    106.     private void StartResponseTimer(GameObject button, float timeout)
    107.     {
    108.         var timedResponseButton = button.GetComponent<TimedResponseButton>();
    109.         if (timedResponseButton == null) timedResponseButton = button.gameObject.AddComponent<TimedResponseButton>();
    110.         timedResponseButton.StartTimer(timeout, OnHidResponse);
    111.     }
    112.  
    113.     public void OnHidResponse()
    114.     {
    115.         // If a response button just disappeared, update the count.
    116.         // If none are left, hide the response menu.
    117.         numVisibleResponses--;
    118.         if (numVisibleResponses <= 0)
    119.         {
    120.             showingInterjectableResponses = false;
    121.             HideResponses();
    122.         }
    123.     }
    124.  
    125.     private void HideResponseWithID(int id)
    126.     {
    127.         // Find button that leads to the dialogue entry with the specified ID:
    128.         var button = dialogue.responseMenu.instantiatedButtons.Find(x => x.GetComponent<UnityUIResponseButton>().response.destinationEntry.id == id);
    129.         if (button != null)
    130.         {
    131.             button.SetActive(false);
    132.             OnHidResponse();
    133.         }
    134.     }
    135.  
    136.     private void HideAllResponses()
    137.     {
    138.         showingInterjectableResponses = false;
    139.         HideResponses();
    140.     }
    141.  
    142.     public override void HideResponses()
    143.     {
    144.         // Only hide if we're not showing interjectable responses:
    145.         if (!showingInterjectableResponses)
    146.         {
    147.             base.HideResponses();
    148.             numVisibleResponses = 0;
    149.         }
    150.     }
    151.  
    152.     public override void OnClick(object data)
    153.     {
    154.         // After clicking, we're no longer showing interjectable responses:
    155.         showingInterjectableResponses = false;
    156.         base.OnClick(data);
    157.     }
    158.  
    159.     private float ComputeDurationUntilHideResponseWithID(int id)
    160.     {
    161.         // To compute the duration, we'll simulate a run through the conversation until
    162.         // we reach [HideResponses], [HideResponse id], or no NPC responses.
    163.         var hideResponseTag = "[HideResponse " + id + "]";
    164.  
    165.         // First, we save the actual state of the variables:
    166.         // var saved = PersistentDataManager.GetSaveData(); //<-- This line is overkill; it saves everything. We just save variables below:
    167.         var savedVariableTable = VariableTableUtility.SaveVariableTable();
    168.  
    169.         // Then we create a new conversation model. This one will run through the
    170.         // conversation's nodes without using a dialogue UI.
    171.         var model = new ConversationModel(DialogueManager.MasterDatabase, DialogueManager.LastConversationStarted,
    172.             DialogueManager.CurrentActor, DialogueManager.CurrentConversant,
    173.             false, DialogueManager.IsDialogueEntryValid, DialogueManager.CurrentConversationState.subtitle.dialogueEntry.id);
    174.         float duration = 0;
    175.         int safeguard = 0; // Follow a maximum of 999 nodes to prevent unexpected infinite loops.
    176.         var done = false;
    177.         var state = model.FirstState;
    178.         while (!done && safeguard < 999)
    179.         {
    180.             var text = state.subtitle.formattedText.text;
    181.             if (!state.HasNPCResponse || text.Contains("[HideResponses]") || text.Contains(hideResponseTag))
    182.             {
    183.                 // If there are no more NPC nodes or we need to hide our response, we're done:
    184.                 done = true;
    185.             }
    186.             else
    187.             {
    188.                 // Otherwise add the node's duration and progress to the next node:
    189.                 duration += GetDefaultSubtitleDuration(text);
    190.                 state = model.GetState(state.FirstNPCResponse.destinationEntry);
    191.             }
    192.         }
    193.  
    194.         // Finally, restore the saved state:
    195.         //PersistentDataManager.ApplySaveData(saved); //<-- Corresponding overkill line.
    196.         VariableTableUtility.RestoreVariableTable(savedVariableTable);
    197.  
    198.         return duration;
    199.     }
    200.  
    201.     private float GetDefaultSubtitleDuration(string text)
    202.     {
    203.         int numCharacters = string.IsNullOrEmpty(text) ? 0 : Tools.StripRichTextCodes(text).Length;
    204.         return Mathf.Max(DialogueManager.DisplaySettings.GetMinSubtitleSeconds(), numCharacters / Mathf.Max(1, DialogueManager.DisplaySettings.GetSubtitleCharsPerSecond()));
    205.     }
    206.  
    207. }
    VariableTableUtility.cs
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Text;
    4. using PixelCrushers.DialogueSystem;
    5.  
    6. /// <summary>
    7. /// Methods to save and restore the Dialogue System variable states:
    8. /// </summary>
    9. public static class VariableTableUtility
    10. {
    11.  
    12.     public static string SaveVariableTable()
    13.     {
    14.         var sb = new StringBuilder();
    15.         sb.Append("Variable={ ");
    16.         var variableTable = Lua.Run("return Variable").AsTable;
    17.         foreach (var key in variableTable.Keys)
    18.         {
    19.             var value = variableTable[key.ToString()];
    20.             var type = value.GetType();
    21.             if (type == typeof(string))
    22.             {
    23.                 value = string.Format("\"{0}\"", DialogueLua.DoubleQuotesToSingle(value.ToString()));
    24.             }
    25.             else if (type == typeof(bool))
    26.             {
    27.                 value = value.ToString().ToLower();
    28.             }
    29.             sb.AppendFormat("{0}={1}, ", new System.Object[] { DialogueLua.StringToTableIndex(key), value });
    30.         }
    31.         sb.Append("}");
    32.         return sb.ToString();
    33.     }
    34.  
    35.     public static void RestoreVariableTable(string savedVariableTable)
    36.     {
    37.         Lua.Run(savedVariableTable);
    38.     }
    39. }
    40.  
    41.  
     
  47. Galahad

    Galahad

    Joined:
    Feb 13, 2012
    Posts:
    72
    I'll try building the scene from scratch since the provided example scene works flawlessly. If even then I cannot make it work I'll cry for help once more =D
     
  48. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Dialogue System for Unity 1.7.7 Released!

    Version 1.7.7 is now available on the Pixel Crushers customer download site. (PM me your Asset Store invoice number if you need access.) It should be available on the Asset Store in 3-7 business days.

    Highlights:
    • New integrations: I2 Localization, Inventory Engine, and Customizable SciFi Holo Interface
    • Several articy:draft import improvements, including support for articy:draft 3.1.8
    • Updated for compatibility with Unity 2017.3


    Version 1.7.7
    Core

    • Updated for compatibility with Unity 2017.3.
    • Editor:
      • Improved: Conversation node editor now highlights incoming link arrows in a different color.
      • Improved: Added option to show node ID on nodes.
      • Improved: Moved Link to: "Another conversation" & "New entry" to top of dropdown menu.
      • Improved: Auto-backups no longer inherit original asset's AssetBundle designation.
      • Improved: Removing field from template now gives option to remove field from all assets in database.
      • Changed: Inspector "-" button no longer also deletes child node, making it more consistent with node context menu.
      • Fixed: Releasing MMB no longer deselects node.
      • Fixed: Auto backup error message.
      • Fixed: When changing databases, actor dropdown didn't refresh to new database's actor list.
      • Improved: Added search bar to localized text table editor.
    • Unity UI:
      • Improved: Dialogue UI now allows overrides on extra actors, not just primary participants.
      • Improved: UnityUITypewriterEffect automatically adds audio source if necessary.
      • Improved: Dialogue UI uses UnityUITypewriterEffect more effectively by calling it directly instead of relying on OnEnable.
      • Fixed: QuestLogWindow auto-scrollbar didn't work when Canvas was set to Pixel Perfect mode.
      • Fixed: Clicking QuestLogWindow Track button on/off many times fast would cause increasingly slower updates.
      • Fixed: UnityUISelectorDisplay & OverrideUnityUIControls continue button weren’t syncing with Dialogue Manager after load game.
      • Changed: Unity UI JRPG prefab now shows player image and name with response menu.
    • Added: Ability to override QuestLog.Current/SetQuestState, Current/SetQuestEntryState, and StringToState implementations.
    • Improved: Conversations now set Variable["ActorIndex"] and Variable["ConversantIndex"] to participants' indices in Actor[] Lua table.
    • Improved: The {{end}} keyword is now available in bark sequences.
    • Improved: Selector / ProximitySelector now has UnityEvents.
    • Improved: Added option to save all conversation fields.
    • Improved: Persistent Active Data now has an option to check the condition on start, not just when applying persistent data.
    • Improved: Exposed utility methods in PersistentDataManager.
    • Fixed: DialogueSystemController acting as singleton now calls Destroy instead of DestroyImmediate.
    • Fixed: Timeline() sequencer command no longer destroys GameObject unless it was instantiated for the command.
    • Fixed: Calling PersistentDataManager.ApplyData() with an empty string and SimStatus enabled now keeps SimStatus intact.
    • Fixed: Persistent Data Manager SimX bug.
    Third Party Support
    • articy:draft:
      • Updated for articy:draft 3.1.8 compatibility.
      • CHANGED: Articy Converter window's "FlowFragments Are" dropdown has new option. Check your current value before converting.
      • Added: support for Documents.
      • Fixed: Conditions on input pins are now converted.
      • Fixed: When "FlowFragments Are" is set to Nested Conversation Groups, conversations follow FlowFragments' subconversations.
      • Fixed: Order of jumps and conditions (by vertical node position).
    • CSV: Added additional error reporting to CSV Converter.
    • Corgi Platformer Engine: Updated for Corgi 1.4 & Inventory Engine support.
    • Customizable SciFi Holo Interface: Added support.
    • NLua: Optimized Get/SetTableField, implemented compress SimStatus saving (smaller save files); handles lank table element names (e.g., blank actor names) more gracefully.
    • I2 Localization: Added support.
    • Ink: Updated for Unity 2017.2 compatibility.
    • Inventory Engine: Added support.
    • PlayMaker:
      • Dialogue System Events To PlayMaker component now doesn't send events to disabled FSMs.
      • Set/GetVariable actions now support Vector3. Added Sync<Type> actions.
    • Realistic FPS Prefab: Updated for RFPS 1.44.
    • Rog: Added ReplaceActorSprite() Lua function.
    • RPG Kit: Updated Dialogue Manager prefab & Loading scene for RPG Kit 3.1.8.
    • TextMesh Pro: Improved dialogue UI animation transitions. Added TextMeshProSelectorDisplay component.
     
    BackwoodsGaming and hopeful like this.
  49. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I feel a little bad because I'm only just now getting to this.

    Took a look at the script. It looks like you added a "[HideResponse #]" section. It also looks like it only supports removing a single one. Is that true? Just checking here to see if I should go with it or my version that supports removing more-than-one-but-less-than-all responses.
     
  50. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    No worries! It's entirely on your schedule.

    You're right; I forgot your tag handles multiple ID numbers. You might want to look at my script for the other part, and then expand it on line 41 to handle multiple numbers. In the current version of the script, the string "idString" contains the "#,#,#" part of "[HideResponse #,#,#]", so you could just parse that into separate IDs.
     
    EternalAmbiguity likes this.