Unity实现一个简单的文字冒险AVG框架-01

准备重新写

先上效果图:
AVG Frame
AVG Frame

项目Package文件百度云:
链接: https://pan.baidu.com/s/1RzKAOg1zUt-4hwlRdGqYTg
提取码: TAVG


水平有限,项目仅供参考
该框架大致有以下功能

  1. 读取文本文件,并根据设定的语法解析成相应的对象模型。
  2. 在设定好UI组件后,可以通过设定的语法播放相应内容。
  3. 显示角色名以及滚动播放对白,可切换播放速度以及设定在某字符处延时。
  4. 角色立绘和背景的渐变切换。
  5. 背景音乐和角色语音的渐变切换。
  6. 设定按钮选择事件,接收事件结果。
  7. 自由暂停,继续,重开流程。
  8. 自定义开始和结束事件。
  9. 自定义动画。
  10. 自定义资源文件的加载方式。
  11. 读取Excel文件解析成剧本 (2020.11.20)
  12. 存档与读档接口 (2020.11.30)

默认使用Resources的方式加载资源文件

项目整体结构:
AVG Frame


一、脚本的语法设定

通过对设定好的语法进行解析以获得要播放的对象模型
最简单易行的就是对文本文件的解析,之后可以通过重写接口并注入的方式来对不同的文件进行解析,如(Excel)
那么就要先设定要脚本的语法
通过对上面的功能分析,大致把对象模型分为以下四种:

1. 对话文本模型
包含角色名称,对白,语音和延时列表,其中延时列表就是在播放到某个字符的时候停顿多少秒,于是分为停顿索引和停顿时长
于是语法设定为:

T/角色名称/对白/语音/延迟索引:延迟时间/.../ (不定长)
示例: T/Jack/Hello/Jack01.mp3/2:0.5/4:0.5/

开头T表示为一个对话文本模型
示例的意思为:角色Jack说了Hello,播放语音Jack01.mp3,在分别说道e和第二个l的时候停顿0.5s

2.系统指令模型
用来控制角色立绘,角色立绘的位置,背景图片,背景音乐的改变
语法设定为:

C/角色立绘文件名/立绘位置/x偏移/y偏移/背景图片文件名/背景音乐文件名/
示例: C/p01/0/0/80/bk2/.mp3/

开头C表示为系统指令模型(Command)
角色立绘改为p01,位置为0(靠左),x偏移0,y偏移80,更改背景图为bk2,更改背景音乐为汐.mp3
其中位置设定好为 0-靠左 1-居中 2-靠右,然后通过xy偏移微调

3.动画模型
在播放过程中可能会播放一些动画,如震动,角色动画等
如演示中的御剑说话时候的屏幕震动

A/动画类型/
示例: A/bk_shake/

bk_shake为默认提供的屏幕整动效果可在AVGAnimationDefault组件上更改参数
也可以实现Duo1JAnimation接口以自定义动画

4.按钮选择模型
在分支选项中使用,提供多个选项并接收玩家选择

B/事件名称/选项1/选项2/.../ (不定长)
示例: B/event1/Study/Shop

其中事件名称用来辨别按钮的结果而造成的改变
Study和Shop为玩家选择

不需要的参数全部留出
如: T/Jack/Hello//2:0.5/4:0.5/ 省略了语音

以 / 结尾

也可以重写解析器以实现自定义语法


二、对象模型

要将以上四种模型统一起来放到一个列表或队列中,首先将其抽象。
所有对象模型继承AVGModel接口
其中Duo1JAVG为整个框架的接口

[Serializable]
public interface AVGModel : Duo1JAVG { }

1.对话文本模型

public class TextModel : AVGModel
{
    private string name; //角色名称
    private string text; //对话文本
    private string voice; //语音文件名
    private List<TextDelay> delayList; //延时列表
	
	//构造方法...    
	//属性访问器...
	
    //使用SubString显示文本
    //delayIndex表示延迟的index,从1开始
    public class TextDelay
    {
        private float delayTime; //延时时长
        private int delayIndex; //延时索引
        
		//构造方法...    
		//属性访问器...
    }
}

2.系统指令模型

public class CommandModel : AVGModel
{
    private ImageModel imageModel; //角色立绘模型
    private string background_music; //背景音乐文件名
    private string background_image; //背景图片文件名

	//构造方法...    
	//属性访问器...    

    public class ImageModel
    {
        private string image; //立绘文件名
        private int pos; //立绘位置 0-居左 1-居中 2-居右
        private int x_offset; //x偏移
        private int y_offset; //y偏移
        
