項目 divider 尚未註冊或是沒有一個 view.php 檔案.

《Carto》實作對話系統03-建構程式

要知道有哪些人,與他們顯示對話的UI物件

新增Role腳本,該腳本用來記錄”會說話的人有誰”。

刪除腳本裡所有程式。

Role

新增叫Role的enum(列舉),並在列舉中加入所有會進行對話的角色名稱或代稱。

如圖中範例A、B、C分別為三個角色,而A、B、C皆可替換成Player等等。

public enum Role
{
    A,
    B,
    C,
}

新增RoleObj的前置動作

在建構該腳本前,需要Import DOTween這個Package進Unity。

還沒擁有DOTween的人,可以點擊下方連結去獲取。

DOTween (HOTween v2) | Animation Tools | Unity Asset Store

如果擁有的話,打開Unity的Package Manager,選擇自己的Assets列表,

PackageManager列表選擇

找到DOTween,並按照圖片所示的進行Download與Import。

PackgeManager的DOTween頁面

找到DOTween,並按照圖片所示的進行Download與Import。

Import Package視窗

按了Import的按鈕後,會跳出Import的視窗。繼續按下Import的按鈕。

DOTween匯入說明視窗

之後會跳出此一新視窗,按下視窗下方的按鈕Open DOTween Utility Panel。

DOTween Utility Panel

之後會跳出此一新視窗,按下視窗下方的Apply按鈕即可,就可以關掉Import DOTween的相關視窗。

DOTween Utility Panel的Set Up頁面

RoleObj

新增RoleObj腳本,該腳本用來記錄”是誰”與”用來對話的UI有哪些跟怎麼用”。

希望能讓對話氣泡打開時是逐漸放大、關閉時是逐漸縮小至看不見。
要簡單的實作這個功能,需要引入DOTween的名稱空間(Namespace)叫做DG.Tweening,如此才能使用其中的擴充方法。

而對於要使用TextMeshPro的類別,也要引入TextMeshPro的名稱空間(Namespace)叫做TMPro。

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

public class RoleObj : MonoBehaviour
{
    public Role role;
    private RectTransform rect_DialogueBubble;
    private TMP_Text Text;

    private void Start()
    {
        rect_DialogueBubble = GetComponentInChildren<Image>().gameObject.GetComponent<RectTransform>();
        //從子物件們上找有Image元件的物件,並取得物件的RectTransform元件。
        Text = GetComponentInChildren<TextMeshProUGUI>();
        //從子物件們上找有沒有TextMeshProUGUI元件,有的話則回傳該元件,沒有的話則回傳null。

        SetDialogueBubble(false);
    }

    /// <summary>
    ///設定對話氣泡物件是否是啟用狀態
    /// </summary>
    public void SetDialogueBubble(bool isActive)
    {
        rect_DialogueBubble.gameObject.SetActive(isActive);
    }
    /// <summary>
    ///設定要顯示的文字
    /// </summary>
    public void SetText(string text)
    {
        Text.text = text;
    }

    /// <summary>
    /// 讓對話氣泡逐漸放大
    /// </summary>
    public void BubbleScaleUp(string text)
    {
        SetDialogueBubble(true);

        SetText(text);

        rect_DialogueBubble.DOScale(Vector3.one, 0.2f);
        //在經過0.2秒後,讓對話氣泡的RectTransform元件的Scale值變成(1,1,1)
    }

    /// <summary>
    /// 讓對話氣泡逐漸縮小
    /// </summary>
    public void BubbleScaleDown()
    {
        rect_DialogueBubble.DOScale(Vector3.zero, 0.2f);
    }

}

要知道誰說的、說了甚麼

Sentence

新增Sentence腳本,該腳本用來記錄”誰說”與”說了甚麼”。

開啟腳本後,刪除類別裡所有程式,並取消繼承MonoBehaviour(因為不需要),但要給兩個類別都加上[System.Serializable],這樣才能顯示在Inspector視窗上對類別屬性做調整。

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

[System.Serializable]
public class Sentence
{
    public Role speaker;//用來記錄是誰說的。

    [TextArea(3, 10)]//設定在Inspector視窗上,String欄位的最小與最大顯示列數。
    public string text;//用來記錄說了甚麼。
}

Dialogue

新增Dialogue腳本,該腳本用來記錄整個對話中”誰說”與”說了甚麼”,所以宣告了Sentence型別的串列。

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

[System.Serializable]
public class Dialogue
{
    public List<Sentence> sentences;
}

