4
\$\begingroup\$

I recently made dialogues. Now I want to add the option to choose yes/no when the dialogue is finished (if it has a choice).

Adding buttons to the dialogue is no problem, the problem I'm having is that I want a "generic" dialogue system. By that I mean, I might have 200 NPCs, and depending on which NPC I am talking to, the yes/no buttons will react differently. This is where I'm stuck. How will the buttons know which methods to call?

Say I have one NPC that offers to sell me an item for 100 gold, and another NPC that offers to show me around the village. How will my system know which functions to call when I press "Yes"?

All I can find on this subject is really complicated using trees and things I don't fully understand yet, and don't really need, since my system is very simple.

\$\endgroup\$
10
\$\begingroup\$

Here you go:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
using UnityEngine.Events;

[RequireComponent (typeof(CanvasGroup))]
public class GenericDialog : MonoBehaviour {

    public Text title;
    public Text message;
    public Text accept, decline;
    public Button acceptButton, declineButton;

    private CanvasGroup cg;

    void Awake(){
        cg = GetComponent<CanvasGroup>();
    }

    public GenericDialog OnAccept(string text, UnityAction action){
        accept.text = text;
        acceptButton.onClick.RemoveAllListeners();
        acceptButton.onClick.AddListener(action);
        return this;
    }



    public GenericDialog OnDecline(string text, UnityAction action){
        decline.text = text;
        declineButton.onClick.RemoveAllListeners();
        declineButton.onClick.AddListener(action);
        return this;
    }

    public GenericDialog Title(string title){
        this.title.text = title;
        return this;
    }

    public GenericDialog Message(string message){
        this.message.text = message;
        return this;
    }

    // show the dialog, set it's canvasGroup.alpha to 1f or tween like here
    public void Show(){
        this.transform.SetAsLastSibling();
        cg.DOKill();
        cg.blocksRaycasts = true;
        cg.interactable = true;
        cg.DOFade(1f, 0.3f);
    }

    public void Hide(){
        cg.DOKill();
        cg.interactable = false;
        cg.DOFade(0f, 0.3f).OnComplete(() => cg.blocksRaycasts = false);
    }

    private static GenericDialog instance;
    public static GenericDialog Instance() {
        if(!instance){
            instance = FindObjectOfType(typeof (GenericDialog)) as GenericDialog;
            if(!instance)
                Debug.Log("There need to be at least one active GenericDialog on the scene");
        }

        return instance;
    }

}

Usage:

GenericDialog dialog = GenericDialog.Instance();
dialog.SetTitle("Hello player!");
dialog.SetMessage("Do you want to exchange some gems for 100 gold?");
dialog.SetOnAccept("Yes", () => { // define what happens when user clicks Yes:
    if(player.HasGems(10)){
        player.RemoveGems(10);
        player.AddGold(100);
    }
    dialog.Hide();
});

dialog.SetOnDecline("No thanks", () => dialog.Hide());
dialog.Show();

You can chain these like:

dialog.SetTitle("ABC").SetMessage("DEF").Show(); // cause of a builder pattern

Now, create your beautiful dialog on the scene, add Text components, Buttons and more. Also, add a CanvasGroup to fade the dialog in and out.

Add this GenericDialog script to the dialog's gameobject on the scene and drag and drop your components on that script's fields.

You can access the above code from anywhere cause of a singleton pattern.

Feel free to ask any questions!

If you don't use any Tweening library like DOTween, just change Show() method contents to:

public void Show(){
    this.transform.SetAsLastSibling();
    cg.interactable = true;
    cg.alpha = 1f;
    cg.blockRaycasts = true;
}

and Hide() method too.

Dialog with pages

Ok so, first of all, we create a Page script and add this code to it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent (typeof(CanvasGroup))]
public class Page : MonoBehaviour {

    public Text pageText;
    private CanvasGroup cg;

    void Awake(){
        cg = GetComponent<CanvasGroup>();
    }

    public void Show(){
        this.cg.alpha = 1f;
        this.cg.interactable = true; // in case it's a scrollview or something and you want to scroll - it has to be interactable to allow clicks
        this.cg.blocksRaycasts = true;
    }

