配置文件

在编辑器的StreamingAssets目录下,放置一个json格式的InitPlanes.json文件,作为配置文件。同时我们也在Resources文件夹下放这个文件,用在后面测试。

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
{
"planes": [
{
"planeId": 0,
"level": 0,
"attack": 5,
"fireRate": 0.8,
"life": 100
},
{
"planeId": 1,
"level": 0,
"attack": 10,
"fireRate": 0.6,
"life": 120
},
{
"planeId": 2,
"level": 0,
"attack": 20,
"fireRate": 0.4,
"life": 150
},
{
"planeId": 3,
"level": 0,
"attack": 40,
"fireRate": 0.2,
"life": 200
}
]
}

配置文件读取

IReader

读取接口定义一种读取器,用于读取各种格式的配置文件,通过实现这个接口,可以实现Json读取、Xml读取、Scriptable Object读取等。

在Scripts——Modle——Reader文件夹内新建IReader文件

索引器和属性的不同点

  1. 索引器不能是静态的,索引器必须通过对应类的实例访问。
  2. 索引器相当于方法,可以被重载。比如this[int key] ; this[int key1,string key2]

索引器和属性的相同点

  1. 都能使用get,set语法。注意索引器的形参不能命名为“value”,这样就在get中重复了

链式写法:定义接口内的方法或索引器时返回接口本身。

1
2
3
4
5
6
7
public interface IReader 
{
IReader this[string key] { get; }
IReader this[int key] { get; }
void Get<T>(System.Action<T> callback);
void SetData(object data);
}

JsonReader(难点)

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
using System;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using LitJson;
using UnityEngine;

public class JsonReader : IReader
{
private JsonData _data;
private JsonData _tempData;
private KeyQueue _keys;
private readonly Queue<KeyQueue> _keyQueues = new();
public IReader this[string key]
{
get
{
if (!SetKey(key))
{
try
{
_tempData = _tempData[key];
}
catch (Exception)
{
Debug.LogError("在数据中无法找到对应Key:" + "Key为:"+ key + "数据为:" + _tempData.ToJson());
}
}
return this;
}
}

public IReader this[int key]
{
get
{
if (!SetKey(key))
{
try
{
_tempData = _tempData[key];
}
catch (Exception)
{
Debug.LogError("在数据中无法找到对应Key:" + "Key为:" + key + "数据为:" + _tempData.ToJson());
}
}
return this;
}
}
private bool SetKey<T>(T key)
{
if (_data == null || _keys != null)//_keys如果不为空,说明缓存还存在
{
if (_keys == null)
_keys = new KeyQueue();

IKey keyData = new Key();
keyData.Set(key);
_keys.Enqueue(keyData);

return true;
}
return false;
}
public void Get<T>(Action<T> callback)
{
if (_keys != null)//如果是从缓存中读取
{
_keys.OnComplete(dataTemp =>//把Reader的回调函数交给Key内的回调
{
T value = GetValue<T>(dataTemp);
ResetData();//重置_tempData
callback(value);//一定要先重置_tempData再执行回调,否则reader的回调内嵌套新的reader时就会出现问题
});

_keyQueues.Enqueue(_keys);//调用到Get方法之后,说明这是一组键值缓存,把这一组放到_keyQueues里
_keys = null;//及时清空,它是SetKey时是否进行key值缓存的依据
ExecuteKeyQueues();//尝试执行所有的回调,在这个方法中,如果_data是空就不执行。
return;
}

if (callback == null)
{
Debug.LogWarning("当前回调方法为空,不返回数据");
ResetData();
return;
}
T data = GetValue<T>(_tempData);
ResetData();
callback(data);//一定要先重置_tempData再执行回调,否则reader的回调内嵌套新的reader时就会出现问题
}

private void ExecuteKeyQueues()
{
if(_data == null) return;

IReader reader = null;
foreach (KeyQueue keyQueue in _keyQueues)
{
foreach (object key in keyQueue)
{
if (key is string)
reader = this[(string)key];
else if (key is int)
reader = this[(int)key];
else
Debug.LogError("当前键值类型还不支持");
}
keyQueue.Complete(_tempData);
}
}
private T GetValue<T>(JsonData data)
{
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
return (T)converter.ConvertTo(data.ToString(), typeof(T));
}
private void ResetData()
{
_tempData = _data;
}

public void SetData(object data)
{
if (data is string)
{
_data = JsonMapper.ToObject(data as string);
ResetData();
ExecuteKeyQueues();
}
else
{
Debug.LogError("传入数据类型错误,当前类只能解析Json");
}
}
}

注意看索引器的写法,JsonReader其实是将LitJson提供的索引访问方式又封装了一层。每次用索引器访问一次,就更新一次tempData的引用,最后使用Get方法获取最终数据并重置tempData

在使用JsonReader获取数据之前,需要执行SetData方法,这样_data才有数据,而SetData方法是在异步读取配置文件的回调中读取的,这意味着我们在调用JsonReader时,里面可能没有数据。这里的解决方案是提前缓存访问的key值,等到配置读取完毕后通过缓存的key值返回数据,而key值可能是string,也可能是int,所以使了一个IKey接口来统一缓存key值,并且实现一个自定义的KeyQueue类,更加方便地遍历缓存的key值。

需要注意的是,一个KeyQueue类保存的是一组访问命令,我们还需要一个Queue<KeyQueue>来缓存多组访问命令。

KeyQueue类

