How to Develop a Game
Script Layer to Native Layer
PublishDate: 2025-06-01 | CreateDate: 2025-06-01 | LastModify: 2025-06-01 | Creator:ljf12825

The Bridge between Native Layer and Script Layer

Unity引擎运行时,本质上是一个C++引擎内核 + C#脚本层的结构

所写的MonoBehaviour只是在C#中的一个代理对象,真正控制游戏运行的逻辑、渲染、物理等是C++层在执行

所以从UnityEngine.Object开始,Unity构建了一个“双向映射体系”

C++对象(native) <--- instance ID --- UnityEngine.Object(C#托管对象)
        ↑                                       ↑
    内存资源                                   脚本代理

从Object到MonoBehaviour的完整继承链

System.Object (纯托管)
└── UnityEngine.Object (托管对象,桥梁类)
    ├── GameObject(托管对象)
    └── Component
        ├── Transform / Renderer / Collider...(托管对象)
        └── MonoBehaviour (托管行为对象,支持生命周期方法)

它们都不是普通的C#对象,它们都与C++侧的“实体”挂钩,甚至生命周期也是引擎控制的

native layer 与 script layer的绑定方式

Unity会通过一套机制将C++层对象暴露给C#层,这其中最关键的桥梁是:instance ID + GCHandle + m_CachedPtr

名称作用
m_CachedPtrUnityEngine.Object中保留的指针,指向C++对象的地址(Unsafe)
GCHandleUnity用于保持托管对象不被GC收走,native端持有
Instance ID每个 C++ native 对象的唯一标识,Unity使用它查找C#代理对象
ScriptingObjectC++对象的基类,用于和C#对象绑定(runtime下存在)
MonoObject*指向 C# 对象的原生指针(Mono环境时)

流程图:

C++对象 (ScriptingObject)
   ↕ instance ID
C#对象(UnityEngine.Object) ← GCHandle ← C++
      m_CachedPtr → C++对象

instance ID

Unity通过使用instance ID统一管理对象

每个在C++层的Unity对象都有一个唯一的标识符,即instance ID,它用于区分不同的C++对象。这个标识符的作用类似于内存中的指针

可以把instance ID想象成一个类似于数据库中的“主键”,它指向C++层中的实际属性。在C#层,Unity通过m_CachedPtr或类似机制与C++对象建立联系。C#调用一个方法或访问一个属性时,实际上就是通过这个instance ID去C++层查找并操作相应对象的

C++层对象的生命周期管理

在Unity中,C++层的对象生命周期是由引擎控制的,而不是像普通的C#对象那样由GC自动回收。也就是说,C++对象在被销毁时,并不会立即被C#垃圾处理器回收,而是由Unity引擎自己管理

关键点:

C#和C++的指针交互

在C#和C++之间,m_CachedPtr是Unity使用的一个关键字段,它保存了C++对象的指针。这个指针并不会直接暴露,而是通过UnityEngine.Object的方法间接访问

例如,当使用Instantiate()克隆一个对象时,C#层会创建一个新的对象,并将其m_CachedPtr指向一个新的C++对象。这种机制确保了C#和C++层可以同步管理对象的创建、销毁和引用

为什么不直接使用C++指针

内存和资源管理:Native与Managed内存

Unity对内存的管理通常分为两类:托管内存(Managed Memory)和原生内存(Native Memory)

托管内存:

原生内存:

资源的加载与卸载的底层机制

Unity的资源管理在C++层也有对应的资源对象,它们通过资源路径和资源管理系统来加载和卸载

当使用Resource.Load()Addressables加载资源时,Unity会在C++层将资源加载到内存中,并返回一个C#层的代理对象。这些资源的引用计数会由C++层管理,当没有对象再引用这些资源时,C++层会负责销毁这些内存并释放内存

性能和优化

  1. 频繁的资源加载和卸载:如果你在每帧都调用 Resources.Load() 或频繁销毁对象,可能会导致性能瓶颈。推荐使用 Addressables 或 Object Pooling 技术来优化资源管理。

  2. 避免大量无效对象:例如,创建大量的 GameObject、MonoBehaviour,然后频繁销毁。这样不仅会增加垃圾回收的负担,还会在 C++ 层产生频繁的对象创建和销毁开销。可以使用对象池来减少这种开销。

  3. 内存泄漏问题:如果对象在 C++ 层没有正确销毁,可能导致内存泄漏。特别是 MonoBehaviour 等绑定对象,它们的销毁需要确保在 C# 层正确解除引用,否则即使对象在 C++ 层销毁,C# 层的引用仍会阻止 GC 回收。

对象创建过程

以创建一个GameObject为例:

GameObject go = new GameObject("Hero");

在背后发生了:

  1. C#调用UnityEngine的构造方法
  2. Unity C#层调用了内部绑定的native构造函数(通过[NativeMethod]extern实现)
  3. C++中创建了一个GameObject对象,并注册instance ID
  4. Unity C++层为这个对象创建一个C#代理,分配内存,绑定m_CachedPtr
  5. 如果启用脚本(MonoBehaviour),则Unity会通过反射或运行时代码绑定,自动挂载脚本(生成MonoObject,绑定)

MonoBehaviour的生命周期的控制

生命周期函数是Unity引擎每帧自动调度的:

MonoBehaviour是怎么挂载的

gameObject.AddComponent<MyScript>();

内部流程:

  1. C#调用泛型方法AddComponent<T>()
  2. UnityC#层调用底层AddComponent(Type t)(native bridge)
  3. 引擎C++层创建一个MonoBehaviour实例(C++对象)
  4. 引擎创建对应的C#代理对象,并调用构造函数
  5. 将代理对象挂到该GameObject下,并添加到调度列表中
  6. 引擎在适当时机调用Awake() -> Start() -> Update()

所以不能用new MyScript()创建MonoBehaviour,它不是纯托管类,是托管↔native绑定类