这份源码是什么
Unity 的核心引擎大部分由 C++ 实现,但开发者日常编写的几乎都是 C#。UnityCsReference 公开的是 Unity 引擎和编辑器的 C# 源码,重点是“C# 侧代码”,而不是完整的 Unity 引擎。
它包含:
- C# 公开 API、类型声明和包装对象。
- 连接 C++ 的绑定声明。
- 一部分纯 C# 托管实现,例如协程推进、Awaitable 状态机、Job 扩展方法。
- 编辑器相关 API,例如
AssetDatabase、SerializedObject、AssetImporter。 NativeArray、AtomicSafetyHandle、Job、Subsystems、UIElements 等模块。
它不包含:
- 完整 C++ 渲染、物理求解器、动画采样器、音频混音器等原生实现。
- PhysX、Burst 编译器后端、图形驱动等第三方或底层代码。
因此,阅读这份仓库最有价值的收获是:理解 Unity 如何组织 C# 与 C++ 之间的边界,而不是期待看到所有算法本体。
仓库顶层结构
仓库并不只有 UnityEngine 一个程序集,而是一个大仓库的 C# 部分。阅读时可以按下面地图定位:
| 路径 | 主要作用 |
|---|---|
Runtime/Export/Scripting | Object、GameObject、Component、Behaviour、MonoBehaviour、协程、Awaitable |
Runtime/Transform/ScriptBindings | Transform、父子层级、位置旋转缩放 |
Runtime/Export/SceneManager | Scene、SceneManager、场景加载 |
Runtime/Export/NativeArray | NativeArray、原生内存管理 |
Runtime/Jobs/Managed | IJob、IJobParallelFor 等 Job 接口 |
Runtime/Export/Jobs | AtomicSafetyHandle、安全读写检查 |
Runtime/Export/Math | Vector3、Quaternion、Matrix4x4 等数学类型 |
Modules/AssetBundle | AssetBundle、AssetBundleRequest 等运行时 API |
Modules/Subsystems | 子系统与 Provider 架构 |
Modules/Physics/ScriptBindings | 物理场景、射线查询、批量命令 |
Runtime/Export/Graphics | Graphics 渲染命令入口 |
Modules/Animation/ScriptBindings | 旧版 Animation、AnimationClip |
Modules/InputLegacy | 旧版 Input 输入查询 |
Modules/Mathematics | float3、quaternion、matrix 等高性能数学类型 |
Modules/UIElements | UIElements/UI Toolkit 的 VisualElement 体系 |
Editor/Mono | AssetDatabase、SerializedObject、AssetImporter 等编辑器 API |
阅读任何一段 Unity C# 源码,先问四个问题:
- 这段代码是在 C# 托管侧运行,还是会通过绑定进入 C++?
- 它操作的是 C# 包装对象、C++ 原生实体、句柄,还是原生内存?
- 它运行在 Unity 主线程、协程、线程池,还是 Job 工作线程?
- 它是在组织参数、做类型转换和默认值,还是在执行真正的引擎计算?
C# API 与 C++ 原生层的绑定
Unity 的典型调用链是:
游戏脚本
-> C# 公开 API
-> C# 包装逻辑(检查参数、默认值、泛型转换)
-> 绑定桥接(extern + FreeFunction/NativeMethod)
-> 参数封送
-> C++ 原生实现
公开 API 通常负责“稳定的调用契约”。底层真正的工作由 C++ 完成。C# 侧并不重写所有逻辑,而是让开发者能安全、方便地调用原生能力。
extern、FreeFunction 和 NativeMethod
以 GameObject.bindings.cs 中的 AddComponent 为例:
[FreeFunction(Name = "MonoAddComponentWithType", HasExplicitThis = true)]
private extern Component Internal_AddComponentWithType(Type componentType);
public Component AddComponent(Type componentType)
{
return Internal_AddComponentWithType(componentType);
}
public T AddComponent<T>() where T : Component
{
return AddComponent(typeof(T)) as T;
}
这里有三个关键概念:
extern:C# 侧没有方法体,真正实现由绑定系统映射到 C++。FreeFunction:说明对应的 C++ 函数是自由函数,不是某个 C++ 类的成员方法。NativeMethod:描述原生方法名、异常行为、线程安全等绑定细节。
where T : Component 是泛型约束,错误类型会在编译期被拒绝。AddComponent(Type) 则保留运行时动态调用能力。两种入口最终复用同一个底层实现。
短重载和默认参数
UnityEngineObject.bindings.cs 中 Destroy 的实现很典型:
[NativeMethod(Name = "Scripting::DestroyObjectFromScripting",
IsFreeFunction = true, ThrowsException = true)]
public extern static void Destroy(Object obj, float t);
public static void Destroy(Object obj)
{
float t = 0.0F;
Destroy(obj, t);
}
Destroy(obj) 是开发者常用的短重载,内部只是把默认延时设置为 0.0F,然后继续调用同一个 extern 方法。这样常用调用更简单,同时不会产生多套底层实现。
不要靠方法名判断是否进入 C++
Internal_AddComponentWithType 名字里有 Internal,但它同时也是 extern,因此会进入 C++。
判断是否跨入原生层的标准是:
- 方法体前是否有
extern。 - 方法或参数上是否有
FreeFunction、NativeMethod、StaticAccessor、UnityMarshalAs等绑定特性。
Internal_ 只是 Unity 内部命名习惯,本身不决定执行位置。
绑定系统与参数封送
绑定系统负责建立 C# 方法和 C++ 函数的对应关系。调用时,它还要把 C# 参数转换为 C++ 能理解的数据,并把返回值或异常转回 C#。
不同数据在绑定层的常见处理方式如下:
| C# 数据 | 常见处理方式 |
|---|---|
int、float、bool、enum | 直接或按底层数值传递 |
Vector3、Quaternion | 按固定字段布局复制 |
UnityEngine.Object | 传 IntPtr、EntityId 或句柄 |
string | 转换为原生字符串 |
| 数组、集合 | 传缓冲区、指针和长度 |
NativeArray | 传原生缓冲区、长度和安全句柄 |
| 委托 | 建立原生可调用的回调入口 |
Vector3 的真实声明
Vector3.cs的关键声明如下:
[NativeHeader("Runtime/Math/Vector3.h")]
[NativeClass("Vector3f")]
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public partial struct Vector3 : IEquatable<Vector3>, IFormattable
{
public float x;
public float y;
public float z;
}
[StructLayout(LayoutKind.Sequential)] 保证字段按声明顺序连续排列,这样 C# 内存布局才能和 C++ 的 Vector3f 对应。
异常如何跨层
C# 能判断的参数错误,通常先在托管侧检查。例如 MonoBehaviour.StartCoroutine 会检查字符串和迭代器是否为 null。
引擎状态错误由 C++ 产生,再由绑定层转换成 C# 异常,常见类型包括:
ArgumentExceptionInvalidOperationExceptionMissingReferenceExceptionNullReferenceExceptionObjectDisposedException
UnityEngine.Object 的双层对象模型
继承 UnityEngine.Object 的对象,通常可以理解为两层:
C# 包装对象:提供 API、托管引用和对象身份
C++ 原生实体:保存引擎状态并执行底层计算
UnityEngineObject.bindings.cs中:
public partial class Object
{
IntPtr m_CachedPtr;
private EntityId m_EntityId;
private string m_UnityRuntimeErrorString;
}
m_CachedPtr 是缓存的原生指针,m_EntityId 是对象身份 ID。传给 C++ 的不是完整 C# 对象副本,而是能够定位原生实体的指针、ID 或句柄。
EntityId 不只是普通 int
同一文件中的 EntityId 是一个 64 位结构:
[StructLayout(LayoutKind.Sequential, Size = 8)]
public struct EntityId : IEquatable<EntityId>, IComparable<EntityId>, IFormattable
{
[SerializeField]
ulong m_rawData;
// 位布局与原生 EntityId 一致:
// [Version:24 | TypeId:12 | Index:28]
internal uint Index => (uint)(m_rawData & 0x0FFFFFFFUL);
internal uint Version => (uint)((m_rawData >> 40) & 0xFFFFFFUL);
}
Index 用于定位实体,Version 用于处理对象复用和生命周期判断。这就是为什么比较两个 Unity 对象不是只比较 C# 引用。
Destroy 后的假 null
Destroy 通常先销毁 C++ 原生实体。如果其他代码仍引用 C# 包装对象,它会暂时留在托管堆,最终由 GC 回收。
Unity 判断 obj == null 时,不是只看 C# 引用是否为 null:
public static implicit operator bool(Object exists)
{
return !CompareBaseObjects(exists, null);
}
static bool CompareBaseObjects(UnityEngine.Object lhs, UnityEngine.Object rhs)
{
bool lhsNull = ((object)lhs) == null;
bool rhsNull = ((object)rhs) == null;
if (rhsNull && lhsNull) return true;
if (rhsNull) return !IsNativeObjectAlive(lhs);
if (lhsNull) return !IsNativeObjectAlive(rhs);
return lhs.m_EntityId == rhs.m_EntityId;
}
所以:
GameObject go = CreateObject();
Destroy(go);
// 包装对象引用可能不是 C# null,但原生实体已经死亡。
if (go == null)
{
// Unity 的 == null 对 destroyed object 返回 true。
}
这就是 Unity 对象“假 null”的由来。
GameObject、Component、Transform、Behaviour、MonoBehaviour
Unity 的基础对象模型是浅层继承加组件组合:
GameObject:容器
Component:能力单元
Transform:位置、旋转、缩放和父子层级
Behaviour:带 enabled 开关的组件
MonoBehaviour:用户脚本基类
真实继承关系可以从源码确认:
public sealed partial class GameObject : Object { }
public partial class Component : UnityEngine.Object { }
public class Behaviour : Component
{
[NativeProperty]
extern public bool enabled { get; set; }
[NativeProperty]
extern public bool isActiveAndEnabled
{
[NativeMethod("IsAddedToManager")]
get;
}
}
public class MonoBehaviour : Behaviour { }
GameObject 本身不保存位置、渲染、碰撞等能力,而是挂载不同的 Component。Transform 是每个 GameObject 必须有的组件,负责位置、旋转、缩放和层级。
GameObject 是容器,Component 是能力
GameObject.bindings.cs中可以看到:
[FreeFunction("GameObjectBindings::CreatePrimitive")]
public extern static GameObject CreatePrimitive(PrimitiveType type);
[FreeFunction(Name = "GameObjectBindings::GetComponentFromType",
HasExplicitThis = true, ThrowsException = true)]
public extern Component GetComponent(Type type);
创建物体、查找组件、获取 Transform 等操作,最终都交给原生 GameObject 或 Component 数据。C# 侧只是封装调用。
Transform 是几何与层级入口
Transform.bindings.cs中:
public extern Vector3 position { get; set; }
public extern Vector3 localPosition { get; set; }
public extern Quaternion rotation { get; set; }
public extern Quaternion localRotation { get; set; }
public extern Vector3 localScale { get; set; }
public Transform parent
{
get { return parentInternal; }
set { parentInternal = value; }
}
position、rotation、localScale 等字段虽然看起来像普通属性,但它们是 extern,所以读写时实际访问的是 C++ 中 Transform 的状态。
生命周期、激活状态与 PlayerLoop
激活状态
GameObject.bindings.cs中:
[NativeMethod(Name = "SetSelfActive")]
public extern void SetActive(bool value);
public extern bool activeSelf
{
[NativeMethod(Name = "IsSelfActive")]
get;
}
public extern bool activeInHierarchy
{
[NativeMethod(Name = "IsActive")]
get;
}
activeSelf:只看对象自身是否激活。activeInHierarchy:同时考虑自身和所有父级。enabled:Behaviour组件自己的开关。
脚本通常需要:
GameObject.activeInHierarchy == true
且
脚本.enabled == true
才会正常参与逐帧更新。
Awake、OnEnable、Start、Update
常见顺序是:
Awake -> OnEnable -> Start -> Update
更完整的理解是:
| 回调 | 触发时机 |
|---|---|
Awake | 脚本实例被创建时执行一次 |
OnEnable | 组件变为激活时执行,可能多次 |
Start | 首次激活后的下一帧开始前执行一次 |
FixedUpdate | 固定时间步执行,常用于物理相关逻辑 |
Update | 每一帧执行 |
LateUpdate | 所有 Update 之后执行 |
OnDisable | 组件失活时执行 |
OnDestroy | 对象销毁前执行 |
这些生命周期回调并不一定由 C# 源码中一个叫 Update() 的方法直接定义,而是由原生 PlayerLoop 通过脚本系统回调到托管方法。
MonoBehaviour 本身也是组件
MonoBehaviour.bindings.cs 中:
public class MonoBehaviour : Behaviour
{
public MonoBehaviour()
{
ConstructorCheck(this);
}
}
ConstructorCheck 是 extern,由原生侧检查脚本是否被正确构造。用户脚本继承 MonoBehaviour,本质上是给 GameObject 添加一个可执行用户逻辑的 Component。
Scene 与 SceneManager
Scene 不是场景文件的完整副本
Scene.bindings.cs中的 Scene 是一个 C# 结构体:
[NativeHeader("Runtime/Export/SceneManager/Scene.bindings.h")]
[StructLayout(LayoutKind.Sequential)]
public partial struct Scene
{
[StaticAccessor("SceneBindings", StaticAccessorType.DoubleColon)]
extern private static bool IsValidInternal(SceneHandle sceneHandle);
[StaticAccessor("SceneBindings", StaticAccessorType.DoubleColon)]
extern private static string GetPathInternal(SceneHandle sceneHandle);
}
它内部通过 SceneHandle 定位原生场景:
public struct SceneHandle : IEquatable<SceneHandle>, IFormattable
{
internal EntityId m_Value;
}
所以 C# 的 Scene 更像轻量句柄。真正包含 GameObject 层级和运行时状态的,是原生引擎中的场景结构。
LoadSceneMode 与参数
SceneManager.cs中:
public enum LoadSceneMode
{
Single = 0,
Additive
}
public struct LoadSceneParameters
{
[SerializeField]
private LoadSceneMode m_LoadSceneMode;
[SerializeField]
private LocalPhysicsMode m_LocalPhysicsMode;
}
Single:替换当前激活场景,通常旧的场景对象被卸载。Additive:保留现有场景,追加新场景。
同步与异步接口都会汇聚到内部绑定方法:
public static Scene LoadScene(string sceneName, LoadSceneParameters parameters)
{
LoadSceneAsyncNameIndexInternal(sceneName, -1, parameters, true);
return GetSceneAt(sceneCount - 1);
}
public static AsyncOperation LoadSceneAsync(int sceneBuildIndex, LoadSceneParameters parameters)
{
return LoadSceneAsyncNameIndexInternal(null, sceneBuildIndex, parameters, false);
}
原生事件回传到 C#
场景加载完成后,原生侧会回调 C#:
[RequiredByNativeCode]
private static void Internal_SceneLoaded(Scene scene, LoadSceneMode mode)
{
sceneLoaded?.Invoke(scene, mode);
}
这是“C++ 主动调用 C#”的典型例子:C# 暴露 RequiredByNativeCode 方法,原生场景管理器在合适时机调用它,再由它触发 C# 事件。
ScriptableObject 与数据驱动
ScriptableObject.bindings.cs 中:
public class ScriptableObject : Object
{
public ScriptableObject()
{
CreateScriptableObject(this);
}
public static T CreateInstance<T>() where T : ScriptableObject
{
return (T)CreateInstance(typeof(T));
}
}
ScriptableObject 不依附 GameObject,适合保存配置、角色定义、技能数值和物品数据。多个对象可以引用同一份资产。
.asset 不是 .so
- Unity 资产文件后缀通常是
.asset。 - Linux 原生共享库后缀是
.so。 ScriptableObject的缩写 SO 和 Linux 的.so不是一回事。
ScriptableObject 为什么适合数据驱动
它把“数据定义”和“行为逻辑”分离:
[CreateAssetMenu(menuName = "Game/EnemyConfig")]
public class EnemyConfig : ScriptableObject
{
public int attack;
public float moveSpeed;
}
多个敌人组件可以引用同一个 EnemyConfig.asset。修改一份配置,所有引用者读取到新的数值。这是数据复用和 Editor 可视化配置的组合。
partial、Obsolete、Attribute 与反射
partial 在编译期合并
partial 表示一个类型可以由多个文件共同定义。C# 编译器会把它们合并成一个类型。
Unity 常用它分离:
- 绑定声明。
- 正常 API。
- 废弃 API。
- 编辑器条件代码。
Obsolete 负责隔离旧 API
partial 本身不表示“废弃”。真正标记废弃的是 Obsolete:
[Obsolete("Use NewMethod instead", false)]
public void OldMethod() { }
第二个参数:
false:调用时产生编译警告。true:调用时产生编译错误。
把旧 API 放到单独的 partial 文件中并标记 Obsolete,可以避免旧项目突然编译失败,同时引导新代码使用新 API。
Field、Property 和 Attribute
Field:类中的直接数据成员。Property:带get、set逻辑的访问入口。Attribute:附加在类、方法、字段等代码元素上的元数据。
Attributes.cs中有很多真实例子:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class RequireComponent : Attribute
{
public Type m_Type0;
public Type m_Type1;
public Type m_Type2;
public RequireComponent(Type requiredComponent)
{
m_Type0 = requiredComponent;
}
}
Attribute 本身不做事
RequireComponent 只是元数据。真正处理它的是 AttributeHelperEngine.cs:
static Type[] GetRequiredComponents(Type klass)
{
List<Type> required = null;
while (klass != null && klass != typeof(MonoBehaviour))
{
RequireComponent[] attrs =
(RequireComponent[])klass.GetCustomAttributes(typeof(RequireComponent), false);
foreach (var attri in attrs)
{
if (required == null)
required = new List<Type>();
if (attri.m_Type0 != null) required.Add(attri.m_Type0);
if (attri.m_Type1 != null) required.Add(attri.m_Type1);
if (attri.m_Type2 != null) required.Add(attri.m_Type2);
}
klass = klass.BaseType;
}
return required?.ToArray();
}
这说明 Attribute 需要 Unity 的 C# 或 C++ 代码通过反射读取后才产生行为。
反射读取的是元数据
C# 编译器把源码编译成 IL 时,也会生成程序集元数据。元数据记录类型、方法、字段、继承关系和 Attribute。
C# 源码 -> 编译器 -> IL + 元数据 -> CLR/Unity -> Reflection API
反射通过 Type、MethodInfo 等 API 查询这些元数据表,不会重新解析 .cs 文件。Type代表一个完整类型的元数据,MemberInfo是所有类型成员的抽象基类,FieldInfo、MethodInfo、PropertyInfo、ConstructorInfo、EventInfo都继承自MemberInfo;其中FieldInfo对应类的字段,可读写字段值,MethodInfo对应方法,支持动态调用,PropertyInfo封装C#属性,底层包装getter与setter两个方法,ConstructorInfo代表构造函数,用于动态创建类型实例,EventInfo对应事件,可操作事件的订阅与解绑,这些Info类都是托管层对程序集元数据的包装,供反射在运行时查询信息、动态操作类型成员。
序列化:SerializeField、SerializeReference 与 Editor
Serialization.cs中:
[AttributeUsage(AttributeTargets.Field)]
public sealed partial class SerializeField : Attribute
{
}
[AttributeUsage(AttributeTargets.Field)]
public sealed partial class SerializeReference : Attribute
{
}
public interface ISerializationCallbackReceiver
{
void OnBeforeSerialize();
void OnAfterDeserialize();
}
区别在于:
| 机制 | 作用 |
|---|---|
SerializeField | 让私有字段显示在 Inspector 中并被 Unity 序列化 |
SerializeReference | 以引用方式序列化,可支持多态、继承和空引用 |
PreferBinarySerialization | 某些资产优先使用二进制序列化 |
ISerializationCallbackReceiver | 在序列化前、反序列化后执行自定义逻辑 |
[SerializeField]仅开启字段序列化,序列化行为由字段类型决定;普通[Serializable]class 默认值拷贝,引用关系在序列化重建后丢失。[SerializeReference]用于普通 C# 托管 class,开启引用序列化,序列化后保留同一个实例的多处引用。
SerializedObject 不是直接改原对象
SerializedObject.bindings.cs中:
public class SerializedObject : IDisposable
{
internal IntPtr m_NativeObjectPtr;
public SerializedObject(Object obj)
{
m_NativeObjectPtr = InternalCreate(new Object[] { obj }, null);
}
extern public bool ApplyModifiedProperties();
extern public void Update();
}
基本流程是:
new SerializedObject(target)
-> FindProperty(...)
-> 修改 SerializedProperty
-> serializedObject.ApplyModifiedProperties()
SerializedObject 是目标对象的一份编辑器可访问表示。Update() 同步最新状态,ApplyModifiedProperties() 把修改写回真实对象。
IEnumerator、foreach 与协程
Transform 的迭代器
Transform.bindings.cs中:
public IEnumerator GetEnumerator()
{
return new Transform.Enumerator(this);
}
private class Enumerator : IEnumerator
{
Transform outer;
int currentIndex = -1;
public object Current
{
get { return outer.GetChild(currentIndex); }
}
public bool MoveNext()
{
int childCount = outer.childCount;
return ++currentIndex < childCount;
}
}
foreach 依赖 GetEnumerator、Current 和 MoveNext。Transform 通过 childCount 和 GetChild 访问原生层级,把内部结构隐藏起来。
协程为什么使用 IEnumerator
包含 yield 的方法会被编译器改造成状态机。状态机保存“执行到哪里”和局部变量,IEnumerator 提供 MoveNext 和 Current。
IEnumerator WaitAndPrint()
{
Debug.Log("A");
yield return new WaitForSeconds(1);
Debug.Log("B");
}
Unity 调度器每帧调用 MoveNext。第一次执行到 yield 后暂停,等待条件满足后继续执行后面的代码。
MonoBehaviour 如何启动协程
MonoBehaviour.bindings.cs中:
public Coroutine StartCoroutine(IEnumerator routine)
{
if (routine == null)
throw new NullReferenceException("routine is null");
if (!IsObjectMonoBehaviour(this))
throw new ArgumentException("Coroutines can only be stopped on a MonoBehaviour");
return StartCoroutineManaged2(routine);
}
StartCoroutineManaged2 是 extern,会把迭代器交给原生协程调度器。真正决定何时继续执行的是 Unity 帧循环。
Awaitable、async/await 与异步状态机
async/await 也会生成状态机,但它不是每帧主动轮询,而是等待一个可等待对象完成。
Unity 提供专门的 Awaitable 类型,源码位于 Runtime/Export/Scripting/Awaitable*.cs。
Awaitable.cs中可以看到它同时实现了 IEnumerator:
[AsyncMethodBuilder(typeof(AwaitableAsyncMethodBuilder))]
public partial class Awaitable : IEnumerator
{
bool IEnumerator.MoveNext()
{
if (IsCompleted)
{
PropagateExceptionAndRelease();
return false;
}
return true;
}
object IEnumerator.Current => null;
}
Awaitable 既可以被 await,也可以在协程体系中工作。它内部会保存 continuation,当任务完成后运行后续代码。
Awaitable 的 AsyncMethodBuilder
Awaitable.AsyncMethodBuilder.cs 中:
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
var box = EnsureStateMachineBox<TStateMachine>();
var typedBox = (StateMachineBox<TStateMachine>)box;
Task.CompletionThreadAffinity =
Thread.CurrentThread.ManagedThreadId == _mainThreadId
? AwaiterCompletionThreadAffinity.MainThread
: AwaiterCompletionThreadAffinity.BackgroundThread;
typedBox.StateMachine = stateMachine;
typedBox.MoveNext();
}
AsyncMethodBuilder 是 C# 异步状态机与 Unity Awaitable 之间的桥接。它会记录完成回调应该运行在主线程还是后台线程。
Awaitable 与原生异步代码
Awaitable.Bindings.cs中:
[FreeFunction("Scripting::Awaitables::AttachManagedWrapper", IsThreadSafe = true)]
private static extern void AttachManagedGCHandleToNativeAwaitable(
IntPtr nativeAwaitable, UIntPtr gcHandle);
[FreeFunction("Scripting::Awaitables::Release", IsThreadSafe = true)]
private static extern void ReleaseNativeAwaitable(IntPtr nativeAwaitable);
[FreeFunction("Scripting::Awaitables::IsCompleted", IsThreadSafe = true)]
private static extern int IsNativeAwaitableCompleted(IntPtr nativeAwaitable);
这说明 Awaitable 也可以包装原生异步对象。C# 保存一个指向原生 Awaitable 的句柄,并通过绑定函数查询完成状态或释放资源。
协程与 async/await 的区别
| 维度 | 协程 | async/await |
|---|---|---|
| 核心机制 | IEnumerator + Unity 调度器 + yield | Awaiter + continuation |
| 推进方式 | 每帧主动调用 MoveNext | 任务完成后触发回调 |
| 典型场景 | 延时、序列动画、逐帧逻辑 | 加载、IO、网络等待 |
| 异常处理 | 较难统一捕获 | 可以用 try/catch |
| 线程含义 | 不创建新线程 | 不自动创建新线程,但可能等待后台完成 |
await 只暂停当前方法,不阻塞线程,也不自动创建新线程。
Delegate、Action、Func 与 event
delegate 定义的是“方法签名类型”:
delegate void DamageHandler(int amount);
DamageHandler handler = TakeDamage;
handler(10);
Action 和 Func 是 .NET 预定义的委托类型:
Action<int> log = value => Debug.Log(value);
Func<int, int> square = value => value * value;
Action<T>:无返回值。Func<T1, TResult>:最后一个泛型参数是返回值。delegate:自定义签名类型。
event 是对委托的访问限制:
public event Action<int> Damaged;
private void Hit(int value)
{
Damaged?.Invoke(value);
}
外部只能 += 订阅和 -= 退订,不能覆盖或触发。普通 public delegate 字段则允许外部直接调用和覆盖,因此更容易被误用。
Task、线程池与 Unity 主线程
Task 是 .NET 的通用任务抽象。
var result = await Task.Run(() => PureCSharpCalculation());
await只暂停当前方法。Task.Run通常把工作提交给线程池。
线程池适合纯 C# 计算、数据解析和部分 I/O,不适合直接操作 GameObject、Transform 等 Unity 对象。
Unity 提供 UnitySynchronizationContext.cs,它实现了 Send 和 Post,用于把后台线程的 continuation 调度回 Unity 主线程。
常见结构是后台计算,完成后回到 Unity 主线程修改场景对象:
Transform target = cachedTransform;
var data = await Task.Run(() => HeavyPureCalculation());
target.position = data.position;
不要直接在后台线程修改 Transform 或调用大多数 Unity API。
Job System:并行任务的 C# 入口
IJob:执行一次
IJob.cs中:
[JobProducerType(typeof(IJobExtensions.JobStruct<>))]
public interface IJob
{
void Execute();
}
它的 Schedule 会:
- 取 Job 数据地址。
- 取反射数据。
- 构造调度参数。
- 调用原生
JobsUtility.Schedule。
unsafe public static JobHandle Schedule<T>(
this T jobData,
JobHandle dependsOn = new JobHandle())
where T : struct, IJob
{
var scheduleParams = new JobsUtility.JobScheduleParameters(
UnsafeUtility.AddressOf(ref jobData),
GetReflectionData<T>(),
dependsOn,
ScheduleMode.Single);
return JobsUtility.Schedule(ref scheduleParams);
}
IJobParallelFor:按索引并行
IJobParallelFor.cs中:
[JobProducerType(typeof(IJobParallelForExtensions.ParallelForJobStruct<>))]
public interface IJobParallelFor
{
void Execute(int index);
}
内部会不断从工作范围中“窃取”一段索引:
while (true)
{
int begin;
int end;
if (!JobsUtility.GetWorkStealingRange(
ref ranges, jobIndex, out begin, out end))
break;
for (var i = begin; i < end; ++i)
jobData.Execute(i);
}
Schedule 与 Complete
JobHandle handle = job.Schedule(positions.Length, 64);
handle.Complete();
Schedule:提交任务并返回JobHandle。Complete:等待任务完成并建立主线程同步点。- 第二个参数
innerloopBatchCount是批次大小,不是线程数量。
Job 可以声明依赖:
JobHandle first = firstJob.Schedule();
JobHandle second = secondJob.Schedule(first);
second.Complete();
NativeArray 与 AtomicSafetyHandle
NativeArray 是非托管内存
NativeArray.cs中:
public unsafe struct NativeArray<T> : IDisposable, IEnumerable<T>
where T : struct
{
internal void* m_Buffer;
internal int m_Length;
internal int m_MinIndex;
internal int m_MaxIndex;
internal AtomicSafetyHandle m_Safety;
internal Allocator m_AllocatorLabel;
}
它使用非托管内存,不会自动被 GC 回收,需要手动 Dispose。构造时:
array.m_Buffer = UnsafeUtility.MallocTracked(
totalSize,
UnsafeUtility.AlignOf<T>(),
allocator,
0,
label.pointer);
为什么 Job 需要 NativeArray
Job 运行在原生工作线程,不能访问托管堆上的引用类型。NativeArray<T> 的元素必须是 unmanaged,这样内存可以被 Job 和 Burst 直接读写。
NativeArray 的读写索引器会先做安全检查:
public T this[int index]
{
get
{
CheckElementReadAccess(index);
return UnsafeUtility.ReadArrayElement<T>(m_Buffer, index);
}
set
{
CheckElementWriteAccess(index);
UnsafeUtility.WriteArrayElement(m_Buffer, index, value);
}
}
AtomicSafetyHandle 不是锁
AtomicSafetyHandle.bindings.cs中:
public struct AtomicSafetyHandle
{
internal IntPtr versionNode;
internal int version;
internal int staticSafetyId;
}
它不是锁,而是 Job System 的安全检查机制。它记录容器的读、写、释放状态,避免:
- 越界访问。
- 重复释放。
- Job 还在运行时非法读写。
- 多个结构体指向同一缓冲区时的非法操作。
例如读检查:
public static unsafe void CheckReadAndThrow(AtomicSafetyHandle handle)
{
var versionPtr = (int*)handle.versionNode;
if (handle.version != ((*versionPtr) & ReadCheck))
CheckReadAndThrowNoEarlyOut(handle);
}
这些检查通常受 ENABLE_UNITY_COLLECTIONS_CHECKS 控制,发布版本可以关闭部分检查以减少开销。
AssetBundle 与 Addressables
AssetBundle 是原生资源包
AssetBundle.bindings.cs中,AssetBundle 是 UnityEngine.Object,提供 LoadFromFileAsync、LoadAssetAsync、Unload 等运行时接口。
AssetBundleRequest.bindings.cs中:
[NativeHeader("Modules/AssetBundle/Public/AssetBundleLoadAssetOperation.h")]
public class AssetBundleRequest : ResourceRequest
{
[NativeMethod("GetLoadedAsset")]
protected override extern Object GetResult();
}
调用 LoadAssetAsync 返回 AssetBundleRequest,真正加载和反序列化由原生 AssetBundle 系统完成。C# 对象只是异步操作的句柄和结果包装。
AssetBundle 与 Addressables 的关系
Addressables 不是完全替代 AssetBundle 的“另一个系统”,而是构建在资源定位、依赖管理和加载生命周期之上的高层封装。
| 能力 | AssetBundle | Addressables |
|---|---|---|
| 加载单位 | 一个 Bundle | 一个 Address 对应一个资源 |
| 依赖管理 | 手动维护 Manifest | 自动处理依赖 |
| 加载调用 | LoadFromFileAsync、LoadAssetAsync | LoadAssetAsync<T>(key) |
| 释放方式 | Unload 手动释放 | Release 句柄引用计数 |
| 分包策略 | 手动规划 Bundle | 自动分组和内容分析 |
Addressables 底层仍可能使用 AssetBundle,但它把 Bundle 的依赖、引用计数、分组和远程内容管理抽象掉了。因此对于大多数普通项目,优先考虑 Addressables;需要精细控制包体、下载粒度或非常规加载方式时,再直接使用 AssetBundle。
Subsystems:可插拔子系统架构
Unity 的 XR、输入、显示等平台能力通常采用“子系统 + Provider”模式。
SubsystemWithProvider.cs中:
public abstract class SubsystemWithProvider : ISubsystem
{
public void Start()
{
if (running)
return;
OnStart();
providerBase.m_Running = true;
running = true;
}
public void Stop()
{
if (!running)
return;
OnStop();
providerBase.m_Running = false;
running = false;
}
public void Destroy()
{
Stop();
if (SubsystemManager.RemoveStandaloneSubsystem(this))
OnDestroy();
}
}
SubsystemProvider.cs中:
public abstract class SubsystemProvider<TSubsystem> : SubsystemProvider
where TSubsystem : SubsystemWithProvider, new()
{
public abstract void Start();
public abstract void Stop();
public abstract void Destroy();
}
统一接口负责生命周期和状态管理,Provider 负责平台相关实现。这样上层逻辑不直接绑定某个平台。
Physics:物理场景与批量射线检测
PhysicsScene 是句柄
PhysicsScene.bindings.cs中:
[StructLayout(LayoutKind.Sequential)]
public partial struct PhysicsScene : IEquatable<PhysicsScene>
{
private int m_index;
private int m_version;
[StaticAccessor("GetPhysicsManager()", StaticAccessorType.Dot)]
[NativeMethod("IsPhysicsSceneValid")]
extern private static bool IsValid_Internal(PhysicsScene physicsScene);
}
PhysicsScene 不是完整的物理世界副本,而是用 index 和 version 定位原生物理场景。
Simulate 会先做 C# 侧校验,再进入原生物理模拟:
public void Simulate(float step)
{
if (IsValid())
{
if (this == GetDefaultScene() &&
Physics.simulationMode != SimulationMode.Script)
{
Debug.LogWarning("...");
return;
}
Physics.Simulate_Internal(this, step, SimulationStage.All, SimulationOption.All);
return;
}
throw new InvalidOperationException("Cannot simulate the physics scene as it is invalid.");
}
RaycastCommand 是批量命令
QueryCommand.bindings.cs中:
public partial struct RaycastCommand
{
public unsafe static JobHandle ScheduleBatch(
NativeArray<RaycastCommand> commands,
NativeArray<RaycastHit> results,
int minCommandsPerJob,
int maxHits,
JobHandle dependsOn = new JobHandle())
{
var jobData = new BatchQueryJob<RaycastCommand, RaycastHit>(commands, results);
var scheduleParams = new JobsUtility.JobScheduleParameters(
UnsafeUtility.AddressOf(ref jobData),
BatchQueryJobStruct<BatchQueryJob<RaycastCommand, RaycastHit>>.Initialize(),
dependsOn,
ScheduleMode.Parallel);
return ScheduleRaycastBatch(
ref scheduleParams,
NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(commands),
commands.Length,
NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(results),
results.Length,
minCommandsPerJob,
maxHits);
}
}
它把 Job System、NativeArray 和 C++ 物理引擎组合起来。C# 只负责准备命令、调度 Job、等待完成和读取结果;真正的碰撞查询由 C++ 物理引擎执行。
Graphics:渲染命令的 C# 入口
Graphics.cs中有大量公开渲染 API。
DrawMesh 有多个重载,常见版本会把参数整理后进入内部绑定方法:
public static void DrawMesh(
Mesh mesh,
Matrix4x4 matrix,
Material material,
int layer,
Camera camera,
int submeshIndex,
MaterialPropertyBlock properties,
ShadowCastingMode castShadows,
bool receiveShadows,
Transform probeAnchor,
LightProbeUsage lightProbeUsage)
{
Internal_DrawMesh(
mesh,
submeshIndex,
matrix,
material,
layer,
camera,
properties,
castShadows,
receiveShadows,
probeAnchor,
lightProbeUsage,
null);
}
SetRenderTarget、Blit 也有类似特点:公开 API 处理多种参数形式和默认值,最终调用 extern 原生方法。
C# 是命令组织者,不是渲染执行者
渲染管线的网格提交、Shader 状态设置、GPU 命令队列等由 C++ 和图形驱动完成。C# 侧负责把 Mesh、Material、矩阵、层、相机和光照参数整理成统一调用。
Animation:动画数据与播放控制
AnimationClip 与 Animation 职责不同
Animation.bindings.cs中:
public sealed class Animation : Behaviour, IEnumerable
{
public extern AnimationClip clip { get; set; }
public extern bool isPlaying { [NativeName("IsPlaying")] get; }
public extern void Stop();
}
AnimationClip:保存关键帧、曲线等动画数据。Animation:控制动画播放、停止、混合和状态查询。
isPlaying 是 extern 属性,因为播放状态保存在原生动画系统中。Stop()、Play() 等真正播放逻辑也在原生层。
为什么 Play 不直接实现完整播放逻辑
动画采样和混合需要访问动画曲线、骨骼层级和时间轴,这些数据在原生引擎中。C# 只负责传递 clip 名称、混合模式和时间参数。
Input:输入状态查询的绑定案例
Input.bindings.cs中,Input 的大部分 API 都是查询原生输入状态:
public static bool GetButtonDown(string buttonName)
=> Internal.InputUnsafeUtility.GetButtonDown(buttonName);
public static bool GetKey(KeyCode key)
=> GetKeyInt(key);
public extern static Touch GetTouch(int index);
这些 extern 方法把输入查询交给原生输入系统。C# 侧基本不做复杂逻辑,只是返回当前帧或当前输入状态。
新输入系统则在独立包中提供更复杂的事件流和设备抽象,但旧版 Input 是理解“C# 绑定到原生状态查询”的最小案例。
Editor:AssetDatabase、AssetImporter、SerializedObject
AssetDatabase 是编辑器资源索引
AssetDatabase.cs提供:
FindAssetsLoadAssetAtPathImportAssetGetAssetPathIsOpenForEditMakeEditable
它主要用于 Editor 工具和自定义导入流程,不用于运行时的游戏资源加载。
AssetImporter 负责资源导入
AssetImporter.bindings.cs中:
[NativeHeader("Editor/Src/AssetPipeline/AssetImporter.h")]
public partial class AssetImporter : Object
{
[NativeName("AssetPathName")]
public extern string assetPath { get; }
[FreeFunction("FindAssetImporterAtAssetPath")]
extern public static AssetImporter GetAtPath(string path);
public void SaveAndReimport()
{
AssetDatabase.ImportAsset(assetPath);
}
}
AssetImporter 保存资源的导入设置,例如纹理压缩、模型缩放、音频格式等。它把设置传给原生导入管线,而不是在 C# 中完成导入计算。
SerializedObject 是编辑器侧的安全修改层
SerializedObject.bindings.cs中:
public class SerializedObject : IDisposable
{
public SerializedObject(Object obj)
{
m_NativeObjectPtr = InternalCreate(new Object[] { obj }, null);
}
extern public bool ApplyModifiedProperties();
extern public void Update();
}
使用它的好处是:
- 统一处理 Undo。
- 支持多对象批量编辑。
- 支持 Prefab 覆盖和属性差异。
- 不直接修改原对象。
Mathematics 与 UIElements
Mathematics 是高性能数学库
Modules/Mathematics 提供 float3、float4、quaternion、float4x4 等类型。它们和 UnityEngine.Vector3、Quaternion 不同,通常用于 Job、Burst 和 DOTS/ECS 场景。
UnityEngine.Vector3 与 Unity.Mathematics.float3 是两套类型:
| 类型 | 特点 |
|---|---|
UnityEngine.Vector3 | Unity 脚本常用,可直接绑定原生数学类型 |
Unity.Mathematics.float3 | 面向高性能、Job、Burst 和 DOTS |
阅读时不要把两者混为一谈。
UIElements 是托管 UI 体系
Modules/UIElements 包含 VisualElement、UQuery、样式、布局和事件系统。它比 UnityEngine.UI 的 GameObject 型 UI 更接近一棵托管对象树。
这部分内容也值得单独阅读,但主线仍然是:哪些是 C# 实现的树结构和布局,哪些需要调用原生渲染和输入系统。
Burst 与 Job 的关系
在 IJob.cs中可以看到 BurstDiscard 和 SharedStatic:
internal struct JobStruct<T> where T : struct, IJob
{
internal static readonly BurstLike.SharedStatic<IntPtr> jobReflectionData =
BurstLike.SharedStatic<IntPtr>.GetOrCreate<JobStruct<T>>();
[BurstDiscard]
internal static unsafe void Initialize()
{
if (jobReflectionData.Data == IntPtr.Zero)
jobReflectionData.Data = JobsUtility.CreateJobReflectionData(
typeof(T),
(ExecuteJobFunction)Execute);
}
}
BurstDiscard 表示这段反射初始化代码不适合 Burst 编译器编译。Burst 编译器会对 Job 的 Execute 做原生 AOT 编译,但反射元数据初始化仍需要托管侧执行。
可以这样理解:
C# Job 源码
-> IL
-> Burst 编译器
-> 优化的原生机器码
-> Job System 调度执行
Burst 适合没有托管引用、没有复杂运行时类型判断、内存布局清晰的 Job 和 NativeContainer 数据。
完整学习路线与阅读清单
如果你要从头系统读 UnityCsReference,建议按下面的顺序:
第一阶段:建立 C#/C++ 边界
Runtime/Export/Scripting/UnityEngineObject.bindings.csRuntime/Export/Scripting/GameObject.bindings.csRuntime/Export/Scripting/Component.bindings.csRuntime/Export/Scripting/Behaviour.bindings.csRuntime/Export/Scripting/MonoBehaviour.bindings.csRuntime/Transform/ScriptBindings/Transform.bindings.cs
目标:理解 Object 双层结构、组件模型、激活状态和生命周期入口。
第二阶段:数据与反射
Runtime/Export/Scripting/Attributes.csRuntime/Export/Scripting/AttributeHelperEngine.csRuntime/Export/Serialization/Serialization.csRuntime/Export/Scripting/ScriptableObject.bindings.csEditor/Mono/SerializedObject.bindings.csEditor/Mono/SerializedProperty.bindings.cs
目标:理解 Attribute 如何通过反射产生行为,以及 Unity 如何序列化对象。
第三阶段:场景与资源
Runtime/Export/SceneManager/Scene.bindings.csRuntime/Export/SceneManager/SceneManager.csModules/AssetBundle/Managed/AssetBundle.bindings.csModules/AssetBundle/Managed/AssetBundleRequest.bindings.cs
目标:理解句柄式对象和异步加载。
第四阶段:执行模型
Runtime/Export/Scripting/Coroutines.csRuntime/Export/Scripting/Coroutine.bindings.csRuntime/Export/Scripting/Awaitable.csRuntime/Export/Scripting/Awaitable.AsyncMethodBuilder.csRuntime/Export/Scripting/Awaitable.Bindings.csRuntime/Export/Scripting/UnitySynchronizationContext.cs
目标:理解协程和 async/await 状态机,以及 Unity 主线程调度。
第五阶段:并行与原生内存
Runtime/Jobs/Managed/IJob.csRuntime/Jobs/Managed/IJobParallelFor.csRuntime/Export/NativeArray/NativeArray.csRuntime/Export/Jobs/AtomicSafetyHandle.bindings.cs
目标:理解 Job 调度、依赖、批次和原生内存安全。
第六阶段:模块案例
Modules/Subsystems/SubsystemWithProvider.csModules/Subsystems/SubsystemProvider.csModules/Physics/ScriptBindings/PhysicsScene.bindings.csModules/Physics/ScriptBindings/QueryCommand.bindings.csRuntime/Export/Graphics/Graphics.csModules/Animation/ScriptBindings/Animation.bindings.csModules/InputLegacy/Input.bindings.csEditor/Mono/AssetDatabase/AssetDatabase.csEditor/Mono/AssetPipeline/AssetImporter.bindings.cs
目标:把前面的绑定、对象模型、异步、Job 和原生内存概念套用到具体模块。


被折叠的 条评论
为什么被折叠?