    public void Hide(){
        this.cg.alpha = 0f;
        this.cg.interactable = false;
        this.cg.blocksRaycasts = false;
    }
}

Then, we create an object like here, add a Page component to it and attach the one object - Text to the script.

enter image description here

Then, you need to make the page a PREFAB - so just move the page object to the Assets window from the scene. When it becomes blue, we're done. You can remove the object from the scene. We'll use the prefab later.

Now we create the PageableDialog class. I've explained how it works inside.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using System;

[RequireComponent (typeof(CanvasGroup))]
public class PageableDialog : MonoBehaviour {

    // title and subtitle for the entire dialog, like NPC name and quest name or something
    public Text title, subtitle;

    // buttons and their text components (Prev/Next and on the last page - Accept)
    public Text nextText, prevText;
    public Button prevButton, nextButton;

    // accept text when we're on the last page of the dialog
    private string acceptText = "Accept Quest"; 

    // what happens when user clicks Accept
    private UnityAction onAcceptAction;

    // canvas group for the entire dialog
    private CanvasGroup cg;

    // prefab for the page object (it has just a text component)
    public Page pagePrefab;

    // list of pages (added through AddPage())
    public List<Page> pages = new List<Page>();

    // current page index 
    private int _currentPage;


    void Awake(){
        cg = GetComponent<CanvasGroup>();

        _currentPage = 0;

        // prevbutton always shows the previous page so (currentPage - 1)
        prevButton.onClick.AddListener(() => {
            if(_currentPage > 0){ // so we don't exceed the pages list
                ShowPage(_currentPage - 1);
            }
        });

        // nextButton always shows the next page (current + 1)
        nextButton.onClick.AddListener(() => {
            if(_currentPage < pages.Count - 1){ // so we don't exceed the pages list
                ShowPage(_currentPage + 1);
            }
        });
    }

    // you can customize this method and the Page class so it has some scrolls, buttons etc.
    // you can add different AddPage methods simultanously, one which adds a scrollview, one adds a text etc. 
    // then, customize the Page class
    public void AddPage(string pageText){
        Page page = Instantiate(pagePrefab, this.transform);
        page.pageText.text = pageText;
        pages.Add(page);

    }

    // what happens on the last page, when user clicks Accept
    public PageableDialog OnAccept(string text, UnityAction action){
        acceptText = text;
        onAcceptAction = action;
        return this;
    }

    public PageableDialog SetTitle(string title){
        this.title.text = title;
        return this;
    }

    public PageableDialog SetSubtitle(string subtitle){
        this.subtitle.text = subtitle;
        return this;
    }

    public void Hide(){
        cg.alpha = 0f;
        cg.interactable = false;
        cg.blocksRaycasts = false;

        // destroy or reset, better destroy cause we create dialogs in DialogManager
        // when we're finished, we destroy the dialog object. reset is not needed, you can remove it.
        Reset();
        Destroy(this.gameObject); 
    }

    // we show the entire dialog and then, show the first page instantly.
    // we also setasLastSibling to make the dialog on top of other things on canvas.
    public void Show(){
        this.transform.SetAsLastSibling();
        cg.alpha = 1f;
        cg.interactable = true;
        cg.blocksRaycasts = true;

        // show the 1st page

        if(pages.Count > 0){
            ShowPage(0);
        } else throw new Exception ("We have an error, m4te! Wrong implementation. You have to add pages to the dialog before showing it.");
    }

    public void ShowPage(int pageNum){

        // safety check
        if(pageNum < 0 || pageNum >= pages.Count)
            pageNum = 0;

        // we set the currentpage
        _currentPage = pageNum;

        // if we show page 0, we disable the "Prev" button
        if(_currentPage == 0) prevButton.gameObject.SetActive(false);
        else prevButton.gameObject.SetActive(true);

        // if we're showing the last page, we change the button's text to our accept text, like "Accept Quest" and add the OnAcceptAction to be performed when clicked.
        if(_currentPage == (pages.Count - 1)){
            nextText.text = acceptText;
            nextButton.onClick.AddListener(onAcceptAction);
        } 
        else // otherwise, we remove this listener or it would accept the quest on each page :D
        {
            nextText.text = "Next Page";
            nextButton.onClick.RemoveListener(onAcceptAction);
        }

        // finally, we iterate through pages, show the one we picked and hide the rest.
        for(int i=0;i<pages.Count;i++){
            if(i == pageNum){
                pages[i].Show();
            } else pages[i].Hide();
        }


    }