知道現在人在哪個地點

將會用OnTriggerEnter2D來判斷說玩家(藍色方塊)目前是移動到哪個Area(區域),圖中綠色的大方塊即為一個區域。

Area表示圖

SimpleMovement

新增SimpleMovement腳本,該腳本用來給予玩家移動行為。

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

public class SimpleMovement : MonoBehaviour
{
    SpriteRenderer spriteRenderer;
    Rigidbody2D rb2d;

    private void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    private void Update()
    {
        float valueH = Input.GetAxisRaw("Horizontal");//根據AD鍵或左右鍵的操作,回傳0,-1或1的值。
        float valueV = Input.GetAxisRaw("Vertical");//根據WS鍵或上下鍵的操作,回傳0,-1或1的值。

        rb2d.velocity = new Vector2(valueH, valueV) * 5;//設定Rigidbody2D元件的速度

        spriteRenderer.flipX = (valueH > 0) ? false : (valueH < 0) ? true : spriteRenderer.flipX;
        //根據水平方向上的輸入,決定SpriteRenderer元件是否要水平翻轉顯示的圖片。
    }
}

Area

新增Area腳本,該腳本用來記錄該元件所在之區域物件編號為何。

刪除類別裡的程式,並宣告int型別的變數num。

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

public class Area : MonoBehaviour
{
    public int num = 0;
}

AreaNumKeeper

新增AreaNumKeeper腳本,該腳本用來記錄玩家目前所在的Area(區域)

刪除類別裡的程式,並取消繼承MonoBehaviour(因為不需要)。

宣告int型別的靜態變數num,因為只會有一個數值,用靜態存取也比較方便。

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

public class AreaNumKeeper
{
    public static int currentNum = -1;//用來記錄當前玩家所在之區域物件編號為何。
}

各個地點有甚麼對話

AreaDialogues

新增AreaDialogues腳本,該腳本用來記錄哪個地點有哪些對話。

因為同個地點在不同情況下可能會有不同的對話。

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

[System.Serializable]
public class AreaDialogues
{
    [Min(0)]//設定變數最小值為0
    public int areaNum;//區域編號
    public List<Dialogue> dialogues;
}

而同個地點要怎麼區分要進行的是哪個對話,就是要看遊戲的進度了。

當前的遊戲進度為何

Task

遊戲進度就是看遊戲裡玩家可以做、要做的事他做到哪。

新增一個Task腳本,用來記錄玩家可以做的每件事,或是開發者希望玩家做到的事。

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


[System.Serializable]
public class Task
{
    public string name;//事情或任務名稱

    public int index;//該任務在任務總覽的腳本裡被記錄的編號或稱索引。

    public bool isCompelete = false;//紀錄任務是否完成
    
    /// <summary>
    //設定任務狀態為完成
    /// </summary>
    public void SetTaskCompelete()
    {
        isCompelete = true;
    }
}

PlayerProgress

新增一個PlayerProgress 腳本,用來設定全部有哪些任務、記錄哪個任務為完成,以及知道某個任務完成的狀況。

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

public class PlayerProgress : MonoBehaviour
{
    public List<Task> tasks;

    private void OnValidate()
    //當在Unity的Inspector視窗上,有數值或資料變化時會被呼叫的方法。
    //這裡預期的是當使用者在Inspector視窗新增Task時,
    //可以自動根據是在任務串列的第幾個任務來設定索引/編碼。
    {
        for (int i = 0; i < tasks.Count; i++)
        {
            tasks[i].index = i;
        }
    }
    
    /// <summary>
    ///設定哪個索引的任務要設定為完成狀態
    /// </summary>
    public void SetTaskCompelete(int index)
    {
        tasks[index].SetTaskCompelete();
    }
 
    /// <summary>
    ///取得特定索引的任務是否已經完成
    /// </summary>
    public bool GetIsTaskCompelete(int index)
    {
        return tasks[index].isCompelete;
    }
}

Condition

有些對話需要在特定進度或條件(Condition)下才能觸發,故新增Condition腳本。

Condition腳本設定當某個任務是否是完成或沒有完成的狀態,到時候會用遊戲進度該任務的狀態來與Condition的設定做比較。

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

[System.Serializable]
public class Condition
{
    public int taskIndex;
    public bool isTaskCompelete;
}

Dialogue

對話在某些條件下才能觸發,所以新增型別為Condition的串列,來記錄觸發條件。

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

[System.Serializable]
public class Dialogue
{
    public List<Condition> taskConditions;

