Unity Editor扩展入门(3) 数据的保存

Unity Editor扩展入门(3) 数据的保存

Translated by Xdestiny
2018/3/4

日文原文地址:http://anchan828.github.io/editor-manual/web/data_storage.html

渣翻,有些东西相当不确定,禁止转载

编辑器扩展功能运行的状态下,有时候会需要将数值保存下来以便以后使用。这些值可能是与编辑器扩展功能相关的设定,或者是与游戏有关系的参数,诸如此类。在Unity里面保存数据的手段大致分为三种。在本章,会依据目的介绍适合的手段,并加以解说。

3.1 EditorPref

用于保存PC里面可以共享的数据。不被Unity项目所限制,适合需要在Unity编辑器中共享值的情况。

影响范围

保存的值会影响Unity的主要版本
Unity 4.x保存的值,只会在Unity 4.x的版本中被处理。Unity 5.x也是一样。
image

保存什么东西

应该在EditorPrefs里面保存的东西是以下几种:位置、大小、Unity Editor的环境设定(类似于Preferences里面的东西)。即使是单独的Asset但有与环境相关的设定的话,也请使用 EditorPrefs。需要注意的是,EditorPres里面保存的都是明文。因此绝对不要把密码之类的重要信息保存在里面。

Platform保存位置
Windows(Unity 4.x)HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 4.x
Windows(Unity 5.x)HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 5.x
Mac OS X(Unity 4.x)~/Library/Preferences/com.unity3d.UnityEditor4.x.plist
Mac OS X(Unity 5.x)~/Library/Preferences/com.unity3d.UnityEditor5.x.plist

EditorPref会依据主要版本进行保存。特别是Windows会将值保存在注册表里面。因此如果这个过程不小心进行了错误的设置,最糟糕的情况下有可能导致windows无法启动。请特别注意。
image

使用方法

在类似OnEnable这种能够调用不止一次以上的函数里面获取值。值变化的时候保存到EditorPrefs里面。

using UnityEngine;
using UnityEditor;

public class ExampleWindow : EditorWindow
{
    int intervalTime = 60;
    const string AUTO_SAVE_INTERVAL_TIME = "AutoSave interval time (sec)";


    [MenuItem ("Window/Example")]
    static void Open ()
    {
        GetWindow <ExampleWindow> ();
    }

    void OnEnable ()
    {
        intervalTime = EditorPrefs.GetInt (AUTO_SAVE_INTERVAL_TIME, 60);
    }

    void OnGUI ()
    {
        EditorGUI.BeginChangeCheck ();

        //シーン自動保存間隔(秒)
        intervalTime = EditorGUILayout.IntSlider ("間隔(秒)", intervalTime, 1, 3600);

        if (EditorGUI.EndChangeCheck ())
            EditorPrefs.SetInt (AUTO_SAVE_INTERVAL_TIME, intervalTime);
    }
}

还有,保存窗口大小的场合下,由于这个数值的重要性不是很高,所以在OnDisable里面保存值会比较合适。绝对不要在每次调用OnGUI的时候保存值。如果在类似OnGUI这种会频繁调用的方法里面进行保存的话,电脑负荷会变得很高。

using UnityEngine;
using UnityEditor;

public class ExampleWindow : EditorWindow
{
    const string SIZE_WIDTH_KEY = "ExampleWindow size width";
    const string SIZE_HEIGHT_KEY = "ExampleWindow size height";

    [MenuItem ("Window/Example")]
    static void Open ()
    {
        GetWindow <ExampleWindow> ();
    }

    void OnEnable ()
    {
        var width = EditorPrefs.GetFloat (SIZE_WIDTH_KEY, 600);
        var height = EditorPrefs.GetFloat (SIZE_HEIGHT_KEY, 400);
        position = new Rect (position.x, position.y, width, height);
    }

    void OnDisable ()
    {
        EditorPrefs.SetFloat (SIZE_WIDTH_KEY, position.width);
        EditorPrefs.SetFloat (SIZE_HEIGHT_KEY, position.height);
    }
}

3.2 EditorUserSettings.Set/GetConfigValue

项目中可以共享数据的保存方法。在这里保存的数据都会被加密,很适合保存个人信息系或是密码之类的东西。

影响范围和保存场所

