上接: Unity实现一个简单的文字冒险AVG框架-05 Excel编写对话剧本
水平有限,项目仅供参考
项目Package文件百度云:
链接: https://pan.baidu.com/s/1RzKAOg1zUt-4hwlRdGqYTg
提取码: TAVG
一、Unity存档
Unity存档分四种
- PlayerPrefs
- JSON
- XML
- 序列化
其实现思路基本相同,这里选用Json格式来实现。
二、JSON存档
Unity提供JsonUtility,可以很方便的实现对象和JSON字符串的相互转换。
namespace UnityEngine {
public static class JsonUtility {
...
public static T FromJson<T>(string json);
public static object FromJson(string json, Type type);
public static string ToJson(object obj);
...
}
}
存档实现思路
用来播放剧本的AVGFrame在一个场景中可能存在多个,可能分布在多个场景,所以要将这些信息包装成模型,用一个AVGSceneManager来统一管理。
设置AVGSetting全局只有一份,因此不用考虑。
在包装模型的时候含有Scene信息,切换场景,再将剧本信息交给AVGSceneManager去做逐一比对。
当要读档的时候,读取到的数据交给一个静态对象储存,切换场景后SceneManager从该对象读取后设置相应AVGFrame执行。
播放进度存档模型PlaySave
//该特性无关紧要,方便序列化存档
[System.Serializable]
public class PlaySave
{
public int currentScene; //场景
public bool useExcel; //是否使用excel
public TextAsset script; //txt剧本
public string excel; //excel剧本
public int index; //播放位置
public PlaySave(int currentScene, bool useExcel, TextAsset script, string excel, int index)
{
this.currentScene = currentScene;
this.useExcel = useExcel;
this.script = script;
this.excel = excel;
this.index = index;
}
//做测试输出
public override string ToString()
{
return JsonUtility.ToJson(this);
}
}
全局设置模型SettingSave
[System.Serializable]
public class SettingSave
{
//和AVGSetting中一致
public float textChangeSpeed = 0.2f;
public float bkAudioVolume = 1.0f;
public bool breakCharVoice = true;
public SettingSave(float textChangeSpeed, float bkAudioVolume,
bool breakCharVoice)
{
this.textChangeSpeed = textChangeSpeed;
this.bkAudioVolume = bkAudioVolume;
this.breakCharVoice = breakCharVoice;
}
public override string ToString()
{
return JsonUtility.ToJson(this);
}
}
AVGSetting修改
public class AVGSetting : Duo1JAVG {
.
.
public static SettingSave GetPlayerSettingModel()
{
return new SettingSave(
TEXT_CHANGE_SPEED,
BK_AUDIO_VOLUME,
BREAK_CHARACTER_VOICE
);
}
public static void SetPlayerSettingModel(SettingSave model)
{
TEXT_CHANGE_SPEED = model.textChangeSpeed;
BK_AUDIO_VOLUME = model.bkAudioVolume;
BREAK_CHARACTER_VOICE = model.breakCharVoice;
}
}
至于选择事件的结果记录应该根据实际情况来实现,这里不再赘述。
Json存储工具类
public class AVGArchive
{
//位于C盘User的AppData中,可输出查看
private static string playDataPath = Application.persistentDataPath + "/Save/SlotList/";
private static string settingDataPath = Application.persistentDataPath + "/Save/";
//对象转换为Json存储
private static void SaveJson(object obj, string path, string fileName)
{
StreamWriter sw = null;
try
{
//对象转Json
string jsonStr = JsonUtility.ToJson(obj);
//文件夹判空
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
//开启文件流写入
sw = new StreamWriter(
new FileStream(path + "/" + fileName.Split('.')[0] + ".json", FileMode.Create),
Encoding.UTF8);
sw.Write(jsonStr);
sw.Flush();
}
catch (Exception e)
{
Debug.LogWarning(e.Message + "::" + e.StackTrace);
}
finally
{
sw.Close();
}
}
//读取Json文件转化为对象
private static T LoadJson<T>(string filePath)
{
StreamReader sr = null;
try
{
//文件夹判空
if (!File.Exists(filePath))
{
Debug.LogWarning("File not exist: " + filePath + " in LoadJson<T>");
return default(T);
}
sr = new StreamReader(new FileStream(filePath, FileMode.Open), Encoding.UTF8);
string jsonStr = sr.ReadToEnd();
//读取到的Json字符串解析为泛型对象返回
return JsonUtility.FromJson<T>(jsonStr);
}
catch (Exception e)
{
Debug.LogWarning(e.Message + "::" + e.StackTrace);
return default(T);
}
finally
{
sr.Close();
}
}
//保存进度
//可根据存档槽位不同存入不同fileName
public static void SavePlayModel(PlaySave model, string fileName)
{
SaveJson(model, playDataPath, fileName);
}
//读取该fileName的进度存档
public static PlaySave LoadPlayModel(string fileName)
{
return LoadJson<PlaySave>(playDataPath + "/" + fileName.Split('.')[0] + ".json");
}
//读取进度存档列表,用以所有存档展示
public static List<PlaySave> LoadPlayModelList()
{
string[] fileList = Directory.GetFiles(playDataPath + "/", "*.json");
List<PlaySave> res = new List<PlaySave>();
foreach (string file in fileList)
{
res.Add(LoadJson<PlaySave>(file));
}
return res;
}
//读取进度存档并加载
public static void LoadPlayModelandLoadScene(string fileName)
{
PlaySave playSave = LoadPlayModel(fileName);
AVGGameManager.ins.CurrentPlaySave = playSave;
UnityEngine.SceneManagement.SceneManager.LoadScene(playSave.currentScene);
}
//储存全局设置
public static void SaveSettingModel(SettingSave model)
{
SaveJson(model, settingDataPath, "Setting.json");
}
//读取全局设置
public static void LoadSettingModel()
{
AVGSetting.SetPlayerSettingModel(LoadJson<SettingSave>(settingDataPath + "/Setting.json"));
}
}
其中读取进度存档并加载逻辑:读取对应fileName的json数据后将其存放于AVGGameManager中,然后切换场景,场景中的AVGSceneManager会读取AVGGameManager中的模型后选择对应的AVGFrame播放。
接下来实现AVGSceneManager和AVGGameManager
AVGSceneManager
public class AVGSceneManager : MonoBehaviour
{
//当前场景的所有AVGFrame
[SerializeField] private AVGFrame[] frameList;
//当前播放的AVGFrame,AVGFrame播放时自己找到AVGSceneManager设置
[SerializeField] private AVGFrame currentFrame;
//测试
#if UNITY_EDITOR
[SerializeField] private int currentScene;
[SerializeField] private bool currentUseExcel;
[SerializeField] private TextAsset currentScript;
[SerializeField] private string currentExcel;
[SerializeField] private int currentIndex;
#endif
private void Awake()
{
//遍历AVGFrame,并设置AVGFrame中的AVGSceneManager为此
foreach (AVGFrame frame in frameList)
{
frame.SceneManager = this;
}
}
//AVGFrame开始播放的时候设置currentFrame
public void SetCurrentFrame(AVGFrame frame)
{
currentFrame = frame;
}
//
#if UNITY_EDITOR
[ContextMenu("Save to Slot1.json")]
public void TestSave()
{
AVGArchive.SavePlayModel(GetPlaySaveModel(), "Slot1");
AVGArchive.SaveSettingModel(AVGSetting.GetPlayerSettingModel());
}
[ContextMenu("Load Slot1.json")]
public void TestLoad()
{
AVGArchive.LoadPlayModelandLoadScene("Slot1.json");
}
#endif
//返回当前播放的存档对象模型
public PlaySave GetPlaySaveModel()
{
if (currentFrame == null)
{
Debug.LogWarning("CurrentFrame is null in AVGSceneManager at " + gameObject.name);
return null;
}
#if UNITY_EDITOR
currentScene = SceneManager.GetActiveScene().buildIndex;
currentUseExcel = currentFrame.useExcel;
currentScript = currentFrame.useExcel ? null : currentFrame.textAsset;
currentExcel = currentFrame.useExcel ? currentFrame.excelName : "";
currentIndex = currentFrame.CurrentIndex();
#endif
//2021.2.17 修改 ------------------------------------------------
return new PlaySave(
SceneManager.GetActiveScene().buildIndex,
currentFrame.useExcel,
currentFrame.useExcel ? null : currentFrame.textAsset,
currentFrame.useExcel ? currentFrame.excelName : "",
currentFrame.CurrentIndex()
);
}
}
对于AVGFrame和AVGController的修改
public class AVGFrame : MonoBehaviour, Duo1JAVG {
...
[SerializeField] private AVGSceneManager sceneManager;
public AVGSceneManager SceneManager { get => sceneManager; set => sceneManager = value; }
private void Start()
{
...
//末尾
if (SceneManager != null)
{
//从AVGGameManager中获取当前应该播放的模型
//检查是否为此AVGFrame
PlaySave current = AVGGameManager.ins.CurrentPlaySave;
if (current != null)
{
autoStart = false;
//如果useExcel不一致肯定不是该AVGFrame
if (current.useExcel == useExcel)
{
//判断脚本是否一致,若一致则认为该AVGFrame播放
if ((useExcel && current.excel.Equals(excelName)) ||
(!useExcel && current.script == textAsset))
{
int i = current.index - 1;
if (i < 0) i = 0;
//设置播放索引并继续
SetCurrentIndexAndRun(i);
}
}
}
}
}
public void Begin()
{
//开始播放就设置currentFrame为此
if (SceneManager != null)
{
SceneManager.SetCurrentFrame(this);
}
...
}
//返回当前索引
public int CurrentIndex() { return controller.CurrentIndex(); }
//设置索引并播放
public void SetCurrentIndexAndRun(int i)
{
if (SceneManager != null)
{
SceneManager.SetCurrentFrame(this);
}
controller.SetCurrentIndexAndRun(i);
}
...
}
AVGController
public class AVGController : MonoBehaviour, Duo1JAVG {
.
.
public int CurrentIndex()
{
return index;
}
//设置索引并继续,并且分析之前的Command模型还原背景,角色立绘,音频
public void SetCurrentIndexAndRun(int i)
{
index = i;
Continue();
PastSearch();
}
//查找index以前的模型对象,还原背景,角色立绘,音频
public void PastSearch()
{
CommandModel last = new CommandModel();
for (int i = 0; i <= index; i++)
{
if (modelList[i].GetType() == typeof(CommandModel))
{
CommandModel temp = (CommandModel)modelList[i];
if (temp.ImageModel0.Image != null && !temp.ImageModel0.Image.Trim().Equals(""))
{
last.ImageModel0 = temp.ImageModel0;
}
if (temp.Background_image != null && !temp.Background_image.Trim().Equals(""))
{
last.Background_image = temp.Background_image;
}
if (temp.Background_music != null && !temp.Background_music.Trim().Equals(""))
{
last.Background_music = temp.Background_music;
}
}
}
//暂停所有干扰协程
StopAllCoroutines();
//更新模型
UpdateShow(last);
}
.
.
private void UpdateShow(AVGModel mi) {
AVGModel model = mi;
//AVGModel model = modelList[index];
//原本传入索引,现直接传入模型解析
.
.
}
}
最后AVGGameManager
public class AVGGameManager
{
public static AVGGameManager ins = new AVGGameManager();
private AVGGameManager()
{
}
//每个场景中的AVGSceneManager会查看此对象
//若有则设置为该AVGFrame播放,若为null则照常播放
public PlaySave CurrentPlaySave { get; set; } = null;
}
可能会存在一些BUG,需根据实际情况修改
梳理一遍,每个场景都有对应的SceneManager,控管该场景下所有AVGFrame。
存档时,将当前播放的AVGFrame连同场景信息包装成模型对象转化为JSON字符串来持久化。可根据不同槽位Slot来规定不同的存档名称。
读档时,将JSON字符串解析获取场景信息和对应的AVGFrame信息,将AVGFrame信息交给GameManager,并切换场景。
切换场景后,SceneManager会根据GameManager中存储的AVGFrame信息找寻对应的AVGFrame并设置index后播放。
使用
现使用AVGSceneManager中的以下两个方法做测试
#if UNITY_EDITOR
[ContextMenu("Save to Slot1.json")]
public void TestSave()
{
AVGArchive.SavePlayModel(GetPlaySaveModel(), "Slot1");
AVGArchive.SaveSettingModel(AVGSetting.GetPlayerSettingModel());
}
[ContextMenu("Load Slot1.json")]
public void TestLoad()
{
AVGArchive.LoadPlayModelandLoadScene("Slot1.json");
}
#endif
现运行,播放到随机位置,选择三个点,点击Save to Slot1.json
查看存档位置
C:\Users\xxxx\AppData\LocalLow\DefaultCompany\TAVG Frame
#Slot1.json
{"currentScene":0,"useExcel":true,"script":{"instanceID":0},"excel":"ExcelDemo01.xlsx","index":11}
#Setting.json
{"textChangeSpeed":0.20000000298023225,"bkAudioVolume":1.0,"breakCharVoice":true}
接下来是读档
首先是同一场景下读取刚才的存档,选择三点,点击Load Slot1.json
然后是一个空场景,读取刚才的存档,场景跳转回刚才的场景,并播放了对应AVGFrame
Unity实现一个简单的文字冒险AVG框架 - 完
Update 2021-1-6
在主动跳转场景的时候,请赋值AVGGameManager.ins.CurrentPlaySave为null
不然在跳转后的场景中可能会由于AVGGameManager.ins.CurrentPlaySave不为null而视作读档的跳转,导致该场景的所有AVGFrame均无法匹配上而autoStart设置为false导致无法播放。
Update 2021-1-22 集合类解析JSON为空
由于JSON是以键值对的形式书写,所以诸如int, char之类的基本类型集合无法解析
例如一个Stack: [1, 2, 3, 4] 无法解析为 key:value 的形式
解决方法有两个:
1.将基本类型包装成对象:
public class IntModel {
public int param;
}
假如param=1,就会解析成 "param":1
2.使用字典列表
Stack<Dictionary<string, int>> list = new Stack<Dictionary<string, int>>();
Dictionary<string, int> dic = new Dictionary<string, int>();
dic.Add("param", 1);
list.Push(dic);
解析出来效果理论上同上,但JSONUtility不支持对复杂对象的解析。
同时,由于JSONUtility不支持对集合的解析,因此要么使用其他的JSON解析插件,要么遍历对象拼接。
这里介绍比较麻烦的,遍历对象拼接字符串,或者可以使用其他JSON解析插件。
public class Test : MonoBehaviour
{
private void Awake()
{
//由于Json是键值对类型,所以这里不能直接Stack<int>,要对其用对象包装
Stack<TestModel> stack = new Stack<TestModel>();
stack.Push(new TestModel(1));
stack.Push(new TestModel(4));
stack.Push(new TestModel(6));
stack.Push(new TestModel(8));
//左括号
string res = "[";
//提前保存栈的容量
int count = stack.Count;
for (int i = 0; i < count; i++)
{
//拼接
res += JsonUtility.ToJson(stack.Pop());
//若不是最后一个,用逗号分割
if (i < count - 1)
{
res += ",";
}
}
res += "]";
print(res);
}
}
//包装int
class TestModel
{
public int param;
public TestModel(int param)
{
this.param = param;
}
}
输出
因此,之前的模型可以修改为:
public class PlaySave
{
public int currentScene;
public bool useExcel;
public TextAsset script;
public string excel;
public int index;
public string stackStr; //CHANGE
.
.
然后在实例化该模型对象的时候将Stack转为JSON后再赋给stackStr即可
//AVGSceneManager.cs
public PlaySave GetPlaySaveModel()
{
.
.
.
Stack<TestModel> stack = new Stack<TestModel>();
stack.Push(new TestModel(1));
stack.Push(new TestModel(4));
stack.Push(new TestModel(6));
stack.Push(new TestModel(8));
//可包装为工具类
string res = "[";
int count = stack.Count;
for (int i = 0; i < count; i++)
{
res += JsonUtility.ToJson(stack.Pop());
if (i < count - 1)
{
res += ",";
}
}
res += "]";
return new PlaySave(
currentScene = SceneManager.GetActiveScene().buildIndex,
currentFrame.useExcel,
currentFrame.useExcel ? null : currentFrame.textAsset,
currentFrame.useExcel ? currentFrame.excelName : "",
currentFrame.CurrentIndex(),
res
);
}
查看存档文件
读档就是把上面的操作反过来做一遍就好了,把stackStr根据逗号拆成单个的对象再解析出来即可。
感谢阅读,如有错误,恳请指正
到目前为止,该框架的丰富度、稳定性、扩展性、复杂性均有待改善 😦
这里仅提供思路参考 :>