Update 3.0 introduces significant performance improvements, and several new foundational features and architecture improvements that will make it easier than ever to work with Odin, and that lay the groundwork for future patches.
3.0 is about the future; it's a major spring cleaning patch where we've gone back through our entire codebase and cleaned up, improved, and re-architected for anticipated future needs.
As such, this is not a patch focused on shiny new features. That being said, we do have a few shiny new features. But before we take a look at them, we want to bring your attention to another important change:
For a long time, the Odin Personal license (formerly named Odin Commercial) has been the only way to buy and use Odin for all customers. Odin Enterprise is a new license that launched with the 3.0 beta, and came with an attendant change in our EULA.
In short, Odin Enterprise is now required for any entity using Odin with revenue or funding greater than $200,000 USD in the last 12 months. For more details on the terms and features of Odin Enterprise, as well as our reasoning behind launching it, see
our updated pricing page and our blog post about the launch of Odin Enterprise.
Now let's take a look at all the new features and improvements that 3.0 brings!
Modules
Starting the trend of preparing for the future, we've added a modules feature to Odin. Modules are small, conditionally toggleable "bundles" of features that can activate and deactivate themselves based on whether their dependencies are present in the project.
They solve a long-standing problem for us, namely how to add support for Unity packages without excessive use of hard-to-maintain reflection code.
To begin with, there won't be a lot of modules, but as time passes and we add support for more and more optional Unity packages, the amount of modules will grow proportionately.
3.0 ships with only two modules: a Unity.Mathematics support module, and an experimental Unity.Entities support module.
We've added a new State system to properties, accessible through InspectorProperty.State, which manages certain states of properties such as Visible, Expanded and Enabled and makes them accessible by anything.
Formerly, the states of properties were inaccessible, usually contained within and managed by the drawers which used those states - for example, whether a property was visible was only controllable and knowable by the ShowIfAttributeDrawer,
whereas now property visibility has turned into an actual public, accessible value that other things can react to.
Additionally, to better facilitate drawer-less property validation and in anticipation of eventually moving away from IMGUI drawing to Unity's new UIElements, we have separated the logic that controls and updates states from the drawer system.
States should now be primarily controlled by the new StateUpdaters. We have removed many drawers such as ShowIfAttributeDrawer, EnableIfAttributeDrawer, and so on, and reimplemented them as StateUpdaters.
This feature means that groups will now automatically become invisible when all their contained properties become invisible, the Project Validator will not validate hidden properties, and so on.
With Odin 2.1, we brought you the ability to write C#-like expressions in resolved attribute strings by prepending @ to the string, instead of only being able to reference members by name.
In Odin 3.0, we're further improving this feature by adding support for assignment operators and a novel, new property query operator.
Assignment Operators
For a long time, attribute expressions have mostly been about somehow getting or creating a value.
However, with the new ActionResolvers (see further down), this all changes. Therefore, we've made
sure to add support for C#'s assignment operators to Odin's expression compiler.
This covers the operators =, +=, -=, *=, /=, %=, <<=, >>=, &=, |= and ^=.
Property Query Operator
The new state system for properties means that it becomes far more meaningful to work directly with properties in attributes. This was possible
before by using the builtin $property named expression argument, but it was quite tedious to type. We've added a new operator to expressions for
getting the properties of other members in the scope of the expression.
The syntax is #(MemberName), and it evaluates to the InspectorProperty instance that represents the MemberName member.
As the example below demonstrates, these additions to the expression compiler have a powerful synergy with the new state system, letting you
create vastly more powerful custom logic in your inspectors than ever before, using only attributes.
// It is generally recommended to use the OnStateUpdate attribute to control the state of properties
[OnStateUpdate("@#(exampleList).State.Expanded = $value.HasFlag(ExampleEnum.UseStringList)")]
public ExampleEnum exampleEnum;
public List exampleList;
[Flags] public enum ExampleEnum { None, UseStringList }
Value and Action Resolvers provide a simple API for taking strings and turning them into values or actions,
as well as providing helpful, detailed error messages if a bad string is given or the string causes exceptions while being resolved.
With these two new systems, we managed to eliminate every single line of manual reflection code related to resolved strings,
throughout all of Odin's drawers, validators and state updaters. This has drastically simplified their implementation.
Value and Action Resolvers don't just make Odin's own attribute implementations more simple, consistent and feature-rich across
the board, though - they also make it effortless for users to create their own attributes that use resolved strings.
You asked (a lot), and we answered! The new Searchable attribute adds a search filter that can search the children of the field or type on which it is applied.
You can put it on types to make the whole type searchable anywhere it appears, put it on members to make the member's contents searchable, put it on
lists or arrays to make them searchable, and so on!
This feature has been a long time coming, and we're happy to report that it's finally here. Search can be configured in a variety of ways by passing in
parameters to the attribute declaration, and by implementing the ISearchFilterable interface.
Note that this does not currently work when directly applied to dictionaries, though a search field "above" the dictionary will still search the dictionary's
properties if it is searching recursively.
OnCollectionChanged can be put on collections, and provides an event callback when the collection is about to be changed through the inspector,
and when the collection has been changed through the inspector. Additionally, it provides a CollectionChangeInfo struct containing information
about the exact changes made to the collection. This attribute works for all collections with a collection resolver, amongst them arrays, lists,
dictionaries, hashsets, stacks and linked lists.
OnStateUpdate
OnStateUpdate provides an event callback when the property's state should be updated, when the StateUpdaters run on the property instance.
This generally happens at least once per frame, and the callback will be invoked even when the property is not visible. This can be used to
approximate custom StateUpdaters like [ShowIf] without needing to make entire attributes and StateUpdaters for one-off cases.
The OnInspectorInit attribute takes in an action string as an argument (typically the name of a method to be invoked, or an expression to be executed),
and executes that action when the property's drawers are initialized in the inspector. Initialization will happen at least once during the first drawn
frame of any given property, but may also happen several times later, most often when the type of a polymorphic property changes and it refreshes its
drawer setup and recreates all its children.
OnInspectorDispose
The OnInspectorDispose attribute takes in an action string as an argument (typically the name of a method to be invoked, or an expression to be executed),
and executes that action when the property's drawers are disposed in the inspector. Disposing will happen at least once, when the inspector changes
selection or the property tree is collected by the garbage collector, but may also happen several times before that, most often when the type of a
polymorphic property changes and it refreshes its drawer setup and recreates all its children, disposing of the old setup and children.
Added a State system to properties, accessible through InspectorProperty.State, which manages certain states of properties such as Visible, Expanded and Enabled and makes them accessible by anything.
Added property query syntax and assignment operators to expressions.
Added [Searchable] attribute.
Added [OnCollectionChanged] attribute.
Added [OnStateUpdate] attribute.
Added Value Resolvers and Action Resolvers
Added a new StateUpdater system; these are type-matched (just like drawers) and attached to a property, and their sole purpose is to receive event invocations and update the property's state. These are all written to exist outside of an IMGUI scope.
Added right-click context menu items "Move element to top", "Move element to bottom" and "Move element to index" to all elements of ordered collections, as well as "Insert element here", that creates a new value using the list's value creation logic, and inserts it at the context element's index.
Added VisibleIf and AnimateVisibility members to all group attributes; all groups can now directly control their own visibility, without ShowIfGroup and HideIfGroup getting involved anywhere.
The validation system has been moved to use the property system as its data backend, instead of using manually implemented reflection.
The project validator will now ignore validation of properties that are not visible.
Experimental features
We've added a new experimental option in the General drawer preferences called Precompute Type Matching, which will attempt to speed up first-time inspector opening times by
using a separate thread to precompute generic type matching for things like drawers, based on a file cache of all matched types from prior domain reloads that is kept in
the project Temp folder. This feature should be particularly impactful in those projects that contain very large numbers of custom drawers, processors and resolvers that
are causing lag spikes the first time an inspector for a given type is opened with Odin. Please try this out if you'd like, and see if you can notice or measure any difference.
Performance
Major optimization passes have been performed throughout the entire codebase, resulting in a major overall increase in performance. This includes drastically rearchitecting many internal systems;
more careful caching of the results of expensive operations; optimizing and limiting the use of .NET's reflection API's; aggressively limiting allocations needed for common operations;
algorithmic improvements on priority sorting of drawers, resolvers and processors; changing code hotspots to enable better optimization by the JIT; and much, much more.
Specific changes made and features affected during this months-long optimization endeavour are far too numerous to list exhaustively, but of particular note is the initialization time
of Odin in general and of the property system in particular. Notably, in our benchmarks the majority of CPU time during the initialization of Odin and first-time use of the property system is now being spent
by the JIT as it compiles Odin to its final bytecode form.
To provide an example, switching the validation system to use Odin's properties resulted in project validation scans becoming 10-20 times slower than in 2.1. With the new optimizations,
this overhead is reduced to project validation scans that range from being about as fast as before, to about twice as slow at worst, according to our tests - this while running on the
far more feature-rich, easy-to-use and extendable property system, as opposed to 2.1's more barebones and spotty validation backend.
Add
[OnInspectorGUI] now also uses the action resolver system when placed directly on methods. This means OnInspectorGUI can now have named context values passed as method parameters, such as the InspectorProperty instance that represents the invoked method.
[OnInspectorInit] and [OnInspectorDispose] can now be placed directly on methods, and the methods can have named context values passed as parameters, such as the InspectorProperty instance that represents the invoked method.
A PropertyTree can now have its target values changed via the SetTargets and SetSerializedObject methods. Along with the new CleanForCachedReuse method on PropertyTree, property trees are now cacheable/reusable. This new feature is used by the Project Validator to speed up scans.
The TypeSearchIndex matching rules have now been reimplemented in a new TypeMatcher/TypeMatcherCreator pattern that is more easily optimized for performance, as each matcher can pre-fetch results of expensive reflection calls like type.GetGenericArguments(), such that they only ever have to be made once. This makes first-time/uncached matching several times faster.
Added "Duplicate element" menu item to all collection elements.
Added "Insert pasted element" context menu item to collection elements, which inserts a pasted element from the clipboard if the clipboard value is compatible.
Added an InvokeOnUndoRedo parameter to the OnValueChanged attribute to control whether to perform the action when an undo or redo event occurs via UnityEditor.Undo.undoRedoPerformed.
Added an ODIN_INSPECTOR_3 preprocessor directive, to help people transition due to the number of breaking changes made in certain APIs.
Added InspectorProperty.IsTreeRoot, which is true if the property is the tree root.
Added LabelText member to BoxGroup, to allow overriding the label text of the box without changing the name of the group.
Added StateUpdaters, a new system for updating property states that is entirely independent of the concept of drawing. Drawers that implement IOnStateUpdate will no longer be created, initialized and have OnStateUpdate invoked during non-GUI events such as a project validation scan, which was causing issues before with drawers that weren't mean to be initialized outside of a GUI event context.
Added support for empty expression strings to actions: the expression string "@" will do nothing, but evaluate to an empty action when resolved by action resolvers.
Added the [OnInspectorInit] and [OnInspectorDispose] attributes. TODO: link docs.
Added the MethodPropertyResolverCreator, which resolves an action as an invocation of the property method itself, for properties that represent methods or delegates.
Added Value And Action Resolvers Example to Custom Drawers demo scene.
Added window popup that asks the user to accept the new EULA.
PropertyTree.EnumerateTree and InspectorProperty.NextProperty now have an extra bool parameter "visibleOnly" that will cause them to only return visible properties.
The static inspector window will now also recursively inspect all members in child properties; this lets you recurse arbitrarily deep into various type reference structures, so be careful with this!
Added DisplayParameters member to the Button attribute. If set to false, the button will not display any parameters as inspector values, but will instead be invoked through an ActionResolver or ValueResolver (based on whether it returns a value), giving access to contextual named parameter values like "InspectorProperty property" that can be passed to the button method.
The TabGroup drawer now exposes the "CurrentTabName", "CurrentTabIndex" and "TabCount" custom states, which say respectively what the name of the currently open tab is, what the index of the currently open tab is, and how many tabs the tab group contains.
Added the NicifyText member to the LabelText attribute. When set to true, the given label text will be nicified before being displayed.
Added OdinEditorWindow.EnsureEditorsAreReady() method that ensures editors are ready to be drawn safely.
Added ValidationRunner.ValidateObjectRecursively overloads to the Project Validator's ValidationRunner type, that take a root value to validate which is not derived from the UnityEngine.Object type.
The AOT support scan will now also scan all addressable assets, if the addressables package is included and being used.
Change
Brought ColorUsage32AttributeDrawer up to modern Odin standards.
Brought ColorUsageAttributeDrawer and ColorUsage32AttributeDrawer up to modern Odin standards.
Changed the default file name of the Odin 3.0 persistent context cache file, so it doesn't conflict with caches from prior versions of Odin.
Changed the hard-coded max size of the persistent context cache to 1GB, though it is really, really not recommended to actually make it that large! :)
DefaultOdinPropertyResolverLocator's SearchIndex and GetEmptyResolverInstance members are now public.
Deleted all property system APIs that were made obsolete with patch 2.0.
FixUnityNullDrawer is now public.
Property order has been changed to a float from an int. This change applies to various APIs, and the [PropertyOrder] attribute. This will make it a lot easier to inject properties in between each other in ways you hadn't originally anticipated when setting up your orders, and means you won't have to leave huge order gaps to accommodate unforeseen future changes.
Local property contexts (IE, all calls to Property.Context.Get<T>(etc)) have been made fully obsolete, upgraded from the warning-level obsolete they were before. Local drawer fields should be used instead.
Removed the Scene Validator which was deprecated a long time ago, and changed relevant examples to use the new validation system instead.
Renamed most resolved strings to remove terminology that references members, since all resolved strings are no longer member exclusive, but can also use expressions and any other functionality registered in the value and action resolver system. All old members have been soft-obsoleted.
The "root properties" of a PropertyTree now have the actual tree RootProperty (formerly SecretRootProperty) as their parent, instead of null. PropertyTree.Draw() no longer manually draws the children of the tree root property one by one - instead, it merely calls RootProperty.Draw(), meaning that Odin PropertyTrees can now directly inspect and correctly draw types such as lists.
The drawer for [ShowDrawerChain] will now also display the path of the property whose drawer chain is being shown.
ValueResolvers and ActionResolvers now have an option for logging exceptions that are thrown during invocation, which is set to true by default.
Range and PropertyRange attributes are now drawn virtually identically by Odin.
EditorOnlyModeConfig and PersistentContextCache are no longer asset-less ScriptableObject-based singletons (via GlobalConfig<T>), but more simple POCO singletons. This change was made because they were causing UnityEngine.Object leaks across domain reloads.
GlobalConfigAttribute.UseAsset has been made error-level obsolete, because it was causing object leaking issues and was fundamentally "useless". Use a POCO singleton or a ScriptableSingleton instead.
OdinEditorWindow.UpdateEditors() has been changed from private to protected so it can be invoked manually by deriving classes.
Turned PreviewFieldAttribute.AlignmentHasValue into a readonly property.
Changed the type of the ValidationSetup.Root field from UnityEngine.Object to System.Object, since any type can act as a validation root now.
Fix
Added ShowIfGroup and HideIfGroup to the Groups category in the attribute overview.
Dictionaries should now work property with [LabelText].
Exceptions thrown by attribute and property processors are now caught and logged.
Fixed case where attributes such as [Range] and [MinValue] would throw exceptions and not work, when applied to Vector2, Vector3 and Vector4's.
Fixed case where the TypeExtensions.GetOperatorMethod(Type, Operator, Type, Type) overload would throw an exception when there was an ambiguous operator match.
Fixed issue in attribute expressions where assigning a constant null value to a reference type member/collection indexer would require casting the null value to the correct type. Null constants are now implicitly converted to any reference type member/indexer to which they are assigned.
Fixed issue where the labels of EnumToggleButtons' selected enum values would not show up in the first ever domain reload in Unity 2019.3+. Issue #671 https://bitbucket.org/sirenix/odin-inspector/issues/671/enum-toggle-buttons-when-selected-have.
Fixed the polymorphic star and the MinMaxSlider slider UI being badly sized/placed in the new 2019.3 UI.
GUIUtility.ExitGUI() is now called after all button clicks, meaning that button methods that invalidate the GUI state should no longer cause a multitude of exceptions related to layout and other GUI states to be thrown after they're done executing.
Odin now by default draws a blue line next to modified prefab values. This can be turned off by toggling the "Show Blue Prefab Value Modified Bar" setting in the General preferences.
OdinEditor now also performs extra warmup repaints after it has been cached and reused by Unity, instead of just when it is created and used for the first time.
Removed superfluous debugging statements from validation.
Soft-obsoleted OdinMenuTree.HandleKeybaordMenuNavigation and added a message to use the property spelled OdinMenuTree.HandleKeyboardMenuNavigation instead.
The PropertyTree.GetPropertyAtPath(path) method will now correctly return the root property if given the path '$ROOT'.
Fixed error where assigning null to the WeakSmartValue of an alias value entry that aliases a struct value for a base value entry of an interface type implemented by the struct, would result in a null reference exception.
The attribute expression compiler will now implicitly cast (and box) enum values to the System.Enum type.
The ChildGameObjectOnly attribute now correctly validates and registers incorrect values in the Project Validator.
OdinEditorWindow.DrawEditor(int index) and DrawEditorPreview(int index, float height) can now be safely invoked before base.OnGUI() has been called once before. This fixes a common issue people were seeing when tweaking the code examples given in the 3-part "Create THE GameManager" tutorial series on YouTube.
Setting a PreviewField attribute's Alignment parameter using the [PreviewField(Alignment = value)] syntax rather than passing the value to the constructor now works properly.
Updated UnitySerializationUtility.GuessIfUnityWillSerialize to not consider types defined in System.dll and System.Core.dll as types that Unity might serialize.
AOTSupportUtilities.GenerateDLL no longer sets obsolete Linux import settings "StandaloneLinux" and "StandaloneLinuxUniversal" in 2019.2+.
Fixed case where the TypeExtensions.GetOperatorMethod(Type, Operator, Type, Type) overload would throw an exception when there was an ambiguous operator match.
Switched serializer unaligned read/write support logic to a whitelist instead of a blacklist approach, and ensured the read test only occurs on whitelisted platforms. This fixes a startup crash on PS Vita.
Mitigated issue with the AOT scan where scanning resource assets would often break and throw MissingReferenceExceptions when Unity's AssetDatabase v2 was enabled. This appears to be an issue with the resource assets being unloaded/destroyed silently while in use, with the issue more likely to happen on slower hardware. The mitigation is to load resources one at a time by path instead of loading them all at once, thus lessening the risk of assets becoming null during the scan.
Fix to HashSet used for deduplication when loading resource assets for AOT scan, to cope with cases where corrupt/bad resources are loaded. It now uses a reference equality comparer to bypass Unity object strangeness.
MissingReferenceExceptions are now caught and logged so the scan can continue when AOT scanning resource assets, in case the null check fails/is bad.