功能需求
我想要实现的功能有:
0.玩家靠近且在前面时人物自动看向玩家,此时才能触发对话;
1.发起对话时弹出对话界面,依次显示文本;
2. 播放语音;
3.对话时人物的嘴动;
4.对话时人物做一些动作;
5.可以有多种回复,有的触发任务,有的不触发;不同回复可能导致下面的不同对话;
数据结构定义
为了记录对话信息,自定义了一种asset文件,里面定义一个结构体,有对话文本、语音文件、做出动作的序号、回复的文本、跳转到的序号。
using System;
using UnityEngine;
[CreateAssetMenu(menuName ="Custom/对话资源")]
public class MyTalkAsset : ScriptableObject
{
[Serializable]
public struct Response{
public string text;
public int jumpTo;
}
[Serializable]
public struct OneWord{//一轮对话
public string text;
public AudioClip audioClip;
public AnimationClip talkingAnim;
public AnimationClip eyesTalkingAnim;
public Response[]responses;
}
public OneWord[] talkData;
}
人物随着任务推进,会使用不同的对话,人物声明一个对话资源的数组和记录当前对话进度的整数:
public MyTalkAsset[] talkAssets;
public int talkIndex;
1.显示文本、文本渐显
人物脚本里写一个StartTalk(),它再调用talkManager.StartTalk();,剩下的工作交给对话管理器处理。
public void StartTalk(){//把这个人物的对话文件传给对话管理器
TalkManager talkManager=StaticMain.LoadPage(StaticMain.UIPrefabsPath+"Dialog Panel").GetComponent<TalkManager>();
RectTransform rectTransform=talkManager.transform as RectTransform;
rectTransform.offsetMin=Vector2.zero;
rectTransform.offsetMax=new Vector2(Screen.width/2,panelHeight);
talkManager.myNPC=this;
talkManager.charName.text=gameObject.name;
talkManager.StartTalk();
animator.SetBool(talkingPara,true);
}
对话管理器的开始对话:
public void StartTalk(){
rectTransform=transform as RectTransform;
rectTransform.sizeDelta=new Vector2(0,dialogPanelHeight);
rectTransform.anchoredPosition=Vector2.zero;
FindObjectOfType<PlayerInput>().enabled=false;
MyInput.Instance.enabled=false;
if(!myNPC.TryGetComponent(out audioSource)){
Debug.Log(myNPC.gameObject.name+"没添加声源!");
}
Cursor.lockState=CursorLockMode.None;
continueButton=transform.Find("Content").GetComponent<Button>();
continueButton.onClick.AddListener(ContinueTalk);
sentenceIndex=0;
UpdateTalkNew(sentenceIndex);
}
从结构体数组读取信息更新对话,比从csv文件读取全文本,还要做字符串处理方便得多:
public void UpdateTalkNew(int i){
if(i<myNPC.talkAssets[myNPC.talkIndex].talkData.Length){
MyTalkAsset.OneWord oneLine=myNPC.talkAssets[myNPC.talkIndex].talkData[i];
myNPC.headConstraint.enabled=oneLine.lookAtPlayer;
Debug.Log("对话管理器打开看向玩家!");
content.text="";
if(oneLine.audioClip){
fadeSpeed=oneLine.audioClip.length/oneLine.text.Length;//根据语音长度计算文字渐显速度
}
StartCoroutine(FadeInText(content,i));
audioSource.clip=oneLine.audioClip;
audioSource.Play();
// animator.SetBool("talking",true);
MyGameManager.Instance.UpdateAnimator(myNPC.animator,defaultTalkAnim.name,myNPC.talkAssets[myNPC.talkIndex].talkData[i].talkingAnim);//把对话状态从上一段话的对话替换成这一段的
MyGameManager.Instance.UpdateAnimator(myNPC.animator,defaultEyesTalkAnim.name,myNPC.talkAssets[myNPC.talkIndex].talkData[i].eyesTalkingAnim);
}else{//对话结束
FindObjectOfType<PlayerInput>().enabled=true;
MyInput.Instance.enabled=true;
Destroy(gameObject);//不能直接删除,还要
Cursor.lockState=CursorLockMode.Locked;//隐藏光标
if(myNPC.talkAssets[myNPC.talkIndex].updateMissionProgress){
myNPC.talkIndex=myNPC.talkAssets[myNPC.talkIndex].nextTalkIndex;
myNPC.missionTrigger.OpenMission();
}
myNPC.animator.SetBool(myNPC.talkingPara,false);//玩家提前点掉对话
}
}
协程实现
文本渐显是在百度搜索直接抄的,用协程实现的:
public IEnumerator FadeInText(Text dialogText,string text){
textFadingIn=true;
for (int i = 0; i < text.Length; i++)
{
// 添加当前字符到文本组件
dialogText.text = text.Substring(0, i + 1);
// 等待一小段时间来模拟"渐显"效果
yield return new WaitForSeconds(fadeSpeed);
}
textFadingIn=false;
}
但是玩家提前点掉对话时出了问题,这个文本渐显无法被打断,和下一句话的文本渐显冲突。
动画实现
玩家靠近时人物自动看向玩家
有多种方案实现,比如1.LookAtConstraint;2.对话系统LookAt IK。
LookAtConstraint方案
初始化:包含给LookAtConstraint加source的代码
Transform head;
[ContextMenu("头约束初始化")]
void HeadConstraintInitialize(){
if(!head){
head=animator.GetBoneTransform(HumanBodyBones.Head);
}
if(!headConstraint){
head.TryGetComponent(out headConstraint);
if(!headConstraint){
headConstraint=head.gameObject.AddComponent<LookAtConstraint>();
}
headConstraint.enabled=false;
List<ConstraintSource> sources=new List<ConstraintSource>();
headConstraint.GetSources(sources);
if(sources.Count==0){
ConstraintSource constraintSource=new ConstraintSource();
constraintSource.weight=1;
constraintSource.sourceTransform=MyInput.Instance.player.head;
headConstraint.AddSource(constraintSource);
headConstraint.constraintActive=true;
}
}
}
执行:动画参数talking==false时,判断玩家在不在人物前方的扇形区域内,是就打开LookAtConstraint;否就关闭
public void NPCLookAtPlayer(){
if(!animator.GetBool("dead")&&lookAtPlayer){
if(!animator.GetBool(talkingPara)){//没在对话
if(JudgePlayerInSector(lookAtRange,lookAtAngle)){//玩家在面前
headConstraint.enabled=true;
}
else{
headConstraint.enabled=false;
}
}
}
else{//人死了
headConstraint.enabled=false;
}
}
动画系统IK方案
public bool lookingAtPlayer=false;
public void CheckLookAtPlayer(){
if(!animator.GetBool("dead")&&lookAtPlayer){
if(!animator.GetBool(talkingPara)){//没在对话
if(CheckPlayerInSector(lookAtRange,lookAtAngle)){//玩家在面前
lookingAtPlayer=true;;
}
else{
lookingAtPlayer=false;;
}
}//不写else,对话时LookAt约束交给对话管理器控制
}
else{//人死了
lookingAtPlayer=false;
}
}
void OnAnimatorIK(){
if(lookingAtPlayer){
LookAtIK();
}
}
public void LookAtIK(){
animator.SetLookAtPosition(MyInput.Instance.player.head.position);
animator.SetLookAtWeight(1);
}
效果:
看向玩家开启关闭的维护
没有对话时,看向玩家取决于玩家相对NPC的位置,需要在每一帧判断,对话时取决于这段对话是否开启了看向玩家,只要在对话开始时判断一次。
动画参数talking==true时LookAt约束交给对话管理器,根据这段话要不要看玩家打开或关闭LookAt约束.
后来这个出了问题。对话中途talking==false时人物没有看向玩家,对话结束后却一直看向玩家。
后来我想起来我在一个动画剪辑里加过开启关闭LookAt约束的关键帧,我把这个动画剪辑从状态机除去,问题就消失了。尽管对话中从来没进过这个状态。好像在动画状态机的任何一个动画剪辑里加了一个属性,这个属性就被动画系统接管了,代码修改它就无效。所以如果一个变量想有时用脚本控制有时用动画控制,则动画应该使用动画事件控制它,不能打关键帧,否则动画系统会完全夺取它的控制权。
相关文章
Unity的Write Defaults->从一个例子谈起 - Esfog - 博客园 (cnblogs.com)
对话时人物做一些动作
需要不同人物播放不同动画。每个人用一个Animator Override Controller,对话脚本里记录每段话要做的动作,替换默认对话动作。给这段代码打上Debug.Log(),逻辑都没问题,但是打印“没看玩家”时LookAt约束在生效。
public void NPCLookAtPlayer(){
Debug.Log(name+"检测看玩家...");
if(!animator.GetBool("dead")&&lookAtPlayer){
if(!animator.GetBool(talkingPara)){//没在对话
Debug.Log(name+"没在对话");
if(CheckPlayerInSector(lookAtRange,lookAtAngle)){//玩家在面前
headConstraint.enabled=true;Debug.Log(name+"看玩家");
}
else{
headConstraint.enabled=false;Debug.Log(name+"没看玩家");
}
}
else{
headConstraint.enabled = false;
}
}
else{//人死了
headConstraint.enabled=false;
}
}
然后我发现运行时把LookAt约束在检查器都关不掉。
对话时人物的嘴动
精确的嘴动画需要根据文本制作,要花费巨量精力。这里就反复用一个嘴动画,但需要在开始播放声音时人物开始嘴动,播放完后停止嘴动。动画状态机使用一个bool参数talking控制嘴动画开始和停止。
人物的眼睛表情
我一开始把眼部表情做在Base层,因为眼部表情和身体动作一样是定制的,嘴部动作是统一的。
但是根据我的理解,Base层其他状态不改变表情,这样播放完对话动作这个表情BlendShape应该维持原状,但是实际情况是人物的眼部表情复原了。
真正的问题是我允许玩家提前点掉对话,人物不通过hasExitTime退出对话动作,然后眼部表情没复原。
然后我决定在Eyes层做眼部表情,对话的时候暂停眨眼,提前点掉对话回到眨眼状态。先试试能不能解决眼部表情不复原的问题。
效果演示
Unity手搓的对话系统_哔哩哔哩bilibili_原神_演示
怎么判断一段话说完?靠语音、动画还是字幕?
3种方案理论上都可以。
1.是在Update()里检查audioSource.isPlaying;
2.需要根据语音定制对应长度的动画;
3.从字幕渐显的代码判断;
2的工作量最大,但也是效果最好的方法。
对话动作在动画状态机里怎么设置?是用bool还是用trigger触发?动画是否勾选Loop time?
观察了一下原神的对话系统,发现这么几种情况:
1.有的对话里人物会反复摆手,即使话说完了,要等玩家点继续才做下面的动作;
2.有的对话进行时人物会进行一个动作,说完后人物循环播放一个空闲动画,此时人物的姿态就是说话动作的最后姿态,如果玩家提前点掉,人物就中断当前动作,进入下一段话的动作;
不过第一种情况算是把摆手作为了空闲动画。总的来说原神对话系统的人物动作是:一段对话开始时人物开始做一个动作,同时开始播放语音,语音和动作的持续时间被做成一致的,
如果语音动画播放完,就保持最后一帧的姿态,同时叠加一个循环播放的空闲动画(身体的轻微摇晃);
如果玩家提前点掉,就直接开始下一段对话。
对话触发检测遇到的问题
现在的检测是否能触发对话是从相机中心发出射线,击中Character层物体,并且人物挂有对话脚本则可以触发对话。但是第三人称时相机中心发出的射线很容易击中玩家,挡住检测射线。
图中第三人称相机发出的射线击中蓝点处,在枪上。也不可能让射线检测跳过枪层,因为捡枪也通过射线检测。
但是射线检测方案保证玩家只能同时对一个人物触发对话,动作菜单就只用显示一个动作,不用做成列表,比较简单。
对话快进功能
允许玩家在对话还在渐显的时候点对话栏,对话立即显示完。此时如果没有回复,点对话栏是开始下一句对话,如果有回复,点对话栏没有效果。对话过程中继续按钮的回调在1.快进对话;2.下一句对话;3.无回调;3种情况间反复跳转。有两种方案:
1.继续按钮在两种情况下有两种回调,按钮的回调由程序维护。
开始渐显文本时执行
button.onClick.AddListener(SkipFadeIn);
SkipFadeIn里有停止渐显协程、显示全部文本、显示回复、移除SkipFadeIn回调
void SkipFadeIn(){
StopCoroutine(showTextIE);
content.text=currentLine.text;
ShowReply(currentLine);
continueButton.onClick.RemoveListener(SkipFadeIn);
}
如果有回复,继续按钮没有回调(通过按回复继续对话);如果没有回复,执行
button.onClick.AddListener(Continue);
Continue里有
button.onClick.RemoveListener(Continue);
button.onClick.AddListener(SkipFadeIn);
2.按钮有一种回调,根据是否正在渐显,回调执行不同的内容。通过一个bool变量标记是否正在渐显:
fadingIn=true;
for (int i = 0; i < text.Length; i++)
{
content.text = text.Substring(0, i + 1);
yield return new WaitForSeconds(fadeSpeed);
}
fadingIn=false;
任务系统
已经做了几个枪战的任务,但是感觉很无聊,就是走路、战斗反复进行。需要
1.使用对话、动画构建一个情景;
2.把其他目标如解救人质、摧毁物品、取得物品、占领区域🏘️和战斗结合起来;
任务的触发方式:
对话触发;
进入区域触发;
一个任务完成后触发;