Search Unity

  1. We're looking for feedback on Unity Starter Kits! Let us know what you’d like.
    Dismiss Notice
  2. Unity 2017.2 beta is now available for download.
    Dismiss Notice
  3. Unity 2017.1 is now released.
    Dismiss Notice
  4. Introducing the Unity Essentials Packs! Find out more.
    Dismiss Notice
  5. Check out all the fixes for 5.6 on the patch releases page.
    Dismiss Notice
  6. Help us improve the editor usability and artist workflows. Join our discussion to provide your feedback.
    Dismiss Notice

Assets [WIP] Odin Inspector & Serializer Looking for Feedback

Discussion in 'Works In Progress' started by jorisshh, Feb 22, 2017.

  1. Tor-Vestergaard


    Mar 20, 2013
    Additionally, for everybody who's been asking for it, the next patch will also add support for having Enums and Guids as key types in dictionaries.
  2. arlevi09


    Dec 31, 2014
    Hi, I just purchased this today. Are there any existing examples of how to serialize a delegate? I get the warning of "There is no custom drawer defined for type..." for a void no-param delegate, and the demo scene does not seem to contain any examples of serialized delegates. And the search bar on the documentation page does not seem to work. Thanks
  3. Tor-Vestergaard


    Mar 20, 2013
    Your delegates are being serialized. It is merely that no drawer currently exists for delegate types, and so you can't actually see it properly in the inspector. If you write logic (in a button, for example) to populate it, though, it will be saved and persisted by Odin.

    Creating a proper general delegate drawer in the style of Unity's event drawer, is on our list of things to do in the near to mid-term future. It's a big task, however, and has to be done right.

    Binding parameters dynamically, targeting other instances in the same component, or objects in the scene, and so forth, are all things we think have to be in a proper delegate drawer - in short, exposing the full power of delegates.

    For now, though, delegates are serialized, but not drawn. This is coming, however.

    Very soon, a new manual section on creating drawers will come out. We also currently have a demo scene demonstrating a variety of custom drawers. If you wish, you can give it a shot yourself and create a simple delegate drawer that does what you need it to, and no more. All the data is there for the inspector to use - it merely has to be translated into an interface.
  4. zzzzzz789


    Dec 21, 2013
    Is there any timeline on the pre-emitted serialization formatters that would enable Odin support for Windows Store? If there is no timeline, what kind of priority does the team have for this feature.

  5. Tor-Vestergaard


    Mar 20, 2013
    Pre-emitted formatters for AoT is probably going to make it out within a month or so, at a guess - we have a few other priorities to deal with first. It's primarily just going to increase serialization performance on AoT platforms, though. It will help a lot with Windows Store, but won't get us all of the way.

    The problem is, the entire reflection platform is diffferent on Windows Store, and so all reflection code in Odin which might run in a player needs to be converted to work with this new system (basically the serialization and the utilitiy .dll's that serialization uses). This is quite a bit more work than simply pre-emitting formatters into an assembly. I think we're going to be experimenting a bit with how to best support Windows Store without having two separate sets of code. A promising approach is using Mono.Cecil to automate the conversion after an assembly has been compiled. It's a bit longer term though - when it arrives depends on how said experimenting goes.

    We will however be introducing an installer at some point within a month or two (hopefully), and that installer will very likely be capable of installing Odin in a serialization-free configuration, where only the attributes .dll is built into players, so that it will be able to compile to Windows Store without the serialization entirely.
  6. zzzzzz789


    Dec 21, 2013
    Thank you so much for the detailed response. We currently support Windows Store in some of our titles and seek to use Odin in our projects. Is there a subset of features we could use safely without compromising Window Store builds?
    If so - Is there a way to have serialized dictionaries that we can edit in Editor that would continue to function on Windows Store builds?
  7. Tor-Vestergaard


    Mar 20, 2013
    Once we have the installer, you would be able to use Odin in your Windows Store projects -- without the custom serialization. That is, without the dictionaries and interfaces and all the other data types you can use with Odin, but still with the ability to customize your editors using attributes.

    Having the ability to use dictionaries on a platform like Windows Store is a bit further into the future, as I mentioned. Essentially, Windows Store runs with .NET Core as a runtime, instead of a regular Mono/.NET runtime. You can see here that it's a somewhat involved process to convert code between the two target runtimes - we'd like to automate this so we can just write regular .NET code and our build process will handle the conversion and .NET core compatibility, if possible, so that's why it's going to take a while to research, test and implement viable approaches. This sort of approach is the only way we're ever going to achieve full Windows Store support, though.
    Last edited: Jun 20, 2017
  8. SuneT


    Feb 29, 2016
  9. Barabicus


    Jun 5, 2013
    Hey so I'm not sure if this is really in the scope of Odin but I think it'd make development easier which I think Odin tries to do. Anyway so the idea is for an attribute which automatically acquires and stores the component of its type during runtime.

    For example, and this is something I often do. I don't like acquiring references through awake or start due to some issues that could arise. Mainly with design. so I often lazy load them. So I'll often do this.

    Code (CSharp):
    1. private Component _component;
    3.         public Component Component { get { return _component ?? (_component = GetComponent<Component>()); } }
    I prefer this way in doing it anyway. So the idea would be to do something like
    Code (CSharp):
    1. [AquireComponent]
    2. private Component _component;
    where you could just easily reference _component without explicitly setting. Also odin could output some error in the inspector if a reference has not been acquired or if the component does not exist.
  10. Tor-Vestergaard


    Mar 20, 2013
    I'm afraid that is a little out of scope for Odin - we can't make fields lazy loaded, for example, without modifying your assemblies in the style of what UNet is doing with its SyncVars, and we don't have easy access to such facilities.

    However, you could easily write an abstract class that, in Start or Awake (or whenever) looks through its own fields marked with that attribute, and uses reflection to set them to the results of GetComponent for the type of the field, and logs errors if the result is null.
  11. Andrew-Carvalho


    Apr 22, 2013
    I have a strange question. I'd say it's a request but I'm also aware of how much time it would take.

    As the only person doing dev/design on my team, I want to use Odin custom inspectors. If I do, however, my art and sound people who don't really use or need the plugin can't run the project in editor because the attributes no longer exist, nor are they interested in buying their own licenses.

    It would be wonderful if the attributes were defined as two partial classes such that the partial class with only definitions and no implementation could be included in the repo for everyone. For me with the plugin, it would find the implementation and work as intended. For everyone else, they would compile to null methods and do nothing, but still allow the code to compile and run. It is a lot cleaner than putting #if ODIN_INSPECTOR everywhere in your game code and would 'just work'. Currently I am writing my own stubs and wrapping them in using #if !ODIN_INSPECTOR for the rest of the team and it's a bit on the annoying side and makes me question whether the plugin is worth the effort.

    If partial classes don't work, simply providing the stubs in a #if !ODIN_INSPECTOR preprocessor define would be really nice.

    I don't know if this use case would benefit anyone else (I imagine yes but wouldn't want to assume) but wanted to throw the idea out there.
    ValooFX likes this.
  12. SuneT


    Feb 29, 2016
    The latest version of Odin is out, and here's the changelog :)

    Also, seeing as Odin has now become more consolidated (500+ users already), the current $35 launch discount price is going to increase at some point during the next months.

    Odin Inspector & Serializer v. changelog

    • Property groups can now be placed within other groups in the same type, using group paths. Additionally, the same member can now be placed in multiple different groups at the same time. See the manual section "Grouping" on the "Design with Attributes" page for more information.
    • Added support for using Enum and Guid keys in dictionaries.

    • Odin's editor compilation now fully validates the type names of all types that it compiles editors for so that editors are not generated for types with names containing invalid characters like spaces, ampersands, and tilde, or with names that are reserved keywords. Editor compilation errors should no longer occur because of invalid type names. The logic implements the official C# language specification for valid identifiers.
    • Fixed an issue where fields would become bigger than the Inspector width.
    • Fixed an issue where Odin serialized enums could not be changed in instances of prefabs where the property path did not exist on the original prefab asset.
    • Fixed an issue where Odin-serialized LayerMasks could not be changed in the inspector.
    • Unity CustomPropertyDrawers which draw abstract UnityEngine.Object types are now properly detected and used by Odin without error messages.
    • Fixed an issue with MathUtilities.Wrap returning negative values when it shouldn't.
    • Fixed an issue where setting an angle to 360 in the SirenixEditorFields.AngleAxisField would result in a broken quaternion value.
    • Fixed an issue where char properties marked with [Delayed] weren't actually delayed.
    • Moved StringMemberHelperExample script to the demo namespace.
    • Delayed sliding handles now works for all primitive fields, except ulong and decimal.
    • Fixed an issue where UnityEvents drawn in InlineEditors would cause null reference exceptions to be thrown when the InlineEditor has been expanded for the second time. This was caused by internal Unity state that was wrongly cached. Odin now clears this invalid internal state at the correct times.
    • Fixed cases where AnimationCurves and Gradients would act up when they were being serialized by Odin, especially when they were being serialized by Odin on prefab instances (in which case, they wouldn't work at all). To facilitate this fix, the concept of enforced (and extendable) type atomicity has been introduced to Odin's property system - AnimationCurves and Gradients are now marked as atomic, and largely act like primitive types in terms of the property system.
    • Fixed case where Gradient.mode would not be serialized in newer versions of Unity where it exists.
    • Fixed an issue where cross-window dragging of list elements would not work in most cases.
    • SpaceAttribute is no longer applied to collection elements.
    • Fixed an issue where, if you had an interface type field or property, the instance creator window would not find structs that implemented that interface.
    • Fixed an issue where dictionary, hashset, list, nullable and generic collection serialization formatters would be broken on IL2CPP builds due to incorrect stripping of their constructors. This would cause a multitude of deserialization errors when running builds on IL2CPP platforms.
    • Fixed an issue where the path for AssetLists would not correctly work with root folders named Assets.
    • Fixed a layout issue caused by InlineButton.
    • Fixed case where serialization for a type would break when a serialized member is hidden by another serialized member in a base type with the same name.

    • Added the missing optional order parameter to HorizontalGroup
    • HorizontalGroupAttribute now has an option for a title.
    • Preferences in Window > Odin Inspector > Preferences > Drawers > General are now stored as EditorPrefs, and are therefore local to the computer.
    • Preferences in Window > Odin Inspector > Preferences > Drawers > Color Palettes are now stored as EditorPrefs, and are therefore local to the computer. This only applies to display preferences - Color Palettes themselves are still saved to the asset and synced over source control.
    • SirenixEditorGUI.BeginBox now alternates between a light and a dark box background based on its z-index.
    • Renamed SirenixEditorGUI.VerticalLineSeperator to SirenixEditorGUI.VerticalLineSeparator and HorizontalLineSeperator to HorizontalLineSeparator.
    • The Serialization Warning message for SerializedMonoBehaviour is no longer shown in the demo examples. Hopefully, this helps new users to not accidentally click the button without reading the warning first.
    • All number fields now show a small handle, which enables you to change the value by dragging. This is useful when the number field does not have a label which also has this behavior, such as in the case of list elements.
    • Vector fields now hides the X, Y, Z and W labels in narrow spaces in order to better show the numbers
      This feature can be disabled in the Odin Preferences Window.
    • Added support for using Enum and Guid keys in dictionaries.
    • Added the TitleGroup attribute which groups properties vertically together with a title, an optional subtitle, and an optional horizontal line.
      This can serve as an alternative to using the PropertyOrder attribute.
    • Added a very rudimentary System.Type drawer, so you can see and change type instances in the inspector.
    • Added SirenixEditorFields.LayerMaskField, which is now used to draw LayerMasks in the inspector.
    • The size of buttons can now be set via the Button attribute.
    • Added PropertyChildren.Recurse, which returns an IEnumerable that recursively yields all children of a property depth first.
    • Added an option to hide the add button for lists in the inspector via ListDrawerSettings attribute. This can be used to create custom add buttons for lists.
    • Added the DictionaryDrawerSettings attribute, which enables you to change the display mode of the dictionary. More options will be added to this attribute in the future.
    • Added SplitVertical to RectExtensions.
    • Added property drawer for char properties.
    • Added support for using Enum and Guid keys in dictionaries.
    • Added inspector drawer for guids, and added SirenixEditorFields.GuidField. Additionally, SirenixEditorGUI.DynamicPrimitiveField now supports Guids.
  13. Froghuto


    Oct 4, 2012
    When having the following:
    Code (CSharp):
    1. [AssetList] public List<AudioClip> _sounds;
    is it possible to change the order of the selected items in the inspector?
  14. Tor-Vestergaard


    Mar 20, 2013
    Hey, sorry for the late reply - we've been attending Unite Europe in Amsterdam, and only got home late yesterday evening.

    What you describe is quite doable already - you can merely include only the Sirenix.OdinInspector.Attributes.dll in your source control, or if you don't use source control, just distribute that sole .dll into your teammates' projects by other means. The attributes .dll is standalone, with no required Odin references, and contains all of the attributes defined in Odin, but none of the implementation. It is mostly meant to facilitate easier uninstalling of Odin (leave the attributes and delete the rest, so you don't get a billion compiler errors), but it makes cases like this easier to handle as well.

    One limitation you would have, though, is that you wouldn't be able to actually reference methods from Odin's other assemblies (our utilities, serialization, or anything else) in code that has to compile on your teammates' computers - you could only reference the attributes in regular code. All other Odin functionality, you would still have to wrap in the #if ODIN_INSPECTOR precompile statements - this is a lot more palatable than having to wrap every single attribute annotation, though.

    Hope that helps!

    This is not currently possible, but it is a feature we can add. There are some potential complexities, so it may not make it in, in the near future, but we'll add it to the list of UX todos.
  15. electroflame


    May 7, 2014
    I'm having some trouble getting the new groups-within-groups feature to work. I'm trying to get some foldout groups inside of a Tab group, but nothing seems to work. I think it might be because the Tab-group is top-level (a Tab group created with the single-parameter constructor), but everything I try throws up an error about no group existing with the specified name.

    Any ideas? I'm probably just using it incorrectly, but the group path names are a bit confusing, especially coming from Advanced Inspector where groups-within-groups "just worked".
  16. Captian-Brink


    Aug 23, 2013
    Since the serialization docs aren't available yet, how should I go about saving and loading object data at runtime? As of now loading fails. I've tried giving my SaveObject instances a transform field too, but it failed.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Sirenix.OdinInspector;
    6. public class SaveObject : SerializedMonoBehaviour {
    7.     public int i;
    8.     public float f;
    9.     public string str;
    10. }

    Code (CSharp):
    1. using System.Collections;
    2. using System.IO;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using Sirenix.OdinInspector;
    6. using Sirenix.Serialization;
    7. using Sirenix.Utilities;
    10. public class SaveRoot : MonoBehaviour {
    11.     public string file;
    12.     private string savepath;
    13.     public GameObject prefab;
    14.     public Vector3 boundary1;
    15.     public Vector3 boundary2;
    16.     public List<SaveObject> saveObjects;
    18.     void Start () {
    19.         savepath = Application.persistentDataPath + file;
    20.     }
    22.     void Update () {
    23.         if(Input.GetKeyUp(KeyCode.UpArrow)){
    24.             Save ();
    25.         }
    26.         if(Input.GetKeyUp(KeyCode.DownArrow)){
    27.             Load();
    28.         }
    29.         if(Input.GetKeyUp(KeyCode.RightArrow)){
    30.             GameObject obj = Create (prefab, RandomVector3 (), UnityEngine.Random.rotationUniform);
    31.             SaveObject so = obj.GetComponent<SaveObject>();
    32.             saveObjects.Add(so);
    33.         }
    34.         if(Input.GetKeyUp(KeyCode.LeftArrow)){
    35.             Destroy(saveObjects[0].gameObject);
    36.             saveObjects.RemoveAt(0);
    37.         }
    38.     }
    40.     void Load(){
    41.         if (saveObjects.Count > 0) {
    42.             for(int i = saveObjects.Count-1; i >= 0; i--){
    43.                 Destroy(saveObjects[i].gameObject);
    44.             }
    45.         }
    46.         byte[] bytes = File.ReadAllBytes(savepath);
    47.         DataFormat format = DataFormat.JSON;
    48.         saveObjects = SerializationUtility.DeserializeValue<List<SaveObject>>(bytes, format);
    49.         if (saveObjects.Count > 0) {
    50.             for (int i = saveObjects.Count - 1; i >= 0; i--) {
    51.                 Instantiate (prefab, saveObjects [i].gameObject.transform.position, saveObjects [i].gameObject.transform.rotation, transform);
    52.             }
    53.         } else {
    54.             Debug.Log ("Nothing to load.");
    55.         }
    57.     }
    59.     void Save(){
    60.         DataFormat format = DataFormat.JSON;
    61.         byte[] bytes = SerializationUtility.SerializeValue(saveObjects, format);
    62.         File.WriteAllBytes(savepath, bytes);
    63.     }
    65.     public GameObject Create(GameObject go, Vector3 pos, Quaternion quat)
    66.     {
    67.         GameObject obj = Instantiate (go, pos, quat, transform);
    68.         return obj;
    69.     }
    71.     public Vector3 RandomVector3()
    72.     {
    73.         return new Vector3 (UnityEngine.Random.Range(boundary1.x, boundary2.x),UnityEngine.Random.Range(boundary1.y, boundary2.y),UnityEngine.Random.Range(boundary1.z, boundary2.z) );
    74.     }
    75. }
  17. Tor-Vestergaard


    Mar 20, 2013
    Groups do "just work" - it's just that there is some oddness with single-parameter tab groups in particular, due to the single-parameter constructor. This is a sort of legacy consequence of how the TabGroup originally included that constructor for convenience's sake, which now makes it terribly confusing - for which we do apologize; the latest patch went out the door rather quickly because we needed it done before Unite Europe, which has occupied a lot of our time at Sirenix.

    What's happening is that the single-parameter tab group constructor assigns tabs to a TabGroup that is invisibly named "_DefaultTabGroup". So for now, you can either add your other groups to your tabs using "_DefaultTabGroup/Tab Name", or you can use the TabGroup constructor that takes two strings, and assign the TabGroup a non-default name, and then use that, as in "Tab Group Name/Tab Name". I think we should probably hardcode an exception into the core group system, so it knows how to handle this constructor being used, and will assign to tabs properly without the default group name being included in the path. I'll make sure to get on that promptly.

    As for why we've chosen to do it this way (unfortunate legacy issues with tab groups aside), the group path system is quite powerful, and very specific as to what exactly you want it to do. You can go as crazy as you want, nesting groups to an arbitrary depth in any order you want, and the paths make it quite clear where everything is supposed to be located, which is very convenient, as nesting a dozen properties several layers deep in half a dozen different groups would otherwise be an utter nightmare, if not outright impossible. It even lets you put the same member in multiple different groups, just by adding extra group attributes with different paths.

    I hope that helps resolve your issue, and our reasons for designing the group system this way :)

    This is a bit of a tricky one you're trying to pull off, but your specific example can be pulled off, albeit with certain limitations as to the sort of data that can be saved. These limitations are due to how Unity is designed. First, some background. This may get complicated, but unfortunately serialization in Unity is just plainly a complicated subject.

    Odin is not capable of directly serializing types derived from UnityEngine.Object, which includes GameObject and MonoBehaviour. This is a very conscious choice of ours, as any capability of doing that would have to be implemented in an extraordinarily ugly way, and would be very incomplete besides. What Odin can do is populate a pre-existing UnityEngine.Object-derived instance with values that Unity would normally not save. This is what's happening in SerializedMonoBehaviour and everywhere else Odin's serialization is run, for example. We just translate all of your custom-serialized members into a kind of data that Unity can understand (such as a byte array) on serialization, and then translate it all back again on deserialization. Odin will never, however, instantiate GameObjects or Components or any other UnityEngine.Object-derived types as part of serialization or deserialization.

    The reason is this: we cannot reference UnityEngine.Object-derived types (assets, prefabs, scene GameObjects, Components, ScriptableObjects, you name it - none of those) in our own custom serialized data. This is simply not possible to do given the way Unity is built and the managed Unity APIs, whether public, private or internal, that are available to us. Any attempt to do so would be exceedingly fragile, unreliable and clunky, not to mention terrible on performance. We could bash together a hodge-podge system, but it would be utterly riddled with bugs and severe limitations.

    The only thing we can do that works reliably when we encounter a UnityEngine.Object reference while serializing, is put it in a list, save its index in our data, and then have Unity save that list for us. On deserialization, we then load up the index from our data, and fetch the value from the list that Unity saved for us. This is how every custom serialization system built for Unity does it, to my knowledge. This, however, means that we can only reference UnityEngine.Object derived types when it is ultimately Unity that is serializing whatever we're saving, because only Unity itself is capable of saving UnityEngine.Object references, as only in that case can we offload it onto Unity.

    Odin, in fact, complains and logs warnings if you ever attempt to force it to serialize Unity Objects directly - you should have encountered those, if you run your example code. We could just throw exceptions instead, to make the illegality of trying this as explicit as possible. Perhaps we should do that.

    Anyways, what does this mean, for your use case? Well, you can't make Unity save GameObjects or Components or anything like that to a file at runtime, so doing it properly is unfortunately entirely out of the question. You can, however, do it sort of improperly.

    There are two routes: the kind of impractical, improper one, where we try to sort of do what you're doing right now, with "saving" types derived from MonoBehaviour, and then there's the one I'd recommend, where we don't serialize or even reference any UnityEngine.Object-derived types at all.

    The trick to both approaches is this: you have to manually instantiate all the special Unity Objects you'll need first, and then afterwards put the data into each of those. In the first approach, we serialize the UnityObjects in the same sort of way that SerializedMonoBehaviour does. We have a utility class for doing this sort of thing, called UnitySerializationUtility. The SaveRoot.cs code for doing it this way would look something like this (with SaveObject.cs unchanged):

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using System.IO;
    4. using Sirenix.Serialization;
    5. using System;
    7. public class SaveRoot : MonoBehaviour
    8. {
    9.     public string file;
    10.     private string savepath;
    11.     public GameObject prefab;
    12.     public Vector3 boundary1;
    13.     public Vector3 boundary2;
    14.     public List<SaveObject> saveObjects;
    16.     private void Start()
    17.     {
    18.         savepath = Application.persistentDataPath + file;
    19.     }
    21.     private void Update()
    22.     {
    23.         if (Input.GetKeyUp(KeyCode.UpArrow))
    24.         {
    25.             Save();
    26.         }
    27.         if (Input.GetKeyUp(KeyCode.DownArrow))
    28.         {
    29.             Load();
    30.         }
    31.         if (Input.GetKeyUp(KeyCode.RightArrow))
    32.         {
    33.             GameObject obj = Create(prefab, RandomVector3(), UnityEngine.Random.rotationUniform);
    34.             SaveObject so = obj.GetComponent<SaveObject>();
    35.             saveObjects.Add(so);
    36.         }
    37.         if (Input.GetKeyUp(KeyCode.LeftArrow))
    38.         {
    39.             Destroy(saveObjects[0].gameObject);
    40.             saveObjects.RemoveAt(0);
    41.         }
    42.     }
    44.     private void Load()
    45.     {
    46.         if (saveObjects.Count > 0)
    47.         {
    48.             for (int i = saveObjects.Count - 1; i >= 0; i--)
    49.             {
    50.                 Destroy(saveObjects[i].gameObject);
    51.             }
    52.         }
    54.         saveObjects.Clear();
    56.         using (var fileStream = File.Open(savepath, FileMode.OpenOrCreate))
    57.         using (var reader = SerializationUtility.CreateReader(fileStream, new DeserializationContext(), DataFormat.JSON))
    58.         {
    59.             // Read the count of objects out (in an empty file, this will default to 0)
    60.             int count;
    61.             reader.ReadInt32(out count);
    63.             if (count > 0)
    64.             {
    65.                 // Create the necessary amount of objects to deserialize into, and deserialize into all the objects
    66.                 for (int i = 0; i < count; i++)
    67.                 {
    68.                     // Create object to deserialize into and add it to the list
    69.                     // Position/rotation/scale is not currently saved, so will be lost
    70.                     var go = Create(prefab,, Quaternion.identity, transform);
    71.                     var obj = go.GetComponent<SaveObject>();
    72.                     saveObjects.Add(obj);
    74.                     // Deserialize into the object
    75.                     Type dummy; // Just exists for accepting the out type parameter, which is null here
    76.                     reader.EnterNode(out dummy);
    77.                     UnitySerializationUtility.DeserializeUnityObject(obj, reader);
    78.                     reader.ExitNode();
    79.                 }
    80.             }
    81.             else
    82.             {
    83.                 Debug.Log("Nothing to load.");
    84.             }
    85.         }
    86.     }
    88.     private void Save()
    89.     {
    90.         using (var fileStream = File.Open(savepath, FileMode.Create))
    91.         using (var writer = SerializationUtility.CreateWriter(fileStream, new SerializationContext(), DataFormat.JSON))
    92.         {
    93.             // First, writer the number of objects that are being saved
    94.             writer.WriteInt32("count", saveObjects.Count);
    96.             // Then save them, one by one
    97.             for (int i = 0; i < saveObjects.Count; i++)
    98.             {
    99.                 // Wrap each object in a node, to bound its data, so that deserializing one object won't read all the available data
    100.                 writer.BeginStructNode("object", null);
    101.                 UnitySerializationUtility.SerializeUnityObject(saveObjects[i], writer, serializeUnityFields: true);
    102.                 writer.EndNode("object");
    103.             }
    104.         }
    105.     }
    107.     public GameObject Create(GameObject go, Vector3 pos, Quaternion quat)
    108.     {
    109.         GameObject obj = Instantiate(go, pos, quat, transform);
    110.         return obj;
    111.     }
    113.     public Vector3 RandomVector3()
    114.     {
    115.         return new Vector3(UnityEngine.Random.Range(boundary1.x, boundary2.x), UnityEngine.Random.Range(boundary1.y, boundary2.y), UnityEngine.Random.Range(boundary1.z, boundary2.z));
    116.     }
    117. }
    As you can see, this is a fairly messy and somewhat impractical way of doing it. Far better would be the second option: to simply save a regular non-Unity class directly instead, and then use that data to recreate your UnityObject-derived instances. In the following example, we declare a new class, SavedData, and serialize that to disk instead.

    Code (CSharp):
    1. // SaveObject.cs
    2. using Sirenix.OdinInspector;
    4. public class SaveObject : SerializedMonoBehaviour
    5. {
    6.     public SavedData data;
    7. }
    9. public class SavedData
    10. {
    11.     public int i;
    12.     public float f;
    13.     public string str;
    14. }
    16. // SaveRoot.cs
    17. using System.Collections.Generic;
    18. using UnityEngine;
    19. using System.IO;
    20. using Sirenix.Serialization;
    22. public class SaveRoot : MonoBehaviour
    23. {
    24.     public string file;
    25.     private string savepath;
    26.     public GameObject prefab;
    27.     public Vector3 boundary1;
    28.     public Vector3 boundary2;
    29.     public List<SaveObject> saveObjects;
    31.     private void Start()
    32.     {
    33.         savepath = Application.persistentDataPath + file;
    34.     }
    36.     private void Update()
    37.     {
    38.         if (Input.GetKeyUp(KeyCode.UpArrow))
    39.         {
    40.             Save();
    41.         }
    42.         if (Input.GetKeyUp(KeyCode.DownArrow))
    43.         {
    44.             Load();
    45.         }
    46.         if (Input.GetKeyUp(KeyCode.RightArrow))
    47.         {
    48.             GameObject obj = Create(prefab, RandomVector3(), UnityEngine.Random.rotationUniform);
    49.             SaveObject so = obj.GetComponent<SaveObject>();
    50.             saveObjects.Add(so);
    51.         }
    52.         if (Input.GetKeyUp(KeyCode.LeftArrow))
    53.         {
    54.             Destroy(saveObjects[0].gameObject);
    55.             saveObjects.RemoveAt(0);
    56.         }
    57.     }
    59.     private void Load()
    60.     {
    61.         if (saveObjects.Count > 0)
    62.         {
    63.             for (int i = saveObjects.Count - 1; i >= 0; i--)
    64.             {
    65.                 Destroy(saveObjects[i].gameObject);
    66.             }
    67.         }
    69.         saveObjects.Clear();
    71.         byte[] bytes = File.ReadAllBytes(savepath);
    73.         List<SavedData> data = SerializationUtility.DeserializeValue<List<SavedData>>(bytes, DataFormat.JSON, new List<UnityEngine.Object>());
    75.         for (int i = 0; i < data.Count; i++)
    76.         {
    77.             // Position/rotation/scale is *still* not saved, so will again be lost.
    78.             // You could include it also in the SavedData class, if you wanted, and then assign it here.
    79.             var go = Create(prefab,, Quaternion.identity, transform);
    80.             var obj = go.GetComponent<SaveObject>();
    82.             // Assign data
    83.    = data[i];
    84.         }
    85.     }
    87.     private void Save()
    88.     {
    89.         // Build data from objects
    90.         List<SavedData> data = new List<SavedData>();
    92.         for (int i = 0; i < saveObjects.Count; i++)
    93.         {
    94.             data.Add(saveObjects[i].data);
    95.         }
    97.         List<UnityEngine.Object> referencedObjects;
    98.         byte[] bytes = SerializationUtility.SerializeValue(data, DataFormat.JSON, out referencedObjects);
    100.         File.WriteAllBytes(savepath, bytes);
    102.         if (referencedObjects != null && referencedObjects.Count > 0)
    103.         {
    104.             Debug.LogError(referencedObjects.Count + " Unity objects were referenced in data saved to disk. This is not possible to do, so these references were lost!");
    105.         }
    106.     }
    108.     public GameObject Create(GameObject go, Vector3 pos, Quaternion quat)
    109.     {
    110.         GameObject obj = Instantiate(go, pos, quat, transform);
    111.         return obj;
    112.     }
    114.     public Vector3 RandomVector3()
    115.     {
    116.         return new Vector3(UnityEngine.Random.Range(boundary1.x, boundary2.x), UnityEngine.Random.Range(boundary1.y, boundary2.y), UnityEngine.Random.Range(boundary1.z, boundary2.z));
    117.     }
    118. }
    Still a bit messy, but saving GameObjects and Components will always be messy and limited due to the way Unity functions.

    I should note, finally, that there is a huge limitation to what you can do here. You cannot save or persist references to UnityEngine.Objects this way. If you refer to textures, materials, gameobjects or the like in the data you save to disk, those references will be lost. It is simply not possible to save them without Unity doing the work, and as noted earlier, you cannot currently make Unity do the work at runtime. Nor do I think they are going to expose this sort of functionality any time soon, alas.

    Well, that was lengthy! I hope it helped, though, and clarified the issues with and limitations of what you're trying to do, while pointing you in the direction of the things that you can do. I'm aware all of this is a bit tedious and clunky, but there's not really much we can do about that, while maintaining the reliability and robustness that we want in Odin.
    Last edited: Jul 2, 2017
    Captian-Brink likes this.
  18. electroflame


    May 7, 2014
    Ah, I don't think I would've figured out the _DefaultTabGroup thing. I actually did try the two-string constructor, but I didn't get that it would have to be GroupName/TabName.

    I don't have a huge problem with the path setup as it it, confusion aside. When I said that Advanced Inspector "just works" I meant that you just stick the attributes on a property and it finds the right groups to put everything in (which is great!). The downside is it's less flexible, and there's no way to put something in multiple groups. Even with Odin being a bit more complex, I prefer it as it's more flexible. I just didn't understand how it worked.

    The DefaultTabGroup definitely works, so that's great.

    As I'm using Odin, I'm finding some little things that'd be really great to have:
    • The ability to define the label width (ideally for the whole class that Odin's drawing, as a class-level attribute).
    • A "Draw as Parent" or something similar that collapses class members into the normal flow of the inspector (i.e. no foldout). You can kind of do this right now with HideLabel, but the fields are still indented.
    • A Header and Background color attribute that'd let you override the color for Foldout/Box headers, and/or their backgrounds.
    • The ability to change the styling of embedded class fields, so they can use the box foldout style headers, rather than Unity's default naked label foldout header.
    Additionally, I've found a couple things that I think may be bugs:
    • Indent(0) doesn't work with HideLabel (i.e. you can't un-indent the already-indented fields).
    • HideReferenceObjectPicker doesn't just hide the picker, it also hides the Foldout/Box header styling (if this isn't a bug, an option would be great!).
    • StringMemberHelper (i.e. $strings) don't seem to work with TabGroup Tab titles (at least, nothing I tried works).
    Thanks for the tip on the _DefaultTabGroup! I'm much closer to completing my move over to Odin. :)
    Last edited: Jul 2, 2017
    Froghuto likes this.
  19. Korigoth


    Jul 21, 2014
    Hey, i have few questions about this assets!

    I would like to know if this asset will serialize dictionnary while using XmlSerializer?

    Does it work with ScriptableObject?

    Thx in advance!
  20. bjarkeck


    Oct 26, 2014
    Awesome suggestions, Keep em coming!

    I've added a LabelWidth attribute and an InlineProperty attribute. The InlineProperty attribute can be applied to classes and structs. All types with the InlineProperty will be rendered in the inspector without a foldout as you suggested. But with a label to the left, and the value to the right.

    Here is an example:

    Code (CSharp):
    2. public class MyComponent : MonoBehaviour
    3. {
    4.     public Vector3Int Vector3Int;
    5.     public BoundsInt BoundsInt;
    6.     public Vector3 Vector3;
    7. }
    9. [Serializable]
    10. [InlineProperty]
    11. public struct Vector3Int
    12. {
    13.     [HorizontalGroup, LabelWidth(15)]
    14.     public int X;
    16.     [HorizontalGroup, LabelWidth(15)]
    17.     public int Y;
    19.     [HorizontalGroup, LabelWidth(15)]
    20.     public int Z;
    21. }
    23. [Serializable]
    24. [InlineProperty]
    25. public struct BoundsInt
    26. {
    27.     [LabelWidth(100)]
    28.     public Vector3Int Size;
    30.     [LabelWidth(100)]
    31.     public Vector3Int Extents;
    32. }

    Was it something like that you were looking for?

    It's intended to hide it completely, you can always just combine it with BoxGroup or FoldoutGroup attribute if you want a box for it :)

    Here is a cool little usecase for it btw:
    [RELEASED] Odin Inspector & Serializer - The Ultimate Workflow Tool ★★★★★

    I did find a couple of issues with Indent and HideLabel, thanks! It's fixed. But I[ndent(0)] won't and shouldn't do anything since the Indent attribute works relative to the current Indent level. That means you can actually specify negative values in there if you want to indent it backward instead. I'm curious to what use case you have for that though :)?


    Thanks. We are aware, and it's on our todo! Hopefully, it will make it in before we submit the next patch this month.

    Yup, Odin Editors works for everything out of the box, and you can use Odin attributes out the box in ScriptableObject, MonoBehaviours, StateMachineBehaviours, and soon also NetworkBehaviour. If you want to serialize Dictionaries and such. Just inherit from SerializedScriptableObject or SerializedMonoBehavour etc.. And everything Unity couldn't serialize and show in the inspector before, will now be shown and serialized.

    In regards to the XmlSerializer, could you clarify a bit more what you mean? Serialized how and where? If you are already serializing the Dictionary, then perhaps there is no need for Odin to serialize it?.

    You can read more about our serialization protocol here.
    Last edited: Jul 5, 2017
  21. electroflame


    May 7, 2014
    Very nice! I was thinking more along the lines of keeping it vertical, just not rendering the header for the class (basically exactly what HideLabel does, just with no indentation). Inline's a great option to have as well.

    Ah, I was thinking that Indent set the current indent level (i.e. overriding it for the current member). I was just trying to reset the indent level for a HideLabel on a class, but it looks like I could just do Indent(-1) to achieve the same thing. Nice!

    Oh, good. I was going crazy thinking it was me -- I'm both glad and sad that it's a bug. :)

    I've been working in earnest today towards migrating fully to Odin, so I've run into some other attributes/features that would be nice to have:
    • The Button attribute should also take a second string that can be used as the Button's Tooltip.
    • InfoBox and DetailedInfoBox should be able to be placed on classes, so they can appear at the top of the inspector. (It makes a bit more sense than putting it on the first member)
    • An attribute (probably class-level?) that lets you stick an InfoBox in the header and/or the footer of the class's inspector. An attribute might not work here -- I think Advanced Inspector handles this with an Interface.
    • An Angle attribute that lets you easily specify angles. (Something kind of like this, maybe?)
    • A Spacing attribute that lets you place some space before and/or after the field.
    • DictionaryDrawerSettings should probably be able to be placed on classes. I've got a custom Dictionary class that implements IDictionary that I can't specify drawer settings for, since child classes are declared like this:
      Code (CSharp):
      1. class DictionaryClass : MyCustomDictionary<string, bool>
      There's no field to apply the attribute to, so I just get the default drawer settings.
    • TimeSpan doesn't currently have a drawer. (Something like a Vector drawer should do, I think. Basically just Days, Hours, Minutes, Seconds, Milliseconds in float fields).
    And one more new bug:
    • ValidateInput messages don't get updated (i.e. hidden properly) if Undo is used to change the value to something valid.
    Thanks for all of your hard work! I'm really digging Odin, and your support is top-notch!
    Last edited: Jul 5, 2017
  22. electroflame


    May 7, 2014
    Quick update, this wasn't actually all that hard to implement. I'll see about cleaning it up and posting it tomorrow for anyone else who wants to add some space before and/or after elements.
  23. arlevi09


    Dec 31, 2014
    Hi, can you provide a straight-forward example for finding valid delegates from components instanced in the scene? I looked through the custom drawer examples but was unable to recognize boilerplate that could be reused for this goal. I originally bought Odin just for the delegate/dict/interface serialization, and was hoping not to spend too much time learning the Odin drawer API for those features (trying to get a project out asap).
  24. Korigoth


    Jul 21, 2014
    we are using XmlSerialiser for saving data and it doesn't serialize dictionnary, so does it work with Odin ?
    or is there any way to serialize a class to XML with Odin that will serialize dictionnary ?

    Code (CSharp):
    2.         public static void Save<T>(T data, string path)
    3.         {
    4.             var serializer = new XmlSerializer(typeof(T));
    6.             using (var stream = new FileStream(path, FileMode.Create))
    7.             {
    8.                 serializer.Serialize(stream, data);
    9.             }
    10.         }
  25. Tor-Vestergaard


    Mar 20, 2013
    Great to hear :D For now we've just added support for Unity's own SpaceAttribute, but perhaps something better is in order. I think I'll let Bjarke answer most of your new big post, as it's a little more his department than mine.

    Okay, so I sat down and played around just to get a feel for how to do something like this in a simple manner, and I actually managed to create a fairly basic delegate drawer from scratch (things got away from me while playing around, and suddenly I had arrived somewhere workable). It supports, well, some common use cases (yours among them), but is rather primitive besides - not anywhere near as comprehensive as we would like to make it in the future. It is functional, though, and is a lot better than nothing.

    This doesn't let you define arguments in the inspector like UnityEvents do, it should be noted - that's one of the features that will make it in, in the future.

    I think we'll include this basic drawer in the next Odin patch, as a stopgap measure while we work on the proper delegate drawer that handles the full breadth of options. Meanwhile, here is the code of the drawer I just made - you can paste it into an editor folder in your project, and just delete it when the next patch of Odin comes out.

    Code (CSharp):
    1. [OdinDrawer]
    2. [DrawerPriority(0.51, 0, 0)] // Just above the regular valueconflict/null value drawers, as we handle that here
    3. public class DelegateDrawer<T> : OdinValueDrawer<T> where T : class
    4. {
    5.     private static MethodInfo invokeMethodField;
    6.     private static bool gotInvokeMethod;
    8.     private static MethodInfo InvokeMethod
    9.     {
    10.         get
    11.         {
    12.             if (!gotInvokeMethod)
    13.             {
    14.                 invokeMethodField = typeof(T).GetMethod("Invoke");
    15.                 gotInvokeMethod = true;
    16.             }
    18.             return invokeMethodField;
    19.         }
    20.     }
    22.     public override bool CanDrawTypeFilter(Type type)
    23.     {
    24.         return !type.IsAbstract && typeof(Delegate).IsAssignableFrom(type) && InvokeMethod != null;
    25.     }
    27.     protected override void DrawPropertyLayout(IPropertyValueEntry<T> entry, GUIContent label)
    28.     {
    29.         Delegate del = (Delegate)(object)entry.SmartValue;
    30.         GUIContent content = GUIHelper.TempContent((string)null);
    31.         bool conflict = false;
    32.         bool targetConflict = false;
    34.         if (entry.ValueState == PropertyValueState.NullReference)
    35.         {
    36.             conflict = true;
    37.             content.text = "Null";
    38.         }
    39.         else if (entry.ValueState == PropertyValueState.ReferenceValueConflict)
    40.         {
    41.             conflict = true;
    42.             content.text = "Multiselection Value Conflict";
    43.         }
    44.         else
    45.         {
    46.             MethodInfo method = del.Method;
    47.             object target = del.Target;
    49.             for (int i = 1; i < entry.ValueCount; i++)
    50.             {
    51.                 var otherDel = (Delegate)(object)entry.Values[i];
    53.                 if (otherDel.Method != method)
    54.                 {
    55.                     conflict = true;
    56.                 }
    58.                 if (otherDel.Target != target)
    59.                 {
    60.                     targetConflict = true;
    61.                 }
    62.             }
    64.             if (conflict)
    65.             {
    66.                 content.text = "Multiselection Method Conflict";
    67.             }
    68.             else
    69.             {
    70.                 content.text = method.DeclaringType.GetNiceFullName() + "." + method.GetFullName();
    72.                 if (method.IsStatic)
    73.                 {
    74.                     content.text = "static " + content.text;
    75.                 }
    76.             }
    77.         }
    79.         bool renderTarget = del != null && !conflict && del.Target is UnityEngine.Object;
    81.         if (renderTarget)
    82.         {
    83.             GUILayout.BeginVertical();
    84.         }
    86.         content.text += " (" + typeof(T).GetNiceName() + ")";
    88.         var rect = EditorGUILayout.GetControlRect();
    89.         rect = EditorGUI.PrefixLabel(rect, label);
    91.         if (GUI.Button(rect, content, EditorStyles.toolbarDropDown))
    92.         {
    93.             Popup(entry, rect);
    94.         }
    96.         if (renderTarget)
    97.         {
    98.             var obj = del.Target as UnityEngine.Object;
    100.             bool previousMixedValue = EditorGUI.showMixedValue;
    101.             {
    102.                 if (targetConflict)
    103.                 {
    104.                     EditorGUI.showMixedValue = true;
    105.                 }
    106.             }
    108.             GUIHelper.PushIndentLevel(EditorGUI.indentLevel + 1);
    109.             var newTarget = EditorGUILayout.ObjectField(GUIHelper.TempContent("Target"), obj, del.Method.DeclaringType, true);
    110.             GUIHelper.PopIndentLevel();
    112.             if (newTarget != null && newTarget != obj)
    113.             {
    114.                 var newDel = Delegate.CreateDelegate(del.GetType(), newTarget, del.Method);
    116.                 for (int i = 0; i < entry.ValueCount; i++)
    117.                 {
    118.                     entry.Values[i] = (T)(object)newDel;
    119.                 }
    120.             }
    122.             if (targetConflict)
    123.             {
    124.                 EditorGUI.showMixedValue = previousMixedValue;
    125.             }
    127.             GUILayout.EndVertical();
    128.         }
    129.     }
    131.     private static void Popup(IPropertyValueEntry<T> entry, Rect rect)
    132.     {
    133.         Type returnType = InvokeMethod.ReturnType;
    134.         Type[] parameters = InvokeMethod.GetParameters().Select(n => n.ParameterType).ToArray();
    136.         GenericMenu menu = new GenericMenu();
    138.         if (typeof(Component).IsAssignableFrom(entry.Property.Tree.TargetType))
    139.         {
    140.             menu.AddItem(new GUIContent("Null"), false, () =>
    141.             {
    142.                 for (int i = 0; i < entry.ValueCount; i++)
    143.                 {
    144.                     entry.Values[i] = null;
    145.                 }
    147.                 entry.ApplyChanges();
    148.             });
    150.             var targetComponents = entry.Property.Tree.WeakTargets.Cast<Component>().ToArray();
    151.             var targetGos = targetComponents.Select(n => n.gameObject).Distinct().ToArray();
    152.             var targetScenes = targetGos.Select(n => n.scene).Distinct().ToArray();
    153.             if (entry.ValueCount == 1)
    154.             {
    155.                 var go = targetGos[0];
    157.                 RegisterGameObject(menu, entry, "Self (" + + ")", go, returnType, parameters);
    158.             }
    160.             // Find delegates on components in scene, if there's just one scene targeted
    161.             if (targetScenes.Length == 1)
    162.             {
    163.                 foreach (var go in SceneRoots())
    164.                 {
    165.                     RegisterGameObject(menu, entry, "Scene/" +, go, returnType, parameters);
    166.                 }
    167.             }
    168.         }
    169.         else if (typeof(ScriptableObject).IsAssignableFrom(entry.Property.Tree.TargetType) && entry.ValueCount == 1)
    170.         {
    171.             menu.AddItem(new GUIContent("Null"), false, () =>
    172.             {
    173.                 for (int i = 0; i < entry.ValueCount; i++)
    174.                 {
    175.                     entry.Values[i] = null;
    176.                 }
    178.                 entry.ApplyChanges();
    179.             });
    181.             ScriptableObject obj = entry.Property.Tree.WeakTargets[0] as ScriptableObject;
    182.             RegisterUnityObject(menu, entry,, obj, returnType, parameters);
    183.         }
    185.         if (menu.GetItemCount() == 0)
    186.         {
    187.             menu.AddDisabledItem(new GUIContent("Delegates currently only support being inspected on Components (limited multi-selection) and ScriptableObjects (no multiselection)."));
    188.         }
    190.         menu.DropDown(rect);
    191.     }
    193.     private static void RegisterGameObject(GenericMenu menu, IPropertyValueEntry<T> entry, string path, GameObject go, Type returnType, Type[] parameters)
    194.     {
    195.         RegisterUnityObject(menu, entry, path + "/GameObject", go, returnType, parameters);
    197.         foreach (var component in go.GetComponents<Component>())
    198.         {
    199.             RegisterUnityObject(menu, entry, path + "/Components/" + component.GetType().GetNiceName(), component, returnType, parameters);
    200.         }
    202.         for (int i = 0; i < go.transform.childCount; i++)
    203.         {
    204.             var child = go.transform.GetChild(i).gameObject;
    206.             RegisterGameObject(menu, entry, path + "/Children/" +, child, returnType, parameters);
    207.         }
    208.     }
    210.     private static void RegisterUnityObject(GenericMenu menu, IPropertyValueEntry<T> entry, string path, UnityEngine.Object obj, Type returnType, Type[] parameters)
    211.     {
    212.         MethodInfo[] methods = obj.GetType()
    213.                                     .GetAllMembers<MethodInfo>(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)
    214.                                     .Where(n =>
    215.                                     {
    216.                                         if (n.ReturnType != returnType)
    217.                                         {
    218.                                             return false;
    219.                                         }
    221.                                         var methodParams = n.GetParameters();
    223.                                         if (methodParams.Length != parameters.Length)
    224.                                         {
    225.                                             return false;
    226.                                         }
    228.                                         for (int i = 0; i < methodParams.Length; i++)
    229.                                         {
    230.                                             if (methodParams[i].ParameterType != parameters[i])
    231.                                             {
    232.                                                 return false;
    233.                                             }
    234.                                         }
    236.                                         return true;
    237.                                     })
    238.                                     .ToArray();
    240.         foreach (var method in methods)
    241.         {
    242.             string name = method.GetFullName();
    243.             MethodInfo closureMethod = method; // For lambda capture
    245.             //if (method.DeclaringType != obj.GetType())
    246.             //{
    247.             name += " (" + method.DeclaringType.GetNiceFullName() + ")";
    248.             //}
    250.             if (method.IsStatic)
    251.             {
    252.                 name += " (static)";
    253.             }
    255.             GenericMenu.MenuFunction func = () =>
    256.             {
    257.                 Delegate del;
    259.                 if (closureMethod.IsStatic)
    260.                 {
    261.                     del = Delegate.CreateDelegate(typeof(T), null, closureMethod);
    262.                 }
    263.                 else
    264.                 {
    265.                     del = Delegate.CreateDelegate(typeof(T), obj, closureMethod);
    266.                 }
    268.                 for (int i = 0; i < entry.ValueCount; i++)
    269.                 {
    270.                     entry.Values[i] = (T)(object)del;
    271.                 }
    273.                 entry.ApplyChanges();
    274.             };
    276.             menu.AddItem(new GUIContent(path + "/" + name), false, func);
    277.         }
    278.     }
    280.     public static IEnumerable<GameObject> SceneRoots()
    281.     {
    282.         var prop = new HierarchyProperty(HierarchyType.GameObjects);
    283.         var expanded = new int[0];
    284.         while (prop.Next(expanded))
    285.         {
    286.             yield return prop.pptrValue as GameObject;
    287.         }
    288.     }
    289. }
    I'm afraid that the way you're doing it there is bypassing Odin's serialization system completely. Odin's serialization, in fact, doesn't even currently support XML as a target format, so unfortunately, if you want dictionaries serialized to XML, you'll have to implement that yourself somehow, separately from Odin.

    If you do figure that out, though, you can add the [ShowOdinSerializedPropertiesInInspector] attribute to your class that is being XML serialized. This will make Odin assume that the class is being specially serialized, and it will show all the things in the inspector that Unity normally wouldn't, such as dictionaries.
  26. Korigoth


    Jul 21, 2014
    We got our Dictionnary Serialized with XML with creating a class like this one, so Odin could serialize this class for UnityInspector?

    Code (CSharp):
    1. public class SerializedDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
    2.     {
    3.         private const string ItemElement = "Item";
    4.         private const string KeyElement = "Key";
    5.         private const string ValueElement = "Value";
    7.         public XmlSchema GetSchema()
    8.         {
    9.             return null;
    10.         }
    12.         public void ReadXml(XmlReader reader)
    13.         {
    14.             var keySerializer = new XmlSerializer(typeof(TKey));
    15.             var valueSerializer = new XmlSerializer(typeof(TValue));
    17.             bool wasEmpty = reader.IsEmptyElement;
    18.             reader.Read();
    20.             if (wasEmpty)
    21.                 return;
    23.             reader.ReadStartElement(ItemElement);
    25.             while (reader.NodeType != XmlNodeType.EndElement)
    26.             {
    27.                 reader.ReadStartElement(KeyElement);
    28.                 var key = (TKey)keySerializer.Deserialize(reader);
    29.                 reader.ReadEndElement();
    31.                 reader.ReadStartElement(ValueElement);
    32.                 var value = (TValue)valueSerializer.Deserialize(reader);
    33.                 reader.ReadEndElement();
    35.                 Add(key, value);
    37.                 reader.MoveToContent();
    38.             }
    40.             reader.ReadEndElement();
    41.         }
    43.         public void WriteXml(XmlWriter writer)
    44.         {
    45.             var keySerializer = new XmlSerializer(typeof(TKey));
    46.             var valueSerializer = new XmlSerializer(typeof(TValue));
    48.             writer.WriteStartElement(ItemElement);
    50.             foreach (var key in Keys)
    51.             {
    52.                 writer.WriteStartElement(KeyElement);
    53.                 keySerializer.Serialize(writer, key);
    54.                 writer.WriteEndElement();
    56.                 writer.WriteStartElement(ValueElement);
    57.                 var value = this[key];
    58.                 valueSerializer.Serialize(writer, value);
    59.                 writer.WriteEndElement();
    60.             }
    62.             writer.WriteEndElement();
    63.         }
    64.     }
    We use this class like this in our ScriptableObject / MonoBehaviour / Object

    Code (CSharp):
    1. public SerializedDictionary<MyEnum, MySerializableObject> Potions;
  27. Tor-Vestergaard


    Mar 20, 2013
    That looks like it'll work - Odin won't care whether it's you or Odin serializing the dictionary. You can either put [ShowInInspector] on your dictionary member and it will show up in the inspector, or you can put [ShowOdinSerializedPropertiesInInspector] on the classes with the dictionaries in them - for example, if you have a component "Foo" that has a SerializedDictionary in it, you can put [ShowOdinSerializedPropertiesInInspector] on it, and the dictionary will also show up.

    Odin, however, will not serialize that class to XML for you - Odin doesn't currently support XML serialization, and it will likely never support the IXmlSerializable interface, due to how slow it is. You need to handle saving it yourself. Odin can, however, display it nicely for you in the inspector, and let you edit it.
  28. electroflame


    May 7, 2014
    Ha, no worries. I know I typed up a bit of a monolith. :)

    As promised, here's how I implemented a "Spacing" attribute that lets you inject spacing before and/or after field rendering:

    Code (CSharp):
    2. namespace Sirenix.OdinInspector
    3. {
    4.    using System;
    5.    using UnityEngine;
    7.    /// <summary>
    8.    /// Spacing is used to inject space before and/or after field rendering.
    9.    /// </summary>
    10.    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
    11.    [DontApplyToListElements]
    12.    public class SpacingAttribute : Attribute
    13.    {
    14.        /// <summary>
    15.        /// The amount of space that should be added before field rendering.
    16.        /// </summary>
    17.        public int Before { get; set; }      
    19.        /// <summary>
    20.        /// The amount of space that should be added after field rendering.
    21.        /// </summary>
    22.        public int After { get; set; }
    24.        public SpacingAttribute() { }
    26.        public SpacingAttribute(int after)
    27.        {
    28.            this.After = after;
    29.        }
    31.        public SpacingAttribute(int before, int after)
    32.        {
    33.            this.After = after;
    34.            this.Before = before;
    35.        }
    36.    }
    37. }
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. namespace Sirenix.OdinInspector.Editor.Drawers
    3. {
    4.     using Utilities.Editor;
    5.     using UnityEngine;
    7.     /// <summary>
    8.     /// Draws properties marked with <see cref="SpacingAttribute"/>.
    9.     /// Adds GUILayout.Space before and/or after field rendering, while maintaining any drawers further down in the drawer chain.
    10.     /// </summary>
    11.     [OdinDrawer]
    12.     [DrawerPriority(DrawerPriorityLevel.SuperPriority)]
    13.     public sealed class SpacingAttributeDrawer : OdinAttributeDrawer<SpacingAttribute>
    14.     {
    15.         /// <summary>
    16.         /// Draws the attribute.
    17.         /// </summary>
    18.         protected override void DrawPropertyLayout(InspectorProperty property, SpacingAttribute attribute, GUIContent label)
    19.         {
    20.             GUILayout.Space(attribute.Before);
    22.             this.CallNextDrawer(property, label);
    24.             GUILayout.Space(attribute.After);          
    25.         }
    26.     }
    27. }
    28. #endif
    Usage is like this:
    Code (CSharp):
    1. [Spacing(Before = 5)]
    2. public int AddSpacingBefore = 0;
    The various constructors work as well, but for this in particular I prefer the named arguments for code clarity.

    It works, but I'm unsure if it's the best way to handle it (it seems like it is, though). Feel free to chime in if there's a better way to do this, as I kind of hacked it together.

    Additionally, I discovered that you can do a similarly-hacky drawer to change header colors by doing something like this:
    Code (CSharp):
    2. var originalFoldout = SirenixGUIStyles.Foldout.normal.background;
    3. var originalHeader = SirenixGUIStyles.BoxHeaderStyle.normal.background;
    5. //Note: MakeTexture just turns the specified sizes and color into a Texture2D.
    6. SirenixGUIStyles.Foldout.normal.background = MakeTexure(1, 1, attribute.Color);
    7. SirenixGUIStyles.BoxHeaderStyle.normal.background = MakeTexure(1, 1, attribute.Color);
    9. this.CallNextDrawer(property, label);
    11. SirenixGUIStyles.Foldout.normal.background = originalFoldout;
    12. SirenixGUIStyles.BoxHeaderStyle.normal.background = originalHeader;
    I'm pretty sure that that's not the best way to do it (as you basically have to find all of the styles you want to change, and cache them pre-render, change them, and restore them post-render) but it certainly works.
    bjarkeck likes this.
  29. electroflame


    May 7, 2014
    The two major things I had left in my migration was the missing Angle and TimeSpan drawers. Angle's likely complicated, so I didn't want to tackle that (especially if you guys end up doing one, as it'll likely end up better than mine anyway). So, instead, I whipped up a TimeSpan drawer that looks like this:


    Pretty basic, but it works well in my limited testing (it should be pretty solid, since I started with the Vector4 drawer as a base). Anyone looking for something like this can just drop this into an Editor folder:

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. namespace Sirenix.OdinInspector.Editor.Drawers
    3. {
    4.     using System;
    5.     using Utilities.Editor;
    6.     using UnityEditor;
    7.     using UnityEngine;
    8.     using Sirenix.Utilities;
    10.     /// <summary>
    11.     /// TimeSpan property drawer.
    12.     /// </summary>
    13.     [OdinDrawer]
    14.     public sealed class TimeSpanDrawer : OdinValueDrawer<TimeSpan>
    15.     {
    16.         /// <summary>
    17.         /// Draws the property.
    18.         /// </summary>
    19.         protected override void DrawPropertyLayout(IPropertyValueEntry<TimeSpan> entry, GUIContent label)
    20.         {
    21.             GUILayout.BeginHorizontal();
    22.             {
    23.                 if(label != null)
    24.                 {
    25.                     //Renders this field's prefix label.
    26.                     //Note that since this uses EditorGUI, we need to add some GUILayout.Space to take up the Rect space in the layouting system.
    27.                     EditorGUI.PrefixLabel(GUIHelper.GetCurrentLayoutRect(), label);
    29.                     //Subtract 8 from the space, to line up with the rest of the inspector control column.
    30.                     //Without this, label text will be indented a bit too much.
    31.                     GUILayout.Space(EditorGUIUtility.labelWidth - 8);
    32.                 }
    34.                 //Make sure all of our labels have a static width of 42 pixels, as that's all we'll need.
    35.                 GUIHelper.PushLabelWidth(42f);
    37.                 //We don't want any indent, so make sure we don't use any.
    38.                 GUIHelper.PushIndentLevel(0);
    40.                 //We'll stack our IntFields into two rows, so start a vertical layout here.
    41.                 GUILayout.BeginVertical();
    42.                 {
    43.                     int days, hours, minutes, seconds;
    45.                     EditorGUI.BeginChangeCheck();
    46.                     {
    47.                         GUILayout.BeginHorizontal();
    48.                         {
    49.                             days = SirenixEditorFields.IntField(GUIHelper.TempContent("Days"), entry.SmartValue.Days);
    50.                             hours = SirenixEditorFields.IntField(GUIHelper.TempContent("Hours"), entry.SmartValue.Hours);
    51.                         }
    52.                         GUILayout.EndHorizontal();
    54.                         GUILayout.BeginHorizontal();
    55.                         {
    56.                             minutes = SirenixEditorFields.IntField(GUIHelper.TempContent("Mins"), entry.SmartValue.Minutes);
    57.                             seconds = SirenixEditorFields.IntField(GUIHelper.TempContent("Secs"), entry.SmartValue.Seconds);
    58.                         }
    59.                         GUILayout.EndHorizontal();
    60.                     }
    61.                     if(EditorGUI.EndChangeCheck())
    62.                     {
    63.                         //If EditorGUI detected any changes, we'll set our SmartValue to a TimeSpan containing our updated values.
    64.                         entry.SmartValue = new TimeSpan(days, hours, minutes, seconds);
    65.                     }
    66.                 }
    67.                 GUILayout.EndVertical();
    69.                 //Clean up and pop our indent and label width.
    70.                 GUIHelper.PopIndentLevel();
    71.                 GUIHelper.PopLabelWidth();
    72.             }
    73.             GUILayout.EndHorizontal();
    74.         }
    75.     }
    76. }
    77. #endif
    Like I said, this has received limited testing, but it seems good enough for me. As I start to get the hang of creating drawers with Odin, I'm really liking it. It's pretty easy to do, it just has a bit of a learning curve.
    Last edited: Jul 7, 2017
  30. ChaseRLewis73003


    Apr 23, 2012
    Hm, recommendation on EnableIf, DisableIf, ShowIf, and HideIf. Might be nice if you could link these to an Enum of some type.

    So if an enum matches the corresponding type passed in the attribute you could hide / show / disable / enable if an enum toggle corresponds to a certain value. Nice if you have more than just 2 states you need to transition between. Overall like the default settings and able to make some pretty neat editors with very little work, thumbs up.
    bjarkeck likes this.
  31. bjarkeck


    Oct 26, 2014
    Thanks, that makes a lot of sense! Added it to our todo list!

    For now you can wrap it in a property:
    Code (CSharp):
    1. public SomeEnum SomeEnum;
    3. [HideIf("SomeEnumIsSomething")]
    4. public int SomeField;
    6. #if UNITY_EDITOR
    7. private bool SomeEnumIsSomething { get {  return  this.SomeEnum == SomeEnum.Something; } }
    8. #endif
  32. bjarkeck


    Oct 26, 2014
    I've made it so that the PropertyTooltip can be applied to methods as well.

    In our upcoming rewrite of the property system, we'll make it so that OdinAttributeDrawers also works with class-level attributes. That way a lot of the attributes like DisableInPlayMode, DictionaryDrawerSettings, ListDrawerSettings, ValidateInput, etc.. will also work with class-level attributes.

    On our todo! I might actually do this one today. :)

    Thanks for the SpaceingAttribute code! We'll add an alternative space attribute as well. We've also just updated our OnInspectorGUI to follow the same convention. Prepend = "OnGUIBeforeProperty", Append = "OnGUIAfterProperty".

    Added to the todo list. And thanks again for sharing the code.
    Last edited: Jul 9, 2017
  33. electroflame


    May 7, 2014
    Very nice! It might be a bit confusing, though, as "Property" doesn't exactly make me think of "Method". Good enough for me, though!

    Perfect. That'd probably be easier in the long run than having to work out which ones to also add class-level support for onesie-twosie style.

    Looking forward to it! I stubbed out an Angle attribute that doesn't really do anything besides provide a float field (just so I could move forward) so having a proper Angle attribute will be great!

    Prepend and Append for OnInspectorGUI sound great. Also, feel free to take any of the code I posted if you want it. Like I said earlier, I'm not sure if any of it was the best way to handle it, as I don't have a very deep understanding of how Odin's drawers work, but if any of it's useful to you you can have it (especially if it saves you some time!).

    Thanks for the update! I'm looking forward to the stuff you outlined! :)
  34. bjarkeck


    Oct 26, 2014
    Right! That's a fair point. Maybe this one is an exception. My thinking is just that if the functionality is already achievable with other attributes, then there is no reason add multiple features to an attribute. That way people might also wonder why there is not tooltip overload for other attributes.

    We do have a [Wrap(0, 360)] attribute btw :)

    Thanks! I'll probably copy paste it in, and spice it up from there. The main difference is that we want to minimize the amount of GUI Layout groups created, as they generate a ton of garbage and makes the UI slow when there is a lot of them.

    For instance:

    Code (CSharp):
    2. GUILayout.BeginHorizontal(); // Creates garbage
    3. {
    4.     // ...
    5.     GUILayout.BeginVertical();// Creates garbage
    6.     {
    7.         GUILayout.BeginHorizontal(); // Creates garbage
    8.         {
    9.             // days, hours
    10.         }
    11.         GUILayout.EndHorizontal(); // Creates garbage
    12.         GUILayout.BeginHorizontal();// Creates garbage
    13.         {
    14.             // minutes, seconds
    15.         }
    16.         GUILayout.EndHorizontal(); // Creates garbage
    17.     }
    18.     GUILayout.EndVertical(); // Creates garbage
    19.     // ...
    20. }
    21. GUILayout.EndHorizontal(); // Creates garbage
    I would change that to something like:

    Code (CSharp):
    1. var rect = EditorGUILayout.GetControlRect(height: heightNeeded); // Also creates garbage
    And then manually splitting the rect as needed with help from EditorGUI.PrefixLabel.
  35. electroflame


    May 7, 2014
    It might make sense to deprecate PropertyTooltip and replace it with something like OdinTooltip, as that doesn't imply it only works on properties.

    True, I was just being lazy and didn't want to replace all of my Angle attributes on stuff with Wrap attributes (especially if Angle is on the way). :)

    No worries -- I hacked it together pretty quickly, so I didn't think of performance when I did it (not to mention I kind of hate working with EditorGUI Rects). If you can salvage anything and make it performant up to your standards, you've got my support. :p
  36. zsaladin


    Jan 20, 2015
    Awesome asset!!

    I'd like to use it, so I've bought it.
    But something was wrong.
    I've used my own 'CustomEditor' for 'Monobehavior' like this(
    My own 'CustorEditor' doesn't work anymore after importing Odin Inspector.
    So I modified Odin sources codes(OdinEditor) to mix them.

    But it's not clean way.
    Is there any way to mix them?
    Last edited: Jul 18, 2017
  37. Tor-Vestergaard


    Mar 20, 2013
    I'm afraid there isn't really a clean way to do this. The closest you'll get is likely what you did. The alternative would be to drop your own system, and using only Odin's instead, as Odin provides the same functionality out of the box, only more thoroughly integrated into the inspector. Your [ExposeProperty] would become Odin's [ShowInInspector] and your [ExposeMethod] would become Odin's [Button].

    To speed along the conversion, you could use an IDE like Visual Studio with strong refactoring capabilities to simply rename your "ExposePropertyAttribute" class to "ShowInInspector" and the "ExposeMethodAttribute" class to "Button", and then deleting both attributes. That way you would have updated all of the places you used those attributes to use Odin's instead.

    Edit: Also, we have a new, non-WIP thread here - we should probably move the conversation there :)
    Last edited: Jul 18, 2017
  38. SuneT


    Feb 29, 2016
    Pimp Your Unity Editor!
    Three months have passed since our release of Odin Inspector, and things are going well!

    But for many, Odin has a lot of untapped potential, so we thought we would try doing a "pimp my editor" day, where we show you exactly what Odin can do.

    If you have Odin, or you are just interested in learning more, feel free to share some of your code. We'll decorate it with attributes from Odin and try to make it as user-friendly as possible. Then we'll give you the pimped code back, along with some screenshots of how it ended up looking.

    Here's an example:

    We'll get to see some use-cases for Odin, and maybe get some exposure, and you'll learn a few new Odin tricks, and get some slick looking editors. If you don't have Odin, you can get a preview of what you could do with it in your project. Win win!

    Upload a script using whatever service you prefer. Screenshots of how it currently looks in your inspector would be much appreciated, and will help us reverse engineer your script if it doesn't compile.
  39. Baste


    Jan 24, 2013
    I'll bite. We're currently looking into if we're going to pick up Odin. It looks promising! What I really want to know is how much time I can save in creating good-looking inspectors.

    Here's the fields of DeathReaction, which is used to set up what things do when they die in World To The West. It's a serializable class, and things that need to die has a field of this type.

    Code (csharp):
    2. using System;
    3. using System.Collections;
    4. using UnityEngine;
    5. #if UNITY_5_5_OR_NEWER
    6. using UnityEngine.AI;
    7. #endif
    8. using Object = UnityEngine.Object;
    10. [Serializable]
    11. public class DeathReaction {
    13.     public float reactionDuration = 1f;
    14.     public float drownAnimationDuration = 2f;
    15.     public string deathAnimBool = "Dead";
    17.     public float freezeTimeScale = .05f;
    18.     public float freezeDuration = .2f;
    20.     public float deathAnimTime = .4f;
    22.     public DeathParticle[] deathParticles;
    24.     public AnimationCurve verticalKnockback = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    25.     public AnimationCurve horizontalKnockback = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    26.     public AnimationCurve xWobble = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    27.     public AnimationCurve zWobble = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    29.     public bool createImpact = false;
    30.     public float impactTime = 1f;
    31.     public float impactRadius = 2f;
    33.     [Serializable]
    34.     public struct DeathParticle {
    35.         public GameObject prefab;
    36.         public float time;
    37.     }
    39.     //code that does stuff goes here, none used by the drawer
    40. }

    Here's the current property drawer for the death reaction:
    Code (csharp):
    2. using UnityEngine;
    3. using UnityEditor;
    4. using UnityEditorInternal;
    6. [CustomPropertyDrawer(typeof (DeathReaction))]
    7. public class DeathReactionDrawer : PropertyDrawer {
    9.     private bool foldOut;
    10.     private bool initialized;
    12.     private const int numProps = 11;
    13.     private const int numLabels = 12;
    15.     private int numLines {
    16.         get {
    17.             int impactExtraLines = (createImpactProp == null || !createImpactProp.boolValue) ? 0 : 4;
    18.             return numProps + numLabels + impactExtraLines;
    19.         }
    20.     }
    22.     private SerializedProperty vertKBProp;
    23.     private SerializedProperty horKBProp;
    24.     private SerializedProperty xWobbleProp;
    25.     private SerializedProperty zWobbleProp;
    27.     private SerializedProperty drDurProp;
    28.     private SerializedProperty drownDurProp;
    29.     private SerializedProperty freezeDurProp;
    30.     private SerializedProperty freezeTScaleProp;
    32.     private SerializedProperty animNameProp;
    33.     private SerializedProperty animTimeProp;
    35.     private SerializedProperty createImpactProp;
    36.     private SerializedProperty impactTimeProp;
    37.     private SerializedProperty impactRadiusProp;
    39.     private ReorderableList deathPartList;
    41.     private void Initialize(SerializedProperty property) {
    42.         vertKBProp = property.FindPropertyRelative("verticalKnockback");
    43.         horKBProp = property.FindPropertyRelative("horizontalKnockback");
    44.         xWobbleProp = property.FindPropertyRelative("xWobble");
    45.         zWobbleProp = property.FindPropertyRelative("zWobble");
    46.         drDurProp = property.FindPropertyRelative("reactionDuration");
    47.         drownDurProp = property.FindPropertyRelative("drownAnimationDuration");
    48.         animNameProp = property.FindPropertyRelative("deathAnimBool");
    49.         animTimeProp = property.FindPropertyRelative("deathAnimTime");
    50.         freezeDurProp = property.FindPropertyRelative("freezeDuration");
    51.         freezeTScaleProp = property.FindPropertyRelative("freezeTimeScale");
    52.         createImpactProp = property.FindPropertyRelative("createImpact");
    53.         impactTimeProp = property.FindPropertyRelative("impactTime");
    54.         impactRadiusProp = property.FindPropertyRelative("impactRadius");
    56.         var deathParticleProp = property.FindPropertyRelative("deathParticles");
    57.         ReorderableList.ElementCallbackDelegate drawElement = (rect, index, active, focused) => {
    58.             var element = deathParticleProp.GetArrayElementAtIndex(index);
    60.             rect.height = 17f;
    61.             var particleProp = element.FindPropertyRelative("prefab");
    62.             EditorGUI.PropertyField(rect, particleProp, new GUIContent("Particle Prefab"));
    64.             rect.y += 21f;
    65.             var timeProp = element.FindPropertyRelative("time");
    66.             timeProp.floatValue = EditorGUI.Slider(rect, "spawn at time", timeProp.floatValue, 0f, drDurProp.floatValue);
    67.             if (timeProp.floatValue < freezeDurProp.floatValue)
    68.                 timeProp.floatValue = freezeDurProp.floatValue;
    69.         };
    71.         deathPartList = EditorTools.CreateReorderable(property.serializedObject, deathParticleProp, new[] {
    72.             "prefab", "time"
    73.         }, drawElement: drawElement);
    75.         initialized = true;
    76.     }
    78.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
    79.         if (!initialized)
    80.             Initialize(property);
    82.         EditorGUI.BeginProperty(position, label, property);
    84.         if (foldOut)
    85.             position.height = (position.height - deathPartList.GetHeight()) / (numLines - 2f);
    87.         foldOut = EditorGUI.Foldout(position, foldOut, label);
    89.         if (foldOut) {
    90.             DrawDeathReaction(position, property);
    91.         }
    93.         EditorGUI.EndProperty();
    94.     }
    96.     private void DrawDeathReaction(Rect position, SerializedProperty property) {
    97.         EditorGUI.indentLevel += 1;
    99.         DrawIndentedProp(ref position, drDurProp, "Time before GameObject is destroyed and loot spawned:");
    100.         if (drDurProp.floatValue < 0f)
    101.             drDurProp.floatValue = 0f;
    102.         DrawIndentedProp(ref position, drownDurProp, "Time before GameObject is destroyed if the creature is drowning");
    104.         DrawIndentedSlider(ref position, freezeDurProp, 0f, drDurProp.floatValue, "Duration of time freeze:");
    105.         DrawIndentedSlider(ref position, freezeTScaleProp, 0f, 1f, "Timescale during time freeze");
    107.         DrawIndentedProp(ref position, animNameProp, "Death bool for the monster's animator:");
    109.         DrawFreezeBoundedSlider(ref position, animTimeProp, "When the death animation starts playing:");
    111.         DrawIndentedProp(ref position, createImpactProp, "Should the death reaction create an Impact (which wakes Grues, etc.)");
    113.         if (createImpactProp.boolValue) {
    114.             EditorGUI.indentLevel += 1;
    115.             DrawFreezeBoundedSlider(ref position, impactTimeProp, "When the Impact should happen:");
    116.             DrawIndentedProp(ref position, impactRadiusProp, "The radius of the Impact");
    117.             EditorGUI.indentLevel -= 1;
    118.         }
    120.         DrawDeathParticlesList(ref position);
    122.         EditorGUI.LabelField(NextPosition(ref position), ""); //space
    123.         EditorGUI.LabelField(NextPosition(ref position), "These start at the end of the time freeze");
    124.         EditorGUI.LabelField(NextPosition(ref position),
    125.                              "So curve x=0 is at " + freezeDurProp.floatValue + " secs, and curve x=1 is at " + drDurProp.floatValue + " secs");
    126.         EditorGUI.PropertyField(NextPosition(ref position), vertKBProp);
    127.         EditorGUI.PropertyField(NextPosition(ref position), horKBProp);
    128.         EditorGUI.PropertyField(NextPosition(ref position), xWobbleProp);
    129.         EditorGUI.PropertyField(NextPosition(ref position), zWobbleProp);
    131.         if (EditorGUI.EndChangeCheck()) {
    132.             property.serializedObject.ApplyModifiedProperties();
    133.         }
    134.         EditorGUI.indentLevel -= 1;
    135.     }
    137.     private void DrawDeathParticlesList(ref Rect position) {
    138.         deathPartList.DoList(NextPosition(ref position));
    140.         position.y += deathPartList.GetHeight() - (position.height * 2f);
    141.     }
    143.     private void DrawIndentedProp(ref Rect position, SerializedProperty prop, string label) {
    144.         EditorGUI.LabelField(NextPosition(ref position), label);
    145.         EditorGUI.indentLevel += 1;
    146.         EditorGUI.PropertyField(NextPosition(ref position), prop);
    147.         EditorGUI.indentLevel -= 1;
    148.     }
    150.     private void DrawIndentedSlider(ref Rect position, SerializedProperty prop, float left, float right, string label) {
    151.         EditorGUI.LabelField(NextPosition(ref position), label);
    152.         EditorGUI.indentLevel += 1;
    153.         prop.floatValue = EditorGUI.Slider(NextPosition(ref position), prop.displayName, prop.floatValue, left, right);
    154.         EditorGUI.indentLevel -= 1;
    155.     }
    157.     private void DrawFreezeBoundedSlider(ref Rect position, SerializedProperty prop, string label) {
    158.         DrawIndentedSlider(ref position, prop, 0f, drDurProp.floatValue, label);
    160.         if (prop.floatValue < freezeDurProp.floatValue)
    161.             prop.floatValue = freezeDurProp.floatValue;
    162.     }
    164.     private Rect NextPosition(ref Rect position) {
    165.         position.y += position.height;
    166.         return position;
    167.     }
    169.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
    170.         var lineHeight = base.GetPropertyHeight(property, label);
    171.         if (!foldOut)
    172.             return lineHeight;
    174.         float listHeight = 0f;
    175.         try {
    176.             listHeight = deathPartList.GetHeight();
    177.         }
    178.         catch {}
    180.         return (lineHeight * (numLines + 1)) + listHeight; //+1 for some extra spacing
    181.     }
    182. }
    And here's how it looks, with a float field above and below for good measure:

  40. bjarkeck


    Oct 26, 2014
    Hi Baste,

    Really cool use-case, thanks! The Range property doesn't support getting its limits from other members. But besides that, everything else is archivable via attributes. I'll share an example here with your code later today.
    Last edited: Jul 25, 2017
  41. bjarkeck


    Oct 26, 2014
    Here is the converted version. I probably didn't get everything right, but you get the idea :)

    Code (CSharp):
    3. [Serializable]
    4. public class DeathReaction
    5. {
    6.     private static float currentReactionDuration;
    8.     [InfoBox("Time before GameObject is destroyed and loot spawned"), OnInspectorGUI("SetCurrentReactionDuration")]
    9.     public float reactionDuration = 1f;
    11.     private void SetCurrentReactionDuration()
    12.     {
    13.         currentReactionDuration = this.reactionDuration;
    14.     }
    16.     [InfoBox("Time before GameObject is destroyed if the creature is drowning")]
    17.     public float drownAnimationDuration = 2f;
    19.     [InfoBox("Death bool for the monster's animator")]
    20.     public string deathAnimBool = "Dead";
    22.     [InfoBox("When the death animation starts playing")]
    23.     public float deathAnimTime = .4f;
    25.     [Range(0, 1f)]
    26.     [InfoBox("Duration of time freeze")]
    27.     public float freezeTimeScale = .05f;
    29.     [InfoBox("Timescale during time freeze"), CustomValueDrawer("DrawFreezeDurationSlider")]
    30.     public float freezeDuration = .2f;
    32. #if UNITY_EDITOR
    33.     private static float DrawFreezeDurationSlider(float value)
    34.     {
    35.         return UnityEditor.EditorGUILayout.Slider("Freeze Duration", value, 0, currentReactionDuration);
    36.     }
    37. #endif
    39.     public DeathParticle[] deathParticles;
    41.     private string GetAnimationCurveInstructions
    42.     {
    43.         get
    44.         {
    45.             return string.Format("These start at the end of the time freeze\nCurve x=0 is at {0} secs, and curve x=1 is at {1} secs",
    46.                 this.freezeDuration.ToString("F2"),
    47.                 this.reactionDuration.ToString("F2"));
    48.         }
    49.     }
    51.     [ToggleLeft]
    52.     [InfoBox("Should the death reaction create an Impact (which wakes Grues, etc.)")]
    53.     public bool createImpact = false;
    55.     [InfoBox("When the Impact should happen:")]
    56.     [Indent, Range(0, 1), EnableIf("createImpact")]
    57.     public float impactTime = 1f;
    59.     [InfoBox("The radius of the Impact")]
    60.     [Indent, EnableIf("createImpact")]
    61.     public float impactRadius = 2f;
    63.     [InfoBox("$GetAnimationCurveInstructions")]
    64.     public AnimationCurve verticalKnockback = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    65.     public AnimationCurve zWobble = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    66.     public AnimationCurve horizontalKnockback = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    67.     public AnimationCurve xWobble = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 0f));
    69.     [Serializable]
    70.     public struct DeathParticle
    71.     {
    72.         [HorizontalGroup]
    73.         [CustomValueDrawer("DrawTimeSlider"), LabelWidth(100)]
    74.         public float time;
    76.         [LabelWidth(100), HorizontalGroup(100), HideLabel]
    77.         public GameObject prefab;
    79. #if UNITY_EDITOR
    80.         private static float DrawTimeSlider(float value)
    81.         {
    82.             return UnityEditor.EditorGUILayout.Slider("Spawn at time", value, 0, currentReactionDuration);
    83.         }
    84. #endif
    85.     }
    86. }

    I went back and forth a lot trying to figure out how best to do the range attributes. One solution would be to simply draw them manually in a method using the OnInspectorGUI attribute and hide the actual field. But that would mean that the object no longer has support for multi-selection and multi-editing, and you wouldn't be able to utilize other attributes that only works for fields and properties. We could also create a CustomRange attribute that lets you specify members which retrieve the lower and upper limits, but that would require a new attribute and a new drawer. Which seems like a lot of effort for something simple.

    So I've made a new attribute called [CustomValueDrawer] which we will in the next patch. So now, instead of making a new attribute, and a new drawer, for a one-time thing, you would make a method that acts as the property drawer.

    Code (CSharp):
    2.     [CustomValueDrawer("DrawFreezeDurationSlider")]
    3.     public float freezeDuration = .2f;
    5. #if UNITY_EDITOR
    6.     private static float DrawFreezeDurationSlider(float value)
    7.     {
    8.         return UnityEditor.EditorGUILayout.Slider("Freeze Duration", value, 0, currentReactionDuration);
    9.     }
    10. #endif
    In the next patch I'll make sure to add support for referencing instance methods, passing the label down to it, specifying whether or not it should work on list elements, etc.. But for now here is an early version of it.

    Code (CSharp):
    1. public class CustomValueDrawerAttribute : Attribute
    2. {
    3.     public readonly string MethodName;
    5.     public CustomValueDrawerAttribute(string methodName)
    6.     {
    7.         this.MethodName = methodName;
    8.     }
    9. }
    11. [OdinDrawer]
    12. public class CustomValueDrawerAttributeDrawer<T> : OdinAttributeDrawer<CustomValueDrawerAttribute, T>
    13. {
    14.     private class Context
    15.     {
    16.         public string ErrorMessage;
    17.         public Func<T, T> CustomValueDrawerStatic;
    18.     }
    20.     protected override void DrawPropertyLayout(IPropertyValueEntry<T> entry, CustomValueDrawerAttribute attribute, GUIContent label)
    21.     {
    22.         var context = entry.Context.Get(this, "context", (Context)null);
    24.         if (context.Value == null)
    25.         {
    26.             context.Value = new Context();
    28.             var methodInfo = entry.ParentType
    29.                 .FindMember()
    30.                 .IsNamed(attribute.MethodName)
    31.                 .IsMethod()
    32.                 .IsStatic()
    33.                 .HasReturnType<T>()
    34.                 .HasParameters<T>()
    35.                 .GetMember<MethodInfo>(out context.Value.ErrorMessage);
    37.             if (context.Value.ErrorMessage == null)
    38.             {
    39.                 context.Value.CustomValueDrawerStatic = (Func<T, T>)Delegate.CreateDelegate(typeof(Func<T, T>), methodInfo);
    41.             }
    42.         }