分享

刨根问底U3D

 阿修罗之狮猿授 2015-12-07

实际中遇到的需求

在做一款对抗类游戏,目前正在调整游戏的平衡性 所以就产生了一个需求 希望可以在Play模式时候对数据源做的更改可以在退出时候被保存下来。

举个Case, 比如 有一个炮塔 可以发射子弹, 然后有一组敌人 去攻击这个炮塔.

首先点击Play按钮 开始执行游戏逻辑

接着在Inspector中调整相应的数值,比如 射速啊,血量啊,敌人移动速度等等.

按原始的做法 就是调整以后 把所有的数据记录下来(截图或者写纸上)

点Stop进入编辑模式,所有数据回滚原始的 然后再一个一个把刚才改过的再重新填上去

一次两次还好,不过平衡性这东西 必然要经常调整 每次这样调整 非常影响工作效率,那有没有办法 可以让U3D在退出Play模式后保留对应的修改呢?

就此我还提了一个问题http://ask./question/37917 不过也没得到比较满意的答案 :(

只好自己瞎折腾了..

实现这个Case的思路

其实思路很简单, 因为 MonoBehaviour 有 void OnApplicationQuit (){} 函数 所以 在该函数内部 序列化相应的Class 然后保存到本地

当下次Awake 时候在反序列化 即可

也是就是

Awake

- 1·从本地Load

- 2·反序列化

OnApplicationQuit

- 3·序列化

- 4·保存到本地

想法很简单 不过却遇到了不少问题..

LitJson 无法序列化float类型

LitJson 用的人应该很多吧,反正我自己一直在用,序列化首先就想到它了,不过JsonMapper.ToJson (this); 直接报错

JsonException: Max allowed object depth reached while trying to export from type System.Single

问答里面一个哥们儿说他那里没问题,我就Google了一下 发现也有几个人遇到了我同样的问题,并且我把所有float都去了 一切就都ok了

网上帖子里面是建议用doulbe然后再自己转换,想想实在太麻烦. 后来又在GoogleCode搜到一个自己改过的LitJson不过也觉得不好.

最后还是在GitHub上发现一个很NB的库

Full Serializer  https://github.com/jacobdufault/fullserializer

代码结构很清晰 并且使用起来也很简单. 试了一下float的问题 完美Fix了. 这样第一个问题就算搞定了

MonoBehaviour 无法被New 出来

序列化的问题是解决了,不过反序列化时候就又有问题了.. 写好反序列化代码 然后运行 直接报Warning

You are trying to create a MonoBehaviour using the 'new' keyword. This is not allowed. MonoBehaviours can only be added using AddComponent(). Alternatively, your script can inherit from ScriptableObject or no base class at all

写的很清楚了, MonoBehaviour 不能用new,只能AddComponent, 并且直接序列化MonoBehaviour到JSON以后里面有很多乱七八糟的东西,比如gameobjct,transforme等等.. 所以看来这条路是走不通了.

嵌套类或Dictionary<string,object>

因为是希望Play时候可以在Inspector中编辑,但是同时又不能是MonoBehaviour(否则无法序列化) 所以想到的两个解决方法 一个就是用单写一个Vo类,然后在MonoBehaviour里面写get/set 方法

Ex:
public class MetaManger:MonoBehaviour
{
  class MetaVo
   {
   public float speed;
  }
  private MetaVo mVo;
  public float Speed
  {
    get{return mVo.speed;}
    set{mVo.speed = value;}
  }
}

这样做应该是可以的(抱歉我没有尝试),不过问题就是 每次新加入一个属性时候要相应的写get/set方法 很麻烦.

第二个方法就是 使用反射,在序列化时候把所有Property Add 到 一个Dictionary<string,object>里面 然后反序列时候再 反射还原。

理论上这个应该是可行的,不过反射弄起来很费劲 尤其是如果出现嵌套Array,枚举啊 什么这种不太普通类型时候.. 应该是个坑 果断放弃了这个思路

那有没有简单可行方法 来实现这个需求呢? 答案是有的(折腾了一天啊...), 不过在这之前 还是先了解一些知识(开始刨根问底..)

为什么U3D 在退出编辑时候无法保存数据?

Google了很多不过没有及时保存 已经找不到了 不过大体的意思就是

Unity会在Play时候 把当前所有所有的数据 序列化一份,然后在退出的时候再全部反序列化回去 就相当于RollBack了 所以 所有的改变自然都回去了.

[System.Serializable] 标签

这个也是我Google时候查到的,

在之前Case中 我也尝试过不对MetaVo的每个属性写get/set方法 就是在MetaManger中 直接public MetaVo vo; 这样Inspector中 无法看到

MetaVo中的内部属性,只能看到一个Vo,但是 如果对MetaVo加上了[System.Serializable]标签 则可以在Inspector中查到了

Google了一下 大部分人都是在复制粘贴 就说加上以后Inspector中就可以显示了. Ok 可是为什么呢?

首先来看下官网这篇文章 http://docs./Manual/script-Serialization.html(这篇文章写的很好,如果没读过的朋友建议仔细看看)

Inspector window. The inspector window doesn’t talk to the C# api to figure out what the values of the properties of whatever it is inspecting is. It asks the object to serialize itself, and then displays the serialized data.

个人理解的inspector组件的工作流程(未被证实,但是可以解释通)

我们先假设inspector是用js写的,脚本代码是用C#写的,底层U3D是C++写的. 序列化的方式采用JSON 那假象的交互流程就是

首先点击Play按钮

Unity通过momo 把C# 中的代码都序列化成JSON 保存在一块内存里面

inspector中读取这块内存中的JSON,然后反序列化成js中可识别的数据类型 接着显示在面板上面