包装了一个Queue<IKey>,并且实现了一种迭代器和一个回调,这个回调是JsonReader确认Json数据读取完毕后,把缓存的访问命令都执行时调用的,之所以会把JsonReader的回调传递到这里是因为我们可能有很多个KeyQueue访问命令需要执行

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
public class KeyQueue : IEnumerable
{
private Queue<IKey> _keys = new Queue<IKey>();
private Action<JsonData> _onComplete;
public void Enqueue(IKey key)
{
_keys.Enqueue(key);
}
public IKey Dequeue()
{
return _keys.Dequeue();
}
public void Clear()
{
_keys.Clear();
}
public void Complete(JsonData jsonData)
{
_onComplete?.Invoke(jsonData);
}
public void OnComplete(Action<JsonData> callback)
{
_onComplete = callback;
}

public IEnumerator GetEnumerator()
{
foreach (IKey key in _keys)
{
yield return key.Get();//在迭代的时候,返回的是object
}
}
}

Key类和IKey接口

设计这两个是为了解决Key的类型很多,无法在一个Queue里遍历的问题。每次添加一个key值都自动记录它的Type(这里并没有记录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface IKey
{
Type KeyType { get; }
void Set<T>(T key);
object Get();
}
public class Key : IKey
{
private object _key;
public Type KeyType { get;private set; }
public object Get()
{
return _key;
}

public void Set<T1>(T1 key)
{
_key = key;
}
}

测试

通过测试代码,能够理解之前缓存Key的意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;

public class GameRoot : MonoBehaviour
{
private void Start()
{
//...

string jsonString = Resources.Load<TextAsset>("InitPlane").text;
JsonReader reader = new JsonReader();
reader["planes"][0]["fireRate"].Get<float>(value => Debug.Log(value));//此时缓存一个KeyQueue
reader["planes"][2]["attack"].Get<int>(value => Debug.Log(value));//此时缓存另外一个KeyQueue
reader.SetData(jsonString);//此时终于获取到了值,之前的两个KeyQueue都会被执行

}
}

ReaderManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Collections.Generic;
using UnityEngine;

public class ReaderMgr : NormalSingleton<ReaderMgr>
{
private Dictionary<string,IReader> _readersDic = new Dictionary<string,IReader>();
public IReader GetReader(string path)
{
IReader reader = null;
if(_readersDic.ContainsKey(path))
reader = _readersDic[path];
else
{
reader = ReaderConfig.GetReader(path);
LoadMgr.Instance.LoadConfig(path,data=>reader.SetData(data));
if(reader != null)
_readersDic[path] = reader;
else Debug.LogError("未获取到对应Reader:" + path);
}
return reader;
}
}

ReaderConfig

使用脚本的形式写Config,使用字典来决定返回的Reader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using UnityEngine;

public class ReaderConfig
{
private static readonly Dictionary<string, Func<IReader>> _readersDic = new Dictionary<string, Func<IReader>>()
{
{".json",()=>new JsonReader()},
//{".xml",()=>new XmlReader() }
};
public static IReader GetReader(string path)
{
foreach (KeyValuePair<string, Func<IReader>> pair in _readersDic)
{
if (path.Contains(pair.Key))
{
return pair.Value();
}
}
Debug.LogError("未找到对应文件的读取器 " + path);
return null;
}
}

注意这里的使用KeyValuePair遍历字典的写法

当需要新的读取器时,在实现对应文件的读取器后只需要在这里的字典中添加一下即可。还是尽可能修改配置的思想

ResouceLoader

修改ILoader,添加读取配置文件的方法

1
2
3
4
5
6
7
8
using System;
using UnityEngine;

public interface ILoader
{
public GameObject LoadPrefab(string path, Transform parent = null);
void LoadConfig(string path,Action<object> onComplete);
}

添加配置文件Path

1
2
3
4
5
6
7
8
using UnityEngine;

public class Path
{
//...
private static readonly string CONFIG_FOLDER = Application.streamingAssetsPath + "/Config";
public static readonly string INIT_PLANE_CONFIG = CONFIG_FOLDER + "/InitPlane.json";
}

我们把配置文件InitPlane.json放在了StreamingAssets目录下,所以需要使用UnityWebRequest来加载。

修改ResourceLoader

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class ResourceLoader : ILoader
{

public GameObject LoadPrefab(string path,Transform parent = null)
{
GameObject prefab = Resources.Load<GameObject>(path);
GameObject temp = Object.Instantiate(prefab,parent);
return temp;
}
public void LoadConfig(string path, System.Action<object> onComplete)
{
CoroutineMgr.Instance.ExecuteOnce(Config(path, onComplete));
}
private IEnumerator Config(string path, System.Action<object> onComplete)
{
using (UnityWebRequest unityWebRequest = UnityWebRequest.Get(path))
{
yield return unityWebRequest.SendWebRequest();
switch (unityWebRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
Debug.Log("连接出错");
yield break;
case UnityWebRequest.Result.ProtocolError:
Debug.Log("协议出错");
yield break;
case UnityWebRequest.Result.DataProcessingError:
Debug.Log("数据处理出错");
yield break;
}
onComplete?.Invoke(unityWebRequest.downloadHandler.text);
Debug.Log("文件加载成功:路径为:" + path);
}
}
}

修改LoadMgr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using UnityEngine;

public class LoadMgr : NormalSingleton<LoadMgr>, ILoader
{
private ILoader m_Loader;
public LoadMgr()
{
m_Loader = new ResourceLoader();
}

public GameObject LoadPrefab(string path,Transform parent = null)
{
return m_Loader.LoadPrefab(path,parent);
}
public void LoadConfig(string path, Action<object> onComplete)
{
m_Loader.LoadConfig(path, onComplete);
}
}