How select in dialogue the choice button with gamepad or keyboard?

I’ve done the dialogue course.
I like to use for my RPG a gamepad or keyboard. But I have problem to select a choiceButton.
How I select in dialogue the choice button with gamepad or keyboard?
The dialogue from the NPC I could manage to use the nextButton. But after that I get stuck.

This is easiest if you’re using the new Input System. While the RPG course doesn’t teach the new input system, several of our courses do, and the Third Person Combat and Traversal course breaks down using the system through code extremely well.

You can install the new Input system and still leave the old Input System active which allows us to introduce the new Input System without breaking the RPG course project.

From here on, this assumes you know how to create an input asset and add an Action Map and mappings directly to that Action Map. Here’s the Action Map I created for this particular task:

So in the Input Asset, I added (separate from any other action maps that might be in the Controls (which is what I named the Input Asset), a Dialogue Action Map. By using separate action maps for different UI enteractions, we can custom tailor an action map JUST for the Dialogue (or perhaps for a store or a Trait Window) that is only active while the Dialogue is active.
Then I added all fo the Actions into the Dialogue Action Map along with the bindings. I’m not getting into the weeds as to how that’s done, just showing you the finished Action Map.

The Select, Left, and Right actions are to move the selection through the buttons. The Exit, we’ll use to link up to the Exit button.

I also set the InputAsset up to generate a C# script (done in the regular inspector for the Controls asset.

Now with the script generated, everything else we need to do can be done in DialogueUI.

First, we need to add the interface generated in the C# class created by the InputAsset. The interface, assuming that your InputAsset is named Controls and your Action Map is named Dialogue will be

Controls.IDialogueActions

Once you add this interface, the editor will complain that DialogueUI does not implement the methods in the Controls.IDialogueActions interface.

So we’ll need to add those methods. In Visual Studio Code, you can usually do this with Cntrl . or in JetBrains Rider (my editor of choice), you can do this by right clicking on the class name and selecting “Implement Missing Members”. This will create the boilerplate code

    public void OnSelect(InputAction.CallbackContext context)
        {
            throw new System.NotImplementedException();
        }

        public void OnLeft(InputAction.CallbackContext context)
        {
            throw new System.NotImplementedException();
        }

        public void OnRight(InputAction.CallbackContext context)
        {
            throw new System.NotImplementedException();
        }



        public void OnExit(InputAction.CallbackContext context)
        {
            throw new System.NotImplementedException();
        }

We’ll be fleshing these out soon.

Next, we’re going to need to keep track of the currently active buttons within the Dialogue. For that, we’ll need a List of Buttons and an index to the currently selected button

List<Button> currentButtons = new List<Button>();
int currentIndex = 0;

We’re also going to need to add 2 lines of code to Start() so that the Controls will work for us:

            controls = new Controls();
            controls.Dialogue.SetCallbacks(this);

This gives the DialogueUI it’s own copy of the controls and tells it to send the callbacks to the methods we just created in the previous step.

In UpdateUI() we need to make a slight change, replacing the line that sets the GameObject’s active and to exit if the PlayerConversant is not currently active with some code to turn the controls on and off.

            bool active = playerConversant.IsActive();
            if (active)
            {
                controls.Enable();
                gameObject.SetActive(true);
            }
            else
            {
                controls.Disable();
                gameObject.SetActive(false);
                return;
            }

Essentially, if the playerConversant is Active, then we enable the controls and gameObject, otherwise we disable the controls so that pressing our keys doesn’t try to select dialogue actions that don’t exist.

At this point, we also need to clear the list of Active buttons

activeButtons.Clear();

as we’re going to rebuild this list shortly.
In BuildChoiceList(), we need to add each button after we’ve set the text to our activeButtons list
I put it in the last line of the foreach loop that creates the buttons:

            foreach (DialogueNode choice in playerConversant.GetChoices())
            {
                GameObject choiceInstance = Instantiate(choicePrefab, choiceRoot);
                var textComp = choiceInstance.GetComponentInChildren<TextMeshProUGUI>();
                textComp.text = choice.GetText();
                Button button = choiceInstance.GetComponentInChildren<Button>();
                button.onClick.AddListener(() => 
                {
                    playerConversant.SelectChoice(choice);
                });
                activeButtons.Add(button);
            }

And finally, at the end of the UpdateUI method, if we don’t have player choices, then we either need to enable or disable the Next button and add it to the list of buttons.
Here’s my complete UpdateUI method, so you can see in context the changes. I also set the selectedButton to 0

        void UpdateUI()
        {
            bool active = playerConversant.IsActive();
            if (active)
            {
                controls.Enable();
                gameObject.SetActive(true);
            }
            else
            {
                controls.Disable();
                gameObject.SetActive(false);
                return;
            }
            conversantName.text = playerConversant.GetCurrentConversantName();
            AIResponse.SetActive(!playerConversant.IsChoosing());
            choiceRoot.gameObject.SetActive(playerConversant.IsChoosing());
            activeButtons.Clear();
            if (playerConversant.IsChoosing())
            {
                BuildChoiceList();
            }
            else
            {
                AIText.text = playerConversant.GetText();
                nextButton.gameObject.SetActive(playerConversant.HasNext());
                if (playerConversant.HasNext())
                {
                    activeButtons.Add(nextButton);
                }
                else
                {
                    activeButtons.Add(quitButton);
                }
            }
            selectedButton = 0;
            HighlightSelectedButton();
        }

Now that last method, HighlightSelectedButton(), isn’t in the original script. This simply sets the button referenced by selectedButton to be selected.

       void HighlightSelectedButton()
        {
            if (selectedButton <= activeButtons.Count)
            {
                activeButtons[selectedButton].Select();
            }
        }

Calling the Select() method on a button puts it in the Selected state, and automatically puts all other buttons to be not selected.

Now with this framework in mind, it’s time to start fleshing out our interface methods from the beginning.

        public void OnSelect(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                if (activeButtons.Count > selectedButton)
                {
                    activeButtons[selectedButton].onClick.Invoke();
                }
            }
        }

