前言
本文將演示在unity中實現類似galgame的對話效果,并且通過Excel進行文本、圖片、選項、賦值、音樂的配置
對話框架效果示意圖

Excel表格總覽
整體框架與主要思路
要實現的效果
每點擊一次鼠標,就出現下一個對話/或者出現選項;
如果出現選項,點擊選項,會有不同的對話;
對話的時候人物的立繪會有改變;
可以進行簡單的數值判斷與賦值;
可以播放音樂與音效
整體思路
通過標志與ID來決定下一個對話播放第幾個
標志位
"#"號是普通的對話行,屏幕里會出現名字與人物
"&"號是選項,這一行會以一個選項的方式出現,內容會以選項上的文字表現

以上圖舉例,最底下的文字就是用"#"號行來表現;右側的兩個選項通過兩行"&"號表現,選項上的文字就是那兩行"內容"列里的文字
"end"標志符就是結束整段對話
跳轉
每一行都有自己的ID,也有"跳轉"列。當這一行結束后(一般通過點擊繼續(xù)放下一個),會跳轉到"跳轉列"的ID所在的行
效果與判定
判定:在上圖ID為2的行里,"判定"那一列有"好感度>10"的文字,如果此時好感度>=10,那么之后會跳轉到5(跳轉列里"&"符號前面的那一個數字),否則就是跳轉到4
效果:在上圖ID為3-5的行里,有"好感度+-10"的文字,"+"用來區(qū)分數值名與數值改變量,所以是"好感度"這個數值,進行"-10"的操作
?*號行
如果需要多個條件都滿足才能跳轉到下一行呢?
ID為7的"是時候打開網抑云了"需要時間晚于23:00,并且活力要大于20,此時我們發(fā)現一行只寫一個判定不夠
故引入"*"號行。這個標志符所代表的行不會出現在游戲的畫面上,不需要玩家點擊才繼續(xù),而是自動觸發(fā)并自動跳轉到下一個要跳轉的行。
"*"號行可以進行效果賦值、條件判斷以及人物清空(這在下文的"人物"板塊會提到)、背景替換等等功能
"*"號行可以多個混合使用,看具體實現情況而定
人物
在對話時,我們需要人物立繪來展現效果(進行激烈的立繪碰撞 bushi)

在Excel示例表中,主要通過"人物立繪圖片","人物位置"進行控制
"人物立繪圖片"由人物的底+人物表情組成,通過"&"符號進行分隔
"人物位置"Left就是在左邊出現,Right就是在右邊出現,Clear會清除這個人所有的立繪
音樂音效
通過"背景音樂"和"音效"控制
"背景音樂"會循環(huán)播放
"音效"只播放一次
實現
導入
在寫完需要導入的劇情表格后,另存為csv格式(注意保存為 UTF-8編碼)
這一部分與本系列第一篇類似,這里直接貼代碼
Excel與Unity工作流(一):使用Excel進行屬性配置-CSDN博客
public void SetTextAsset(string ResourcesPath)
{
textAsset = Resources.Load<TextAsset>(ResourcesPath);
textAssetPath = ResourcesPath;
}
public void ReadText(TextAsset _textAsset)
{
dialogRows = null;
dialogRows = _textAsset.text.Split('\n');
cells = new string[dialogRows.Length][];
for (int i = 0; i < dialogRows.Length; i++)
{
cells[i] = dialogRows[i].Split(',');
}
}
對話框架
具體的unity內部圖片與按鈕放置可以看參考鏈接視頻(本文的對話框架就是基于那個視頻進行的修改添加)
https://www.bilibili.com/video/BV1v5411D79x/文章來源地址http://www.zghlxwxcb.cn/news/detail-851274.html
string textAssetPath;
[SerializeField] TextAsset textAsset;//輸入的文本文件CSU
[Header("分支選項")]
[SerializeField] GameObject OptionalButton;
[SerializeField] Transform buttonGroup;
[Header("普通對話")]
[SerializeField] GameObject chatPanel;
[SerializeField] GameObject talkBox;
TMP_Text dialogText;
[SerializeField] Image backGroundPic;
int dialogIndex = 0;
int beginID = 0;
string[][] cells;
string[] dialogRows;
bool isBegin;
bool canNext;
public void BeginChat()
{
{
chatPanel.transform.Find("Sprite").gameObject.SetActive(true);
talkBox.gameObject.SetActive(true);
//把所有圖片換成透明(具體方法在下文的"圖片"中)
ClearAllPic();
ClearAllChoices();
ClearImageLiHui();
}
print("BeginChat" + textAssetPath + "beginID" + beginID);
isBegin = true;
//這里的TextAsset會為null
if (textAsset == null)
{
SetTextAsset(textAssetPath);
}
ReadText(textAsset);
dialogIndex = beginID;
ShowDialogRow();
canNext = true;
}
public void ShowDialogRow()
{
for (int i = 1; i < dialogRows.Length; i++)//遍歷來尋找正確的一行 首行是列名當然不含信息 所以i從1開始
{
if (int.Parse(cells[i][1]) == dialogIndex)
{
//播放音樂
if (cells[i].Length > 10)
{
if (cells[i][10] != "")
{
PlayBackGroundMusic(cells[i][10]);
}
}
//播放音效
if (cells[i].Length > 11)
{
if (cells[i][11] != "")
{
print("PlayEffectMusic" + cells[i][11]);
PlayEffectMusic(cells[i][11]);
}
}
if (cells[i][0] == "*")
{
UpdateBackGround(cells[i][9]);
UpdateImage(cells[i][7], cells[i][8]);
if (cells[i][5] != "")
{
OptionEffect(cells[i][5]);
}
if (cells[i][4].Contains('&'))
{
string[] judge = cells[i][6].Split('>');
string[] jump = cells[i][4].Split('&');
OptionJudge(judge[0], judge[1], int.Parse(jump[0]), int.Parse(jump[1]));
}
else
{
dialogIndex = int.Parse(cells[i][4]);
ShowDialogRow();
}
break;
}
if (cells[i][0] == "#")
{
UpdateBackGround(cells[i][9]);
UpdateImage(cells[i][7], cells[i][8]);
UpdateText(cells[i][2], cells[i][3]);
if (cells[i][5] != "")
{
OptionEffect(cells[i][5]);
}
if (cells[i][4].Contains('&'))
{
string[] judge = cells[i][6].Split('>');
string[] jump = cells[i][4].Split('&');
OptionJudge(judge[0], judge[1], int.Parse(jump[0]), int.Parse(jump[1]));
}
else
{
dialogIndex = int.Parse(cells[i][4]);
}
break;
}
if (cells[i][0] == "&")
{
canNext = false;
GenerateOption(i);
break;
}
if (cells[i][0] == "end")
{
EndChat();
break;
}
}
}
}
public void EndChat()
{
print("EndChat" + textAsset.name);
canNext = false;
dialogIndex = 0;
isBegin = false;
if (chatPanel != null)
{
ClearAllPic();
ClearAllChoices();
//把所有Prefb消除
ClearImageLiHui();
ClearChatBoxText();
chatPanel.transform.Find("Sprite").gameObject.SetActive(false);
if (talkBox.activeSelf)
{
talkBox.gameObject.SetActive(false);
}
}
//把當前path清空
textAssetPath = null;
}
選項與賦值
int likeValue;
int energyValue;
int[] time;
//選擇按鈕
public void OnOptionClick(int _id)
{
dialogIndex = _id;
for (int i = 0; i < buttonGroup.childCount; i++)
{
Destroy(buttonGroup.GetChild(i).gameObject);
canNext = true;
}
ShowDialogRow();
}
public void GenerateOption(int _index)//此處的——index是總的序列號
{
string[] cells = dialogRows[_index].Split(',');
if (cells[0] == "&")
{
GameObject button = Instantiate(OptionalButton, buttonGroup);
button.GetComponentInChildren<TMP_Text>().text = cells[3];
button.GetComponent<Button>().onClick.AddListener(
delegate
{
if (cells[6] != "")
{
string[] judge = cells[6].Split('>');
string[] jump = cells[4].Split('&');
OptionJudge(judge[0], judge[1], int.Parse(jump[0]), int.Parse(jump[1]));
}
else if (cells[5] != "")
{
OptionEffect(cells[5]);
OnOptionClick(int.Parse(cells[4]));
}
else if (cells[5] == "")
{
OnOptionClick(int.Parse(cells[4]));
}
}
);
GenerateOption(_index + 1);
}
}
/// <summary>
/// 選項賦值效果
/// </summary>
/// <param name="_effect">哪個值改變</param>
/// <param name="_param">值改變的大小</param>
public void OptionEffect(string effect)
{
string[] effects = effect.Split('+');
string _effect = effects[0];
int _param = int.Parse(effects[1]);
if (_effect == "好感度")
{
likeValue += _param;
print("好感度" + _param);
}
if (_effect == "活力")
{
energyValue += _param;
print("活力" + _param);
}
}
/// <summary>
/// 根據文件中的條件來判斷應該跳轉到哪;一般是>=這個條件的跳轉到位置1,否則跳轉到位置2
/// </summary>
/// <param name="_judge">判斷的條件</param>
/// <param name="_param2">條件的大小</param>
/// <param name="_jump1">跳轉的位置1</param>
/// <param name="_jump2">跳轉的位置2</param>
void OptionJudge(string _judge, string _param2, int _jump1, int _jump2)
{
if (_judge == "好感度")
{
if (likeValue >= int.Parse(_param2))
{
OnOptionClick(_jump1);
}
else
{
OnOptionClick(_jump2);
}
}
else if (_judge == "時間")
{
string[] timeJudge = _param2.Split(':');
if (time[0] >= int.Parse(timeJudge[0]) && time[1] >= int.Parse(timeJudge[1]))
{
OnOptionClick(_jump1);
}
else
{
OnOptionClick(_jump2);
}
}
else if (_judge == "活力")
{
if (energyValue >= int.Parse(_param2))
{
OnOptionClick(_jump1);
}
else
{
OnOptionClick(_jump2);
}
}
}
/// <summary>
/// 把所有選項都刪除
/// </summary>
void ClearAllChoices()
{
Transform[] childs = buttonGroup.transform.GetComponentsInChildren<Transform>();
//第0個是這個物體本身
for (int i = 1; i < childs.Length; i++)
{
Destroy(childs[i].gameObject);
}
}
圖片
圖片讀取(運用圖集Atlas實現)
//所有圖片,包括人物立繪與場景,都放在這個圖集里
SpriteAtlas _atals;
string atlasResourcesPath = "Atalas/ChatAtalas";
public Sprite LoadAtlasSprite(string _atalsname, string spriteName)
{
LoadAtalas();
switch (_atalsname)
{
case "_atals":
return LoadAtlasSprite(_atals, spriteName);
}
return null;
}
Sprite LoadAtlasSprite(SpriteAtlas _atals, string spriteName)
{
if (_atals == null)
{
Debug.Log(_atals.name + "_atals == null");
return null;
}
if (_atals.GetSprite(spriteName) != null)
{
return _atals.GetSprite(spriteName);
}
Debug.Log("NotGetAtlasPic");
return null;
}
void LoadAtalas()
{
if (_atals == null)
{
_atals = Resources.Load<SpriteAtlas>(atlasResourcesPath);
}
}
圖片與對話系統(tǒng)接入
圖片層級參考
立繪Prefb層級參考
[SerializeField] Image leftPic;
[SerializeField] Image rightPic;
[Header("全屏對話的人物Prefb")]
[SerializeField] GameObject aPrefb;
[SerializeField] GameObject bPrefb;
/// <summary>
/// 只針對全屏對話
/// </summary>
/// <param name="picName"></param>
/// <param name="picturePos"></param>
void UpdateImage(string picName, string picturePos)
{
LoadImageLiHui(picName, picturePos);
}
/// <summary>
/// 根據圖片名稱給出準確的底圖+表情
/// </summary>
void LoadImageLiHui(string combineName, string picturePos)
{
string[] s = combineName.Split("&");
if (s.Length < 1) return;
if (picturePos == "Clear")
{
//遍歷Sprite的所有子物體,看哪個子物體里面有s[0]
foreach (Transform child in transform.Find("Sprite").GetComponentInChildren<Transform>())
{
if (child.childCount > 0)
{
//如果這個節(jié)點里有這個名字,就刪掉這個子物體
if (child.GetChild(0).name.Contains(s[0]))
{
Destroy(child.GetChild(0).gameObject);
}
}
}
return;
}
GameObject talkerObj = null;
GameObject prefb = null;
//print(combineName + picturePos);
//print("s[0]" + s[0]);
switch (s[0])
{
case "A":
prefb = aPrefb;
break;
case "B":
prefb = bPrefb;
break;
}
if (prefb == null) return;
//prefab先根據位置生成
switch (picturePos)
{
//如果這個原本沒有prefb,就生成新的,如果有,就刪掉之前的,生成一個新的;如果是同名的prefb,就改表情和其他Adding
//每個角色都應該有一個“默認”表情
case "Left":
talkerObj = ChangePosLiHui(leftPic.transform, prefb, s[0]);
break;
case "Right":
talkerObj = ChangePosLiHui(rightPic.transform, prefb, s[0]);
break;
}
//print("talkerObj.transform.parent.name" + talkerObj.transform.parent.name);
//根據Excell里的拆分文字換不同圖片
//大部分角色有一個默認表情,如果沒有表情,就用默認的
if (s.Length == 1) return;
//換表情
if (LoadAtlasSprite("_atals", s[0] + "_" + s[1]) != null)
{
talkerObj.transform.Find("Emotion").GetComponent<Image>().sprite = LoadAtlasSprite("_atals", s[0] + "_" + s[1]);
}
}
/// <summary>
/// /改變制定位置的立繪
/// </summary>
GameObject ChangePosLiHui(Transform pos, GameObject prefb, string name)
{
GameObject talkerObj = null;
if (pos.childCount == 0)
{
//print("pos.childCount == 0");
talkerObj = Instantiate(prefb, pos);
}
else
{
if (pos.GetChild(0).name.Contains(prefb.name))
{
//print("pos.GetChild(0).name.Contains(prefb.name)");
talkerObj = pos.GetChild(0).gameObject;
LiHuiReturnToDefalt(talkerObj, name);
}
else
{
//print("pos.GetChild(0).name.Don't Contains(prefb.name)");
Destroy(pos.transform.GetChild(0).gameObject);
talkerObj = Instantiate(prefb, pos);
}
}
return talkerObj;
}
/// <summary>
/// 立繪返回默認狀態(tài)
/// </summary>
void LiHuiReturnToDefalt(GameObject talkerObj, string name)
{
//print("LiHuiReturnToDefalt" + name);
//換成默認表情
if (LoadAtlasSprite("_atals", name + "_默認") == null)
{
return;
}
talkerObj.transform.Find("Emotion").GetComponent<Image>().sprite = LoadAtlasSprite("_atals", name + "_默認");
}
/// <summary>
/// 把所有的圖片全都換成透明(全屏對話)
/// </summary>
public void ClearAllPic()
{
leftPic.sprite = LoadAtlasSprite("_atals", "透明");
rightPic.sprite = LoadAtlasSprite("_atals", "透明");
backGroundPic.sprite = LoadAtlasSprite("_atals", "透明");
}
void ClearImageLiHui()
{
//遍歷Sprite的所有子物體,看哪個子物體里面有s[0]
foreach (Transform child in chatPanel.transform.Find("Sprite").GetComponentInChildren<Transform>())
{
if (child.childCount > 0)
{
Destroy(child.GetChild(0).gameObject);
}
}
}
public void UpdateBackGround(string picName)
{
if (picName == "Clear")
{
backGroundPic.sprite = null;
//backGroundPic.sprite= backgroundDic["透明"];
backGroundPic.sprite = LoadAtlasSprite("_atals", "透明");
}
else if (picName != "")
{
//backGroundPic.sprite = backgroundDic[picName];
//print("picName" + picName);
backGroundPic.sprite = LoadAtlasSprite("_atals", picName);
}
}
音頻
[SerializeField] AudioSource backgroundSource;//背景音樂
[SerializeField] AudioSource effectSource;//音效音樂
/// <summary>
/// 默認循環(huán),開始播放就會切掉其他的音樂
/// </summary>
public void PlayBackGroundMusic(string musicName)
{
//print("play" + musicName);
AudioClip audioClip = Resources.Load<AudioClip>("Audio/" + musicName);
if (backgroundSource == null)//針對沒有UI的測試Scene
{
print("backgroundSource == null");
backgroundSource = GetComponent<AudioSource>();
}
if (audioClip == null) return;
backgroundSource.clip = audioClip;
backgroundSource.Play();
backgroundSource.loop = true;
}
/// <summary>
/// 播放音效 只播放一次
/// </summary>
public void PlayEffectMusic(string musicName)
{
AudioClip audioClip = Resources.Load<AudioClip>("Audio/" + musicName);
if (audioClip == null)
{
//print(musicName + "audioClip == null");
return;
}
//如果沒有Canvas,或者Canvas里面沒有EffectSource
if (effectSource == null)
{
effectSource = GameObject.Find("Canvas").transform.Find("EffectSound").GetComponent<AudioSource>();
if (effectSource == null)
{
//print("effectSource == null" + musicName);
return;
}
}
effectSource.clip = audioClip;
effectSource.Play();
effectSource.loop = false;
}
文本
[SerializeField] GameObject nameObj;
[SerializeField] TMP_Text nameText;
/// <summary>
/// 更新對話框
/// </summary>
/// <param name="_name"></param>
/// <param name="_text"></param>
void UpdateText(string _name, string _text)
{
if (_name == "")
{
print("_name == 空");
nameObj.SetActive(false);
}
else
{
nameObj.SetActive(true);
}
nameText.text = _name;
dialogText.text = _text;
}
public void ClearChatBoxText()
{
//清空內容
dialogText.text = "";
nameText.text = "";
}
生命周期
void Start()
{
BeginChat();
}
// Update is called once per frame
void Update()
{
//按空格鍵繼續(xù)對話
if ((Input.GetKeyDown(KeyCode.Space)||Input.GetMouseButtonDown(0) )&& canNext)
{
ShowDialogRow();
}
}
拓展
Excel與Unity工作流(三):對話框架拓展:Excel表內變量導入 賦值 判斷-CSDN博客
Excel與Unity工作流(四):對話框架拓展:結合MVE實現Excel調用函數與批量支線導入管理思路-CSDN博客文章來源:http://www.zghlxwxcb.cn/news/detail-851274.html
參考:
https://www.bilibili.com/video/BV1v5411D79x/
到了這里,關于Excel與Unity工作流(二):基礎對話框架的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!