    [Space(15)]//用來在Inspector視窗上的兩個變數中間增加一些空間。
    public List<Sentence> sentences;
}

某些對話是否有完成也算是遊戲任務(Task),要在Dialogue宣告變數isCompelete,用以紀錄是否對話已完成。

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

[System.Serializable]
public class Dialogue
{
    public bool isCompelete = false;
    
    public List<Condition> taskConditions;

    [Space(15)]
    public List<Sentence> sentences;
}

AreaDialogues

而要從AreaDialogues中取出對應的Dialogue要看條件是否符合以及是否已經完成對話

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

[System.Serializable]
public class AreaDialogues
{
    [Min(0)]
    public int areaNum;

    public List<Dialogue> dialogues;

    /// <summary>
    /// 核對Dialogue所要求的條件與現在PlayerProgress紀錄的任務完成狀況,來決定是否進行該對話的條件已達成
    /// </summary>
    private bool IsConditionsCompelete(PlayerProgress playerProgress, List<Condition> conditions)
    {
        for (int i = 0; i < conditions.Count; i++)
        {
            if (playerProgress.GetIsTaskCompelete(conditions[i].taskIndex)
                != conditions[i].isTaskCompelete)
            {
                return false;
            }
        }
        return true;
    }

    /// <summary>
    ///取得可以觸發的對話
    /// </summary>
    public Dialogue GetDialogue(PlayerProgress playerProgress)
    {
        for (int i = 0; i < dialogues.Count; i++)
        {
            if (IsConditionsCompelete(playerProgress, dialogues[i].taskConditions) && dialogues[i].isCompelete == false)
            {
                return dialogues[i];
            }
        }
        return null;
    }
}

對話管理器輸出對應對話

DialoguesManager

新增一個DialoguesManager腳本,用來儲存所有對話與其觸發條件,當輸入的位置資訊、遊戲進度符合條件時,將會輸出對應對話。

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

public class DialoguesManager : MonoBehaviour
{
    private Dialogue dialogue;//當前要進行的對話

    private bool isDialogueCompelete = true;
    public bool IsDialogueCompelete
    {
        get
        {
            return isDialogueCompelete;
        }
    }
    public Dialogue Dialogue
    {
        get
        {
            return dialogue;
        }
    }

    private RoleObj speakingRoleObjs;//當前正在說話的RoleObj
    private RoleObj[] roleObj;//用來儲存場景中所有RoleObj
    private Dictionary<Role, RoleObj> dic_Role_and_DialogueObj = new Dictionary<Role, RoleObj>();
    //用來儲存每個Role與其對應的RoleObj。
    //因為Sentence紀錄的是Role,而不是直接紀錄RoleObj。
    //使用Dictionary會比每次都要一個個的在roleObj裡面核對是哪個roleObj來的快。

    /// <summary>
    ///取得所有RoleObj
    /// </summary>
    private void GetAllRoleObj()
    {
        roleObj = FindObjectsOfType<RoleObj>();
    }

    /// <summary>
    ///設定Dictionary中哪個Role對應哪個RoleObj
    /// </summary>
    private void Set_Role_and_DialogueObj()
    {
        for (int i = 0; i < roleObj.Length; i++)
        {
            dic_Role_and_DialogueObj.Add(roleObj[i].role, roleObj[i]);
        }
    }

    /// <summary>
    ///根據是哪個Role,取得對應的RoleObj
    /// </summary>
    private RoleObj GetRoleObjs(Role role)
    {
        if (dic_Role_and_DialogueObj.ContainsKey(role))
        {
            return dic_Role_and_DialogueObj[role];
        }
        return null;
    }





    public List<AreaDialogues> areaDialogues;

    public PlayerProgress playerProgress;

    /// <summary>
    ///取得玩家現在所在區域的AreaDialogues
    /// </summary>
    private AreaDialogues GetAreaDialogues()
    {
        for (int i = 0; i < areaDialogues.Count; i++)
        {
            if (areaDialogues[i].areaNum == AreaNumKeeper.currentNum)
            {
                return areaDialogues[i];
            }
        }
        return null;
    }

    /// <summary>
    ///如果有取得AreaDialogues的話,取得遊玩進度滿足其條件的Dialogue。
    /// </summary>
    public void GetDialogue()
    {
        if (isDialogueCompelete)
        {
            AreaDialogues areaDialogues = GetAreaDialogues();

            if (areaDialogues != null)
            {
                dialogue = areaDialogues.GetDialogue(playerProgress);
            }
        }
    }

