├── BlackBoard.cs ├── DictionaryDrawer.cs ├── Images └── Blackboard.png ├── LICENSE.md └── README.md /BlackBoard.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System.Text.RegularExpressions; 8 | 9 | 10 | [Serializable] 11 | public class Blackboard : Dictionary, ISerializationCallbackReceiver 12 | { 13 | [Serializable] 14 | //struct represents the string key, and the serialized value from the dictionary. 15 | private struct SaveItem 16 | { 17 | public string key; 18 | public string value; 19 | public int index; 20 | 21 | public SaveItem(string key, string val, int index) 22 | { 23 | this.key = key; 24 | this.value = val; 25 | this.index = index; 26 | } 27 | } 28 | 29 | //All serialized items except for objects in a scene, which have to be handled separately. 30 | [SerializeField, HideInInspector] 31 | private List saveItems; 32 | 33 | //We need a different struct and list for Object references in scene :( 34 | [Serializable] 35 | private struct NonAssetSaveItem 36 | { 37 | public string key; 38 | public UnityEngine.Object obj; 39 | public int index; 40 | 41 | public NonAssetSaveItem(string key, UnityEngine.Object obj, int index) 42 | { 43 | this.key = key; 44 | this.obj = obj; 45 | this.index = index; 46 | } 47 | } 48 | [SerializeField, HideInInspector] 49 | private List sceneObjectSaveItems; 50 | 51 | /// 52 | /// Takes all of the keyvalue pairs from the Dictionary and stores them as Serializable lists. 53 | /// 54 | public void OnBeforeSerialize() 55 | { 56 | sceneObjectSaveItems = new List(); 57 | saveItems = new List(); 58 | List keys = this.Keys.ToList(); 59 | List values = this.Values.ToList(); 60 | 61 | for (int i = 0; i < Count; i++) 62 | { 63 | object value = values[i]; 64 | string encode = ""; 65 | 66 | //Unhandled Enum Types 67 | if (value is Enum) 68 | { 69 | Enum enumValue = (Enum)value; 70 | Console.WriteLine("Enum Value: " + enumValue.ToString()); 71 | encode = $"({value.GetType().AssemblyQualifiedName}){enumValue}"; 72 | saveItems.Add(new SaveItem(keys[i], encode, i)); 73 | continue; 74 | } 75 | 76 | switch (value) 77 | { 78 | case null: encode = "(null)"; break; 79 | case int: encode = "(int)" + ((int)value).ToString("F9"); break; 80 | case float: encode = "(float)" + ((float)value).ToString("F9"); break; 81 | case double: encode = "(double)" + ((double)value).ToString("F9"); break; 82 | case long: encode = "(long)" + ((long)value).ToString(); break; 83 | case string: encode = "(string)" + (string)value; break; 84 | case bool: encode = "(bool)" + (((bool)value) == true ? "true" : "false"); break; 85 | case Vector2Int: encode = "(Vector2Int)" + ((Vector2Int)value).ToString(); break; 86 | case Vector3Int: encode = "(Vector3Int)" + ((Vector3Int)value).ToString(); break; 87 | case Vector2: encode = "(Vector2)" + ((Vector2)value).ToString(); break; 88 | case Vector3: encode = "(Vector3)" + ((Vector3)value).ToString(); break; 89 | case Vector4: encode = "(Vector4)" + ((Vector4)value).ToString(); break; 90 | case Bounds: encode = "(Bounds)" + ((Bounds)value).ToString(); break; 91 | case Rect: encode = "(Rect)" + ((Rect)value).ToString("F9"); break; 92 | case Color: encode = "(Color)" + JsonUtility.ToJson((Color)value); break; 93 | case AnimationCurve: encode = "(AnimationCurve)" + Serializer.SerializeAnimationCurve((AnimationCurve)value); break; 94 | case Gradient: encode = "(Gradient)" + Serializer.SerializeGradient((Gradient)value); break; 95 | case UnityEngine.Object obj: 96 | string assetPath = Application.isEditor ? AssetDatabase.GetAssetPath(obj) : null; 97 | if (!string.IsNullOrEmpty(assetPath)) 98 | { 99 | encode = "(UnityEngine.Object)" + assetPath; 100 | } 101 | else 102 | { 103 | sceneObjectSaveItems.Add(new NonAssetSaveItem(keys[i], obj, i)); 104 | } 105 | break; 106 | //Try to serialize to JSON. May be empty if type is not supported 107 | default: encode = $"({value.GetType().AssemblyQualifiedName}){JsonUtility.ToJson(value)}"; break; 108 | } 109 | 110 | if (!string.IsNullOrEmpty(encode)) 111 | { 112 | saveItems.Add(new SaveItem(keys[i], encode, i)); 113 | } 114 | } 115 | } 116 | 117 | /// 118 | /// Loads the two lists back into the Dictionary, using the Merge Linked Lists method. 119 | /// 120 | public void OnAfterDeserialize() 121 | { 122 | this.Clear(); 123 | int i = 0; 124 | int j = 0; 125 | 126 | //Ensure that the lists are not null to ensure no errors when accessing list.Count 127 | saveItems = saveItems == null ? new List() : saveItems; 128 | sceneObjectSaveItems = sceneObjectSaveItems == null ? new List() : sceneObjectSaveItems; 129 | 130 | while (i < saveItems.Count && j < sceneObjectSaveItems.Count) 131 | { 132 | if (saveItems[i].index < sceneObjectSaveItems[j].index) 133 | { 134 | string key = saveItems[i].key; 135 | int openIndex = saveItems[i].value.IndexOf('('); 136 | int closeIndex = saveItems[i].value.IndexOf(')'); 137 | string contentType = saveItems[i].value.Substring(openIndex + 1, closeIndex - openIndex - 1); 138 | string encode = saveItems[i].value.Substring(closeIndex + 1); 139 | DeserializeItem(contentType, key, encode); 140 | i++; 141 | } 142 | else 143 | { 144 | Add(sceneObjectSaveItems[j].key, sceneObjectSaveItems[j].obj); 145 | j++; 146 | } 147 | } 148 | 149 | for(; i < saveItems.Count;i++) 150 | { 151 | string key = saveItems[i].key; 152 | int openIndex = saveItems[i].value.IndexOf('('); 153 | int closeIndex = saveItems[i].value.IndexOf(')'); 154 | string contentType = saveItems[i].value.Substring(openIndex + 1, closeIndex - openIndex - 1); 155 | string encode = saveItems[i].value.Substring(closeIndex + 1); 156 | DeserializeItem(contentType, key, encode); 157 | } 158 | 159 | for (; j < sceneObjectSaveItems.Count; j++) 160 | { 161 | Add(sceneObjectSaveItems[j].key, sceneObjectSaveItems[j].obj); 162 | } 163 | } 164 | 165 | /// 166 | /// Takes the key and encoded string from a serialized item and adds it back into the dictionary. 167 | /// 168 | private void DeserializeItem(string contentType, string key, string encodedValue) 169 | { 170 | switch (contentType) 171 | { 172 | case "null": Add(key, null); return; 173 | case "int": Add(key, (int)int.Parse(encodedValue)); return; 174 | case "float": Add(key, (float)float.Parse(encodedValue)); return; 175 | case "double": Add(key, (double)double.Parse(encodedValue)); return; 176 | case "long": Add(key, (long)long.Parse(encodedValue)); return; 177 | case "string": Add(key, (string)encodedValue); return; 178 | case "bool": Add(key, (bool)(encodedValue == "true" ? true : false)); return; 179 | case "Vector2": Add(key, Serializer.ParseVector2(encodedValue)); return; 180 | case "Vector3": Add(key, Serializer.ParseVector3(encodedValue)); return; 181 | case "Vector2Int": Add(key, Serializer.ParseVector2Int(encodedValue)); return; 182 | case "Vector3Int": Add(key, Serializer.ParseVector3Int(encodedValue)); return; 183 | case "Vector4": Add(key, Serializer.ParseVector4(encodedValue)); return; 184 | case "Bounds": Add(key, Serializer.ParseBounds(encodedValue)); return; 185 | case "Rect": Add(key, Serializer.ParseRect(encodedValue)); return; 186 | case "Color": Add(key, JsonUtility.FromJson(encodedValue)); return; 187 | case "AnimationCurve": Add(key, Serializer.DeserializeAnimationCurve(encodedValue)); return; 188 | case "Gradient": Add(key, Serializer.DeserializeGradient(encodedValue)); return; 189 | case "UnityEngine.Object": 190 | if(Application.isEditor) 191 | { 192 | EditorApplication.delayCall += () => Add(key, AssetDatabase.LoadAssetAtPath(encodedValue)); 193 | } 194 | else 195 | { 196 | Add(key, Resources.Load(encodedValue)); 197 | } 198 | return; 199 | default: break; 200 | } 201 | 202 | //Different process for enums (of any type) 203 | if(Serializer.EnumDeserialize(contentType, encodedValue, out object enumValue)) 204 | { 205 | Add(key, enumValue); 206 | } 207 | 208 | //Tries to de-serialize a struct or class using JsonUtility.FromJson 209 | if(Serializer.TryDeserializeJSON(contentType, encodedValue, out object result)) 210 | { 211 | Add(key, result); 212 | } 213 | } 214 | 215 | public void SetOrAdd(string key, object ob) 216 | { 217 | if (this.ContainsKey(key)) 218 | { 219 | this[key] = ob; 220 | } 221 | else 222 | { 223 | this.Add(key, ob); 224 | } 225 | } 226 | 227 | [System.Serializable] 228 | private static class Serializer 229 | { 230 | #region GradientSerialization 231 | [System.Serializable] 232 | private class SerializableGradient 233 | { 234 | public SerializableColorKey[] colorKeys; 235 | public SerializableAlphaKey[] alphaKeys; 236 | public GradientMode mode; 237 | 238 | public SerializableGradient(Gradient gradient) 239 | { 240 | colorKeys = new SerializableColorKey[gradient.colorKeys.Length]; 241 | for (int i = 0; i < gradient.colorKeys.Length; i++) 242 | { 243 | colorKeys[i] = new SerializableColorKey(gradient.colorKeys[i]); 244 | } 245 | 246 | alphaKeys = new SerializableAlphaKey[gradient.alphaKeys.Length]; 247 | for (int i = 0; i < gradient.alphaKeys.Length; i++) 248 | { 249 | alphaKeys[i] = new SerializableAlphaKey(gradient.alphaKeys[i]); 250 | } 251 | 252 | mode = gradient.mode; 253 | } 254 | 255 | public Gradient ToGradient() 256 | { 257 | Gradient gradient = new Gradient(); 258 | GradientColorKey[] gradientColorKeys = new GradientColorKey[colorKeys.Length]; 259 | for (int i = 0; i < colorKeys.Length; i++) 260 | { 261 | gradientColorKeys[i] = colorKeys[i].ToGradientColorKey(); 262 | } 263 | 264 | GradientAlphaKey[] gradientAlphaKeys = new GradientAlphaKey[alphaKeys.Length]; 265 | for (int i = 0; i < alphaKeys.Length; i++) 266 | { 267 | gradientAlphaKeys[i] = alphaKeys[i].ToGradientAlphaKey(); 268 | } 269 | 270 | gradient.SetKeys(gradientColorKeys, gradientAlphaKeys); 271 | gradient.mode = mode; 272 | 273 | return gradient; 274 | } 275 | } 276 | 277 | [System.Serializable] 278 | private class SerializableColorKey 279 | { 280 | public Color color; 281 | public float time; 282 | 283 | public SerializableColorKey(GradientColorKey colorKey) 284 | { 285 | color = colorKey.color; 286 | time = colorKey.time; 287 | } 288 | 289 | public GradientColorKey ToGradientColorKey() 290 | { 291 | return new GradientColorKey(color, time); 292 | } 293 | } 294 | 295 | [System.Serializable] 296 | private class SerializableAlphaKey 297 | { 298 | public float alpha; 299 | public float time; 300 | 301 | public SerializableAlphaKey(GradientAlphaKey alphaKey) 302 | { 303 | alpha = alphaKey.alpha; 304 | time = alphaKey.time; 305 | } 306 | 307 | public GradientAlphaKey ToGradientAlphaKey() 308 | { 309 | return new GradientAlphaKey(alpha, time); 310 | } 311 | } 312 | 313 | public static string SerializeGradient(Gradient gradient) 314 | { 315 | SerializableGradient serializableGradient = new SerializableGradient(gradient); 316 | return JsonUtility.ToJson(serializableGradient); 317 | } 318 | 319 | public static Gradient DeserializeGradient(string json) 320 | { 321 | SerializableGradient serializableGradient = JsonUtility.FromJson(json); 322 | 323 | if (serializableGradient != null) 324 | { 325 | return serializableGradient.ToGradient(); 326 | } 327 | 328 | Debug.LogError("Failed to deserialize Gradient from JSON: " + json); 329 | return new Gradient(); // Return a default Gradient or handle the error as needed 330 | } 331 | #endregion 332 | #region AnimationCurveSerialization 333 | [System.Serializable] 334 | private struct SerializableKeyframe 335 | { 336 | public float time; 337 | public float value; 338 | public float inTangent; 339 | public float outTangent; 340 | 341 | public SerializableKeyframe(Keyframe keyframe) 342 | { 343 | time = keyframe.time; 344 | value = keyframe.value; 345 | inTangent = keyframe.inTangent; 346 | outTangent = keyframe.outTangent; 347 | } 348 | } 349 | 350 | [System.Serializable] 351 | private struct SerializableAnimationCurve 352 | { 353 | public WrapMode preWrapMode; 354 | public WrapMode postWrapMode; 355 | public SerializableKeyframe[] keys; 356 | 357 | public SerializableAnimationCurve(AnimationCurve curve) 358 | { 359 | preWrapMode = curve.preWrapMode; 360 | postWrapMode = curve.postWrapMode; 361 | keys = new SerializableKeyframe[curve.length]; 362 | for (int i = 0; i < curve.length; i++) 363 | { 364 | keys[i] = new SerializableKeyframe(curve[i]); 365 | } 366 | } 367 | } 368 | 369 | /// 370 | /// Serializes an AnimationCurve to a JSON string. 371 | /// 372 | public static string SerializeAnimationCurve(AnimationCurve curve) 373 | { 374 | SerializableAnimationCurve serializableCurve = new SerializableAnimationCurve(curve); 375 | string json = JsonUtility.ToJson(serializableCurve); 376 | return json; 377 | } 378 | 379 | /// 380 | /// Produces an AnimationCurve from a json string. 381 | /// 382 | public static AnimationCurve DeserializeAnimationCurve(string json) 383 | { 384 | SerializableAnimationCurve serializableCurve = JsonUtility.FromJson(json); 385 | 386 | Keyframe[] keyframes = new Keyframe[serializableCurve.keys.Length]; 387 | for (int i = 0; i < keyframes.Length; i++) 388 | { 389 | keyframes[i] = new Keyframe( 390 | serializableCurve.keys[i].time, 391 | serializableCurve.keys[i].value, 392 | serializableCurve.keys[i].inTangent, 393 | serializableCurve.keys[i].outTangent 394 | ); 395 | } 396 | 397 | AnimationCurve curve = new AnimationCurve(keyframes); 398 | curve.postWrapMode = serializableCurve.postWrapMode; 399 | curve.preWrapMode = serializableCurve.postWrapMode; 400 | return curve; 401 | } 402 | #endregion 403 | #region VectorSerialization 404 | public static Vector2 ParseVector2(string vectorString) 405 | { 406 | vectorString = vectorString.Replace("(", "").Replace(")", ""); 407 | string[] components = vectorString.Split(','); 408 | 409 | if (components.Length == 2 && 410 | float.TryParse(components[0], out float x) && 411 | float.TryParse(components[1], out float y)) 412 | { 413 | return new Vector2(x, y); 414 | } 415 | 416 | Debug.LogError("Failed to parse Vector2 from string: " + vectorString); 417 | return Vector2.zero; 418 | } 419 | 420 | public static Vector2Int ParseVector2Int(string vectorString) 421 | { 422 | vectorString = vectorString.Replace("(", "").Replace(")", ""); 423 | string[] components = vectorString.Split(','); 424 | 425 | if (components.Length == 2 && 426 | int.TryParse(components[0], out int x) && 427 | int.TryParse(components[1], out int y)) 428 | { 429 | return new Vector2Int(x, y); 430 | } 431 | 432 | Debug.LogError("Failed to parse Vector2 from string: " + vectorString); 433 | return Vector2Int.zero; 434 | } 435 | 436 | public static Vector3 ParseVector3(string vectorString) 437 | { 438 | vectorString = vectorString.Replace("(", "").Replace(")", ""); 439 | string[] components = vectorString.Split(','); 440 | 441 | if (components.Length == 3 && 442 | float.TryParse(components[0], out float x) && 443 | float.TryParse(components[1], out float y) && 444 | float.TryParse(components[2], out float z)) 445 | { 446 | return new Vector3(x, y, z); 447 | } 448 | 449 | Debug.LogError("Failed to parse Vector3 from string: " + vectorString); 450 | return Vector3.zero; 451 | } 452 | 453 | public static Vector3Int ParseVector3Int(string vectorString) 454 | { 455 | vectorString = vectorString.Replace("(", "").Replace(")", ""); 456 | string[] components = vectorString.Split(','); 457 | 458 | if (components.Length == 3 && 459 | int.TryParse(components[0], out int x) && 460 | int.TryParse(components[1], out int y) && 461 | int.TryParse(components[2], out int z)) 462 | { 463 | return new Vector3Int(x, y, z); 464 | } 465 | 466 | Debug.LogError("Failed to parse Vector3Int from string: " + vectorString); 467 | return Vector3Int.zero; 468 | } 469 | 470 | public static Vector4 ParseVector4(string vectorString) 471 | { 472 | vectorString = vectorString.Replace("(", "").Replace(")", ""); 473 | string[] components = vectorString.Split(','); 474 | 475 | if (components.Length == 4 && 476 | float.TryParse(components[0], out float x) && 477 | float.TryParse(components[1], out float y) && 478 | float.TryParse(components[2], out float z) && 479 | float.TryParse(components[3], out float w)) 480 | { 481 | return new Vector4(x, y, z, w); 482 | } 483 | 484 | Debug.LogError("Failed to parse Vector4 from string: " + vectorString); 485 | return Vector4.zero; 486 | } 487 | #endregion 488 | #region BoundsSerialization 489 | /// 490 | /// Produces a Bounds object from the result of Bounds.ToString(). Returns a Bounds with all zero values if unable to parse. 491 | /// 492 | public static Bounds ParseBounds(string boundsString) 493 | { 494 | // Remove parentheses and labels from the string 495 | boundsString = Regex.Replace(boundsString, @"[^\d\.\-,]", ""); 496 | 497 | string[] components = boundsString.Split(','); 498 | 499 | if (components.Length == 6 && 500 | float.TryParse(components[0], out float center_x) && 501 | float.TryParse(components[1], out float center_y) && 502 | float.TryParse(components[2], out float center_z) && 503 | float.TryParse(components[3], out float extent_x) && 504 | float.TryParse(components[4], out float extent_y) && 505 | float.TryParse(components[5], out float extent_z)) 506 | { 507 | Vector3 center = new Vector3(center_x, center_y, center_z); 508 | Vector3 size = new Vector3(extent_x, extent_y, extent_z) * 2f; 509 | return new Bounds(center, size); 510 | } 511 | 512 | Debug.LogWarning("Failed to parse Bounds from string: " + boundsString); 513 | return new Bounds(Vector3.zero, Vector3.zero); 514 | } 515 | 516 | /// 517 | /// Produces a BoundsInt object from the result of BoundsInt.ToString(). Returns a Bounds with all zero values if unable to parse. 518 | /// 519 | public static BoundsInt ParseBoundsInt(string boundsString) 520 | { 521 | // Remove parentheses and labels and any unwanted decimals from the string 522 | boundsString = Regex.Replace(boundsString, @"[^\d\-,]", ""); 523 | 524 | string[] components = boundsString.Split(','); 525 | 526 | if (components.Length == 6 && 527 | int.TryParse(components[0], out int center_x) && 528 | int.TryParse(components[1], out int center_y) && 529 | int.TryParse(components[2], out int center_z) && 530 | int.TryParse(components[3], out int extent_x) && 531 | int.TryParse(components[4], out int extent_y) && 532 | int.TryParse(components[5], out int extent_z)) 533 | { 534 | Vector3Int center = new Vector3Int(center_x, center_y, center_z); 535 | Vector3Int size = new Vector3Int(extent_x, extent_y, extent_z) * 2; 536 | return new BoundsInt(center, size); 537 | } 538 | 539 | Debug.LogWarning("Failed to parse BoundsInt from string: " + boundsString); 540 | return new BoundsInt(Vector3Int.zero, Vector3Int.zero); 541 | } 542 | #endregion 543 | #region RectSerialization 544 | 545 | /// 546 | /// Takes the string result of Rect.ToString() and produces the original Rect. Returns a zero-rect if unable to parse. 547 | /// 548 | public static Rect ParseRect(string rectString) 549 | { 550 | // Remove parentheses and labels from the string 551 | rectString = Regex.Replace(rectString, @"[^\d\.\-,]", ""); 552 | 553 | string[] components = rectString.Split(','); 554 | 555 | if (components.Length == 4 && 556 | float.TryParse(components[0], out float x) && 557 | float.TryParse(components[1], out float y) && 558 | float.TryParse(components[2], out float width) && 559 | float.TryParse(components[3], out float height)) 560 | { 561 | Rect rect = new Rect(x, y, width, height); 562 | return rect; 563 | } 564 | 565 | Debug.LogWarning("Failed to parse Rect from string: " + rectString); 566 | return new Rect(0, 0, 0, 0); 567 | } 568 | 569 | /// 570 | /// Takes the string result of RectInt.ToString() and produces the original RectInt. Returns a zero-rect if unable to parse. 571 | /// 572 | public static RectInt ParseRectInt(string rectString) 573 | { 574 | // Remove parentheses and labels from the string 575 | rectString = Regex.Replace(rectString, @"[^\d\-,]", ""); 576 | 577 | string[] components = rectString.Split(','); 578 | 579 | if (components.Length == 4 && 580 | int.TryParse(components[0], out int x) && 581 | int.TryParse(components[1], out int y) && 582 | int.TryParse(components[2], out int width) && 583 | int.TryParse(components[3], out int height)) 584 | { 585 | RectInt rect = new RectInt(x, y, width, height); 586 | return rect; 587 | } 588 | 589 | Debug.LogWarning("Failed to parse RectInt from string: " + rectString); 590 | return new RectInt(0, 0, 0, 0); 591 | } 592 | 593 | #endregion 594 | 595 | /// 596 | /// Takes the type, encoded as string, and the enum value and produces an Enum of the proper type. 597 | /// 598 | public static bool EnumDeserialize(string contentType, string encodedValue, out object enumValue) 599 | { 600 | Type type = Type.GetType(contentType); 601 | if (type != null && type.IsEnum) 602 | { 603 | if (Enum.TryParse(type, encodedValue, out object enumIntermediateValue)) 604 | { 605 | enumValue = Convert.ChangeType(enumIntermediateValue, type); 606 | return true; 607 | } 608 | } 609 | enumValue = null; 610 | return false; 611 | } 612 | 613 | /// 614 | /// Takes as input a string to be converted to a type, which is used to produce a Deserialized object. 615 | /// 616 | public static bool TryDeserializeJSON(string contentType, string json, out object result) 617 | { 618 | Type type = Type.GetType(contentType); 619 | if (type != null) 620 | { 621 | result = JsonUtility.FromJson(json, type); 622 | if (result != null) 623 | { 624 | return true; 625 | } 626 | } 627 | result = null; 628 | return false; 629 | } 630 | } 631 | } 632 | 633 | 634 | -------------------------------------------------------------------------------- /DictionaryDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityObject = UnityEngine.Object; 6 | using System.Reflection; 7 | 8 | public abstract class DictionaryDrawer : PropertyDrawer 9 | { 10 | private Dictionary _Dictionary; 11 | private bool _Foldout; 12 | private const float kButtonWidth = 18f; 13 | private static float lineHeight = EditorGUIUtility.singleLineHeight + 4; 14 | private float spacing = 12f; 15 | private float fieldPadding = 1f; 16 | 17 | private GUIStyle addEntryStyle; 18 | private GUIContent addEntryContent; 19 | private GUIStyle clearDictionaryStyle; 20 | private GUIContent clearDictionaryContent; 21 | //reuses clearDictionaryStyle. I am adding it for readability 22 | private GUIStyle removeEntryStyle; 23 | private GUIContent removeEntryContent; 24 | 25 | private GUIStyle HeaderStyle; 26 | 27 | private Rect buttonRect; 28 | 29 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 30 | { 31 | CheckInitialize(property, label); 32 | if (_Foldout) 33 | { 34 | //Height of the main Header and the two column headers + height of all the drawn dictionary entries + a little padding on the bottom. 35 | return (GetDictionaryElementsHeight() + (lineHeight * 2)) + 14f; 36 | } 37 | return lineHeight+ 4f; 38 | } 39 | 40 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 41 | { 42 | CheckInitialize(property, label); 43 | 44 | position.height = 20f; 45 | DrawHeader(position, property, label); 46 | 47 | 48 | if (!_Foldout) 49 | return; 50 | position.y += 5f + lineHeight * 2; 51 | foreach (var item in _Dictionary) 52 | { 53 | var key = item.Key; 54 | var value = item.Value; 55 | 56 | var keyRect = position; 57 | keyRect.width /= 3; 58 | keyRect.x += 10; 59 | //Apply vertical padding 60 | keyRect.y += fieldPadding; 61 | keyRect.height -= fieldPadding * 2; 62 | 63 | EditorGUI.BeginChangeCheck(); 64 | var newKey = DoField(keyRect, typeof(TK), (TK)key); 65 | if (EditorGUI.EndChangeCheck()) 66 | { 67 | try 68 | { 69 | _Dictionary.Remove(key); 70 | _Dictionary.Add(newKey, value); 71 | } 72 | catch (Exception e) 73 | { 74 | _Dictionary.Remove(key); 75 | Debug.Log(e.Message); 76 | } 77 | break; 78 | } 79 | 80 | var valueRect = position; 81 | valueRect.x = keyRect.xMax + spacing; 82 | valueRect.y += fieldPadding; 83 | //Apply vertical padding 84 | valueRect.height -= fieldPadding * 2; 85 | valueRect.width = (position.width - keyRect.width) - ((kButtonWidth + 2) * 2f) - valueRect.size.y - (spacing* 2.5f); 86 | EditorGUI.BeginChangeCheck(); 87 | value = DoField(valueRect, typeof(TV), (TV)value); 88 | 89 | 90 | Rect changeValueRect = new Rect(new Vector2(buttonRect.x - 2f, valueRect.position.y), new Vector2(kButtonWidth, valueRect.size.y)); 91 | value = ChangeValueType(changeValueRect, key, value); 92 | 93 | if (EditorGUI.EndChangeCheck()) 94 | { 95 | _Dictionary[key] = value; 96 | break; 97 | } 98 | EditorGUIUtility.AddCursorRect(changeValueRect, MouseCursor.Link); 99 | 100 | var removeRect = valueRect; 101 | removeRect.x = buttonRect.x + kButtonWidth; 102 | removeRect.width = kButtonWidth; 103 | if (GUI.Button(removeRect, removeEntryContent, removeEntryStyle)) 104 | { 105 | RemoveItem(key); 106 | break; 107 | } 108 | EditorGUIUtility.AddCursorRect(removeRect, MouseCursor.Link); 109 | position.y += Mathf.Max(GetEntryHeight(key) ,GetEntryHeight(value)); 110 | } 111 | } 112 | 113 | /// 114 | /// Gets the combined height of all dictionary elements 115 | /// 116 | /// 117 | private float GetDictionaryElementsHeight() 118 | { 119 | float height = 0; 120 | foreach(var item in _Dictionary) 121 | { 122 | var key = item.Key; 123 | var value = item.Value; 124 | height += Mathf.Max(GetEntryHeight(key), GetEntryHeight(value)); 125 | } 126 | 127 | return height; 128 | } 129 | 130 | private void DrawColumn(Rect position, GUIStyle style) 131 | { 132 | Rect columnRect = new Rect(position.x, position.yMax - 1, position.width, GetDictionaryElementsHeight() + 12f); 133 | GUI.Box(columnRect, GUIContent.none, style); 134 | } 135 | 136 | private void DrawHeader(Rect position, SerializedProperty property, GUIContent label) 137 | { 138 | Rect headerRect = new Rect(position.position, new Vector2(position.size.x - kButtonWidth * 1.5f, lineHeight)); 139 | GUI.Box(headerRect, GUIContent.none, HeaderStyle); 140 | var foldoutRect = position; 141 | foldoutRect.x += 4f; 142 | foldoutRect.width -= 2 * kButtonWidth; 143 | EditorGUI.BeginChangeCheck(); 144 | if(_Dictionary.Count > 0) 145 | { 146 | _Foldout = EditorGUI.Foldout(foldoutRect, _Foldout, label, true); 147 | } 148 | else 149 | { 150 | foldoutRect.x += 4f; 151 | EditorGUI.LabelField(foldoutRect, label); 152 | _Foldout = false; 153 | } 154 | if (EditorGUI.EndChangeCheck()) 155 | { 156 | EditorPrefs.SetBool(label.text, _Foldout); 157 | } 158 | 159 | //Draw the Add Item Button 160 | buttonRect = position; 161 | buttonRect.x = position.width - 20 - kButtonWidth + position.x + 1; 162 | buttonRect.width = kButtonWidth; 163 | 164 | GUIStyle headerButtonStyle = new GUIStyle(HeaderStyle); 165 | headerButtonStyle.padding = new RectOffset(0, 0, 0, 0); 166 | Rect headerButtonRect = new Rect(buttonRect.position, new Vector2(kButtonWidth * 1.5f, lineHeight)); 167 | if (GUI.Button(headerButtonRect, addEntryContent, headerButtonStyle)) 168 | { 169 | AddNewItem(); 170 | } 171 | EditorGUIUtility.AddCursorRect(headerButtonRect, MouseCursor.Link); 172 | buttonRect.x -= kButtonWidth; 173 | 174 | //Draw the Item count label 175 | GUIStyle headerItemCountLabelStyle = new GUIStyle("MiniLabel"); 176 | GUIContent headerItemCountLabelContent = new GUIContent(); 177 | if(_Dictionary.Count == 0) 178 | { 179 | headerItemCountLabelContent = new GUIContent("Empty"); 180 | } 181 | else 182 | { 183 | headerItemCountLabelContent = new GUIContent($"{_Dictionary.Count} Item{(_Dictionary.Count == 1 ? "" : "s")}"); 184 | } 185 | 186 | GUI.Label(new Rect(buttonRect.x - 30f, buttonRect.y, 50f, headerRect.height), headerItemCountLabelContent, headerItemCountLabelStyle); 187 | 188 | 189 | //Draw the header labels (Keys - Values) 190 | if(_Foldout) 191 | { 192 | //Draw "Keys" header 193 | position.y += headerRect.height; 194 | Rect keyHeaderRect = new Rect(position.x, position.y - 1, position.width /3f + kButtonWidth - 1, headerRect.height); 195 | GUIStyle columnHeaderStyle = new GUIStyle("GroupBox"); 196 | columnHeaderStyle.padding = new RectOffset(0, 0, 0, 0); 197 | columnHeaderStyle.contentOffset = new Vector2(0, 3f); 198 | GUI.Box(keyHeaderRect, new GUIContent("Keys"), columnHeaderStyle); 199 | 200 | //Draw "Values" header 201 | Rect valuesHeaderRect = new Rect(keyHeaderRect.xMax - 1, keyHeaderRect.y, (position.width - keyHeaderRect.width - kButtonWidth * 0.5f), keyHeaderRect.height); 202 | GUI.Box(valuesHeaderRect, new GUIContent("Values"), columnHeaderStyle); 203 | //Draw the Columns for the keys and values. 204 | DrawColumn(keyHeaderRect, columnHeaderStyle); 205 | DrawColumn(valuesHeaderRect, columnHeaderStyle); 206 | 207 | position.y += headerRect.height; 208 | } 209 | 210 | /* 211 | if (GUI.Button(buttonRect, clearDictionaryContent, clearDictionaryStyle)) 212 | { 213 | ClearDictionary(); 214 | } 215 | */ 216 | } 217 | 218 | #region TypeControls 219 | private static float GetEntryHeight(T value) 220 | { 221 | switch (value) 222 | { 223 | case Bounds: return lineHeight * 2; 224 | case BoundsInt: return lineHeight * 2; 225 | case Rect: return lineHeight * 2; 226 | case RectInt: return lineHeight * 2; 227 | default: return lineHeight; 228 | } 229 | } 230 | 231 | private static T DoField(Rect rect, Type type, T value) 232 | { 233 | if (typeof(UnityObject).IsAssignableFrom(type)) 234 | return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true); 235 | switch (value) 236 | { 237 | case null: EditorGUI.LabelField(rect, "null"); return value; 238 | case long: return (T)(object)EditorGUI.LongField(rect, (long)(object)value); 239 | case int: return (T)(object)EditorGUI.IntField(rect, (int)(object)value); 240 | case float: return (T)(object)EditorGUI.FloatField(rect, (float)(object)value); 241 | case double: return (T)(object)EditorGUI.DoubleField(rect, (double)(object)value); 242 | case string: return (T)(object)EditorGUI.TextField(rect, (string)(object)value); 243 | case bool: return (T)(object)EditorGUI.Toggle(rect, (bool)(object)value); 244 | case Vector2Int: return (T)(object)EditorGUI.Vector2IntField(rect, GUIContent.none, (Vector2Int)(object)value); 245 | case Vector3Int: return (T)(object)EditorGUI.Vector3IntField(rect, GUIContent.none, (Vector3Int)(object)value); 246 | case Vector2: return (T)(object)EditorGUI.Vector2Field(rect, GUIContent.none, (Vector2)(object)value); 247 | case Vector3: return (T)(object)EditorGUI.Vector3Field(rect, GUIContent.none, (Vector3)(object)value); 248 | case Vector4: return (T)(object)EditorGUI.Vector4Field(rect, GUIContent.none, (Vector4)(object)value); 249 | case BoundsInt: return (T)(object)EditorGUI.BoundsIntField(rect, (BoundsInt)(object)value); 250 | case Bounds: return (T)(object)EditorGUI.BoundsField(rect, (Bounds)(object)value); 251 | case RectInt: return (T)(object)EditorGUI.RectIntField(rect, (RectInt)(object)value); 252 | case Rect: return (T)(object)EditorGUI.RectField(rect, (Rect)(object)value); 253 | case Color: return (T)(object)EditorGUI.ColorField(rect, (Color)(object)value); 254 | case AnimationCurve: return (T)(object)EditorGUI.CurveField(rect, (AnimationCurve)(object)value); 255 | case Gradient: return (T)(object)EditorGUI.GradientField(rect, (Gradient)(object)value); 256 | case UnityObject: return (T)(object)EditorGUI.ObjectField(rect, (UnityObject)(object)value, type, true); 257 | } 258 | 259 | if (value.GetType().IsEnum) 260 | { 261 | if (Enum.TryParse(value.GetType(), value.ToString(), out object enumValue)) 262 | { 263 | return (T)(object)EditorGUI.EnumPopup(rect, (Enum)enumValue); 264 | } 265 | } 266 | 267 | //Setup GUIStyle and GUIContent for the "Clear Dictionary" button 268 | GUIStyle style = new GUIStyle(EditorStyles.miniButton); 269 | style.padding = new RectOffset(2, 2, 2, 2); 270 | GUIContent content = new GUIContent(EditorGUIUtility.IconContent("d_UnityEditor.ConsoleWindow@2x")); 271 | content.tooltip = "Debug Values"; 272 | 273 | Type fieldType = value.GetType(); 274 | bool isStruct = fieldType.IsValueType && !fieldType.IsEnum; 275 | EditorGUI.LabelField(rect, $"{fieldType.ToString().Replace("+", ".")} {(isStruct ? "struct" : "class")} instance"); 276 | if( GUI.Button(new Rect(rect.xMax - kButtonWidth, rect.y, kButtonWidth, kButtonWidth), content, style)) 277 | { 278 | Debug.Log(JsonUtility.ToJson(value)); 279 | } 280 | 281 | //DrawSerializableObject(rect, value); 282 | return value; 283 | } 284 | 285 | //Unfinished 286 | /* 287 | public static void DrawSerializableObject(Rect rect, object obj) 288 | { 289 | if (obj == null) 290 | { 291 | Console.WriteLine("Object is null."); 292 | return; 293 | } 294 | 295 | Type type = obj.GetType(); 296 | FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); 297 | 298 | foreach (FieldInfo field in fields) 299 | { 300 | object value = field.GetValue(obj); 301 | rect.y += GetEntryHeight(value); 302 | Debug.Log($"{field.Name}: {value}"); 303 | if(value != null) 304 | { 305 | DoField(rect, value.GetType(), value); 306 | } 307 | } 308 | 309 | } 310 | */ 311 | 312 | private TV ChangeValueType(Rect rect, TK key, TV value) 313 | { 314 | GUIContent content = EditorGUIUtility.IconContent("_Popup"); 315 | content.tooltip = "Change Value Type"; 316 | GUIStyle changeItemStyle = new GUIStyle(EditorStyles.miniButton); 317 | changeItemStyle.padding = new RectOffset(2, 2, 2, 2); 318 | 319 | if (GUI.Button(rect, content, changeItemStyle)) 320 | { 321 | GenericMenu genericMenu = new GenericMenu(); 322 | genericMenu.AddItem(new GUIContent("Numbers/int"), value is int, () => { _Dictionary[key] = (TV)(object)default(int); }); 323 | genericMenu.AddItem(new GUIContent("Numbers/float"), value is float, () => { _Dictionary[key] = (TV)(object)default(float); }); 324 | genericMenu.AddItem(new GUIContent("Numbers/double"), value is double, () => { _Dictionary[key] = (TV)(object)default(double); }); 325 | genericMenu.AddItem(new GUIContent("Numbers/long"), value is long, () => { _Dictionary[key] = (TV)(object)default(long); }); 326 | genericMenu.AddItem(new GUIContent("Vectors/Vector2"), (value is Vector2 && !(value is Vector2Int)), () => { _Dictionary[key] = (TV)(object)default(Vector2); }); 327 | genericMenu.AddItem(new GUIContent("Vectors/Vector3"), (value is Vector3 && !(value is Vector3Int)), () => { _Dictionary[key] = (TV)(object)default(Vector3); }); 328 | genericMenu.AddItem(new GUIContent("Vectors/Vector4"), value is Vector4, () => { _Dictionary[key] = (TV)(object)default(Vector4); }); 329 | genericMenu.AddItem(new GUIContent("Vectors/Vector2Int"), value is Vector2Int, () => { _Dictionary[key] = (TV)(object)default(Vector2Int); }); 330 | genericMenu.AddItem(new GUIContent("Vectors/Vector3Int"), value is Vector3Int, () => { _Dictionary[key] = (TV)(object)default(Vector3Int); }); 331 | genericMenu.AddItem(new GUIContent("Bounds/Bounds"), value is Bounds && value is not BoundsInt, () => { _Dictionary[key] = (TV)(object)default(Bounds); }); 332 | genericMenu.AddItem(new GUIContent("Bounds/BoundsInt"), value is BoundsInt, () => { _Dictionary[key] = (TV)(object)default(BoundsInt); }); 333 | genericMenu.AddItem(new GUIContent("Rects/Rect"), value is Rect && value is not RectInt, () => { _Dictionary[key] = (TV)(object)default(Rect); }); 334 | genericMenu.AddItem(new GUIContent("Rects/RectInt"), value is RectInt, () => { _Dictionary[key] = (TV)(object)default(RectInt); }); 335 | genericMenu.AddItem(new GUIContent("string"), value is string, () => { _Dictionary[key] = (TV)(object)""; }); 336 | genericMenu.AddItem(new GUIContent("bool"), value is bool, () => { _Dictionary[key] = (TV)(object)default(bool); }); 337 | genericMenu.AddItem(new GUIContent("Color"), value is Color, () => { _Dictionary[key] = (TV)(object)default(Color); }); 338 | genericMenu.AddItem(new GUIContent("AnimationCurve"), value is AnimationCurve, () => { _Dictionary[key] = (TV)(object)(new AnimationCurve()); }); 339 | genericMenu.AddItem(new GUIContent("Gradient"), value is Gradient, () => { _Dictionary[key] = (TV)(object)(new Gradient()); }); 340 | genericMenu.AddItem(new GUIContent("Unity Object"), value is UnityObject, () => { _Dictionary[key] = (TV)(object)(new UnityObject()); }); 341 | genericMenu.ShowAsContext(); 342 | } 343 | 344 | return (TV)value; 345 | } 346 | #endregion 347 | 348 | private void RemoveItem(TK key) 349 | { 350 | _Dictionary.Remove(key); 351 | } 352 | 353 | private void CheckInitialize(SerializedProperty property, GUIContent label) 354 | { 355 | if (_Dictionary == null) 356 | { 357 | SetupStyles(); 358 | var target = property.serializedObject.targetObject; 359 | _Dictionary = fieldInfo.GetValue(target) as Dictionary; 360 | if (_Dictionary == null) 361 | { 362 | _Dictionary = new Dictionary(); 363 | fieldInfo.SetValue(target, _Dictionary); 364 | } 365 | 366 | _Foldout = EditorPrefs.GetBool(label.text); 367 | } 368 | } 369 | 370 | private void SetupStyles() 371 | { 372 | //Setup GUIStyle and GUIContent for the "Add Item" button 373 | addEntryStyle = new GUIStyle(EditorStyles.miniButton); 374 | addEntryStyle.padding = new RectOffset(3, 3, 3, 3); 375 | addEntryContent = new GUIContent(EditorGUIUtility.IconContent("d_CreateAddNew@2x")); 376 | addEntryContent.tooltip = "Add Item"; 377 | 378 | //Setup GUIStyle and GUIContent for the "Clear Dictionary" button 379 | clearDictionaryStyle = new GUIStyle(EditorStyles.miniButton); 380 | clearDictionaryStyle.padding = new RectOffset(2, 2, 2, 2); 381 | clearDictionaryContent = new GUIContent(EditorGUIUtility.IconContent("d_winbtn_win_close@2x")); 382 | clearDictionaryContent.tooltip = "Clear dictionary"; 383 | 384 | removeEntryContent = new GUIContent(EditorGUIUtility.IconContent("d_winbtn_win_close@2x")); 385 | removeEntryContent.tooltip = "Remove Item"; 386 | removeEntryStyle = new GUIStyle(clearDictionaryStyle); 387 | 388 | HeaderStyle = new GUIStyle("MiniToolbarButton"); 389 | HeaderStyle.fixedHeight = 0; 390 | HeaderStyle.fixedWidth = 0; 391 | HeaderStyle.padding = new RectOffset(2,2,2,2); 392 | } 393 | 394 | private void ClearDictionary() 395 | { 396 | _Dictionary.Clear(); 397 | } 398 | 399 | private void AddNewItem() 400 | { 401 | TK key; 402 | if (typeof(TK) == typeof(string)) 403 | key = (TK)(object)""; 404 | else key = default(TK); 405 | 406 | if (typeof(TV) == typeof(object)) 407 | { 408 | var value = (TV)(object)1; 409 | try 410 | { 411 | _Dictionary.Add(key, value); 412 | } 413 | catch (Exception e) 414 | { 415 | Debug.Log(e.Message); 416 | } 417 | } 418 | else 419 | { 420 | var value = default(TV); 421 | try 422 | { 423 | _Dictionary.Add(key, value); 424 | } 425 | catch (Exception e) 426 | { 427 | Debug.Log(e.Message); 428 | } 429 | } 430 | } 431 | } 432 | 433 | 434 | [CustomPropertyDrawer(typeof(Blackboard))] 435 | public class BlackboardDrawer : DictionaryDrawer { } 436 | -------------------------------------------------------------------------------- /Images/Blackboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sterberino/UnitySerializedDictionary/c780a85e5f79a2fa90a8945656cbaf2f4ba11408/Images/Blackboard.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zachary Ruiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnitySerializedDictionary 2 | A serializable Dictionary and property drawer for the Unity Editor, modeled visually after the Odin serialized dictionary. 3 | 4 | ![alt text](https://github.com/Sterberino/UnitySerializedDictionary/blob/main/Images/Blackboard.png) 5 | 6 | ## How It Works 7 | 8 | It uses ISerializationCallbackReceiver to convert keys and values to serializable lists in the background, to allow for dictionary serialization in the editor. 9 | Mostly uses switch statements to identify the type and handle drawing the editor fields and serializing/ de-serializing values. 10 | It allows for dynamically changing dictionary value types at runtime, and it supports: 11 | - [UnityEngine.Object](https://docs.unity3d.com/ScriptReference/Object.html) (asset references and in scene components/ GameObjects) 12 | - all the basic Number types, strings, and bool 13 | - enums of any type 14 | - All [Vector](https://docs.unity3d.com/Manual/VectorCookbook.html) types 15 | - [Bounds](https://docs.unity3d.com/ScriptReference/Bounds.html) / [BoundsInt](https://docs.unity3d.com/ScriptReference/BoundsInt.html) and [Rect](https://docs.unity3d.com/ScriptReference/Rect.html) / [RectInt](https://docs.unity3d.com/ScriptReference/RectInt.html) 16 | - [Gradients](https://docs.unity3d.com/ScriptReference/Gradient.html), [Animation Curves](https://docs.unity3d.com/ScriptReference/AnimationCurve.html), and [Color](https://docs.unity3d.com/ScriptReference/Color.html) 17 | - Any struct or class that is serializable using [JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) (Although there is no general drawer for it yet) 18 | 19 | ## How To Use 20 | 21 | For those not familiar with Editor scripting, it is important to note that the DictionaryDrawer file must be located under a folder named "Editor" in the assets folder. 22 | This file can be placed at "Assets/Editor", or "Assets/[...some path]/Editor" 23 | --------------------------------------------------------------------------------