游戏中有很多数据需要在运行时读取或者写入,最典型的就是游戏的存档功能。Unity自己也提供了一套存档API,但是功能比较单一,只支持保存int、float、和string这三种类型。不过C#支持文件的读写,我们可以灵活拓展它。
PlayerPrefs PlayerPrefs是Unity自带的存档方式,它的优点是使用起来非常方便。引擎已经封装好GetKey()
以及SetKey()
方法 ,并且还做保存数据的优化。由于保存数据可能是个耗时操作,频繁地保存可能会带来卡顿,所以Unity默认会在应用程序切入后台时统一保存文件,开发者也可以强制调用PlayerPrefs.Save()
来保存
然而它的缺点就是,编辑模式下查看存档非常不方便,MacOS的存档在~/Library/Preferences/folder目录下,Windows存档在HKCU\Software[company name][product name]注册表中
新建PlayerPrefsDataTest
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using System.Collections;using System.Collections.Generic;using UnityEngine;public class PlayerPrefsDataTest : MonoBehaviour { void Start () { PlayerPrefs.SetInt("MyInt" , 100 ); PlayerPrefs.SetFloat("MyFloat" , 200f ); PlayerPrefs.SetString("MyString" , "ATAO2017" ); Debug.Log(PlayerPrefs.GetInt("MyInt" , 0 )); Debug.Log(PlayerPrefs.GetFloat("MyFloat" , 0f )); Debug.Log(PlayerPrefs.GetString("MyString" , "默认值" )); if (PlayerPrefs.HasKey("MyInt" )) { Debug.Log("含有键" ); } PlayerPrefs.Save(); } private void OnGUI () { if (GUILayout.Button("删除键" )) { PlayerPrefs.DeleteKey("MyInt" ); PlayerPrefs.DeleteAll(); } } }
EditorPrefs 在编辑器模式下,Unity也提供了一组存档功能它不需要考虑运行时的效率,所以没有用PlayerPrefs的优化方式,而是立即保存,其使用方法和PlayerPrefs类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEditor;public class EditorPlayerPrefsTest : MonoBehaviour { [MenuItem("Root/Save" ) ] static void Save () { EditorPrefs.SetInt("MyInt" , 100 ); EditorPrefs.SetFloat("MyFloat" , 200f ); EditorPrefs.SetString("MyString" , "ATAO2017" ); Debug.Log(EditorPrefs.GetInt("MyInt" , 0 )); Debug.Log(EditorPrefs.GetFloat("MyFloat" , 0f )); Debug.Log(EditorPrefs.GetString("MyString" , "默认值" )); if (EditorPrefs.HasKey("MyInt" )) { } EditorPrefs.DeleteKey("MyInt" ); EditorPrefs.DeleteAll(); } }
PlayerPrefs保存复杂结构 PlayerPrefs可以保存字符串,结合JSON序列化和反序列化功能,它就可以保存各种复杂的数据结构了。另外,保存存档取决于硬件当时的条件,完全有保存不上的情况,所以可以通过try……catch来捕获保存时的错误异常 。
新建JsonToPlayerPrefs
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 using System.Collections;using System.Collections.Generic;using UnityEngine;public class JsonToPlayerPrefs : MonoBehaviour { private void Start () { Record record = new Record(); record .stringVaule = "ATAO2022" ; record .intValue = 5 ; record .name = new List<string >() { "文字1" , "文字2" }; string json = JsonUtility.ToJson(record ); try { PlayerPrefs.SetString("record" , json); } catch (System.Exception err) { Debug.Log("Got:" +err); } record = JsonUtility.FromJson<Record>(PlayerPrefs.GetString("record" )); Debug.LogFormat("stringValue: {0},intValue: {1}," , record .stringVaule, record .intValue); } } [System.Serializable ] public class Record { public string stringVaule; public int intValue; public List<string > name; }
凡是参与JSON序列化的对象都需要标记[System.Serializable]
TextAsset TextAsset是Unity提供的一个文本对象,它可以通过Resources.Load()或者AssetBundle来读取数据,其中数据是string格式的。当然我们也可以按byte[]读取,它支持读取的文本格式包括txt、html、htm、xml、bytes、json、csv、yaml和fnt。
Resources.Load(string path)方法中的path不能带文件类型后缀
新建TextAssetTest
脚本,用来读取Resources文件里面的MyText文本。
1 2 3 4 5 6 7 8 9 using UnityEngine;public class TextAssetTest : MonoBehaviour { void Start () { Debug.Log(Resources.Load<TextAsset>("MyText" ).text); } }
读写文本 Unity可以利用C#的File
类来读写文本,此时只需要提供一个目录即可。编辑器模式 下读写文本是很方便的,但是一旦打包发布,Assets/目录都不存在了,运行时是无法读取它目录下的文本的。
使用File.WriteAllText()
和File.ReadAllText()
来对文本进行读取和写入
新建FileClassTest
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using UnityEngine;using System.IO;using System.Text;using UnityEditor;public class FileClassTest { [MenuItem("Root/File" ) ] static void FIleClassReadWrite () { string path = Path.Combine(Application.dataPath, "FileClassTest" ); if (File.Exists(path)) File.Delete(path); StringBuilder sb = new StringBuilder(); sb.AppendFormat("第一行:{0}" , 100 ).AppendLine(); sb.AppendFormat("第二行:{0}" , 200 ).AppendLine(); File.WriteAllText(path, sb.ToString()); Debug.Log(File.ReadAllText(path)); } }
Runtime读写文本 在游戏运行期间,只有Resources和StreamingAssets目录具有读取权限,其中Resources用来读取游戏数据,而StreamingAssets可使用FIle
类来读取文件(除了个别平台外比如Android、Web),但都是只读的。只有Application.persistentDataPath目录是可读、可写的。
我们先在编辑器Project窗口的Resources文件夹下读取上一节使用的“MyText.txt”文件,新建StreamingAssets文件夹,在其中新建Test.txt文本。
新建RunTimeIOTest
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System.Collections;using System.Collections.Generic;using UnityEngine;public class RunTimeIOTest : MonoBehaviour { string m_ResourceText = string .Empty; string m_StreamAssetsText = string .Empty; string m_PersistentDataText = string .Empty; private void Start () { m_ResourceText = Resources.Load<TextAsset>("MyText" ).text; m_StreamAssetsText = System.IO.File.ReadAllText(System.IO.Path.Combine(Application.streamingAssetsPath, "test.txt" )); } private void OnGUI () { GUILayout.Label(string .Format("<size=50>Resources : {0}</size>" , m_ResourceText)); GUILayout.Label(string .Format("<size=50>StreamingAssets : {0}</size>" , m_StreamAssetsText)); GUILayout.Label(string .Format("<size=50>PersistentDataPath : {0}</size>" , m_PersistentDataText)); if (GUILayout.Button("<size=50>写入并读取时间</size>" )) { string path = System.IO.Path.Combine(Application.persistentDataPath, "test.txt" ); System.IO.File.WriteAllText(path, System.DateTime.Now.ToString()); m_PersistentDataText = System.IO.File.ReadAllText(path); } } }
我们需要将场景build出来才能测试这个脚本。
关于Application.persistentDataPath
在不同平台指向的位置:
Unity - Scripting API: Application.persistentDataPath (unity3d.com)
关于不同环境下Application.Datapath
指向的位置
Unity - Scripting API: Application.dataPath (unity3d.com)
PersistentDataPath目录 调用EditorUtility.RevealInFinder()
方法,帮助我们在开发中快速打开当前平台的PersistentDataPath目录指向的文件资源管理器窗口
新建OpenPersistentDataFolder
脚本
1 2 3 4 5 6 7 8 9 10 11 using UnityEngine;using UnityEditor;public class OpenPersistentDataFolder { [MenuItem("Root/Open PersistentDataPath" ) ] static void Open () { EditorUtility.RevealInFinder(Application.persistentDataPath); } }
游戏存档 我们需要一个不影响开发的存档,并且查看要非常方便。
我们可以自己写一个存档类,在Editor模式下将存档保存在Assets同级目录下,这样查看存档内容就方便多了
新建RecordUtil
脚本,在Editor模式下将数据保存与Application.dataPath目录,而在游戏真实平台下将数据保存在Application.persistentDataPath下。只有调用Save()
方法时,数据才会被强制写入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 using System.Collections;using System.Collections.Generic;using UnityEngine;using System.IO;using System;using System.Text;public class RecordUtil { static string RecordRootPath { get { #if (UNITY_EDITOR || UNITY_STANDALONE) return Application.dataPath + "/../Record/" ; #else return Application.persistentDataPath #endif } } static Dictionary<string , string > recordDic = new Dictionary<string , string >(); static List<string > recordDirty = new List<string >(); static List<string > deleteDirty = new List<string >(); static List<string > readDirty = new List<string >(); static private readonly UTF8Encoding UTF8 = new UTF8Encoding(false ); static RecordUtil () { readDirty.Clear(); if (Directory.Exists(RecordRootPath)) { foreach (string file in Directory.GetFiles(RecordRootPath,"*.record" ,SearchOption.TopDirectoryOnly)) { string name = Path.GetFileNameWithoutExtension(file); if (!readDirty.Contains(name)) { readDirty.Add(name); Get(name); } } } } public static void Save () { foreach (string key in deleteDirty) { try { string path = Path.Combine(RecordRootPath, key + ".record" ); if (recordDirty.Contains(key)) recordDirty.Remove(key); if (File.Exists(path)) File.Delete(path); } catch (Exception ex) { Debug.LogError(ex.Message); } } deleteDirty.Clear(); foreach (string key in recordDirty) { string value ; if (recordDic.TryGetValue(key,out value )) { if (!readDirty.Contains(key)) readDirty.Add(key); string path = Path.Combine(RecordRootPath, key + ".record" ); recordDic[key] = value ; try { Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, value , UTF8); } catch (Exception ex) { Debug.LogError(ex.Message); } } } recordDirty.Clear(); } public static void Set (string key,string value ) { recordDic[key] = value ; if (!recordDirty.Contains(key)) recordDirty.Add(key); #if UNITY_EDITOR || UNITY_STANDALONE Save(); #endif } public static string Get (string key ) { return Get(key, string .Empty); } public static string Get (string key,string defaultValue ) { if (readDirty.Contains(key)) { string path = Path.Combine(RecordRootPath, key + ".record" ); try { string readStr = File.ReadAllText(path, UTF8); recordDic[key] = readStr; } catch (Exception ex) { Debug.LogError(ex.Message); } readDirty.Remove(key); } string value ; if (recordDic.TryGetValue(key, out value )) return value ; else return defaultValue; } public static void Delete (string key ) { if (recordDic.ContainsKey(key)) recordDic.Remove(key); if (!deleteDirty.Contains(key)) deleteDirty.Add(key); #if UNITY_EDITOR || UNITY_STANDALONE Save(); #endif } }
关于这堆代码的一些Tips:
我们执行Set()
方法时,在Editor和UnityStandalone会执行Save()
方法,执行了Save()
方法后,deleteDirty(删除标记)和recordDirty(保存标记)都会被清空,并且在过程中回将新添加进来的key写在readDirty(读取标记)里,这个把key写进readDirty的过程只有一次,如果我们再执行Set()
方法,并且使用了相同的key的话,readDirty不会更新。这样我们执行Get()
方法,会根据readDirty的标记从本地文件里把数据读出来
注意#if UNITY_EDITOR || UNITY_STANDALONE预定义,在编辑器模式下不用关心性能所以有Save()
操作
RecordUtil
实例化过程中,会利用readDirty(将存档文件全部读取入recordDic里面,通过调用Get(string)方法的重载Get(string,string),在过程中清空了readDirty
接下来新建一个CustomSaveClassTest
脚本,来实际使用一下自己写的存档类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 using System.Collections;using System.Collections.Generic;using UnityEngine;public class CustomSaveClassTest : MonoBehaviour { void Start () { Setting setting = new Setting(); setting.stringValue = "测试字符串" ; setting.intValue = 100 ; RecordUtil.Set("setting" , JsonUtility.ToJson(setting)); } private Setting m_Setting = null ; private void OnGUI () { if (GUILayout.Button("<size=50>获取存档</size>" )) { m_Setting = JsonUtility.FromJson<Setting>(RecordUtil.Get("setting" )); } if (m_Setting != null ) { GUILayout.Label(string .Format("<size=50>{0},{1}</size>" , m_Setting.intValue, m_Setting.stringValue)); } } private void OnApplicationPause (bool pause ) { if (pause) RecordUtil.Save(); } } [System.Serializable ] class Setting { public string stringValue; public int intValue; }
默认情况下,应用进入后台后会保存数据。如果应用进入后台发生了闪退现象,那么数据就无法保存了,所以某些重要的数据需要强制调用Save()
方法