    /// <summary>
    ///藉由設定對話的狀態為未完成,來重複某個對話。
    /// </summary>
    public void KeepingTheDialogue(int dialogeIndex)
    {
        AreaDialogues areaDialogues = GetAreaDialogues();
        areaDialogues.dialogues[dialogeIndex].isCompelete = false;
    }





    private Vector3 dialogueRolesCenterPosition;//用來儲存對話的Role們,他們的中心座標/位置。
    public Vector3 DialogueRolesCenterPosition
    {
        get
        {
            return dialogueRolesCenterPosition;
        }
    }

    /// <summary>
    ///計算中心座標
    /// </summary>
    private void CalculateCenterPosition()
    {
        dialogueRolesCenterPosition = Vector3.zero;
        for (int l = 0; l < roleObj.Length; l++)
        {
            dialogueRolesCenterPosition += roleObj[l].transform.position;
        }
        dialogueRolesCenterPosition = dialogueRolesCenterPosition / roleObj.Length;
    }

    /// <summary>
    ///縮小所有對話氣泡
    /// </summary>
    private void ScaleDownAllBubble()
    {
        for (int l = 0; l < roleObj.Length; l++)
        {
            roleObj[l].BubbleScaleDown();
        }
    }

    /// <summary>
    ///逐字顯示對話內容
    /// </summary>
    private IEnumerator DialogueTextWriter(Dialogue dialogue)
    {
        ScaleDownAllBubble();
        CalculateCenterPosition();

        bool isFinishTextSentence = false;//用來記錄是否逐字顯示完當前句子了

        int sentenceCounter = 0;//用來記錄顯示到第幾個句子了

        while (sentenceCounter < dialogue.sentences.Count)//當 當前顯示的句子數 小於 對話的總句子數 時
        {
            speakingRoleObjs = GetRoleObjs(dialogue.sentences[sentenceCounter].speaker);


            if (isFinishTextSentence == false)
            {
                string sentenceContent = dialogue.sentences[sentenceCounter].text;//取得句子的文字內容
                speakingRoleObjs.BubbleScaleUp("<color=#00000000>" + sentenceContent + "</color>");
                //呼叫對話氣泡的放大顯示方法,並輸入句子內容
                //句子內容前後的<color=#00000000>跟</color>,為設定讓文字變成透明的標籤

                yield return new WaitForSeconds(0.2f);//停個0.2秒,讓對話框放大顯示完成


                float time = 0, duration = 0.04f;

                for (int j = 0; j < dialogue.sentences[sentenceCounter].text.Length; j++)
                {
                    string txt = sentenceContent.Substring(0, j + 1) + "<color=#00000000>" + sentenceContent.Substring(j + 1) + "</color>";
                    //把句子的逐個文字設定成不透明

                    speakingRoleObjs.SetText(txt);

                    //每顯示一個字,就等0.04秒
                    while (time < duration)
                    {
                        if (Input.GetKeyDown(KeyCode.Space))
                        //如果玩家在文字逐個顯示的過程中按下空白鍵,則加倍顯示速度
                        {
                            duration = 0.02f;
                        }
                        time += Time.deltaTime;
                        yield return new WaitForSeconds(Time.deltaTime);
                    }

                    time = 0;//歸零計時器
                }

                isFinishTextSentence = true;
            }
            else
            {
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    if (sentenceCounter < dialogue.sentences.Count - 1)
                    {
                        sentenceCounter++;//用來繼續下一輪while迴圈
                        isFinishTextSentence = false;

                        speakingRoleObjs.BubbleScaleDown();
                        yield return new WaitForSeconds(0.2f);
                    }
                    else if (sentenceCounter >= dialogue.sentences.Count - 1)
                    {
                        sentenceCounter++;//用來跳出while迴圈

                        speakingRoleObjs.BubbleScaleDown();
                        yield return new WaitForSeconds(0.2f);
                        speakingRoleObjs = null;


                        isDialogueCompelete = true;
                        dialogue.isCompelete = true;
                    }
                }
            }
            yield return null;//等待一幀的時間
        }
    }

    /// <summary>
    ///開始進行對話
    /// </summary>
    public void StartDialogue()
    {
        if (dialogue != null)
        {
            isDialogueCompelete = false;
            StartCoroutine(DialogueTextWriter(dialogue));
        }
    }

    private void Start()
    {
        GetAllRoleObj();
        Set_Role_and_DialogueObj();
    }
}