使用这个API保存的数据只会对自身的Project产生影响。由于数据的保存地是Library/EditorUserSettings.asset,因此如果Library文件夹没有雨其他人共享的话,也就不会与其他人共享信息了。

保存什么

由于我们会使用到一堆的工具,为了登录他们,邮箱地址、密码什么的都是必要的。Oauth的访问令牌也是一样的。
EditorUserSettings.asset是使用二进制进行保存的,因此想要轻易看到是不可能的。不过Unity提供了binary2text来讲二进制转换为文本形式,这点需要注意。

使用方法

试着保存一下数据吧。

using UnityEditor;

public class NewBehaviourScript
{
    [InitializeOnLoadMethod]
    static void SaveConfig ()
    {
        EditorUserSettings.SetConfigValue ("Data 1", "text");
    }
}

一起来确认一下是不是真的保存了。将EditorUserSettings.asset从二进制转换到文本形式。

cd /Applications/Unity/Unity.app/Contents/Tools
./binary2text /path/to/unityproject/Library/EditorUserSettings.asset

可以看到数值都被加密了

External References


ID: 1 (ClassID: 162) EditorUserSettings
    m_ObjectHideFlags 0 (unsigned int)
    m_ConfigValues  (map)
        size 2 (int)
        data  (pair)
            first "Data 1" (string)
            second "17544c12" (string)
        data  (pair)
            first "vcSharedLogLevel" (string)
            second "0a5f5209" (string)

    m_VCAutomaticAdd 1 (bool)
    m_VCDebugCom 0 (bool)
    m_VCDebugCmd 0 (bool)
    m_VCDebugOut 0 (bool)
    m_SemanticMergeMode 2 (int)
    m_VCShowFailedCheckout 1 (bool)

3.3 ScriptableObject

Project内部共享数据的保存方法。对于多种情况都有效。想要保存游戏中的设定、大量数据的话就选择这种方法。

影响范围

ScriptableObject是Unity项目中保存数据的主要方法。Unity项目中,任何时候都能将数据作为Asset进行保存,都能通过Script将数据读取出来。
image

using UnityEngine;
using UnityEditor;

[CreateAssetMenu]
public class NewBehaviourScript : ScriptableObject
{
    [Range(0,10)]
    public int number = 3;

    public bool toggle = false;

    public string[] texts = new string[5];
}

保存什么

通过Editor扩展生成的Asset数据或者设定文件、编译后作为游戏数据使用,起到一个数据库的作用。

保存ScriptableObject的地方

只要是在Project下面的Assets文件夹里面就可以。如果是专门用于编辑器扩展的ScriptableObject的话,更倾向于保存在Editor文件夹下面。

使用方法

由于内容很多,所以详细的都在第四章《ScrptableObject》里面介绍。

3.4 JSON

JSON是一种以文本形式存储数据的语言。一般是Web客户端从服务器获取数据时,用这种形式组织数据。不过这种数据保存方式没有局限,在很多领域都有应用。
从Unity5.3开始,正式添加了JsonUtility这个模块,对于JSON有了官方的解决方法。
不过,这个解决方法与大家在平常使用的JSON库相比,速度和性能都没有优势,使用也有一定的限制。
Object与JSON间的相互变换的条件与Unity的序列化条件相同。下面是具体的条件:
- 类上面添加了Serializable属性
- 字段上面有SerializeField的属性,或者是公有字段
- 其他条件请参考第五章《SerializedObject》里面的详细内容

对于Unity中无法序列化的东西,JSON也无法序列化。
- 无法序列化Dictionary类型
- 类似object[]List<object>这样的object数组也无法序列化
- 数组对象也不能直接进行转换(JsonUtility.ToJson(List<T>))

使用方法

使用方法很简单,就是调用JsonUtility.ToJsonJsonUtility.FromJson进行object
与JSON间的变换。

[Serializable]
public class Example
{
    [SerializeField]
    string name = "hoge";

    [SerializeField]
    int number = 10;
}

/*
会得到下面的JSON
{
    "name": "hoge",
    "number": 10
}
*/
Debug.Log(JsonUtility.ToJson(new Example(), true));

JsonUtility和EditorJsonUtility

通过JsonUtility,并不能将UnityEngine.Object直接变换为JSON
UnityEngine.Object与JSON间的转换可以通过EditorJsonUtility进行。只不过,EditorJsonUtility对于数组的处理需要花一点功夫。

