设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 2406|回复: 3
打印 上一主题 下一主题

[交流讨论] MV源码解析之存档序列化与反序列化

[复制链接]

Lv3.寻梦者

梦石
0
星屑
1882
在线时间
1552 小时
注册时间
2013-4-13
帖子
917
跳转到指定楼层
1
发表于 2019-10-29 22:44:01 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有帐号?注册会员

x
本帖最后由 沉滞的剑 于 2019-10-29 22:46 编辑

今天来聊一聊存档的话题,
众所周知在RM中存档和读档是由StorageManager完成的
但其实StorageManager底层使用的是RM拓展的JsonEx完成的
这次我们就主要研究其中JsonEx.parse和JsonEx.stringify两个方法
依照惯例, 只粘贴有意义代码并且做可读性上的修改

0.当我们在讨论存档的时候我们在讨论什么
存档的主要目的是让玩家可以回到游戏的某一时刻的状态继续游戏
虽然可以把内存中的所有状态直接用快照的方式保存下来
但是更可靠的方式是将有用的信息以一定格式存储起来
不是所有信息都是必要的, 也不是所有信息都是有用的
对于RM来说只要保存几个游戏对象就足够了

  1. DataManager.makeSaveContents = function() {
  2.     // A save data does not contain $gameTemp, $gameMessage, and $gameTroop.
  3.     var contents = {};
  4.     contents.system       = $gameSystem;
  5.     contents.screen       = $gameScreen;
  6.     contents.timer        = $gameTimer;
  7.     contents.switches     = $gameSwitches;
  8.     contents.variables    = $gameVariables;
  9.     contents.selfSwitches = $gameSelfSwitches;
  10.     contents.actors       = $gameActors;
  11.     contents.party        = $gameParty;
  12.     contents.map          = $gameMap;
  13.     contents.player       = $gamePlayer;
  14.     return contents;
  15. };
复制代码

1.序列化
这些都是Game_XXX的实例, 我们知道这些都是对象, 对象的结构是树状的
而contents其实就是这颗巨大的树的根
对于存储来说, 我们把这棵树转换成JSON格式的字符串就可以了
然而这样就遇到了两个问题:
  1.如何处理原型链
  2.如何处理循环引用
对于1的解决方法是将原型的constructor的记录下来
不过有一些特殊的类比如Date, Function这些类很难处理,
所以不做修改是无法复原原来的对象的
对于2的解决方法是将每个引用对象记录下来
如果遇到重复的对象, 则指向第一次遇到时候的引用
因为我们遍历这颗树的时候采取的遍历方式都是一致的
所以这个引用其实就是一个索引就可以了
下面首先来看一下我们的编码入口方法(和RM原版略有区别)

  1. const stringify = value => {
  2.   // 初始化循环引用数组
  3.   const circular = [];
  4.   // 编码, 详情请阅下文
  5.   encode(value, circular);
  6.   // 将编码后的对象序列化
  7.   const string = JSON.stringify(value);
  8.   // 还原编码对象
  9.   restoreObject(value, circular);
  10.   // 返回字符串
  11.   return string;
  12. };
复制代码

注意编码的操作是在原对象上进行的, 得到序列化后的字符串后需要还原
不过还是先来看看编码的部分,
这是一个深度优先遍历, 我们必须能保证能访问到所有子节点
但又不需要访问原型链里的东西(__proto__)

  1. for (const key in value) {
  2.   encode(value[key], circular);
  3. }
复制代码

首先对于JS来说数据类型总共分为两种, 基础类型和引用类型
在内存中基础类型存储的是它的值, 而引用类型存储的是指向值的指针
对于我们来说分别就是布尔值, 数字, 字符串, null, undefined等等和对象,数组
我们写一个简单的方法来判断一个变量的类型是什么
RM中判断得更细, 比如是不是数组, 是不是纯对象等等, 其实只需要分为两大类即可

  1. const isPrimitive = o => o !== Object(o);
复制代码

当我们序列化基础类型的时候只需要将它的值记录下来就可以成为可识别的JSON的值
而对于引用类型我们如果是第一次遇到, 我们要把他记录到数组里, 并且记录它的类型名
否则就只保留一个它在数组里的索引, 我们不需要再对它的属性进行重复遍历了
我的circular比RM的简洁了一些, 只用了数组索引, 不过原理是一致的

  1. const encode = (value, circular) => {
  2.   if (!isPrimitive(value)) {
  3.     circular.push(value);
  4.     value["@constructor"] = value.constructor.name;
  5.     value["@circularID"] = circular.length - 1;
  6.     for (const key in value) {
  7.       if (circular.includes(value[key])) {
  8.         value[key] = { "@reference": value[key]["@circularID"] };
  9.       } else {
  10.         encode(value[key], circular);
  11.       }
  12.     }
  13.   }
  14. };
复制代码

然后我们还需要还原被修改过的对象,
我把RM中的两步合并成一步: 1.去掉标签, 2.还原引用

  1. const restoreObject = (value, circular) => {
  2.   if (!isPrimitive(value)) {
  3.     delete value["@constructor"];
  4.     delete value["@circularID"];
  5.     for (const key in value) {
  6.       const circularID = value[key]["@reference"];
  7.       if (circularID !== undefined) {
  8.         value[key] = circular[circularID];
  9.       } else {
  10.         restoreObject(value[key], circular);
  11.       }
  12.     }
  13.   }
  14. };
复制代码

这个时候circular里的引用对象还是精确的, 所以直接替换掉索引即可

2.反序列化
反序列化的步骤其实是类似的

  1. const parse = string => {
  2.   const value = JSON.parse(string);
  3.   const circular = [];
  4.   decode(value, circular);
  5.   return value;
  6. };
复制代码

在解码的时候我们需要还原原型链的信息
首先根据名称我们可以从顶级对象中获取继承的类名
这就有几个需要考虑的问题
1. 通过匿名定义的类没有名字
2. 忘记设置 Class.prototype.constructor = Class 的类没有名字
3. 自定义的类往往因为不想污染全局变量而有自己的命名空间
这几个情况就是导致存档读取后数据错误, 丢失和报错的原因
解决方法有:
1. 使用具名类,或者设置Class.prototype.constructor
2. 修改decode方法, 让decode可以在一个命名空间数组内寻找类
替代原型链的方法可以使用Object.setPrototypeOf
RM还对不支持这个方法的浏览器做了polyfill

  1. const decode = (value, circular) => {
  2.   if (!isPrimitive(value)) {
  3.     circular[value["@circularID"]] = value;
  4.     // 下面这一句可以改写来支持独立命名空间的类的访问
  5.     const constructor = window[value["@constructor"]] || Object;
  6.     Object.setPrototypeOf(value, constructor.prototype);
  7.     delete value["@constructor"];
  8.     delete value["@circularID"];
  9.     for (const key in value) {
  10.       const circularID = value[key]["@reference"];
  11.       if (circularID !== undefined) {
  12.         value[key] = circular[circularID];
  13.       } else {
  14.         decode(value[key], circular);
  15.       }
  16.     }
  17.   }
  18. };
复制代码

注意对于meta数据的清理我也合并为一步完成了