用户在inspector中修改相应数据,inspector把修改的数据再序列化成JSON 放回那块内存里面

底层U3D代码 反序列化那个json到C++中可识别的数据类型,然后再根据相应的数值 操作场景上面的对象渲染

这个整个流程肯定是YY的 不过里面的逻辑我觉得应该是对的 就像文档中那句说的

Serialization of “things” is at the very core of Unity. Many of our features build ontop of the serialization system:

那整个又和[System.Serializable]标签有什么关系呢?

其实这个是因为,Unity只能序列化MonoBehavior和后面要提到的ScriptableObject 对于其他类型 他不知道这是什么东西 所以自然无法序列化了,无法序列化 自然也就无法在inspector中显示出来了

http://www.cnblogs.com/oldman/articles/2409523.html 这篇也提到了我说的这个问题

有时候我们会自定义一些单独的class/struct, 由于这些类并没有从 MonoBehavior 派生所以默认并不被Unity3D识别为可以Serialize的结构。自然也就不会在Inspector中显示。我们可以通过添加 [System.Serializable]这个Attribute使Unity3D检测并注册这些类为可Serialize的类型。

当然U3D 自己的序列化中还有很多需要注意的,比如什么类型无法序列化,包括对于复杂类型如何写自己的序列化Callback函数. 我就不一一说了

http://docs./Manual/script-Serialization.html 这篇文章已经说的很全了.

第二个要说的就是 ScriptableObject

Unity对于这种Vo数据类型的存储及序列化有着自己的解决方案 就是 ScriptableObject

http://docs./Manual/class-ScriptableObject.html

ScriptableObject is a class that allows you to store large quantities of shared data independent from script instances.

具体如何使用 可以参考

http:///gamedev/unity3d/unity-serialization-behind-scriptableobject/

http://maluoi./2014_05_01_archive.html

http://godstamps./2012/02/unity-3d-scriptableobject-assetbundle.html

思路就是 首先通过编辑器先产生一个ScriptableObject类型的asset文件,然后在对应的把GameObject和这个文件Link起来. 具体的思路在解决方案二中有提及

有一点需要仔细理解一下 就是文档中提到的那个4MB 和 40MB 的例子 我个人认为 这个应该是ScriptableObject的核心。

ok废话就到这里 说两种我已经试验成功的解决方案

解决方案一

使用 [System.Serializable]标签 + Full Serializer。 这个方案应该很好理解 还是之前Case中的四步

Full Serializer解决的是

无法序列化float类型的问题,[System.Serializable]标签 解决的是直接public MetaVo vo 无法在inspector中显示的问题

public class MetaManger:MonoBehaviour
{
  [System.Serializable]
  class MetaVo
  {
    public float speed;
   }
   public MetaVo vo;
}

(抱歉 代码没有试 感觉应该可以,因为我实际的代码 要涉及到单例 以及个个Serializable的类嵌套 比较复杂 无法直接贴上来)

只要把MetaVo 前面加上[System.Serializable]Tag 即可 目前实际项目中 使用一切都Ok ,其中包括

1`对于 [System.Serializable]内部嵌套另一个 [System.Serializable] 的 类, inspector中显示OK 并且有折叠 编辑起来很舒服

Full Serializer序列化和反序列化也Ok 毕竟这只是普通类而已..

2`类中包含有枚举类型,inspector显示ok ,Full Serializer 序列化也ok 并且序列化后的JSON中枚举类型被转化为String,这样即使以后枚举中有增删改 照样可以反序列化回来 不知道LitJSON是不是 没有试过

解决方法二

第一种方案 其实是挺正统的一种方法,就是 做一个单例的MetaManger 然后所有的实例对象 比如炮塔啊 子弹啊 敌人啊 当需要相应数据时候 向MetaManger中去要.

不过 用ScriptableObject的特性 可以用另外一种方式 来做.

把刚才的例子变得复杂一些, 炮塔变成 红黄两种, 炮塔属性相同 都只有一个speed,和damage项需要配置 此时可以

1· 写MetaClass

public TowerMeta:ScriptableObject
{
public float speed;
public float damage;
}

2· 执行命令生成两个asset 一个交 RedTowerMeta.asset, 一个叫 YellowTowerMeta.asset

3· 写TowerRender类

public TowerRender:MonoBehaviour
{
public TowerMeta meta;
...
}

4· 在场景上建立 红黄两个塔实例,然后分别对应拖入RedTowerMeta.asset和YellowTowerMeta.asset并存成两个Prefab

5· 用代码或者直接拖拽都行,在场景上 建立10个红塔 10个黄塔

6· 点击Play按钮 然后 点击 Hierarchy中的RedTowerMeta.asset或者YellowTowerMeta.asset 此时 Inspector中 应该可以显示出来对应的数值的, 并且可以直接更改 即使退出Play模式以后不发生回滚

这里其实就是用了ScriptableObject得特性, 即使产生了10个红塔 10个黄塔 但是他们引用的Meta(ScriptableObject) 是同一份.

方案一和方案二的优劣性

方案一 最终生成的是JSON,优势是明文可读 项目大了以后 可以单独写编辑器解析对应JSON 进行编辑适配。劣势 就是JSON体积大 体积大 需要自己压缩.

方案二 优势是生成的asset是被压缩过的体积小,并且和Unity配合很紧密 一切托托拽拽即可 也不用管初始化 每个类Start()以后直接取用就好

劣势 就是一个是要写命令产生对应的asset 第二个就是 如果数据格式发生变更 比如 TowerMeta中加入新的属性 int cost ,那之前的asset就不能用了

因人而宜吧,我个人是比较倾向于 [System.Serializable] + Full Serializer 操作起来很方便 :)

Ok 就叨唠这么多好了

Thanks

Best

Eran

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多