/*
可以得到下面这样的JSON文件
{"key名":[{"name":"hoge"},{"name":"hoge"}]}
*/
public static string ToJson(string key, Object[] objs)
{
    var json = objs.Select(obj => EditorJsonUtility.ToJson(obj)).ToArray();
    var values = string.Join(",", json);
    return string.Format("{\"{0}\":{1}]}", key, values);;
}

数组的处理

大部分的JSON库都可以处理数组的序列化。不过,同样的流程用JsonUtility处理就不行。

var list = new List<Example>{
  new Example(),
  new Example()
};

/*
会返回{}
想要的取得的是下面这样的结果
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
JsonUtility.ToJson(list)

如果不论如何都需要序列化数组的话,就需要下一些功夫了。

可以序列化的类中的字段可以序列化

类中的字段可以进行序列化,所以首先要做出这种状态。
下面是对泛型List进行序列化的代码

/*
使用方法与List一样(没有AddRange,需要自己制作)
*/
[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;

    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }

    public void OnAfterDeserialize()
    {
        Clear();
        foreach (var item in items)
            Add(item);
    }
}

这时候使用JsonUtility进行序列化,会得到下面的结果。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

/*
会生成下面这样的JSON
{"items":[{"name":"hoge","number":10},{"name":"hoge","number":10}]}
*/
Debug.Log(JsonUtility.ToJson(serializedList));

这里的要点在于ISerializationCallbackReceiver。通过JsonUtility将object变换为JSON的时候,ISerializationCallbackReceiver中的OnBeforeSerializeOnAfterSerialize会被调用。利用这一点,调用ToJson方法的时候,object会被代入到可以进行序列化的字段中,由此达到所需的目的。
能够序列化是一件好事,不过最后的JSON结果如果是使用数组的表现形式就更好了。(也就是说没有items这个键值)
下面创建SerializableList类里面的ToJson方法,达到修改生成内容的目的。

public string ToJson()
{
    var result = "[]"
    var json = JsonUtility.ToJson(this);
    var regex = new Regex("^{\"items\":(?<array>.*)}$");
    var match = regex.Match(json);
    if (match.Success)
        result = match.Groups["array"].Value;

    return result;
}

调用这个ToJson方法会得到下面的结果。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

/*
会出来下面的文本内容
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
Debug.Log(serializedList.ToJson());

不过这个方便的手段并不支持反序列化,因此还需要自己实现FromJson方法

public static SerializableList<T> FromJson(string arrayString)
{
    var json = "{\"items\":" + arrayString + "}";
    return JsonUtility.FromJson<SerializableList<T>>(json);
}

这样子就能够支持反序列化了。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

var json = serializedList.ToJson();
var serializableList = SerializableList<Example>.FromJson(json);
// 能够获取两个Example实例
Debug.Log(serializableList.Count == 2);

SerializableList.cs的完整内容如下

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using UnityEngine;

[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;

    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }

    public void OnAfterDeserialize()
    {
        Clear();
        foreach (var item in items)
            Add(item);
    }

    public string ToJson(bool prettyPrint = false)
    {
        var result = "[]";
        var json = JsonUtility.ToJson(this, prettyPrint);
        var pattern = prettyPrint ? "^\\{\n\\s+\"items\":\\s(?<array>.*)\n\\s+\\]\n}$" : "^{\"items\":(?<array>.*)}$";
        var regex = new Regex(pattern, RegexOptions.Singleline);
        var match = regex.Match(json);
        if (match.Success)
        {
            result = match.Groups["array"].Value;
            if (prettyPrint)
                result += "\n]";
        }
        return result;
    }

    public static SerializableList<T> FromJson(string arrayString)
    {
        var json = "{\"items\":" + arrayString + "}";

        return JsonUtility.FromJson<SerializableList<T>>(json);
    }
}

Dictionary的处理

通过JsonUtility来处理Dictionary非常的困难。首先,由于类似其他JSON库那样的序列化方法无法适用于Unity,因此基本上大部分的功能都得要自己实现。 这样一来就没有使用JsonUtility的意义了。因此,使用类似MiniJSON这样其他与Unity对应的JSON库会比较好一些。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值