TalkBehav

而要讓對話管理器輸出對話,是由玩家按下對話按鍵來啟動管理器,所以新增玩家對話行為的腳本。

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

public class TalkBehav : MonoBehaviour
{
    private RoleObj roleObj;
    private DialoguesManager dialoguesManager;
    private SpriteRenderer spR;

    private void Start()
    {
        roleObj = GetComponent<RoleObj>();

        dialoguesManager = FindObjectOfType<DialoguesManager>();
        spR = GetComponent<SpriteRenderer>();
    }

    private void Update()
    {
        InvokeDialogue();
        FaceTheSpeakerInDialogue();
    }

    /// <summary>
    ///啟動對話
    /// </summary>
    private void InvokeDialogue()
    {
        if (isInDialogueTrigger && dialoguesManager.IsDialogueCompelete)
        {            
            dialoguesManager.GetDialogue();

            if (Input.GetKeyDown(KeyCode.Space))
            {
                dialoguesManager.StartDialogue();
            }
        }
    }

    /// <summary>
    ///讓角色面對在對話的人
    /// </summary>
    private void FaceTheSpeakerInDialogue()
    {
        if (dialoguesManager.IsDialogueCompelete == false)
        {
            Vector3 speakerPosition = dialoguesManager.DialogueRolesCenterPosition;

            if (speakerPosition.x > transform.position.x)
            {
                spR.flipX = false;
            }
            else if (speakerPosition.x < transform.position.x)
            {
                spR.flipX = true;
            }
        }
    }

    private bool isInDialogueTrigger = false;

    //當進入到勾選Trigger的Collider的瞬間會執行的方法
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.layer == LayerMask.NameToLayer("Area"))
        //如果碰到的 勾選Trigger的Collider的物件 Layer是Area
        {
            AreaNumKeeper.currentNum = other.GetComponent<Area>().num;
            Debug.Log(AreaNumKeeper.currentNum);
        }
        else if (other.gameObject.layer == LayerMask.NameToLayer("DialogueTrigger"))
        {
            isInDialogueTrigger = true;
            ShowTheHint();
        }
    }

    /// <summary>
    ///顯示提示訊息
    /// </summary>
    public void ShowTheHint()
    {
        roleObj.BubbleScaleUp("...");
    }

    //當離開勾選Trigger的Collider的瞬間會執行的方法
    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.gameObject.layer == LayerMask.NameToLayer("Area"))
        {
            if (AreaNumKeeper.currentNum == other.GetComponent<Area>().num)
            {
                AreaNumKeeper.currentNum = -1;
                Debug.Log(AreaNumKeeper.currentNum);
            }
        }
        else if (other.gameObject.layer == LayerMask.NameToLayer("DialogueTrigger"))
        {
            isInDialogueTrigger = false;
            roleObj.BubbleScaleDown();
        }
    }
}

TalkBehav_NPC

而對話過程中NPC也會有回應,所以也新增NPC的對話行為腳本。

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

public class TalkBehav_NPC : MonoBehaviour
{
    private DialoguesManager dialoguesManager;
    private SpriteRenderer spR;

    private void Start()
    {
        spR = GetComponent<SpriteRenderer>();
        dialoguesManager = FindObjectOfType<DialoguesManager>();
    }

    // Update is called once per frame
    private void Update()
    {
        FaceTheSpeakerInDialogue();
    }
    private void FaceTheSpeakerInDialogue()
    {
        if (dialoguesManager.IsDialogueCompelete == false)
        {
            Vector3 speakerPosition = dialoguesManager.DialogueRolesCenterPosition;

            if (speakerPosition.x > transform.position.x)
            {
                spR.flipX = false;
            }
            else if (speakerPosition.x < transform.position.x)
            {
                spR.flipX = true;
            }
        }
    }
}

現在我們就完成了基本的對話系統腳本。

接著,就是把腳本加到相應的物件上做使用!

系列文章

《Carto》實作對話系統01-前言
《Carto》實作對話系統02-建立遊戲所需物件
《Carto》實作對話系統04-把腳本加到對應的物件上
《Carto》實作對話系統05-當TextMeshPro沒有辦法顯示中文
《Carto》實作對話系統06-改善對話系統的操作介面
《Carto》實作對話系統07-加入對話結束後會觸發的事件
《Carto》實作對話系統08-當條件滿足時,不用按按鍵,可以直接觸發的對話

Colin TPL
Colin TPL

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *