Forum rules - please read before posting.

Enable dialog option based on stat, otherwise unclickable/greyed out

edited July 2023 in Technical Q&A

I'd like a dialog system where a dialog option is clickable only if the player has a certain value in a certain stat; otherwise it'd be greyed out. But it's important that the option is clearly visible either way.
Right now I keep all the options available as per the default settings, and then I check the variable afterwards, and give a fail message if the value is too low. But it'd save us all some time, and look prettier, if there was an easy way of making an option clickable only if the criteria match.

I could, awkwardly, code an isolated instance of this, but it would be great to have it automated, so that I could easily assign a variable (or a 'stat') and a value, and the dialog option would change its look and clickability accordingly.

Ideas?

Comments

  • Options can be hidden based on what items the Player is carrying, but for something more detailed such as variable values, you'll need to rely on scripting.

    If your Conversation menu relies on Unity UI, you can use scripting to control the "Interactable" state of each option's linked UI Button. If this option is unchecked in the Button component, it'll be visible but not clickable - and its colour can be configured as well.

    Such a script, attached to each Conversation, would need to have an array of data that references the option, and its linked variable ID/value to sync it to. Something along these lines:

    using UnityEngine;
    using AC;
    
    public class DynamicOptions : MonoBehaviour
    {
    
        [SerializeField] private OptionCondition[] optionConditions = new OptionCondition[0];
    
        private void OnEnable ()
        { 
            EventManager.OnMenuTurnOn += OnMenuTurnOn; 
            EventManager.OnMenuElementShift += OnMenuElementShift; 
        }
    
        private void OnDisable ()
        { 
            EventManager.OnMenuTurnOn -= OnMenuTurnOn; 
            EventManager.OnMenuElementShift -= OnMenuElementShift; 
        }
    
        void OnMenuTurnOn (Menu menu, bool isInstant)
        {
            foreach (MenuElement element in menu.elements)
            {
                MenuDialogList dialogList = (MenuDialogList) element;
                UpdateUI (KickStarter.playerInput.activeConversation, dialogList);
            }
        }
    
        void OnMenuElementShift (MenuElement element, AC_ShiftInventory shiftType)
        {
            MenuDialogList dialogList = (MenuDialogList) element;
            UpdateUI (KickStarter.playerInput.activeConversation, dialogList);
        }
    
        private void UpdateUI (Conversation conversation, MenuDialogList dialogList)
        {
            if (dialogList == null || conversation == null || conversation.gameObject != gameObject) return;
    
            for (int i = 0; i < dialogList.GetNumSlots (); i++)
            {
                int ID = dialogList.GetDialogueOption (i).ID;
                foreach (OptionCondition condition in optionConditions)
                {
                    if (condition.optionID == ID)
                    {
                        dialogList.uiSlots[i].uiButton.interactable = condition.ConditionIsMet ();
                        break;
                    }
                }
            }
        }
    
        [System.Serializable]
        private class OptionCondition
        {
    
            public int optionID;
            public int variableID;
            public int minStatValue;
    
            public bool ConditionIsMet ()
            {
                return GlobalVariables.GetVariable (variableID).IntegerValue >= minStatValue;
            }
    
        }
    
    }
    
  • Thanks for the script.
    Everything compiles nicely and I don't get any errors. However, there's no real difference when I try it out.

    I also notice that even when I uncheck the 'interactable' box of each button in the Unity UI, they are still perfectly clickable once I run the dialog, with or without the script you provided attached. Do the conversation menu over-rule the interactability of the Unity UI?

    Also, just to be sure:
    Option ID is simply the number of the dialog option?
    And Variable ID is the number before the variable in the list of variables?
    Just making sure that I'm not supposed to use those long constant ID numbers anywhere...

  • edited July 2023

    Wait, now I'm getting super-weird effects whenever I have this attached to a conversation... everything gets really buggy, she gets stuck with her walkcycle looping, and the controls are all messed up...

    Console is giving an InvalidCastException:

    InvalidCastException: Specified cast is not valid.
    DynamicOption.OnMenuTurnOn (AC.Menu menu, System.Boolean isInstant) (at Assets/Scripts/DynamicOption.cs:23)
    AC.EventManager.Call_OnMenuTurnOn (AC.Menu menu, System.Boolean isInstant) (at Assets/AdventureCreator/Scripts/Managers/EventManager.cs:847)
    AC.Menu.TurnOn (System.Boolean doFade) (at Assets/AdventureCreator/Scripts/Menu/Menu classes/Menu.cs:1999)
    AC.PlayerMenus.AssignSpeechToMenu (AC.Speech speech) (at Assets/AdventureCreator/Scripts/Controls/PlayerMenus.cs:2424)
    AC.Dialog.StartDialog (AC.Char _speaker, System.String _text, System.Boolean isBackground, System.Int32 lineID, System.Boolean noAnimation, System.Boolean preventSkipping, UnityEngine.AudioClip audioOverride, UnityEngine.TextAsset lipsyncOverride) (at Assets/AdventureCreator/Scripts/Speech/Dialog.cs:163)
    AC.ActionSpeech.StartSpeech (UnityEngine.AudioClip audioClip, UnityEngine.TextAsset textAsset) (at Assets/AdventureCreator/Scripts/Actions/ActionSpeech.cs:912)
    AC.ActionSpeech.Run () (at Assets/AdventureCreator/Scripts/Actions/ActionSpeech.cs:196)
    AC.ActionList+d__45.MoveNext () (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:460)
    UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at <88f69663e9a64d00b2091dd8dfd4d38f>:0)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:410)
    AC.ActionList:ProcessActionEnd(ActionEnd, Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:609)
    AC.ActionList:EndAction(Action) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:568)
    AC.d__45:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:532)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:410)
    AC.ActionList:ProcessActionEnd(ActionEnd, Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:609)
    AC.ActionList:EndAction(Action) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:568)
    AC.d__45:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:532)
    UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)

  • The IDs match the numbers to the left of the labels, yes.

    Let's try a different approach - and just force the state in LateUpdate:

    using System.Collections;
    using UnityEngine;
    using AC;
    
    public class DynamicOptions : MonoBehaviour
    {
    
        [SerializeField] private OptionCondition[] optionConditions = new OptionCondition[0];
        [SerializeField] private string menuName = "Conversation";
        [SerializeField] private string elementName = "DialogueList";
        private MenuDialogList dialogList;
    
        private void LateUpdate ()
        { 
            if (dialogList == null) dialogList = (MenuDialogList) PlayerMenus.GetElementWithName (menuName, elementName);
            UpdateUI (KickStarter.playerInput.activeConversation, dialogList);
        }
    
        private void UpdateUI (Conversation conversation, MenuDialogList dialogList)
        {
            if (dialogList == null || conversation == null || conversation.gameObject != gameObject) return;
    
            for (int i = 0; i < dialogList.GetNumSlots (); i++)
            {
                int ID = dialogList.GetDialogueOption (i).ID;
                foreach (OptionCondition condition in optionConditions)
                {
                    if (condition.optionID == ID)
                    {
                        dialogList.uiSlots[i].uiButton.interactable = condition.ConditionIsMet ();
                        break;
                    }
                }
            }
        }
    
        [System.Serializable]
        private class OptionCondition
        {
    
            public int optionID;
            public int variableID;
            public int minStatValue;
    
            public bool ConditionIsMet ()
            {
                return GlobalVariables.GetVariable (variableID).IntegerValue >= minStatValue;
            }
    
        }
    
    }
    
  • This works perfectly!
    Thanks Chris, you're the best

  • One tiny issue remains:
    Keyboard numbers can still be used to choose an option, even if it's disabled. I'd like to preserve the number key functuality (I know I can uncheck it altogether) but maybe this is hard to work around?

  • You'd need to uncheck the Dialogue options can be selected with number keys? option in favour of a custom script attached to each Button in your Conversation menu's UI prefab, that detects its own input and ignores it if the Button itself is non-interactable:

    using UnityEngine;
    using AC;
    
    public class CustomConversationKeys : MonoBehaviour
    {
    
        public KeyCode keyCode;
        public int optionIndex;
    
        void OnGUI ()
        {
            Event e = Event.current;
            if (e.isKey && e.type == EventType.KeyDown && e.keyCode == keyCode)
            {
                Conversation conversation = KickStarter.playerInput.activeConversation;
                if (conversation == null) return;
    
                if (!GetComponent<UnityEngine.UI.Button> ().interactable) return;
    
                conversation.RunOption (optionIndex);
                return;
            }
        }
    
    }
    
  • edited October 2023

    Late reply here, but anyway;

    I created the script above and attached it to each button. Everything compiles neatly.
    First I feel kind of stupid because I don't find the simple "1" key, so I have to go with the "keypad 1" which is fine for now, but still...

    Second, even when I put "1" in the option index, and 2 for number 2, there's some kind of displacement happening, because keypad 1 ends up picking option #2, and keypad 2 triggers option #3, etc, and the first option can't even be picked because keypad 0 doesn't do anything.

    I solved this simply by putting 0 for option 1, and so on, so it works well now. Just thought you should know!

  • I don't find the simple "1" key, so I have to go with the "keypad 1"

    Use "Alpha 1" to reference the "1" key at the top of the keyboard.

    keypad 1 ends up picking option #2, and keypad 2 triggers option #3, etc

    The option index references an array, which starts from zero. If you want 1 to equate to the first entry, 2 for the 2nd, etc, replace:

    conversation.RunOption (optionIndex);
    

    with:

    conversation.RunOption (optionIndex-1);
    
  • edited October 2023

    Thanks, this is all clear now.

    One remaining problem:
    While the keyboard number keys work as intended, I'm still unable to navigate the options with arrow keys, or joystick/controller if the first option(s) is an non-interactable one.

    I assume the custom script complicates things, because the way it's coded now, the options that the player can't choose are non-interactable. If the first option is such an non-interactable, the player can't click on anything to proceed.

    This is obviously important if I want to able to play with a controller...

  • PS: If I, when I use the Select Element action, set the slot index to one that I know is clickable, it works fine. If there existed an action for checking whether an option is interactable, I could fix this by changing the selected slot until if finds an interactable one.
    But I suspect this needs a custom script...

  • edited October 2023

    Yes - the custom script works by overriding the Interactable state of your Buttons, irrespective of what AC wants to select.

    The best time to update these states is only when the UI is changed - i.e. the Menu turns on, or the options are shifted. That way, we can run additional code afterwards to correct the selection.

    At the moment, we've only been able to get this working in an Update loop - i.e. every frame.

    Let's try again with the first approach. Here's a modified script - it needs to be placed on the Conversation object, not the UI prefab. Is it working now? If so, we can then extend it to handle selection:

    using UnityEngine;
    using AC;
    
    public class DynamicOptions : MonoBehaviour
    {
    
        [SerializeField] private OptionCondition[] optionConditions = new OptionCondition[0];
    
        void OnEnable ()
        { 
            EventManager.OnMenuTurnOn += OnMenuTurnOn; 
            EventManager.OnMenuElementShift += OnMenuElementShift; 
        }
    
        void OnDisable ()
        { 
            EventManager.OnMenuTurnOn -= OnMenuTurnOn; 
            EventManager.OnMenuElementShift -= OnMenuElementShift; 
        }
    
        void OnMenuTurnOn (Menu menu, bool isInstant)
        {
            foreach (MenuElement element in menu.elements)
            {
                if (!(element is MenuDialogList)) continue;
                MenuDialogList dialogList = (MenuDialogList) element;
                UpdateUI (KickStarter.playerInput.activeConversation, dialogList);
            }
        }
    
        void OnMenuElementShift (MenuElement element, AC_ShiftInventory shiftType)
        {
            if (!(element is MenuDialogList)) return;
            MenuDialogList dialogList = (MenuDialogList) element;
            UpdateUI (KickStarter.playerInput.activeConversation, dialogList);
        }
    
        void UpdateUI (Conversation conversation, MenuDialogList dialogList)
        {
            if (dialogList == null || conversation == null || conversation.gameObject != gameObject) return;
    
            for (int i = 0; i < dialogList.GetNumSlots (); i++)
            {
                int ID = dialogList.GetDialogueOption (i).ID;
                foreach (OptionCondition condition in optionConditions)
                {
                    if (condition.optionID == ID)
                    {
                        dialogList.uiSlots[i].uiButton.interactable = condition.ConditionIsMet ();
                        break;
                    }
                }
            }
        }
    
        [System.Serializable]
        private class OptionCondition
        {
    
            public int optionID;
            public int variableID;
            public int minStatValue;
    
            public bool ConditionIsMet ()
            {
                return GlobalVariables.GetVariable (variableID).IntegerValue >= minStatValue;
            }
    
        }
    
    }
    
  • This script does not disable any options when I fill the fields with the requirements.

    It compiles and all the fields appear as before, but has no effect.

  • OK, I've done some digging on this. The issue is that AC is updating it at the same time. This'll be compounded if your Menu has a transition - or does it appear instantly?

    If it uses a transitionn, the most reliable way to avoid the conflict is to instead affect a CanvasGroup component that's you'd need to attach to each Button in the prefab, by replacing:

    dialogList.uiSlots[i].uiButton.interactable = condition.ConditionIsMet ();
    

    with:

    dialogList.uiSlots[i].uiButton.GetComponentInParent<CanvasGroup> ().interactable = condition.ConditionIsMet ();
    

    However, we might find that the core idea of setting the interactability of individual buttons conflicts with direct-navigation. This may not be an issue if all options are displayed at once - but if you involve scrolling (i.e. not all options are visible at once), then you're likely to get issues - since scrolling requires that options be selectable to navigate between them.

    What might be a preferable approach would be to instead still allow Buttons to be interactable - but instead change their appearance and click behaviour so that they have no effect.

    If you can share details on the UI's exact needs, I can help further.

  • Amazingly, it works if I just set transition type to none!

    It doesn't automatically select the first interactable option (which would be optimal), but at least it allows me to toggle to one, if the first one is disabled. Then it prevents me from toggling back to the disabled one. So I'm happy with this :)

  • Alas, I might have spoken too soon - it works most of the times, but not always. Sometimes the same conversation works at one point only to go back to the frozen state next time.

    I think I'll just work my way around this issue by simply making sure the top dialogue option is always interactable, and never subject to any skill checks. That seems like a fools proof solution...

  • The other approach would be to make them interactable, but unresponsive. That same line could be replaced with e.g.:

    var colors = button.colors;
    colors.normalColor = condition.ConditionIsMet () ? Color.green : Color.red;
    button.colors = colors;
    

    which will make the Button's colour change, but still be interactable.

    In terms of making it unresponsive: currently there's no way to prevent clicking an option from running it, but I can look into providing such an option as part of the next release.

  • Hello again, sorry for revisiting this thread so late, but the reason is the code and the dialogues work perfectly, so I've not been inclined to meddle more with it. However, I still get red warnings sometimes, so I thought I'd address this before releasing the game.

    The code:

    using System.Collections;
    using UnityEngine;
    using AC;
    
    public class DynamicOptions : MonoBehaviour
    {
    
        [SerializeField] private OptionCondition[] optionConditions = new OptionCondition[0];
        [SerializeField] private string menuName = "Conversation";
        [SerializeField] private string elementName = "DialogueList";
        private MenuDialogList dialogList;
    
        private void LateUpdate()
        {
            if (dialogList == null) dialogList = (MenuDialogList)PlayerMenus.GetElementWithName(menuName, elementName);
    
            UpdateUI(KickStarter.playerInput.activeConversation, dialogList);
        }
    
        private void UpdateUI(Conversation conversation, MenuDialogList dialogList)
        {
            if (dialogList == null || conversation == null || conversation.gameObject != gameObject) return;
    
            for (int i = 0; i < dialogList.GetNumSlots(); i++)
            {
                int ID = dialogList.GetDialogueOption(i).ID;
                foreach (OptionCondition condition in optionConditions)
                {
                    if (condition.optionID == ID)
                    {
                        dialogList.uiSlots[i].uiButton.interactable = condition.ConditionIsMet();
                        break;
                    }
                }
            }
        }
    
        [System.Serializable]
        private class OptionCondition
        {
    
            public int optionID;
            public int variableID;
            public int minStatValue;
    
            public bool ConditionIsMet()
            {
                return GlobalVariables.GetVariable(variableID).IntegerValue >= minStatValue;
            }
    
        }
    
    }
    

    The warning:
    NullReferenceException: Object reference not set to an instance of an object
    DynamicOptions.UpdateUI (AC.Conversation conversation, AC.MenuDialogList dialogList) (at Assets/Scripts/DynamicOptions.cs:26)
    DynamicOptions.LateUpdate () (at Assets/Scripts/DynamicOptions.cs:17)

    I suppose it's just another null check, but I'm quite terrified of screwing up this close to launch...

  • edited April 18

    Is there a pattern as to when exactly the errors show? It may be down to e.g. a single-frame where a Conversation is active, but the DialogList element hasn't been set to display it.

    If that's the case, a null-check ought to be OK. Since the code runs each frame, any momentary issue should resolve itself in the next call.

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.