OnSelect simply tries to invoke the currently selected button. It only takes action if the button has been pressed. If it has, then it makes sure that selectedButton is valid (it should always be between zero and the number of elements in the activeButtons, but I have a firm belief that you should always check to ensure things don’t go wrong. If it’s valid, then the button is invoked.

If the player is not choosing, and there is a HasNext() with the Next button active, then selectedButton will be zero, and the Next Button will be the one invoked (remember, we added it to the empty list if there were no player responses and a HasNext() was valid.

If the player is not choosing and there’s no Next(), then the QuitButton is the 0th element, and pressing the Select button will simply close the window just like if we had called Exit.

If the player is choosing, then the index will correspond to the choices in the list, and that’s the button that will be pressed.

That gets us to the player actually choosing… which are our Left and Right methods… these simply increase or decrease the selectedButton index, keeping the value valid.

        public void OnLeft(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                selectedButton -= 1;
                if (selectedButton < 0) selectedButton += activeButtons.Count;
                HighlightSelectedButton();
            }
        }
        
        public void OnRight(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                selectedButton += 1;
                if (selectedButton >= activeButtons.Count) selectedButton = 0;
                HighlightSelectedButton();
            }
        }

Finally, the Exit handles calling the quit button directly

        public void OnExit(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                quitButton.onClick.Invoke();
            }
        }

This should allow both the Keyboard and the Gamepad/Xbox controller to select the buttons.

Here’s the complete DialogueUI script

DialogueUI.cs
using System.Collections.Generic;
using UnityEngine;
using RPG.Dialogue;
using RPG.InputReading;
using TMPro;
using UnityEngine.InputSystem;
using UnityEngine.UI;

namespace RPG.UI
{
    public class DialogueUI : MonoBehaviour, Controls.IDialogueActions
    {
        PlayerConversant playerConversant;
        [SerializeField] TextMeshProUGUI AIText;
        [SerializeField] Button nextButton;
        [SerializeField] GameObject AIResponse;
        [SerializeField] Transform choiceRoot;
        [SerializeField] GameObject choicePrefab;
        [SerializeField] Button quitButton;
        [SerializeField] TextMeshProUGUI conversantName;

        private Controls controls;
        
        private List<Button> activeButtons = new List<Button>();
        private int selectedButton = 0;
        
        // Start is called before the first frame update
        void Start()
        {
            playerConversant = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerConversant>();
            playerConversant.onConversationUpdated += UpdateUI;
            nextButton.onClick.AddListener(() => playerConversant.Next());
            quitButton.onClick.AddListener(() => playerConversant.Quit());
            controls = new Controls();
            controls.Dialogue.SetCallbacks(this);
            UpdateUI();
        }

        void UpdateUI()
        {
            bool active = playerConversant.IsActive();
            if (active)
            {
                controls.Enable();
                gameObject.SetActive(true);
            }
            else
            {
                controls.Disable();
                gameObject.SetActive(false);
                return;
            }
            conversantName.text = playerConversant.GetCurrentConversantName();
            AIResponse.SetActive(!playerConversant.IsChoosing());
            choiceRoot.gameObject.SetActive(playerConversant.IsChoosing());
            activeButtons.Clear();
            if (playerConversant.IsChoosing())
            {
                BuildChoiceList();
            }
            else
            {
                AIText.text = playerConversant.GetText();
                nextButton.gameObject.SetActive(playerConversant.HasNext());
                if (playerConversant.HasNext())
                {
                    activeButtons.Add(nextButton);
                }
                else
                {
                    activeButtons.Add(quitButton);
                }
            }
            selectedButton = 0;
            HighlightSelectedButton();
        }


        
        private void BuildChoiceList()
        {
            foreach (Transform item in choiceRoot)
            {
                Destroy(item.gameObject);
            }

            foreach (DialogueNode choice in playerConversant.GetChoices())
            {
                GameObject choiceInstance = Instantiate(choicePrefab, choiceRoot);
                var textComp = choiceInstance.GetComponentInChildren<TextMeshProUGUI>();
                textComp.text = choice.GetText();
                Button button = choiceInstance.GetComponentInChildren<Button>();
                button.onClick.AddListener(() => 
                {
                    playerConversant.SelectChoice(choice);
                });
                activeButtons.Add(button);
            }
        }
        
