Excel与Unity工作流(二):基础对话框架

前言

本文将演示在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/

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.bilibili.com/video/BV1v5411D79x/

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值