要知道有哪些人,與他們顯示對話的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列表,
找到DOTween,並按照圖片所示的進行Download與Import。
找到DOTween,並按照圖片所示的進行Download與Import。
按了Import的按鈕後,會跳出Import的視窗。繼續按下Import的按鈕。
之後會跳出此一新視窗,按下視窗下方的按鈕Open DOTween Utility Panel。
之後會跳出此一新視窗,按下視窗下方的Apply按鈕即可,就可以關掉Import DOTween的相關視窗。
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(區域),圖中綠色的大方塊即為一個區域。
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-當條件滿足時,不用按按鍵,可以直接觸發的對話