准备重新写
先上效果图:
项目Package文件百度云:
链接: https://pan.baidu.com/s/1RzKAOg1zUt-4hwlRdGqYTg
提取码: TAVG
水平有限,项目仅供参考
该框架大致有以下功能
- 读取文本文件,并根据设定的语法解析成相应的对象模型。
- 在设定好UI组件后,可以通过设定的语法播放相应内容。
- 显示角色名以及滚动播放对白,可切换播放速度以及设定在某字符处延时。
- 角色立绘和背景的渐变切换。
- 背景音乐和角色语音的渐变切换。
- 设定按钮选择事件,接收事件结果。
- 自由暂停,继续,重开流程。
- 自定义开始和结束事件。
- 自定义动画。
- 自定义资源文件的加载方式。
- 读取Excel文件解析成剧本 (2020.11.20)
- 存档与读档接口 (2020.11.30)
默认使用Resources的方式加载资源文件
项目整体结构:
一、脚本的语法设定
通过对设定好的语法进行解析以获得要播放的对象模型
最简单易行的就是对文本文件的解析,之后可以通过重写接口并注入的方式来对不同的文件进行解析,如(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;
//构造方法...
//属性访问器...
}
三、脚本解析为对象模型
脚本和对象模型都设定好后,就要创建两者之间的桥梁:脚本解析器
首先创建接口以便于后期使用其他的实现方式:
//可自行实现此接口挂在到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/成步堂龙一/...../
运行
脚本解析器基本完成,可通过实现Duo1JLoader接口自定义脚本和解析方式。
将各部分组件完成后,最终用AVGFrame类统一调度。
AVGFrame只要通过调用 Analysis() 即可获得一个 List< AVGModel > 用以播放。
水平有限,仅供参考,如有错误和建议,还望提出
下接: Unity实现一个简单的文字冒险AVG框架-02