Unity对话系统和任务系统笔记

功能需求

我想要实现的功能有:

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;
}

107669aade354ba59c2da78edef6263b.png

人物随着任务推进,会使用不同的对话,人物声明一个对话资源的数组和记录当前对话进度的整数:

 public MyTalkAsset[] talkAssets;
 public int talkIndex;

35c0d02cbe234899beb4efd2c44026b2.png

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;
    }

但是玩家提前点掉对话时出了问题,这个文本渐显无法被打断,和下一句话的文本渐显冲突。

3140d8b3158f4beb94f00ccfd47d51f2.gif

动画实现

玩家靠近时人物自动看向玩家

有多种方案实现,比如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);
    }

效果:

66545ff0033f4a65a7add995ba20ea85.png

看向玩家开启关闭的维护

没有对话时,看向玩家取决于玩家相对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约束在检查器都关不掉。

fe6190a9cd3e473ebc3d7be93ae71e52.png

对话时人物的嘴动

精确的嘴动画需要根据文本制作,要花费巨量精力。这里就反复用一个嘴动画,但需要在开始播放声音时人物开始嘴动,播放完后停止嘴动。动画状态机使用一个bool参数talking控制嘴动画开始和停止。

人物的眼睛表情

我一开始把眼部表情做在Base层,因为眼部表情和身体动作一样是定制的,嘴部动作是统一的。

d113f8928e364efa90c310d7c7e99044.png

但是根据我的理解,Base层其他状态不改变表情,这样播放完对话动作这个表情BlendShape应该维持原状,但是实际情况是人物的眼部表情复原了。

真正的问题是我允许玩家提前点掉对话,人物不通过hasExitTime退出对话动作,然后眼部表情没复原。

然后我决定在Eyes层做眼部表情,对话的时候暂停眨眼,提前点掉对话回到眨眼状态。先试试能不能解决眼部表情不复原的问题。

效果演示

Unity手搓的对话系统_哔哩哔哩bilibili_原神_演示

怎么判断一段话说完?靠语音、动画还是字幕?

3种方案理论上都可以。

1.是在Update()里检查audioSource.isPlaying;

2.需要根据语音定制对应长度的动画;

3.从字幕渐显的代码判断;

32dfd11b52f649c5866c238eeaabdc52.png

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.把其他目标如解救人质、摧毁物品、取得物品、占领区域🏘️和战斗结合起来;

任务的触发方式:

对话触发;

进入区域触发;

一个任务完成后触发;

Unity3D 与NPC对话的完整流程一般包括以下几个步骤: 1. 创建NPC角色和对话框 首先需要创建一个NPC角色,可以使用Unity3D内置的角色模型或者自己制作模型。同时,在NPC身上添加一个Collider组件,以便与玩家角色进行交互。然后创建一个对话框UI界面,用于显示NPC对话内容。 2. 触发对话事件 当玩家角色进入NPC的交互范围时,需要触发对话事件。可以通过检测碰撞触发器或者射线检测等方式实现。当玩家进入交互范围后,NPC对话框会弹出。 3. 显示对话内容 当对话框弹出后,需要根据NPC对话内容来显示对话框中的内容。可以使用文本框或者图文混排等方式来显示对话内容。 4. 玩家选择对话选项 当NPC对话内容显示完成后,玩家需要选择对话选项来回复NPC。可以使用按钮或者滑动条等方式来实现。 5. 处理对话逻辑 根据玩家选择的对话选项,需要执行相应的对话逻辑。例如,如果玩家选择了“接受任务”,则需要将任务添加到任务列表中;如果玩家选择了“拒绝任务”,则需要关闭对话框。 下面是一个简单的代码示例,用于实现与NPC对话功能: ```csharp using UnityEngine; using UnityEngine.UI; public class NPCDialog : MonoBehaviour { // NPC对话框UI界面 public GameObject dialogUI; // 对话内容文本框 public Text dialogText; // 对话选项按钮 public Button acceptButton; public Button rejectButton; // 玩家与NPC的距离 public float distance = 2f; // NPC对话内容 private string[] dialogContent = new string[] { "你好,欢迎来到我的店铺!", "有什么需要帮忙的吗?", "我这里有些不错的商品,你可以看看。", "好的,祝你好运!" }; // 当玩家进入NPC的交互范围时触发 void OnTriggerEnter(Collider other) { // 判断进入交互范围的对象是否为玩家 if (other.CompareTag("Player")) { // 计算玩家与NPC的距离 float distance = Vector3.Distance(transform.position, other.transform.position); // 如果距离小于指定距离,则弹出对话框 if (distance <= this.distance) { // 显示对话框 dialogUI.SetActive(true); // 显示对话内容 dialogText.text = dialogContent[0]; // 隐藏拒绝按钮 rejectButton.gameObject.SetActive(false); } } } // 当玩家离开NPC的交互范围时触发 void OnTriggerExit(Collider other) { // 判断离开交互范围的对象是否为玩家 if (other.CompareTag("Player")) { // 隐藏对话框 dialogUI.SetActive(false); } } // 玩家选择“接受”按钮时触发 public void OnAcceptButtonClicked() { // 显示下一条对话内容 dialogText.text = dialogContent[1]; // 显示拒绝按钮 rejectButton.gameObject.SetActive(true); } // 玩家选择“拒绝”按钮时触发 public void OnRejectButtonClicked() { // 隐藏对话框 dialogUI.SetActive(false); } } ``` 以上代码实现了一个简单的NPC对话功能,具体实现可以根据需求进行修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值