Guides Odin Migration Guide - Ambiguous-Interactive/unity-helpers GitHub Wiki
A practical guide for migrating from Odin Inspector to Unity Helpers. Examples are verified against the actual source code.
Use Tools > Wallstop Studios > Unity Helpers > Odin Migration to preview or apply conservative source edits
under Assets. The selected-script commands accept selected .cs files and selected folders; the
Assets commands scan all scripts under Assets. Generated scripts are skipped.
The tool rewrites only parameterless ReadOnly and EnumToggleButtons attributes on public fields,
or non-public fields carrying the exact global::UnityEngine.SerializeField or
global::UnityEngine.SerializeReference attribute (with or without the Attribute suffix). This
textual check does not validate whether Unity supports the field's type. Properties, static fields,
constants, read-only fields, and
NonSerialized fields remain unchanged for review. Type, method, event, and ambiguous targets also
remain unchanged, including when several attribute lists precede a declaration.
Automatic rewrites require the exact global qualification
global::Sirenix.OdinInspector.Attribute. Non-global qualified names, namespace aliases, attribute
aliases, and unqualified attributes brought into scope by using Sirenix.OdinInspector; remain
review items: textual analysis cannot prove that any of those names was not shadowed in a nearer
scope. Explicit attribute targets are also checked; only no target or field: is eligible for an
automatic edit. Every globally qualified Odin inspector attribute appears in the
report, including attributes for which Unity Helpers has no automatic migration.
Button, Required, ShowIf, and HideIf remain manual because their semantics are not identical
enough to infer from source text alone. Even ShowIf(nameof(member)) and
HideIf(nameof(member)) stay unchanged: source text does not prove that the condition is Boolean,
and WShowIf does not reproduce Odin's animated transition. The report explains this boundary.
For Button, the report now calls out both checks that need a person: WButton targets methods on
Unity objects, and its supported arguments and method signatures differ from Odin's.
Resolver strings such as "enabled" are never rewritten.
The quick-reference table below lists conceptual replacements. It does not promise that the tool
can rewrite every row safely. The report identifies unsupported attributes and options for manual
review. It reports serialized Odin bases (including SerializedNetworkBehaviour and
SerializedUnityObject), OdinSerialize, and Odin-owned dictionary or set shapes as blockers and
never rewrites them. A source file containing SerializationData,
UnitySerializationUtility, ISupportsPrefabSerialization, or
ShowOdinSerializedPropertiesInInspector (including its Attribute suffix) also blocks automatic
rewrites. This includes direct calls, aliases, and using static imports of the utility. Escaped C#
identifiers such as @SerializationData are recognized. These names can be shadowed by project types, so
each finding means the file needs inspection; the text scanner does not prove that Odin owns its
data.
Always run Preview first and inspect the Console report. Cancelling a scan stops before reading the
next file. Cancellation or any source-script scan failure disables Apply, so a partial source scan
cannot write source files.
Every Preview and Apply command also checks Unity text .unity, .prefab, and .asset files under
Assets, even when source scripts are selected. It reports the file and line of possible Odin
serializationData or _serializationData payloads and prefab overrides that target those fields.
The data scan uses the authored-asset reader, which recognizes quoted keys and skips text inside
block scalars.
These findings need manual review; migrate the data only when Odin owns it. They do not block Apply, which changes only the
proven-equivalent inspector attributes in source files and leaves serialized data untouched.
Unreadable assets and files with no Unity YAML document appear as data-scan failures; they do not block those
source-only edits, but the data report is incomplete. The scan reads files as text and does not
load Unity objects or change serialized data. Set Edit > Project Settings > Editor > Asset
Serialization to Force Text to make Unity assets inspectable, then preview again. A clear
report means only that these recognized payload shapes were absent from the text files scanned; it
is not a data migration or proof that all Odin-owned state has been converted.
Caution
Replacing an Odin serialized base, [OdinSerialize], or a custom serialization path is a data
migration. Existing scene, prefab, and asset data can remain in Odin's serialization payload and
disappear when the source type changes. The automated tool deliberately leaves these constructs
unchanged.
Apply checks the complete source batch against its preview and creates every byte backup before
writing any target. It checks each target again immediately before its staged durable-file
replacement, then attempts to roll back completed writes if a later write fails. It refuses that rollback when the live bytes show a newer external edit. These checks reduce
common editor races but cannot coordinate atomically with other processes. Timestamped byte-for-byte backups are retained under
Library/WallstopOdinMigrationBackups. New backups include unchanged .meta bytes and a bounded,
checksummed recovery record before the first target write. Backups use short numbered filenames to
avoid extending asset paths beyond Windows limits. The recovery record maps each backup to its
original asset path; recovery also reads the older mirrored-directory layout. Imported metadata must
exist before Apply.
Recovery records support source and Unity text assets under Assets, with 16 MiB per file,
256 MiB of combined original and replacement bytes, and 10,000 asset/metadata entries per batch.
This is a Tier C filesystem operation: Unity Undo does not cover it. Commit or stash your work before Apply, keep the backup until the project has compiled and assets have been validated, and use version control for the authoritative rollback. The tool preserves each supported file's encoding, byte-order mark, and newline style, then refreshes the Asset Database once after a successful batch.
Editor scripts can use OdinMigrationTool in
WallstopStudios.UnityHelpers.Editor.Tools.OdinMigration without opening dialogs:
-
AnalyzeSource(source)returns the proposed source, immutable changes, blockers, and review findings. It performs no file I/O. CheckIsValidandFailurebefore using the result. -
TryReadSerializedNodes(source, out payloads, out failure)reads Odin Nodes format (2) from Unity text YAML without loading assets or constructing recorded types. Each immutable payload retains its owning document's file ID and script GUID, original payload text and line range, ordered Unity references, and orderedName, numericEntry, andDatarecords. -
TryConvertSerializedDictionary<TKey, TObject>(payload, fieldName, out yaml, out failure)converts a complete enum-to-object-list dictionary payload into a Unity Helpers field fragment. It writes no files and does not change the source type. -
TryConvertSerializedDictionaries(payload, fields, out yaml, out failure)converts a payload containing multiple dictionary fields with separately declared original key and element types. Create immutable field schemas withOdinMigrationDictionaryField.Create<TKey, TObject>(name). Every retained root and every schema must be consumed exactly once; output follows retained record order and failures return a null fragment rather than partial data. -
TryBindSerializedDictionaryFields(payload, fields, out binding, out failure)matches the retained script GUID and requested field types to metadata generated during editor compilation. It returns immutable source checksums, original field type spans, and assembly identities. -
TryConvertBoundSerializedDictionaries(payload, fields, owningAssetPath, liveReferencedObjects, out binding, out yaml, out failure)combines compiler binding, complete Nodes conversion, and live reference validation. Any failure returns both a null binding and a null fragment. -
TryVerifyConvertedDictionaries(payload, fields, owningAssetPath, importedOwner, liveReferencedObjects, out failure)checks a loaded imported document against its retained Nodes values. It checks owner/script identity, keys, entry/list counts, nulls and object addresses. -
TryPreviewDictionarySources(payload, fields, out preview, out failure)proposes source changes for compiler-bound fields without writing files. Each immutable proposal contains its compiler path, original source, and proposed source. -
TryPreviewDictionaryMigration(targets, out preview, out failure)coordinates source proposals with complete retained asset proposals for the selected Unity documents. EachOdinMigrationDictionaryTargetcaptures an asset path, owner file ID, field schemas, and the complete live reference table. Its five-argument constructor can also receive a caller-loaded owner without retained Odin data. An optional cancellation callback runs before each asset read. -
TryInventoryDictionaryMigration(preview, out inventory, out failure)scans serialized files underAssetsfor original declaring types and their derived Unity scripts. It reports retained documents omitted from the paired preview and failures that make the inventory incomplete. -
TryApplyDictionaryMigration(preview, compilation, out apply, out failure)starts a paired source and data migration after checking a successful, matching compilation receipt and a fresh complete inventory. Every affected document must have a selected retained payload or a selected supported live capture. Save scenes and assets before starting, leave Prefab Mode, and wait for compilation, import, and Play Mode to finish. Atruereturn means the transaction started. Pollapply.TryReadStatus(out state, out failure); onlyVerifiedmeans the replacement assemblies loaded and the retained keys, list values, nulls, reference addresses, and runtime types matched the imported documents. -
TryReadDictionaryApply(backupRoot, out apply, out failure)reconnects to the durable operation after domain reload or restart. Keepapply.BackupRootbefore reload. The coordinator restores the original bytes after interruption, cancellation, compilation errors, or failed verification, then waits for recovery import;Recoveredmeans original file and metadata hashes matched and captured live dictionaries were restored through generated writers and checked again.apply.TryCancel(out failure)requests recovery of the active operation.RecoveryFailedretains the backups and active marker for manual repair. Changes to targets, metadata, journals, or the active marker prevent unsafe replacement. This Tier C operation cannot reverse arbitrary consumer import callbacks through Unity Undo. Recovery records preserve full 64-bit file IDs. When the only loaded saved scene is affected, the coordinator keeps an empty temporary scene while compiling and restores the saved scene afterward; changes to that temporary scene prevent it from being discarded. -
TryPreviewAssets(out preview, out failure)scans all scripts and serialized assets underAssetsin the current project. -
TryPreview(sourceAssetPaths, out preview, out failure)limits source analysis to the supplied project-relative.cspaths and still scans all serialized assets underAssets. Duplicate paths are scanned once. Paths must use forward slashes and cannot contain traversal segments, symbolic links, or reparse points. - Both preview methods accept an optional
shouldCancel(path, index, total)callback. Returningtruecancels before that file is read. Cancellation and source failures returnfalseand retain the available report inpreview; discovery, path, and callback failures can return a null preview. Neither path permits Apply after an incomplete source scan. - A preview exposes immutable
SourceAnalyses,SerializedAnalyses,Failures, andSerializedScanFailures, file/replacement counts, and the completeReport. A successful scan can have no planned changes; checkCanApplyseparately. Serialized findings and failures have the same source-only scope as the menu commands. -
TryApply(preview, out backupRoot, out failure)uses the preview's captured source bytes and the same transaction as the menu tool. A successful preview can be applied once. A failure reports any attempted backup location and rollback failures; keep those backups for review. -
TryRestoreBackup(backupRoot, out failure)restores the recorded bytes after a reload or restart. Call on the editor thread when compilation and import have finished. It first validates the entire recovery record, backups, target bytes, and unchanged metadata. Changed or missing files prevent recovery before writes. A write failure during recovery can leave a partially restored batch; the reported failure and retained backups support manual recovery. Repeating successful recovery is safe. Refresh the Asset Database after success. Backups made before recovery records were introduced still require manual restoration.
Call project preview and apply methods on the Unity editor thread. They do not display progress
bars or dialogs. The source-only TryApply does not refresh the Asset Database: call
UnityEditor.AssetDatabase.Refresh() on the editor thread after a successful batch, when ready
for Unity to import the changed sources. The menu tool performs that refresh itself.
Programmatic writes remain Tier C operations outside Unity Undo. They do not convert Odin-owned
serialized data or establish that Odin can be removed from the project.
The node reader accepts plain folded strings, single-quoted strings, and double-quoted strings
with JSON-compatible escapes. It limits input to 4,194,304 characters and 65,536 lines. Counts
and identifiers in record data remain strings; they do not size an allocation. Missing or
duplicate record fields, unknown entry tags, byte payloads, other Odin formats, and unsupported
YAML scalar/sequence forms return false with an empty payload list. A valid Unity asset with
no Odin payload returns true and an empty list.
using System.Collections.Generic;
using System.IO;
using WallstopStudios.UnityHelpers.Editor.Tools.OdinMigration;
string yaml = File.ReadAllText(assetFileSystemPath);
if (!OdinMigrationTool.TryReadSerializedNodes(
yaml,
out IReadOnlyList<OdinMigrationSerializedData> payloads,
out string failure))
{
ReportMigrationFailure(failure);
return;
}
foreach (OdinMigrationSerializedData payload in payloads)
{
ReviewRetainedNodes(payload.FileId, payload.SerializationNodes, payload.ReferencedUnityObjects);
}This API preserves raw records. It does not validate typed values, graph references, declared
array counts, or prefab overrides. Prefab metadata remains in Source for the conversion stage.
The source-only preview/apply APIs do not convert these payloads. Paired source/data conversion
must validate the destination schema and retained references before removing Odin-owned state.
The dictionary converter supports int-backed enum keys and List<TObject> values where
TObject derives from UnityEngine.Object. Supply the original compiled key and element types.
Recorded generic type shapes must match those types by full name; assembly names are not used
to construct or resolve recorded types. The output uses SerializableDictionary<TKey, List<TObject>>
with Unity's packed enum keys and boxed list values. Entries and references keep their input order.
Null object elements become {fileID: 0}; repeated object references remain repeated list elements.
The converter checks default enum comparer state, unique node identities and dictionary keys,
complete record boundaries, delivered array counts, and reference indices. Node identities are
unique across the complete payload, including multiple dictionaries. It does not allocate
from a claimed count. Missing comparer records from older Odin data use the default comparer.
Custom comparers, null lists, shared list references, other field shapes, additional records,
unknown retained metadata, linked prefab state, and prefab modifications require further
conversion and return false with a null fragment.
if (!OdinMigrationTool.TryConvertSerializedDictionary<ImpactSize, UnityEngine.Sprite>(
retainedPayload,
nameof(Variations),
out string fieldYaml,
out string failure))
{
ReportMigrationFailure(failure);
return;
}For multiple retained dictionaries, declare their original shapes together:
OdinMigrationDictionaryField[] fields =
{
OdinMigrationDictionaryField.Create<ImpactSize, UnityEngine.Sprite>(nameof(Variations)),
OdinMigrationDictionaryField.Create<DamageType, UnityEngine.Material>(nameof(Materials)),
};
if (!OdinMigrationTool.TryConvertSerializedDictionaries(
retainedPayload, fields, out string fieldYaml, out string failure))
{
ReportMigrationFailure(failure);
return;
}For a live editor conversion, pass the complete Unity object table in the same order as
ReferencedUnityObjects. The owning asset path resolves references that carry only a local file ID:
if (!OdinMigrationTool.TryConvertValidatedSerializedDictionaries(
retainedPayload,
fields,
owningAssetPath,
liveReferencedObjects,
out string fieldYaml,
out string failure))
{
ReportMigrationFailure(failure);
return;
}Call this method on the Unity editor thread. It checks each used object's declared element type,
asset GUID, and local file ID without resolving recorded type names or loading assets. Saved-scene
references use Unity's scene identity; prefab instance references also match the saved scene's
source asset GUID, signed source file ID, and instance ID. Scene text reads are bounded to
4,194,304 characters and 65,536 lines. Wrapped source-object flow mappings written by supported
Unity versions are read across their continuation lines; malformed mappings are refused.
Derived objects can satisfy a base element type. Recorded null slots remain null; missing or destroyed
objects, wrong types or identities, and incomplete or extra live table entries return false
with no fragment. Identity is checked at conversion time; validate again before a future paired apply.
The fragment uses LF newlines and two-space field indentation. Use
TryConvertBoundSerializedDictionaries to check the original source declarations and owning script
before preparing a paired source/asset preview. Call it on the Unity editor thread after compilation
completes. The generator resolves aliases, private nested enums, partial classes, and inherited
fields. A referenced base assembly must register its own generated binding; its original source
must remain available under Assets for a coordinated rewrite. Private and readonly fields retain
the base assembly's generated capture and recovery delegates. It captures the original class and base declarations,
enum declarations, and element class declarations; every captured source must still match its
SHA-256 checksum. Each source read is bounded to 4,194,304 characters. Field names and both key and
element assembly identities must match. Ambiguous inherited names, missing compiler metadata,
changed source, wrong types, and invalid source spans return no binding.
Constructed generic names retain their arguments, including nested enums and arrays. Static type
metadata accounts for framework type forwarding between compiler facades and Unity's runtime.
After compilation and import finish, pass the loaded owner to TryVerifyConvertedDictionaries
with the original payload, field schemas, owning path and complete live reference table. The verifier
checks the owner's saved GUID/file ID and script GUID, then reads its serialized dictionary arrays.
Keys and value lists must retain their counts and order. Each null and object reference must match
the retained address. It detects unsaved live mutations as well as changed imported data.
Call on the editor thread. The API loads no assets and writes no files or serialized properties;
Unity can invoke serialization callbacks when it reads the loaded object. This verifies values for
the supported conversion schemas. It does not apply files, prove project-wide migration coverage,
or replace coordinated rollback and post-import source validation.
The independent WallstopStudios.UnityHelpers.OdinMigration.Generator.dll compiler input produces
these bindings without depending on the protobuf generator DLL or enabling WallstopProto.
Its importer excludes it from runtime plugins and player builds. The binding registry and generated
registration are editor-only. The public OdinMigrationBindingAttribute type remains available in
both editor and player compilations, so consumer references to its type, constructor, or properties
remain valid in both. Its Conditional("UNITY_EDITOR") declaration omits attribute applications
from player assembly metadata. Referenced owner markers expose binding identities without exposing
private field members.
Resolution combines source snapshots, field declarations, base edits, and affected types only after
all referenced bindings register. Missing bindings, invalid inheritance, cycles, and conflicting
source checksums refuse the complete binding. Replacing a registration invalidates resolved children.
Extensible Unity object classes register compiler metadata even when they declare no dictionaries,
so a dictionary-free helper in a referenced assembly can participate in the complete inheritance chain.
Owners and containing types marked obsolete with a compile-time error are skipped; the generator
does not introduce forbidden type references into consumer compilation.
External custom bases without compiler bindings remain unsupported; Unity and supported Odin root
bases need no dictionary binding. Closed generic bases in referenced assemblies remain unsupported.
The snapshots describe the captured declarations; they do not validate every project dependency
or prove that replacement source compiles. Recompile and bind again after source or dependency
changes. The combined conversion supplies evidence for a future paired preview; it writes no files.
TryPreviewDictionarySources checks the binding and source checksums again before constructing
proposals. The compiler records replacements for the original field type and explicit dictionary
initializer type, rather than searching source text for a name. Aliases and private nested enums
remain valid. Generic base fields retain their original type parameters. Private fields receive
SerializeField, OdinSerialize becomes SerializeField, existing SerializeField attributes are
retained without duplication, and readonly is removed from fields that must become Unity-serialized.
Direct SerializedMonoBehaviour and SerializedScriptableObject bases become Unity's corresponding
base classes. Explicit and target-typed empty or dictionary-copy constructors are supported.
Every field sharing one dictionary declaration must be requested together. The preview refuses
missing metadata, conflicting or overlapping replacements, unsupported factory/capacity initializers,
and fields carrying NonSerialized or SerializeReference. A refusal returns no partial preview.
Original comments, unrelated source, and line endings remain in the proposals.
These proposals do not compile the consumer's replacement sources or inventory all affected assets
and dependent callers. They are inputs to a future paired source/data preview, not an applicable
migration batch. Check the complete retained payload with TryConvertBoundSerializedDictionaries,
then compile and review every affected source and asset before a coordinated migration.
Create one OdinMigrationDictionaryTarget per retained document, then call
TryPreviewDictionaryMigration on the editor thread. The preview reads bounded existing
.asset, .prefab, and .unity files under Assets. It verifies each selected document's original
script, compiler binding, complete Nodes payload, and live references before replacing the payload
with converted Unity Helpers fields in the proposed text. The original script GUID and owner file
ID remain unchanged. Unrelated documents and fields, including unselected opaque Odin payloads,
remain intact. Each inserted fragment uses the original payload's newline style.
For a document without retained Odin data, pass its saved, caller-loaded original owner as the
fifth constructor argument. Generated readers copy accessible original fields directly; the preview
does not load an owner or use reflection to read its members. Generated recovery writers must also
be available. The preview verifies saved owner and script identity, current compiler checksums,
dictionary order, and saved reference identities before adding the converted fields to the proposal.
It refuses dirty or destroyed owners, existing destination fields, null dictionaries or lists,
custom comparers, shared lists across selected fields or owners, and inaccessible fields in
non-partial owners. Private and protected fields can use generated accessors when their owner and
every containing class are non-generic partial classes in the current compilation. These accessors
live inside the owner, can use its private enum types, and also support initialized readonly
dictionaries. No runtime reflection is used. Referenced-assembly fields still need their original
source available for conversion.
Accessible readonly dictionaries are supported when their original field holds an initialized
dictionary with the default comparer. Recovery validates every key and object before restoring the
contents of that dictionary. It preserves the dictionary instance; value lists are rebuilt. The
source proposal removes readonly so Unity can serialize the converted field.
Live references may name saved assets under Assets or ordinary objects in the owner's saved scene;
cross-scene references and prefab instance overrides need further conversion support.
Compiler metadata exposes CanCapture, CanRestore, TryCapture, and TryRestore for original
fields. OdinMigrationLiveDictionary snapshots preserve null dictionaries and lists, shared list
identity, key order, and exact object references. They allow inspection of shapes that coordinated
apply currently refuses. Capture accepts at most 4,096 entries and 65,536 delivered object slots,
including repeated slots in shared lists. Snapshots do not retain later container mutations.
Generated recovery restores explicit null lists and preserves list sharing within each dictionary,
including empty shared lists. Separate lists with equal contents remain separate. Recovery validates
all delivered references before changing an existing dictionary; inconsistent shared inputs fail
without clearing it. These recovery guarantees do not expand the shapes accepted by coordinated
source/data apply, whose Unity inline list fields cannot preserve null or shared lists.
Version 2 apply journals retain captured live values for recovery after reload or restart. The coordinator restores those values into the original fields after restoring source and asset bytes; the original class still controls whether its dictionaries persist through future editor restarts. Recovery keeps the journal and backups. Version 1 journals remain readable and cannot claim live recovery; older package versions refuse version 2 journals.
Multiple selected documents can share an asset, and multiple source selections can share a source file. Source edits are merged and identical edits are deduplicated. Conflicting source snapshots, overlapping edits, duplicate owner IDs or selections, existing destination fields, nested payloads, and incomplete declarations fail with no partial preview. Any asset, source, reference, or cancellation failure discards the whole proposed batch. Target arrays and returned proposal collections are immutable snapshots. Preview preserves the original bytes, encoding, and BOM on disk.
Asset reads are bounded to 16,777,220 delivered bytes, 4,194,304 decoded characters, and 65,536 lines; the selection limit is 4,096 documents. These bounds do not trust counts recorded inside the payload. The result exposes complete original/proposed source and asset text for review. It does not inventory every affected asset or dependent caller, compile replacement sources, or authorize an apply. Those checks and coordinated source/data writes remain required before removing Odin.
Run TryInventoryDictionaryMigration after constructing a paired preview. Editor compiler metadata
records the original bound Unity type, dictionary declaring types, and rewritten Odin base types.
Inherited-only bindings retain the original type. When a closed declaring type cannot appear in
a type literal, generated metadata follows the known original type's base chain instead of dropping
that owner. This reads type identity without inspecting members or instantiating recorded types.
The inventory finds
saved documents whose original script type derives from those types; it uses authored script
references and MonoScript.GetClass, without inspecting fields or loading consumer asset instances.
Unrelated native asset data such as textures is identified through asset type metadata.
The immutable report distinguishes a complete scan from complete selection. Complete means
every candidate path was inspected without failure or cancellation. AllRetainedDocumentsSelected
also requires that every discovered affected Odin payload is included in the paired preview.
Affected documents without retained payloads remain visible. Declaring-type coverage is conservative
and can include additional instances that share an original source owner.
Missing or changed selected assets, missing/unresolved script references, duplicate saved IDs, unreadable or oversized candidate files, stale compiler sources, and cancellation prevent a complete inventory. Failures retain findings for review; they never mark the scan complete. Scan bounds are the same as the paired asset reader. The inventory validates saved asset coverage at scan time; it does not prove dependent source callers compile or authorize an apply. Recheck coverage and live state immediately before a future coordinated migration.
Call TryCompileDictionaryMigration with the paired preview on the editor thread. Keep the returned
handle and poll Completed from an editor callback. Succeeded requires every changed assembly and
its transitive editor callers to compile in dependency order, against the proposed dependency DLLs.
The compiler copies source text into an owned Library directory and never imports or loads candidate
assemblies. Original scripts and saved assets remain unchanged. The tool invokes the C# compiler
bundled with the running Unity Editor in a separate process. It supplies the captured assembly
defines, references, language version, analyzer inputs, and supported compiler options directly.
It disables the compiler's default response file, so project-wide defaults cannot add extra defines
or references. Unsupported compiler layouts or options fail before target writes.
Supported layouts include the versioned bundled .NET SDK used by Unity 6000.6, the earlier
DotNetSdkRoslyn layout, and the legacy Mono compiler. The versioned SDK selection follows
Unity's compiler discovery.
An incomplete bundled SDK fails instead of falling back to a different compiler.
Every candidate uses its captured editor compiler context, including runtime-capable assemblies.
An IL2CPP player setting does not change the editor defines used for this receipt.
The handle exposes assembly names, bounded compiler diagnostics, cancellation, and failure details.
Call Dispose to stop the isolated compiler process and clean its staging files.
TryValidateCurrent rechecks source, reference, analyzer input, and compiler configuration snapshots.
TryValidateSources(sources, out failure) also requires the exact compiled source paths, original
text, and proposed text. A receipt for another preview cannot authorize those replacements.
Added or removed assemblies also invalidate the result. Public Unity compiler option metadata is
read across editor API versions; consumer members are never inspected through reflection.
Compilation currently refuses response files and additional arguments other than warning exclusions. Unknown Unity compiler options also require explicit support. A successful result covers the current editor configuration, not every player target or conditional symbol combination. Domain reload, input drift, compiler errors, and staging cleanup failures prevent success. Coordinated apply and post-import verification remain separate requirements.
The API does not apply a conversion or guarantee that the project can remove Odin. The existing source preview/apply API remains source-only.
| Odin Feature | Unity Helpers Equivalent |
|---|---|
[Button] |
[WButton] |
[ReadOnly] |
[WReadOnly] |
[ShowIf] / [HideIf]
|
[WShowIf] |
[EnumToggleButtons] |
[WEnumToggleButtons] |
[ValueDropdown] |
[WValueDropDown], [IntDropDown], [StringInList]
|
[BoxGroup] |
[WGroup] |
[FoldoutGroup] |
[WGroup(collapsible: true)] |
[InlineEditor] |
[WInLineEditor] |
[Required] |
[WNotNull], [ValidateAssignment]
|
SerializedMonoBehaviour |
Staged data migration to standard MonoBehaviour
|
SerializedDictionary |
Staged data migration to SerializableDictionary<K,V>
|
| N/A (paid feature) | SerializableHashSet<T> |
Odin:
using Sirenix.OdinInspector;
using Sirenix.Serialization;
public class Example : SerializedMonoBehaviour
{
public Dictionary<string, int> scores;
}Unity Helpers:
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;
public class Example : MonoBehaviour
{
[SerializeField]
private SerializableDictionary<string, int> scores = new SerializableDictionary<string, int>();
}Key differences:
- No special base class required (use standard
MonoBehaviour) - Must use
[SerializeField]orpublic - Initialize with
newto avoid null references (good practice, Unity will initialize this like it does List and arrays)
Odin:
using Sirenix.OdinInspector;
using Sirenix.Serialization;
public class Example : SerializedMonoBehaviour
{
public HashSet<string> unlockedItems;
}Unity Helpers:
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;
public class Example : MonoBehaviour
{
[SerializeField]
private SerializableHashSet<string> unlockedItems = new SerializableHashSet<string>();
}Odin:
[Button("Regenerate")]
private void RegenerateLevel() { }
[Button, ButtonGroup("Actions")]
private void Save() { }Unity Helpers:
[WButton("Regenerate")]
private void RegenerateLevel() { }
[WButton(groupName: "Actions")]
private void Save() { }Additional options:
// Control button order within a group
[WButton(drawOrder: 1, groupName: "Debug")]
private void PrintDebugInfo() { }
// Control group placement (top or bottom of inspector)
[WButton(groupName: "Authoring", groupPlacement: WButtonGroupPlacement.Top)]
private void GenerateIds() { }Odin Inspector Support:
WButton works with Odin's SerializedMonoBehaviour and SerializedScriptableObject without additional setup. Use [WButton] on your methods.
Manual integration may be needed if:
- You create a custom
OdinEditorfor a specific type - See Inspector Buttons - Custom Editors for details
Odin:
public bool showAdvanced;
[ShowIf("showAdvanced")]
public float advancedSetting;Unity Helpers:
public bool showAdvanced;
[WShowIf(nameof(showAdvanced))]
public float advancedSetting;Odin:
[HideIf("isDisabled")]
public float value;Unity Helpers:
[WShowIf(nameof(isDisabled), inverse: true)]
public float value;Odin:
public AttackType attackType;
[ShowIf("attackType", AttackType.Ranged)]
public float range;Unity Helpers:
public AttackType attackType;
[WShowIf(nameof(attackType), AttackType.Ranged)]
public float range;Odin:
[ShowIf("@level >= 5")]
public Ability ultimateAbility;Unity Helpers:
[WShowIf(nameof(level), WShowIfComparison.GreaterThanOrEqual, 5)]
public Ability ultimateAbility;Available comparisons: Equal, NotEqual, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, IsNull, IsNotNull, IsNullOrEmpty, IsNotNullOrEmpty
Odin:
[EnumToggleButtons]
public Direction direction;
[EnumToggleButtons]
public MovementFlags flags; // [Flags] enumUnity Helpers:
[System.Flags]
public enum MovementFlags { None = 0, Walk = 1, Run = 2, Jump = 4 }
[WEnumToggleButtons]
public Direction direction;
[WEnumToggleButtons(showSelectAll: true, showSelectNone: true)]
public MovementFlags flags; // [Flags] enumControl buttons per row:
[System.Flags]
public enum DamageType { None = 0, Physical = 1, Fire = 2, Ice = 4 }
[WEnumToggleButtons(buttonsPerRow: 4)]
public DamageType damageTypes;Odin:
[ValueDropdown("GetFrameRates")]
public int targetFrameRate;
private int[] GetFrameRates() => new[] { 30, 60, 120 };Unity Helpers:
// Inline values (simplest)
[IntDropDown(30, 60, 120)]
public int targetFrameRate;
// Or with provider method
[IntDropDown(nameof(GetFrameRates))]
public int targetFrameRate;
private IEnumerable<int> GetFrameRates() => new[] { 30, 60, 120 };Odin:
[ValueDropdown("GetDifficulties")]
public string difficulty;Unity Helpers:
// Inline values
[StringInList("Easy", "Normal", "Hard")]
public string difficulty;
// Or with provider
[StringInList(nameof(GetDifficulties))]
public string difficulty;Unity Helpers:
// Static provider from another class
[WValueDropDown(typeof(AudioManager), nameof(AudioManager.GetSoundNames))]
public string soundEffect;
// Instance provider (method on same class)
[WValueDropDown(nameof(GetAvailableWeapons), typeof(WeaponData))]
public WeaponData selectedWeapon;Odin:
[BoxGroup("Movement")]
public float speed;
[BoxGroup("Movement")]
public float jumpHeight;
[FoldoutGroup("Advanced")]
public float acceleration;Unity Helpers:
// Auto-include next N fields
[WGroup("Movement", autoIncludeCount: 2)]
public float speed; // Field 1: in group
public float jumpHeight; // Field 2: in group (auto-included, last by count)
// Or explicit end marker
[WGroup("Movement")]
public float speed; // In group
public float jumpHeight; // In group (auto-included)
[WGroupEnd] // friction IS included, then group closes
public float friction; // In group (last field)
// Collapsible (foldout)
[WGroup("Advanced", collapsible: true, startCollapsed: true)]
public float acceleration;Unity Helpers:
[WGroup("Character", displayName: "Character Settings")]
public string characterName;
[WGroup("Stats", parentGroup: "Character")]
public int health;
public int mana;Odin:
[InlineEditor]
public EnemyConfig config;
[InlineEditor(InlineEditorModes.GUIOnly)]
public ItemData item;Unity Helpers:
public sealed class EnemyConfig : ScriptableObject { }
public sealed class ItemData : ScriptableObject { }
[WInLineEditor]
public EnemyConfig config;
[WInLineEditor(WInLineEditorMode.FoldoutExpanded, inspectorHeight: 200f)]
public ItemData item;Available modes: AlwaysExpanded, FoldoutExpanded, FoldoutCollapsed
Odin:
[Required]
public GameObject prefab;
[Required("Player reference is required!")]
public Transform player;Unity Helpers:
[WNotNull]
public GameObject prefab;
[WNotNull(WNotNullMessageType.Error, "Player reference is required!")]
public Transform player;
// Runtime validation in Awake/Start
private void Awake()
{
this.CheckForNulls(); // Extension method
}For validating that collections aren't empty:
public sealed class EnemyData : ScriptableObject { }
[ValidateAssignment]
public List<Transform> spawnPoints; // Warns if null or empty
[ValidateAssignment(ValidateAssignmentMessageType.Error, "Need at least one enemy type")]
public List<EnemyData> enemyTypes;
// Runtime check
private void Start()
{
this.ValidateAssignments();
}Odin:
[ReadOnly]
public string generatedId;Unity Helpers:
[WReadOnly]
public string generatedId;Before (Odin):
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;
using System.Collections.Generic;
public class EnemySpawner : SerializedMonoBehaviour
{
[BoxGroup("Settings")]
[Required]
public GameObject enemyPrefab;
[BoxGroup("Settings")]
[ShowIf("useWaves")]
public int wavesCount = 3;
public bool useWaves;
[EnumToggleButtons]
public SpawnPattern pattern;
[ValueDropdown("GetSpawnRates")]
public float spawnRate;
public Dictionary<string, int> enemyWeights;
[Button("Spawn Wave")]
private void SpawnWave() { }
private float[] GetSpawnRates() => new[] { 0.5f, 1f, 2f };
}After (Unity Helpers):
using UnityEngine;
using System.Collections.Generic;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;
public enum SpawnPattern { Burst, Trickle, Ring }
public class EnemySpawner : MonoBehaviour
{
[WGroup("Settings", autoIncludeCount: 2)]
[WNotNull]
[SerializeField]
private GameObject enemyPrefab;
[WShowIf(nameof(useWaves))]
[SerializeField]
private int wavesCount = 3;
[SerializeField]
private bool useWaves;
[WEnumToggleButtons]
[SerializeField]
private SpawnPattern pattern;
[WValueDropDown(0.5f, 1f, 2f)]
[SerializeField]
private float spawnRate;
[SerializeField]
private SerializableDictionary<string, int> enemyWeights =
new SerializableDictionary<string, int>();
[WButton("Spawn Wave")]
private void SpawnWave() { }
}// Attributes
using WallstopStudios.UnityHelpers.Core.Attributes;
// Serializable collections
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;-
No special base class - Use standard
MonoBehaviour/ScriptableObject -
Use
nameof()- Unity Helpers usesnameof()for condition fields (type-safe) -
Initialize collections - Initialize
new SerializableDictionary<K,V>()etc. to avoid null references -
[HideIf] becomes inverse - Use
[WShowIf(..., inverse: true)]instead of[HideIf] -
Numeric conditions - Use
WShowIfComparisonenum instead of expression strings -
Groups auto-include -
[WGroup]can auto-include subsequent fields withautoIncludeCount
- Inspector Overview - Complete inspector features guide
- Serialization Types - All serializable types
- Inspector Buttons - WButton detailed guide
- Inspector Conditional Display - WShowIf detailed guide
- Inspector Selection Attributes - Dropdowns and toggles