		//构造方法...    
		//属性访问器... 
    }
}

3.动画模型

public class AnimationModel : AVGModel
{
    private string type;

    public AnimationModel(string type)
    {
        this.Type = type;
    }

    public string Type { get => type; set => type = value; }
}

4.选择按钮模型

public class ChooseModel : AVGModel
{
    private List<Choose> chooses; //选择列表
    private string eventTag; //事件名称
	//构造方法...    
	//属性访问器... 
}

public class Choose
{
    string text;
    int index;
	//构造方法...    
	//属性访问器... 
}

三、脚本解析为对象模型

脚本和对象模型都设定好后,就要创建两者之间的桥梁:脚本解析器
AVG Frame
首先创建接口以便于后期使用其他的实现方式:

//可自行实现此接口挂在到AVGFrame同物体下以自动注入
public interface Duo1JLoader : Duo1JAVG
{
    //解析脚本文件并返回实体模型AVGModel列表
    //2020.11.23更新 UnityEngine.TextAsset改为使用object针对不同类型剧本
    bool Analysis(out List<AVGModel> modelList, UnityEngine.TextAsset textAsset);
}

默认实现类,解析文本文件
基本思路为用Split分割 /,再根据索引位置创建对象模型
若脚本书写不当,Split容易出错,将代码try…catch上易于查错
if 语句可改用switch

解析Excel表: Unity实现一个简单的文字冒险AVG框架-05 Excel编写对话剧本

//由于需要挂载在物件上,因此需要继承MonoBehaviour
public class AVGLoader : MonoBehaviour, Duo1JLoader
{
    //解析TextAsset返回实体模型列表
    //2020.11.23更新 UnityEngine.TextAsset改为使用object针对不同类型剧本
    //在Analysis方法中强转
    public virtual bool Analysis(out List<AVGModel> modelList, TextAsset textAsset)
    {
        try
        {
            modelList = new List<AVGModel>();
            //2020.11.23更新,添加强制转换,(TextAsset)textAsset.text.Split('\n');
            string[] split = textAsset.text.Split('\n');

            foreach (string s in split)
            {
                AVGModel model = null;

                if (s[0] == '#' || s[0] == '\n' || s[0] == ' ') { continue; } //省略注释
                else if (s[0].ToString().ToLower().Equals("c"))
                {
                    model = DealwithCommand(s);
                    if (model == null)
                    {
                        throw new Exception("Command analysis error!");
                    }
                }
                else if (s[0].ToString().ToLower().Equals("t"))
                {
                    model = DealwithText(s);
                    if (model == null)
                    {
                        throw new Exception("Text analysis error!");
                    }
                }
                else if (s[0].ToString().ToLower().Equals("b"))
                {
                    model = DealwithChooseButton(s);
                    if (model == null)
                    {
                        throw new Exception("Choose analysis error!");
                    }
                }
                else if (s[0].ToString().ToLower().Equals("a"))
                {
                    model = DealwithAnimation(s);
                    if (model == null)
                    {
                        throw new Exception("Animation analysis error!");
                    }
                }
                else
                {
                    throw new Exception("Model analysis error at index 0!");
                }

                AddModel(modelList, model);
            }
            return true;
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message + " at Analysis() at AVGLoader.cs\n" + e.StackTrace);
            modelList = null;
            return false;
        }
    }

    /**
     * 系统指令: C/角色img/img位置/x偏移/y偏移/背景img/背景audio/
     * x,y偏移默认为0,区分正负
     * img位置只能填入0 1 2,  0-靠左,1-居中,2-靠右
     * 只有audio相关需带上文件后缀
     */
    private AVGModel DealwithCommand(string s)
    {
        try
        {
            string[] split = s.Split('/');
            return new CommandModel(
                ParamUtil.StringNotNull(split[1]),
                ParamUtil.ParseString2Int(split[2], 1),
                ParamUtil.ParseString2Int(split[3], 0),
                ParamUtil.ParseString2Int(split[4], 0),
                ParamUtil.StringNotNull(split[5]),
                ParamUtil.StringNotNull(split[6])
            );
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message + " at DealwithCommand() at AVGLoader.cs\n" + e.StackTrace);
            return null;
        }
    }

    //文本指令: T/角色名/对话文本/语音/延迟索引:延迟时间/.../ (不定长)
    //exp: T/Jack/Hello/Jack01.mp3/2:0.5/   (在播放到e时候延迟0.5s)
    private AVGModel DealwithText(string s)
    {
        try
        {
            string[] split = s.Split('/');
            List<TextModel.TextDelay> delayList = new List<TextModel.TextDelay>();
            for (int i = 4; i < split.Length - 1; i++)
            {
                string[] delaySplit = split[i].Split(':');
                if (delaySplit.Length == 2)
                {
                    delayList.Add(new TextModel.TextDelay(
                        ParamUtil.ParseString2Float(delaySplit[1], 0),
                        ParamUtil.ParseString2Int(delaySplit[0], 0)));
                }
                else
                {
                    if (delaySplit[0] == "")
                        continue;
                    Debug.LogError("Text Delay Format Error: " + i + ": " + split[i]);
                }
            }
            return new TextModel(
                ParamUtil.StringNotNull(split[1]),
                ParamUtil.StringNotNull(split[2]),
                ParamUtil.StringNotNull(split[3]),
                delayList);
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message + " at DealwithText at AVGLoader.cs\n" + e.StackTrace);
            return null;
        }
    }

    //选择按钮指令: B/事件名称/选项1/选项2/.../ (不定长)
    //exp: B/event1/Study/Shop
    private AVGModel DealwithChooseButton(string s)
    {
        try
        {
            string[] split = s.Split('/');
            ChooseModel res = new ChooseModel(ParamUtil.StringNotNull(split[1]));
            for (int i = 2; i < split.Length - 1; i++)
            {
                res.Chooses.Add(new Choose(ParamUtil.StringNotNull(split[i]), i - 1));
            }
            return res;
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message + " at DealwithChooseButton at AVGLoader.cs\n" + e.StackTrace);
            return null;
        }
    }

    /**
     * 动画指令: A/动画类型/
     * 动画类型不区分大小写
     * exp: A/ch_shake/
     */
    private AVGModel DealwithAnimation(string s)
    {
        try
        {
            return new AnimationModel(ParamUtil.StringNotNull(s.Split('/')[1]));
        }
        catch (Exception e)
        {
            Debug.LogError(e.Message + " at DealwithAnimation at AVGLoader.cs\n" + e.StackTrace);
            return null;
        }
    }

    //添加模型到列表
    private void AddModel(List<AVGModel> modelList, AVGModel model)
    {
        modelList.Add(model);
    }
}