最后附上一段最后的序列化和反序列化的代码,
支持可以通过registerNamespace来添加反序列化时侯对应的命名空间


  1. const namespaces = [window];

  2. const registerNamespace = namespaces.push.bind(namespaces);

  3. const isPrimitive = o => o !== Object(o);

  4. const encode = (value, circular) => {
  5.   if (!isPrimitive(value)) {
  6.     circular.push(value);
  7.     value["@constructor"] = value.constructor.name;
  8.     value["@circularID"] = circular.length - 1;
  9.     for (const key in value) {
  10.       if (circular.includes(value[key])) {
  11.         value[key] = { "@reference": value[key]["@circularID"] };
  12.       } else {
  13.         encode(value[key], circular);
  14.       }
  15.     }
  16.   }
  17. };

  18. const stringify = value => {
  19.   const circular = [];
  20.   encode(value, circular);
  21.   const string = JSON.stringify(value);
  22.   restoreObject(value, circular);
  23.   return string;
  24. };

  25. const restoreObject = (value, circular) => {
  26.   if (!isPrimitive(value)) {
  27.     delete value["@constructor"];
  28.     delete value["@circularID"];
  29.     for (const key in value) {
  30.       const circularID = value[key]["@reference"];
  31.       if (circularID !== undefined) {
  32.         value[key] = circular[circularID];
  33.       } else {
  34.         restoreObject(value[key], circular);
  35.       }
  36.     }
  37.   }
  38. };

  39. const parse = string => {
  40.   const value = JSON.parse(string);
  41.   const circular = [];
  42.   decode(value, circular);
  43.   return value;
  44. };

  45. const getConstructor = name => {
  46.   const constructor = namespaces.find(namespace =>
  47.     namespace.hasOwnProperty(name)
  48.   )[name];
  49.   if (constructor) {
  50.     return constructor;
  51.   }
  52.   return Object;
  53. };

  54. const decode = (value, circular) => {
  55.   if (!isPrimitive(value)) {
  56.     circular[value["@circularID"]] = value;
  57.     const constructor = getConstructor(value["@constructor"]);
  58.     Object.setPrototypeOf(value, constructor.prototype);
  59.     delete value["@constructor"];
  60.     delete value["@circularID"];
  61.     for (const key in value) {
  62.       const circularID = value[key]["@reference"];
  63.       if (circularID !== undefined) {
  64.         value[key] = circular[circularID];
  65.       } else {
  66.         decode(value[key], circular);
  67.       }
  68.     }
  69.   }
  70. };
  71. // 以下是测试
  72. {
  73.   const obj = {};
  74.   obj.self = obj;
  75.   obj.array = ["a", "b", obj.self];
  76.   const string = stringify(obj);
  77.   console.debug(parse(string));
  78. }
复制代码

夏普的道具店

塞露提亚-道具屋的经营妙方同人作品
发布帖:点击这里

Lv4.逐梦者

梦石
0
星屑
14257
在线时间
718 小时
注册时间
2011-7-16
帖子
1428

开拓者

2
发表于 2019-10-31 10:51:35 | 只看该作者
这就有几个需要考虑的问题
1. 通过匿名定义的类没有名字
2. 忘记设置 Class.prototype.constructor = Class 的类没有名字
3. 自定义的类往往因为不想污染全局变量而有自己的命名空间
这几个情况就是导致存档读取后数据错误, 丢失和报错的原因


这可能就是为什么在新增加了插件或者删除了某个插件之后导致之前的存档无法读取或读取之后报错的原因吧。
大佬,这意思是不是说如果写插件能够考虑到这几个方面的话,那么对于新旧版本游戏的存档的继承将不会再出现无法读取或读取错误的问题?

点评

ok,了解了,多谢大佬  发表于 2019-10-31 15:11
就是复原的时候丢失了原型链的信息的缘故  发表于 2019-10-31 11:18
RMMV网络插件,开源免费,内含服务器端,无需强制登录,云数据,弹幕,云存档,排名,兑换码,版本检测,可自由上架下架删除。q群399090587
免打包运行MV游戏,云游戏,安卓App雷神游戏厅,在线玩游戏,上传下载游戏
开源游戏:重装机兵之重装归来【RMMV制作】全球首款按照美剧分季分集的方式发布的游戏
体素画 -- MV画3D像素图的画板
RMMV显示3D模型和场景的插件
RMMV显示spine骨骼动画的插件
RMMV秘密通道插件
突破敌群数量上限8个的插件
在rmmv中显示gif动态图片的插件
一款可以在mv游戏界面的任意位置显示任意文字的插件
RMMV Toast 插件 带物品得失提示,可以设置开启关闭 兼容yep itemcore
制作一个改名卡道具插件、调整标题页面菜单的插件、在标题页面之前显示大段文字的插件、标题页面显示版本号的插件
物品得失自动提示自动上色自动换行插件
我的Q群 663889472
另外,我的插件、范例、游戏都在这里
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2024-5-13 23:22

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表