Unity实现一个简单的文字冒险AVG框架-06 存档与读档接口

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

水平有限,项目仅供参考
项目Package文件百度云:
链接: https://pan.baidu.com/s/1RzKAOg1zUt-4hwlRdGqYTg
提取码: TAVG


一、Unity存档

Unity存档分四种

  1. PlayerPrefs
  2. JSON
  3. XML
  4. 序列化

其实现思路基本相同,这里选用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全局只有一份,因此不用考虑。
AVGFrame

在包装模型的时候含有Scene信息,切换场景,再将剧本信息交给AVGSceneManager去做逐一比对。

当要读档的时候,读取到的数据交给一个静态对象储存,切换场景后SceneManager从该对象读取后设置相应AVGFrame执行。
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播放。
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
AVGFrame
查看存档位置

C:\Users\xxxx\AppData\LocalLow\DefaultCompany\TAVG Frame

AVGFrame

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

输出
JSON
在这里插入图片描述
因此,之前的模型可以修改为:

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

查看存档文件
JSON
读档就是把上面的操作反过来做一遍就好了,把stackStr根据逗号拆成单个的对象再解析出来即可。



感谢阅读,如有错误,恳请指正

到目前为止,该框架的丰富度、稳定性、扩展性、复杂性均有待改善 😦

这里仅提供思路参考 :>

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值