第三章 編輯器下的數據保存
我們在擴展編輯器的時候,經常需要把一些數據保存下來,比如跟編輯器本身相關的一些設置參數或者跟游戲有關的一些參數,以便下次使用。
在Unity中保存數據的主要方法有三種。
3.1 使用EditorPrefs保存數據 (以明文保存)
這是一種可以在項目之間共享的數據保存方式,適用於跨Unity編輯器共享數據而不受項目約束。
影響范圍
保存的值可能會受到Unity大版本的影響。
比如在Unity 4.x中保存的值只能在Unity 4.x中使用,Unity 5.x也僅在Unity 5.x中可用。
注意:1、它不需要考慮運行時的效率問題,所以沒有采用PlayerPrefs的優化方式,而是直接保存了。EditorPrefs中比PlayerPrefs多了一個Bool類型的鍵值對。
2、所有通過EditorPrefs保存的值均以明文形式保存,所以切勿保存重要信息,例如密碼。
3、在Unity編輯器環境中,謹慎調用EditorPrefs.DeleteAll()。如果這樣做下次打開Unity會發現以前保存的打開項目,全部消失了。
猜測Unity自身也是使用EditorPrefs保存了所有編輯器默認的數據,像以前打開的工程、退出時保存的場景、設置的編輯環境等等都保存在EditorPrefs中,估計還會有其他編輯環境的數據,只是沒有發現。因此建議不要輕易調用
存儲EditorPrefs的位置
平台位置 |
---|
Windows(Unity4.x)HKEY_CURRENT_USER \軟件\ Unity技術\ UnityEditor 4.x |
Windows(Unity5.x)HKEY_CURRENT_USER \軟件\ Unity技術\ UnityEditor 5.x |
Mac OS X(Unity4.x)〜/庫/首選項/ com.unity3d.UnityEditor4.x.plist |
Mac OS X(Unity5.x)〜/庫/首選項/ com.unity3d.UnityEditor5.x.plist |
Unity每個主要版本的EditorPrefs都會分別保存。特別是Windows在注冊表中存儲的值。如果僅使用EditorPrefs是沒有問題的,但是由於也可以直接操作注冊表,因此可能會在此過程中進行錯誤的設置,需要足夠小心。
上圖: 在Xcode中打開的com.unity3d.UnityEditor5.x.plist
1.SetInt(); 保存整型數據;
2.GetInt(); 讀取整形數據;
3.SetFloat(); 保存浮點型數據;
4.GetFloat(); 讀取浮點型數據;
5.SetString(); 保存字符串型數據;
6.GetString(); 讀取字符串型數據;
EditorPrefs.DeleteKey (key : string) 刪除指定數據;
EditorPrefs.DeleteAll() 刪除全部鍵 ;
EditorPrefs.HasKey (key : string) 判斷數據是否存在;
3.2 EditorUserSettings.Set / GetConfigValue (以二進制形式保存)
此方法存儲的值是以二進制方式保存的,我們無法看到明文,相當於簡單的加了密,所以它適用於存儲個人信息,例如密碼。
范圍和存放位置
使用此API保存的數據一般只會在本項目中使用。由於數據是存儲在Library/EditorUserSettings.asset
中的,因此除非與他人共享“Library”文件夾,否則不會與他人共享信息。
應用場景
有時候我們在使用工具的時候,可能需要電子郵件地址和密碼才能登錄,其中之一是Oauth訪問令牌。
EditorUserSettings.asset以二進制格式保存,因此不容易看到其內容。但是,因為沒有采用任何加密的算法,我們還是可以使用Unity提供的binary2text將二進制文件轉換為文本格式並進行查看。
如何使用
using UnityEditor;
using UnityEngine;
public class EditorTest: EditorWindow
{
[InitializeOnLoadMethod]
static void SaveConfig()
{
EditorUserSettings.SetConfigValue("Password", "xxxxx");
}
}
3.3 腳本化對象 ScriptableObject
這是一種應用廣泛的數據存儲方法,Unity里面的很多資源都是采用這是方式存儲的。如果我們需要在項目中共享設置或要存儲大量數據,就可以使用此方法。
影響范圍
ScriptableObject是用Unity項目中存儲數據的主要格式。我們可以隨時通過將數據另存為Unity項目中的資源來保存數據,並且可以從腳本中加載該數據。
using UnityEngine;
[CreateAssetMenu(fileName = "EditorTest", menuName = "TestSO", order = 1)]
public class EditorTest: ScriptableObject
{
[Range(0, 10)]
public int number = 3;
public bool toggle = false;
public string[] texts = new string[5];
}
可以在監視器中編輯值
應用場景
它可以用作通過編輯器擴展創建的資源數據或者配置文件的數據庫,以及在創建后用作游戲數據。
保存位置
可以將其保存在Assets文件夾下的任何地方。如果只是編輯器擴展的ScriptableObject,我們最好把它放到“Editor”文件夾下,跟我們項目中的正式資源區分開來。
3.4 JSON - JsonUtility
Json(JavaScript Object Notation, JS 對象表示法)是一種輕量級的數據交換格式。通常情況下,它被用來從Web或服務器檢索數據的數據格式,被廣泛使用。
從Unity5.3開始,已正式添加JsonUtility類,並已正式支持JSON。
但是,盡管它比我們通常使用的JSON庫要快,但它的性能並不高,並且使用受到限制。
將對象轉換為JSON的條件與Unity序列化中的條件相同:
1:[Serializable] Serializable是.Net自帶的序列化,可以對class、struct、enum、delegate進行序列化,但是無法對屬性進行序列化。
有時候我們會自定義一些單獨的class/struct, 由於這些類並沒有從 MonoBehavior 派生所以默認並不被Unity3D識別為可以序列化的結構,自然也就不會在Inspector中顯示。
我們可以通過添加 [System.Serializable]這個屬性使Unity3D檢測並注冊這些類為可序列化的類型。
2:[SerializeField] public變量是默認被視為可以被序列化的,所以public聲明的變量在Inspector面板中是可見的。
SerializeField允許我們強制unity去序列化一個私有域,這是一個unity內部的序列化功能,有時候我們需要序列化一個private或者protected的屬性,這個時候可以使用[SerializeField]這個屬性。
3:SerializedObject ScriptableObject類型經常用於存儲一些unity3d本身不可以打包的一些object,比如字符串,類對象等。
用這個類型的子類型,則可以用BuildPipeline打包成assetbundle包供后續使用,非常方便,具體請參考后續專門的章節 腳本化對象ScriptableObject。
4:[System.NonSerialized] 有時候我們需要定義一些public變量方便操作,但是又不希望這些變量保留,這個時候就可以使用[System.NonSerialized]來完成這個操作。
注意:默認情況下,protected, private, internal變量將不會被serialize,如果變量加入了readonly, const, static等修飾符,無論他的serialize設置如何,都將不會進行serialize。
使用Unity的序列化程序意味着該序列化程序無法處理的任何內容都不能序列化成JSON格式。
1、字典Dictionary無法序列化
2、無法序列化對象數組,如object[]、List<object>
3、即使按原樣傳遞數組對象,也無法序列化(無法完成JsonUtility.ToJson(List<T>))
使用方式
JsonUtility使用起來很簡單,通過JsonUtility.ToJson和JsonUtility.FromJson來分別進行序列化和反序列化。
using System;
using UnityEditor;
using UnityEngine;
[Serializable]
public class EditorTest
{
public int m_ID = 1;
[SerializeField]
private string m_Name = "HeiHei";
[SerializeField]
internal int m_Number = 10;
}
Debug.Log(JsonUtility.ToJson(new EditorTest(), true));
EditorJsonUtility
我們無法在JsonUtility中將UnityEngine.Object轉換為Json格式(雖然絕大多數對象確實無法進行序列化,但包括ScriptableObject在內的某些對象還是可以的)。
如果我們一定要序列化這種數據,可以使用特用於編輯器的EditorJsonUtility將UnityEngine.Object轉換為Json。但是,EditorJsonUtility並不支持數組,所以最終的Json格式是通過串聯字符串創建的。
public static string ToJson(string key, UnityEngine.Object[] objs)
{
var json = objs.Select(obj => EditorJsonUtility.ToJson(obj)).ToArray();
var values = string.Join(",", json);
return string.Format("{\"{0}\":{1}]}", key, values);
}
數組處理
許多Json庫也允許序列化數組,比如Newtonsoft.Json。但是,以相同方式使用Unity的JsonUtility並不會序列化。如果真的想序列化數組,則需要進行一番設計。
可序列化類的任何字段變量都可以被序列化
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Collections.ObjectModel;
[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<EditorTest>
{
new EditorTest(),
new EditorTest()
};
Debug.Log(JsonUtility.ToJson(serializedList));
這里要着重提到的是ISerializationCallbackReceiver。在使用JsonUtility轉換為Json時,將調用ISerializationCallbackReceiver.OnBeforeSerialize/OnAfterDeserialize。
有時候我們希望在序列化后顯示成數組樣式而不是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;
}
但是,如果使用此方法,則無法進行反序列化,因此我將自己再處理下FromJson。
public static SerializableList<T> FromJson(string arrayString)
{
var json = "{\"items\":" + arrayString + "}";
return JsonUtility.FromJson<SerializableList<T>>(json);
}
現在就可以進行反序列化了
var serializedList = new SerializableList<EditorTest>
{
new EditorTest(),
new EditorTest()
};
var json = serializedList.ToJson();
var serializableList = SerializableList<EditorTest>.FromJson(json);
Debug.Log(serializableList.Count == 2);
SerializableList.cs
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
[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);
}
}
字典處理
在JsonUtility中序列化Dictionary是一件幾乎不可能的事情,我們無法像其他Json庫那樣進行序列化,因此必須自己編寫幾乎所有功能。所以這時候完全沒有必要使用JsonUtility,使用MiniJSON 、Newtonsoft.Json會更加方便快捷。
參考文章:Unity編輯器拓展手冊日文版 http://49.233.81.186/guicreation.html