PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/JSONConfig.cs

https://bitbucket.org/rdw/unity-utils
C# | 273 lines | 193 code | 22 blank | 58 comment | 55 complexity | b769fd0efb0dbc2c93226ee60a919dcf MD5 | raw file
  1. using UnityEngine;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.IO;
  8. // By Ryan Duane Williams, with slight modifications by Johannes Hoff and Chris Pine
  9. /*
  10. * Attach this script to a single object in the scene which has the "Static" tag. This is the "Unitary" pattern.
  11. *
  12. * JSON files are expected to live in Assets/Resources/Config in the source tree and have the
  13. * extension ".bytes" (the extension is so Unity's asset bundling doesn't munge their contents). If you're
  14. * building a webplayer you should create a corresponding Config/ directory at the same level as webplayer.html.
  15. * The player will try to load files from that directory, and if it certain errors occur it'll
  16. * fall back on the configs from Assets/Resources which will be packed into the ".unity3d" file. Most errors
  17. * it'll just bitch at you continually until you fix the files.
  18. *
  19. * Here's a very basic sample usage:
  20. JSONConfig.inst.LoadConfig("Config/fish.bytes", (f, dict) => {
  21. JSONConfig.SetFields(this, dict);
  22. Debug.Log("Loaded " + f);
  23. });
  24. */
  25. public class JSONConfig : MonoBehaviour {
  26. public static JSONConfig _inst = null;
  27. public static JSONConfig inst {
  28. get {
  29. if(_inst == null) _inst = GameObject.FindGameObjectWithTag("Static").GetComponent<JSONConfig>();
  30. return _inst;
  31. }
  32. }
  33. // don't even try WWW on these platforms because it just times out (but: can cause you to introduce race conditions, so be sure to test with skipWeb == false)
  34. private bool skipWeb {
  35. get {
  36. return (Application.platform != RuntimePlatform.OSXWebPlayer &&
  37. Application.platform != RuntimePlatform.WindowsWebPlayer &&
  38. Application.platform != RuntimePlatform.NaCl);
  39. }
  40. }
  41. public delegate void LoadDelegate(string s, Dictionary<string, object> d);
  42. private Dictionary<string, Dictionary<string, object>> loadedConfigs = new Dictionary<string, Dictionary<string, object>>();
  43. private Dictionary<string, LoadDelegate> pendingLoads = new Dictionary<string, LoadDelegate>();
  44. // convenience method to parse an enum value out of a string
  45. public static T GetEnum<T>(string v)
  46. {
  47. return (T)Enum.Parse(typeof(T),v);
  48. }
  49. // create an instance of an object and immediately set fields on it from the json dict. Requires a zero-args constructor on the type.
  50. public static T CreateInstance<T>(Dictionary<string, object> dict) {
  51. var obj = System.Activator.CreateInstance<T>();
  52. SetFields (obj, dict);
  53. return obj;
  54. }
  55. // non-generic CreateInstance sometimes comes in handy
  56. public static object CreateInstance(Type t, Dictionary<string, object> dict) {
  57. var obj = System.Activator.CreateInstance(t);
  58. SetFields (obj, dict);
  59. return obj;
  60. }
  61. public static void SetFields(System.Object obj, Dictionary<string, object> dict) {
  62. Type type = obj.GetType();
  63. SetFields (type, obj, dict);
  64. }
  65. /* Sets all members on the object *obj* based on the appropriate key from *dict*, which is presumed to be in JSON form.
  66. * So if you have an instance of a Thing:
  67. * public class Thing {
  68. * float m1;
  69. * string m2;
  70. * }
  71. *
  72. * You can modify its fields with the parsed form of this JSON:
  73. * {"m1":1.0, "m2":"test"}
  74. *
  75. * Ignores any name mismatches that might arise because that's more useful than being strict for me right now. Sets static and private variables.
  76. */
  77. public static void SetFields(Type type, System.Object obj, Dictionary<string, object> dict) {
  78. if(dict == null) return;
  79. foreach (FieldInfo f in type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) {
  80. if(dict.ContainsKey(f.Name)) {
  81. SetField(f, obj, dict[f.Name]);
  82. }
  83. }
  84. }
  85. // Sets an individual field on an object from a parsed JSON structure.
  86. // Uses many different rules, based mostly on the type of the field, to decide how to do the setting.
  87. public static void SetField(FieldInfo f, object obj, object value) {
  88. f.SetValue(obj, GetValueOfType(f.FieldType, value));
  89. }
  90. private static object GetValueOfType(Type fieldType, object value) {
  91. if (fieldType == typeof(float)) {
  92. return Convert.ToSingle(value);
  93. }
  94. if (fieldType == typeof(int)) {
  95. return Convert.ToInt32(value);
  96. }
  97. if (fieldType == typeof(string)) {
  98. return value;
  99. }
  100. if (fieldType == typeof(List<int>)) { // List<int>, [1,2,3] => new List<int>({1,2,3});
  101. var c = ((List<object>)value).ConvertAll<int>(Convert.ToInt32);
  102. return c;
  103. }
  104. if (fieldType.IsEnum) { // AudioRolloffMode, "Custom" => AudioRolloffMode.Custom
  105. return Enum.Parse(fieldType, (string)value, true);
  106. }
  107. if (fieldType == typeof(Vector2)) { // Vector2, [1,2] => new Vector2(1,2);
  108. var c = ((List<object>)value).ConvertAll<float>(Convert.ToSingle);
  109. return new Vector2(Convert.ToSingle(c[0]), Convert.ToSingle(c[1]));
  110. }
  111. if (fieldType == typeof(Color)) { // Color, [1,1,1,1] => new Color(1,1,1,1)
  112. var c = ((List<object>)value).ConvertAll<float>(Convert.ToSingle);
  113. if (c.Count == 3) {
  114. return new Color(c[0], c[1], c[2]);
  115. }
  116. if (c.Count == 4) {
  117. return new Color(c[0], c[1], c[2], c[3]);
  118. }
  119. }
  120. if (fieldType.IsGenericType) {
  121. // this chunk of code handles generic dictionaries and lists; it only works with string keys on the dictionaries, and for now any values must have zero-args constructors
  122. if (fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) {
  123. Type[] typeParameters = fieldType.GetGenericArguments();
  124. if (typeParameters[0] == typeof(string)) {
  125. var sourceDict = (Dictionary<string, object>)value;
  126. var destDict = Activator.CreateInstance(fieldType);
  127. var addToDest = fieldType.GetMethod("Add", typeParameters);
  128. foreach (var kv in sourceDict) {
  129. if (typeParameters[1] == typeof(object)) {
  130. addToDest.Invoke(destDict, new[] { kv.Key, kv.Value });
  131. } else {
  132. var subValue = CreateInstance(typeParameters[1], (Dictionary<string, object>)kv.Value);
  133. addToDest.Invoke(destDict, new[] { kv.Key, subValue });
  134. }
  135. }
  136. return destDict;
  137. }
  138. Debug.Log("Dictionary doesn't have string keys"); // could theoretically handle non-string keys, but it's more work
  139. return null;
  140. }
  141. if (fieldType.GetGenericTypeDefinition() == typeof(List<>)) {
  142. Type[] typeParameters = fieldType.GetGenericArguments();
  143. var sourceList = (List<object>)value;
  144. var destList = Activator.CreateInstance(fieldType);
  145. var addToDest = fieldType.GetMethod("Add", typeParameters);
  146. foreach (var val in sourceList.Select(item => GetValueOfType(typeParameters[0], item))) {
  147. addToDest.Invoke(destList, new[] { val });
  148. }
  149. return destList;
  150. }
  151. }
  152. if (fieldType.GetMethod("FromJson") != null) { // if there's a custom parser method on the class, delegate all work to that
  153. MethodInfo m = fieldType.GetMethod("FromJson");
  154. return m.Invoke(null, new[] { value });
  155. }
  156. if (fieldType.IsClass) {
  157. // Try instantiating a single object with CreateInstance
  158. var memb = CreateInstance(fieldType, (Dictionary<string, object>)value);
  159. return memb;
  160. }
  161. // catchall, this will probably never work
  162. return value;
  163. }
  164. // Loads a json file and calls the callback when it's completely parsed.
  165. // Filename should be of the form "Config/foo.bytes"
  166. public void LoadConfig(string filename, LoadDelegate cb) {
  167. LoadConfig(filename, cb, new string[]{}, false);
  168. }
  169. // More arguments to LoadConfig. Dependencies specify a list of other configs that must be loaded (and their own callbacks called) before the callback is called. This is definitely not the right system level to express initialization dependencies so try not to use this argument unless you are super lazy.
  170. // The *force* boolean forces the file to be reloaded (instead of using an in-memory cache); useful if you want to check for changes without reloading the game.
  171. public void LoadConfig(string filename, LoadDelegate cb, IEnumerable<string> dependencies, bool force) {
  172. if (!force && loadedConfigs.ContainsKey(filename) && areDependenciesLoaded(dependencies)) {
  173. cb(filename, loadedConfigs[filename]);
  174. }
  175. if(pendingLoads.ContainsKey(filename)) {
  176. pendingLoads[filename] = pendingLoads[filename] + cb;
  177. } else {
  178. pendingLoads[filename] = cb;
  179. StartCoroutine(WaitForLoad(filename, dependencies));
  180. }
  181. }
  182. void FinishLoad(string filename, Dictionary<string, object> dict) {
  183. loadedConfigs[filename] = dict;
  184. pendingLoads[filename](filename, dict);
  185. pendingLoads.Remove(filename);
  186. }
  187. IEnumerator WaitForLoad(string filename, IEnumerable<string> dependencies) {
  188. if(skipWeb) {
  189. var dict = LoadFromResources(filename);
  190. while (!areDependenciesLoaded(dependencies)) {
  191. Debug.Log ("Waiting for dependencies for " + filename);
  192. yield return new WaitForSeconds(0f);
  193. }
  194. FinishLoad(filename, dict);
  195. yield break;
  196. }
  197. while (true) {
  198. WWW www = new WWW(filename);
  199. yield return www;
  200. if (www.error == null) {
  201. var dict = MiniJSON.Json.Deserialize(www.text) as Dictionary<string, object>;
  202. while (!areDependenciesLoaded(dependencies)) yield return new WaitForSeconds(0.1f);
  203. FinishLoad(filename, dict);
  204. break;
  205. }
  206. else {
  207. if (www.error.EndsWith("(Domain name not found)") || www.error.EndsWith("Host not found")) {
  208. Debug.Log("Falling back to bundled resource for " + filename);
  209. var dict = LoadFromResources(filename);
  210. while (!areDependenciesLoaded(dependencies)) yield return new WaitForSeconds(0.1f);
  211. FinishLoad(filename, dict);
  212. break;
  213. }
  214. else {
  215. Debug.LogError("Error loading from server: " + www.error);
  216. Debug.Log("Error loading from server " + www.error);
  217. yield return new WaitForSeconds(0.5f);
  218. }
  219. }
  220. }
  221. }
  222. Dictionary<string, object> LoadFromResources(string filename) {
  223. string resource_filename = Path.ChangeExtension(filename, null); // for some reason it prefers them without extensions
  224. TextAsset asset = (TextAsset)Resources.Load(resource_filename);
  225. var dict = MiniJSON.Json.Deserialize(asset.text) as Dictionary<string, object>;
  226. return dict;
  227. }
  228. private bool areDependenciesLoaded(IEnumerable<string> deps) {
  229. bool loaded = true;
  230. foreach (var dep in deps) {
  231. loaded = loaded && loadedConfigs.ContainsKey(dep);
  232. }
  233. return loaded;
  234. }
  235. }
  236. /*
  237. public class TestClass {
  238. public string key;
  239. public TestClass() {
  240. key = "wrong";
  241. }
  242. }
  243. Test code:
  244. LoadConfig("Config/test.json", f, d => {
  245. Debug.Log("loaded " + f);
  246. var tc = new TestClass();
  247. SetFields(tc, d);
  248. Debug.Log ("ok " + tc.key);
  249. Debug.Log("ok " + tc.key);
  250. });*/