    // if you want to reuse dialogs, use this method to reset them, but don't reuse. 
    private void Reset(){
        foreach(Page p in pages){
            p.Hide();
        }

        _currentPage = 0;
    }

}

Then, we create a dialog on the scene so it looks similar to this:

enter image description here

Then, add a PageableDialog component to your new dialog. Remember to add a CanvasGroup to the main object. Then, two things.

Drop the Page prefab (this one made before) onto the PageableDialog object on the scene and set it's width/height so it looks like you want on the dialog. Then, drop the Page back onto the prefab to save changes and REMOVE Page object from the PageableDialog so it looks exactly like on the image above.

Then, set the CanvasGroup alpha back to 0 so it's invisible during start and save PageableDialog as a prefab too so you have two prefabs now.

Here's how it should look like:

enter image description here

Finally, create a 3rd class, DialogsManager:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DialogsManager : MonoBehaviour {

    public PageableDialog pdPrefab;
    public Canvas canvas; // dialogs must be on some canvas

    public PageableDialog CreatePageableDialog(){
        return Instantiate(pdPrefab, canvas.transform);
    }

    private static DialogsManager instance;
    public static DialogsManager Instance() {
        if(!instance){
            instance = FindObjectOfType(typeof (DialogsManager)) as DialogsManager;
            if(!instance)
                Debug.Log("There need to be at least one active DialogsManager on the scene");
        }

        return instance;
    }
}

A singleton pattern creates a new dialog for us. We can customize it and show, wherever we want.

To use a dialog, make some button on your scene and make it run this code:

PageableDialog dialog = DialogsManager.Instance().CreatePageableDialog();
dialog.AddPage("Hello there 1");
dialog.AddPage("Hello there 2");
dialog.AddPage("Hello there 3");
dialog.OnAccept("Accept!", () => { 
    Debug.Log("FINISHED");
    dialog.Hide();
});
dialog.SetTitle("Some NPC");
dialog.SetSubtitle("Some quest");
dialog.Show();

Also, create a Close Button in the PageableDialog prefab. Make it run method PageableDialog.Hide() like there:

enter image description here

so it closes and destroys the dialog.

If you're lazy, here's a complete package I've just shown above: https://www.dropbox.com/s/lirllhhx3afaag0/DialogsScene.unitypackage?dl=0

Create a new project, click Assets -> Import -> Custom Package. Open the TestScene and click the button in the game.

Have fun! :)

\$\endgroup\$
  • \$\begingroup\$ Thank you so much! This looks like exactly what I need with a few modifications :) Without having tested it the only question I have is it possible to create, for example, 4 "pages" (i.e click to get more text), and then display a page with options? \$\endgroup\$ – Majs Jun 10 '17 at 14:59
  • 1
    \$\begingroup\$ You could make another class, for example PageableDialog and modify it so it has a list of PageDialogs (single dialogs, but not singletons!!!). Then, in the PageableDialog we'd need to create a method AddPage(string message) which would add a new page, modify the Show() method to show only the first page and mess with listeners, so they swap pages until the last page. I'll write it when I come back home in couple hours ;) \$\endgroup\$ – Jacob Jun 10 '17 at 15:15
  • \$\begingroup\$ Love you get back safely \$\endgroup\$ – Majs Jun 10 '17 at 15:40
  • \$\begingroup\$ This is a brilliant idea. Good job. \$\endgroup\$ – S. Tarık Çetin Jun 11 '17 at 10:47
  • 2
    \$\begingroup\$ Here you go guys, I've added pageable dialogs. Love you \$\endgroup\$ – Jacob Jun 11 '17 at 13:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.