ParamUtil工具类判断是否为空

public class ParamUtil : Duo1JAVG
{
    public static string StringNotNull(string s)
    {
        return s == null || s == "" ? null : s;
    }

    public static int ParseString2Int(string s, int defaultInt)
    {
        return s == null || s == "" ? defaultInt : int.Parse(s);
    }

    public static float ParseString2Float(string s, float defaultFloat)
    {
        return s == null || s == "" ? defaultFloat : float.Parse(s);
    }
}

测试

public class AVGLoaderTest : MonoBehaviour
{
    private Duo1JLoader loader;
    
    public TextAsset textAsset;
    
    [SerializeField]
    public List<AVGModel> model;

    private void Start()
    {
        loader = GetComponent<Duo1JLoader>();
        if (loader.Analysis(out model, textAsset))
        {
            print(model.Count); //19
        }
    }
}
//-----------------------------------------
//demo.txt 演示例子 包含注释共20句
#这是一条注释
C/p01/0/0/80/bk2/.mp3/
T/成步堂龙一/那个. . .御剑呀//3:0.5/5:0.5/7:0.5/
T//今晚来我房间一下/
C/b01/2/0/80///
T/御剑怜侍/?/
C/b02/2/0/80///
A/ch_shake/
A/bk_shake/
T/御剑怜侍/. . ?你说什么?//2:0.5/4:0.5/6:0.5/
C/p02/0/0/80/bk1/いつかの情景.mp3/
T/成步堂龙一/怎么来这么慢?快过来赶紧把案子商量完!///
C/b03/2/60/80///
T/御剑怜侍/哎,原来是商量案子呀!/
C/p01/0/0/80///
T/成步堂龙一/..你说什么?/
C/b03/2/60/80///
B/Event1/没什么/我还以为.../
C/p01/0/0/80///
T/成步堂龙一/...../

运行
AVG Frame

脚本解析器基本完成,可通过实现Duo1JLoader接口自定义脚本和解析方式。
将各部分组件完成后,最终用AVGFrame类统一调度。
AVGFrame只要通过调用 Analysis() 即可获得一个 List< AVGModel > 用以播放。

水平有限,仅供参考,如有错误和建议,还望提出
下接: Unity实现一个简单的文字冒险AVG框架-02

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值