WARNING: This is an extremely advanced subject, and is not recommended for users unfamiliar with unsafe code or C++.
All classes that inherit from UnityEngine.Object are actually wrappers for a native object on the C++ side of the engine. The address of the native object is stored in the class, as seen in the layout below:
class Object { // Cached pointer to the native object IntPtr m_CachedPtr; // Cached instance ID from native object int m_InstanceID; // True purpose unknown string m_UnityRuntimeErrorString; }
The cached pointer can be accessed through reflection in two ways:
Once you have retrieved the pointer, you can access the native object by defining the appropriate structs and using unsafe code.
The C++ class hierarchy is as follows: NonCopyable → Object → EditorExtension → NamedObject
You can define this from C# using the following set of types (for the sake of brevity, these are simplified definitions):
public struct NonCopyable { public IntPtr MethodTable; } public struct AllocationRootWithSalt { public uint m_Salt; public uint m_RootReferenceIndex; } public struct MemLabelId { #if UNITY_EDITOR || DEVELOPMENT_BUILD public AllocationRootWithSalt m_RootReferenceWithSalt; #endif public int identifier; } public struct ScriptingGCHandle { public IntPtr m_Handle; public int m_Weakness; public IntPtr m_Object; } // Note that this struct layout differs between Unity versions. // This is correct for Unity 2020, but hasn't been tested with // other engine versions. // // The type has been renamed to NativeObject in order to avoid // conflicts with UnityEngine.Object. // public struct NativeObject { public NonCopyable Base; public int m_InstanceID; // This is represented on the C++ side as several bit-fields. public int m_BitFields; public IntPtr m_EventIndex; #if UNITY_EDITOR || DEVELOPMENT_BUILD public MemLabelId m_FullMemoryLabel; #endif public ScriptingGCHandle m_MonoReference; #if UNITY_EDITOR public uint m_DirtyIndex; public ulong m_FileIDHint; public bool m_IsPreviewSceneObject; #endif #if UNITY_EDITOR || DEVELOPMENT_BUILD public int m_ObjectProfilerListIndex; #endif } public struct PPtr<T> where T : unmanaged { public int m_InstanceID; } // Renamed to NativePrefab to avoid name clashes public struct NativePrefab { public NativeObject Base; // ... } // Renamed to NativePrefabInstance to avoid name clashes public struct NativePrefabInstance { public NativeObject Base; // ... } public struct EditorExtension { public NativeObject Base; #if UNITY_EDITOR public PPtr<EditorExtension> m_CorrespondingSourceObject; public PPtr<EditorExtension> m_DeprecatedExtensionPtr; public PPtr<NativePrefab> m_PrefabAsset; public PPtr<NativePrefabInstance> m_PrefabInstance; public bool m_IsClonedFromPrefabObject; #endif } public struct ConstantString { public unsafe byte* m_Buffer; } public struct NamedObject { public EditorExtension Base; public ConstantString m_Name; }
All of the above information was obtained from inspecting the .pdb files that are included with Unity installations. Some versions of Unity contain more information in these files than others.
Once you have access to native objects, you can perform many different actions that are usually only possible in the editor via SerializedObject and SerializedProperty. For example, changing the m_LineSpacing value on a Font object:
// Since these structures are accessed through pointers, // we don't need to define the entire structure in order // to use it. Fonts have several more fields in them that // are not particularly useful to access in this way. public struct NativeFont { public NamedObject Base; public float m_LineSpacing; public int m_FontSize; } // Make a new Font and access its native object. Font f = new Font(); var fp = (NativeFont*)f.GetCachedPtr(); // Now we can simply set m_LineSpacing on it fp->m_LineSpacing = 12f; // This should print 12 Debug.Log(f.lineHeight);