====== Object Layout ======
**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:
* Accessing the m_CachedPtr field
* Calling the 'IntPtr GetCachedPtr()' method
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 =====
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
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 m_CorrespondingSourceObject;
public PPtr m_DeprecatedExtensionPtr;
public PPtr m_PrefabAsset;
public PPtr 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.
===== Advanced Use Cases =====
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);