        public void OnSelect(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                if (activeButtons.Count > selectedButton)
                {
                    activeButtons[selectedButton].onClick.Invoke();
                }
            }
        }
        
        public void OnLeft(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                selectedButton -= 1;
                if (selectedButton < 0) selectedButton += activeButtons.Count;
                HighlightSelectedButton();
            }
        }
        
        public void OnRight(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                selectedButton += 1;
                if (selectedButton >= activeButtons.Count) selectedButton = 0;
                HighlightSelectedButton();
            }
        }
        
        public void OnExit(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                quitButton.onClick.Invoke();
            }
        }

        void HighlightSelectedButton()
        {
            if (selectedButton <= activeButtons.Count)
            {
                activeButtons[selectedButton].Select();
            }
        }
    }
}
1 Like

Hi Brian
Thank you so much for your answer!!! Very super explanation.
And sorry my late reply, I was on a musicfestival last days.
Today I have worked on the code. It works, but I have still some failures.

Choosing a choiceButton works correct only if I use the left-Button on the gamepad/keyboard. Otherwise it will give always the selectedButton = 0. The ExitButton too.
Just at the end of the dialogue, the ExitButton works correctly. But then the other problem is, that the player movements don’t working anymore. I have to press again the exit Button on the gamepad/keyboard and then it works.
If I exit the dialogue with the gamepad-ExitButton or EscapeButton from the keyboard, it works correct.
The last problem is: When I start the dialogue, the next button is pressed automatically and the first node is not visible.

Here is the code how I invoke the dialogue.

private void SelectNPC()
        {
            if (menuIsOpen!= true)
            {
                int layerMask = 1 << 8;
                //layerMask = ~layerMask;
                Vector3 versatz = new Vector3(0f, 3f, 0f);


                RaycastHit hitInfo;
                if (Physics.Raycast(transform.position, transform.TransformDirection(lookDirection), out hitInfo, Mathf.Infinity, layerMask))
                {

                    Debug.DrawRay(transform.position + versatz, transform.TransformDirection(lookDirection) * hitInfo.distance, Color.red);
                    Debug.Log(hitInfo.transform.name + " HitDistance = " + hitInfo.distance);

                    if (hitInfo.distance < selectDistance)
                    {
                        canSelect = true;

                        dialogueAI = hitInfo.transform.GetComponentInParent<AIConversant>();
                        Debug.Log("Player in select distance");
                    }
                    else { canSelect = false; }

                }
                else
                {
                    Debug.DrawRay(transform.position + versatz, transform.TransformDirection(lookDirection) * 1000, Color.blue);                    
                }

                float selectNPC = playerInput.Player.Select.ReadValue<float>();                

                if (selectNPC == 1 && canSelect == true)
                {
                    dialogueAI.HandleDialogue(this);
                    Debug.Log("You Select an NPC");
                    menuIsOpen = true;
                    dialogueIsActive = true;
                }                
            }            
        }

I was getting similarly odd results if I didn’t tell the Event System not to send navigation events. The EventSystem is kept within the persistentObjectPrefab in the course.
image

Hi Brian
Thank you so much!!!
Now it works fine.

But just two things still don’t works and I couldn’t solve them. :grimacing:

  1. If I start the Dialogue, the root node showing very quick and then it goes automatically to the next node. Then it works fine. It looks like a double click.

  2. If I close the Dialogue through the Exit Button then My Movements don’t working. I have alway to press the Exit Button on my controller too. It seems the Exit-Button in the dialogue don’t exit definitly.

That seems odd, as I set the OnExit() to go through quitButton.OnClick. In other words, the Quit button is being clicked by the controller’s OnExit…
What does happen when you click the Dialogue’s exit button?

  • I can choose the Diaogue exit button with the controller just at the end of a dialogue. The Dialogue quit. Seems good but then my movement inputs doesn’t works. I have to press the exit button on the controller.

  • The same if I cklick the dialogue exit button with the Mouse at any dialogue node.

  • If I exit with the controller for example in the middle of the dialogue, it works everything perfect.

So when you enter a Dialogue, are your turning off the movement inputs? (Please show what you’re doing there)

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms