前言
本文将演示在unity中实现类似galgame的对话效果,并且通过Excel进行文本、图片、选项、赋值、音乐的配置
对话框架效果示意图
Excel表格总览
整体框架与主要思路
要实现的效果
每点击一次鼠标,就出现下一个对话/或者出现选项;
如果出现选项,点击选项,会有不同的对话;
对话的时候人物的立绘会有改变;
可以进行简单的数值判断与赋值;
可以播放音乐与音效
整体思路
通过标志与ID来决定下一个对话播放第几个
标志位
"#"号是普通的对话行,屏幕里会出现名字与人物
"&"号是选项,这一行会以一个选项的方式出现,内容会以选项上的文字表现
以上图举例,最底下的文字就是用"#"号行来表现;右侧的两个选项通过两行"&"号表现,选项上的文字就是那两行"内容"列里的文字
"end"标志符就是结束整段对话
跳转
每一行都有自己的ID,也有"跳转"列。当这一行结束后(一般通过点击继续放下一个),会跳转到"跳转列"的ID所在的行
效果与判定
判定:在上图ID为2的行里,"判定"那一列有"好感度>10"的文字,如果此时好感度>=10,那么之后会跳转到5(跳转列里"&"符号前面的那一个数字),否则就是跳转到4
效果:在上图ID为3-5的行里,有"好感度+-10"的文字,"+"用来区分数值名与数值改变量,所以是"好感度"这个数值,进行"-10"的操作
*号行
如果需要多个条件都满足才能跳转到下一行呢?
ID为7的"是时候打开网抑云了"需要时间晚于23:00,并且活力要大于20,此时我们发现一行只写一个判定不够
故引入"*"号行。这个标志符所代表的行不会出现在游戏的画面上,不需要玩家点击才继续,而是自动触发并自动跳转到下一个要跳转的行。
"*"号行可以进行效果赋值、条件判断以及人物清空(这在下文的"人物"板块会提到)、背景替换等等功能
"*"号行可以多个混合使用,看具体实现情况而定
人物
在对话时,我们需要人物立绘来展现效果(进行激烈的立绘碰撞 bushi)
在Excel示例表中,主要通过"人物立绘图片","人物位置"进行控制
"人物立绘图片"由人物的底+人物表情组成,通过"&"符号进行分隔
"人物位置"Left就是在左边出现,Right就是在右边出现,Clear会清除这个人所有的立绘
音乐音效
通过"背景音乐"和"音效"控制
"背景音乐"会循环播放
"音效"只播放一次
实现
导入
在写完需要导入的剧情表格后,另存为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/文章来源地址https://www.toymoban.com/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);
}
}
图片与对话系统接入
图片层级参考
立绘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)
{
//如果这个节点里有这个名字,就删掉这个子物体
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>
/// 立绘返回默认状态
/// </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>
/// 默认循环,开始播放就会切掉其他的音乐
/// </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()
{
//按空格键继续对话
if ((Input.GetKeyDown(KeyCode.Space)||Input.GetMouseButtonDown(0) )&& canNext)
{
ShowDialogRow();
}
}
拓展
Excel与Unity工作流(三):对话框架拓展:Excel表内变量导入 赋值 判断-CSDN博客
Excel与Unity工作流(四):对话框架拓展:结合MVE实现Excel调用函数与批量支线导入管理思路-CSDN博客文章来源:https://www.toymoban.com/news/detail-851274.html
参考:
https://www.bilibili.com/video/BV1v5411D79x/
到了这里,关于Excel与Unity工作流(二):基础对话框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!