UnityReference阅读笔记

这份源码是什么

Unity 的核心引擎大部分由 C++ 实现,但开发者日常编写的几乎都是 C#。UnityCsReference 公开的是 Unity 引擎和编辑器的 C# 源码,重点是“C# 侧代码”,而不是完整的 Unity 引擎。

它包含:

  • C# 公开 API、类型声明和包装对象。
  • 连接 C++ 的绑定声明。
  • 一部分纯 C# 托管实现,例如协程推进、Awaitable 状态机、Job 扩展方法。
  • 编辑器相关 API,例如 AssetDatabaseSerializedObjectAssetImporter
  • NativeArrayAtomicSafetyHandle、Job、Subsystems、UIElements 等模块。

它不包含:

  • 完整 C++ 渲染、物理求解器、动画采样器、音频混音器等原生实现。
  • PhysX、Burst 编译器后端、图形驱动等第三方或底层代码。

因此,阅读这份仓库最有价值的收获是:理解 Unity 如何组织 C# 与 C++ 之间的边界,而不是期待看到所有算法本体。

仓库顶层结构

仓库并不只有 UnityEngine 一个程序集,而是一个大仓库的 C# 部分。阅读时可以按下面地图定位:

路径主要作用
Runtime/Export/ScriptingObjectGameObjectComponentBehaviourMonoBehaviour、协程、Awaitable
Runtime/Transform/ScriptBindingsTransform、父子层级、位置旋转缩放
Runtime/Export/SceneManagerSceneSceneManager、场景加载
Runtime/Export/NativeArrayNativeArray、原生内存管理
Runtime/Jobs/ManagedIJobIJobParallelFor 等 Job 接口
Runtime/Export/JobsAtomicSafetyHandle、安全读写检查
Runtime/Export/MathVector3QuaternionMatrix4x4 等数学类型
Modules/AssetBundleAssetBundleAssetBundleRequest 等运行时 API
Modules/Subsystems子系统与 Provider 架构
Modules/Physics/ScriptBindings物理场景、射线查询、批量命令
Runtime/Export/GraphicsGraphics 渲染命令入口
Modules/Animation/ScriptBindings旧版 AnimationAnimationClip
Modules/InputLegacy旧版 Input 输入查询
Modules/Mathematicsfloat3quaternionmatrix 等高性能数学类型
Modules/UIElementsUIElements/UI Toolkit 的 VisualElement 体系
Editor/MonoAssetDatabaseSerializedObjectAssetImporter 等编辑器 API

阅读任何一段 Unity C# 源码,先问四个问题:

  1. 这段代码是在 C# 托管侧运行,还是会通过绑定进入 C++?
  2. 它操作的是 C# 包装对象、C++ 原生实体、句柄,还是原生内存?
  3. 它运行在 Unity 主线程、协程、线程池,还是 Job 工作线程?
  4. 它是在组织参数、做类型转换和默认值,还是在执行真正的引擎计算?

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.csDestroy 的实现很典型:

[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
  • 方法或参数上是否有 FreeFunctionNativeMethodStaticAccessorUnityMarshalAs 等绑定特性。

Internal_ 只是 Unity 内部命名习惯,本身不决定执行位置。

绑定系统与参数封送

绑定系统负责建立 C# 方法和 C++ 函数的对应关系。调用时,它还要把 C# 参数转换为 C++ 能理解的数据,并把返回值或异常转回 C#。

不同数据在绑定层的常见处理方式如下:

C# 数据常见处理方式
intfloatboolenum直接或按底层数值传递
Vector3Quaternion按固定字段布局复制
UnityEngine.ObjectIntPtrEntityId 或句柄
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# 异常,常见类型包括:

  • ArgumentException
  • InvalidOperationException
  • MissingReferenceException
  • NullReferenceException
  • ObjectDisposedException

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 本身不保存位置、渲染、碰撞等能力,而是挂载不同的 ComponentTransform 是每个 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 等操作,最终都交给原生 GameObjectComponent 数据。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; }
}

positionrotationlocalScale 等字段虽然看起来像普通属性,但它们是 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:同时考虑自身和所有父级。
  • enabledBehaviour 组件自己的开关。

脚本通常需要:

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);
    }
}

ConstructorCheckextern,由原生侧检查脚本是否被正确构造。用户脚本继承 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:带 getset 逻辑的访问入口。
  • 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

反射通过 TypeMethodInfo 等 API 查询这些元数据表,不会重新解析 .cs 文件。Type代表一个完整类型的元数据,MemberInfo是所有类型成员的抽象基类,FieldInfoMethodInfoPropertyInfoConstructorInfoEventInfo都继承自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 依赖 GetEnumeratorCurrentMoveNextTransform 通过 childCountGetChild 访问原生层级,把内部结构隐藏起来。

协程为什么使用 IEnumerator

包含 yield 的方法会被编译器改造成状态机。状态机保存“执行到哪里”和局部变量,IEnumerator 提供 MoveNextCurrent

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);
}

StartCoroutineManaged2extern,会把迭代器交给原生协程调度器。真正决定何时继续执行的是 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 调度器 + yieldAwaiter + continuation
推进方式每帧主动调用 MoveNext任务完成后触发回调
典型场景延时、序列动画、逐帧逻辑加载、IO、网络等待
异常处理较难统一捕获可以用 try/catch
线程含义不创建新线程不自动创建新线程,但可能等待后台完成

await 只暂停当前方法,不阻塞线程,也不自动创建新线程。

Delegate、Action、Func 与 event

delegate 定义的是“方法签名类型”:

delegate void DamageHandler(int amount);
DamageHandler handler = TakeDamage;
handler(10);

ActionFunc 是 .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,不适合直接操作 GameObjectTransform 等 Unity 对象。

Unity 提供 UnitySynchronizationContext.cs,它实现了 SendPost,用于把后台线程的 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 会:

  1. 取 Job 数据地址。
  2. 取反射数据。
  3. 构造调度参数。
  4. 调用原生 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中,AssetBundleUnityEngine.Object,提供 LoadFromFileAsyncLoadAssetAsyncUnload 等运行时接口。

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 的“另一个系统”,而是构建在资源定位、依赖管理和加载生命周期之上的高层封装。

能力AssetBundleAddressables
加载单位一个 Bundle一个 Address 对应一个资源
依赖管理手动维护 Manifest自动处理依赖
加载调用LoadFromFileAsyncLoadAssetAsyncLoadAssetAsync<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 不是完整的物理世界副本,而是用 indexversion 定位原生物理场景。

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);
}

SetRenderTargetBlit 也有类似特点:公开 API 处理多种参数形式和默认值,最终调用 extern 原生方法。

C# 是命令组织者,不是渲染执行者

渲染管线的网格提交、Shader 状态设置、GPU 命令队列等由 C++ 和图形驱动完成。C# 侧负责把 MeshMaterial、矩阵、层、相机和光照参数整理成统一调用。

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:控制动画播放、停止、混合和状态查询。

isPlayingextern 属性,因为播放状态保存在原生动画系统中。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提供:

  • FindAssets
  • LoadAssetAtPath
  • ImportAsset
  • GetAssetPath
  • IsOpenForEdit
  • MakeEditable

它主要用于 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 提供 float3float4quaternionfloat4x4 等类型。它们和 UnityEngine.Vector3Quaternion 不同,通常用于 Job、Burst 和 DOTS/ECS 场景。

UnityEngine.Vector3Unity.Mathematics.float3 是两套类型:

类型特点
UnityEngine.Vector3Unity 脚本常用,可直接绑定原生数学类型
Unity.Mathematics.float3面向高性能、Job、Burst 和 DOTS

阅读时不要把两者混为一谈。

UIElements 是托管 UI 体系

Modules/UIElements 包含 VisualElementUQuery、样式、布局和事件系统。它比 UnityEngine.UIGameObject 型 UI 更接近一棵托管对象树。

这部分内容也值得单独阅读,但主线仍然是:哪些是 C# 实现的树结构和布局,哪些需要调用原生渲染和输入系统。

Burst 与 Job 的关系

在 IJob.cs中可以看到 BurstDiscardSharedStatic

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++ 边界

  1. Runtime/Export/Scripting/UnityEngineObject.bindings.cs
  2. Runtime/Export/Scripting/GameObject.bindings.cs
  3. Runtime/Export/Scripting/Component.bindings.cs
  4. Runtime/Export/Scripting/Behaviour.bindings.cs
  5. Runtime/Export/Scripting/MonoBehaviour.bindings.cs
  6. Runtime/Transform/ScriptBindings/Transform.bindings.cs

目标:理解 Object 双层结构、组件模型、激活状态和生命周期入口。

第二阶段:数据与反射

  1. Runtime/Export/Scripting/Attributes.cs
  2. Runtime/Export/Scripting/AttributeHelperEngine.cs
  3. Runtime/Export/Serialization/Serialization.cs
  4. Runtime/Export/Scripting/ScriptableObject.bindings.cs
  5. Editor/Mono/SerializedObject.bindings.cs
  6. Editor/Mono/SerializedProperty.bindings.cs

目标:理解 Attribute 如何通过反射产生行为,以及 Unity 如何序列化对象。

第三阶段:场景与资源

  1. Runtime/Export/SceneManager/Scene.bindings.cs
  2. Runtime/Export/SceneManager/SceneManager.cs
  3. Modules/AssetBundle/Managed/AssetBundle.bindings.cs
  4. Modules/AssetBundle/Managed/AssetBundleRequest.bindings.cs

目标:理解句柄式对象和异步加载。

第四阶段:执行模型

  1. Runtime/Export/Scripting/Coroutines.cs
  2. Runtime/Export/Scripting/Coroutine.bindings.cs
  3. Runtime/Export/Scripting/Awaitable.cs
  4. Runtime/Export/Scripting/Awaitable.AsyncMethodBuilder.cs
  5. Runtime/Export/Scripting/Awaitable.Bindings.cs
  6. Runtime/Export/Scripting/UnitySynchronizationContext.cs

目标:理解协程和 async/await 状态机,以及 Unity 主线程调度。

第五阶段:并行与原生内存

  1. Runtime/Jobs/Managed/IJob.cs
  2. Runtime/Jobs/Managed/IJobParallelFor.cs
  3. Runtime/Export/NativeArray/NativeArray.cs
  4. Runtime/Export/Jobs/AtomicSafetyHandle.bindings.cs

目标:理解 Job 调度、依赖、批次和原生内存安全。

第六阶段:模块案例

  1. Modules/Subsystems/SubsystemWithProvider.cs
  2. Modules/Subsystems/SubsystemProvider.cs
  3. Modules/Physics/ScriptBindings/PhysicsScene.bindings.cs
  4. Modules/Physics/ScriptBindings/QueryCommand.bindings.cs
  5. Runtime/Export/Graphics/Graphics.cs
  6. Modules/Animation/ScriptBindings/Animation.bindings.cs
  7. Modules/InputLegacy/Input.bindings.cs
  8. Editor/Mono/AssetDatabase/AssetDatabase.cs
  9. Editor/Mono/AssetPipeline/AssetImporter.bindings.cs

目标:把前面的绑定、对象模型、异步、Job 和原生内存概念套用到具体模块。

源码链接: https://pan.quark.cn/s/46590cc698ca 在信息技术领域,特别是在网络应用程序开发和用户界面设计方面,构建支持多选项选择的下拉选择框是一项普遍的需求。常规的下拉选择框往往仅限于让用户选择一个选项,然而,通过定制和扩展,我们能够构建一个能够支持多个选项选择的下拉选择框。以下将对这一主题进行深入探讨。 我们将探讨“支持多选项选择的下拉选择框”的构建方法。这种功能通常应用于用户需要从众多选项中进行选择,而全部选项不可能在页面上完全展示的情况。在这种情况下,一个可进行多选项选择的下拉选择框提供了一种既高效又节省空间的解决方案。描述中提到,这种多选项下拉选择框是通过一个被称为“checkboxlist”的元素构建的,这可能是使用特定的编程语言(如JavaScript、HTML5或特定的前端框架如React、Vue)中的一个组件或控件。 在网络应用程序开发中,实现此类功能通常需要以下步骤: 1. **HTML结构**:构建一个基础的下拉选择框结构,通常使用`<select>`元素,并为其附加`multiple`属性以启用多选项选择功能。每个选项则由`<option>`元素表示。 2. **CSS样式**:为了使下拉选择框看起来更像一个列表,可能需要对其进行个性化设置,例如添加背景色、边框等。可以使用CSS来调整`<select>`元素的样式。 3. **JavaScript交互**:为了实现checkboxlist的效果,通常会运用JavaScript或jQuery来处理用户的交互事件,如点击、键盘操作等,同时更新选定的选项状态。 4. **自定义控件**:在某些场景下,为了获得更佳的用户体验,开发者可能会选择创建自定义的用户控...
代码转载自:https://pan.quark.cn/s/f81e48336f75 在当前流媒体服务广泛应用的背景下,于Android系统平台完成网络视频的播放功能是一项普遍需求。 为了达成这一目标,开发者必须熟练掌握若干核心的技术要点。 以下提供一份详尽的说明: 1. **播放器库的应用**:Android系统自带的MediaPlayer类能够播放本地及网络媒体资源,但其功能较为有限,对于网络视频的兼容性表现不佳。 因此,开发者常常会选用第三方库,例如ExoPlayer。 ExoPlayer是由Google研发的一款具备高性能且可灵活定制的媒体播放器,能够支持多种格式和网络流媒体,涵盖DASH、HLS以及Progressive Download。 2. **视频链接的获取**:网络视频播放的首要步骤是获取视频的URL地址。 这可能需要与服务端进行交互,比如通过发送HTTP请求或调用API来获取视频的链接地址。 3. **播放器的配置**:在建立ExoPlayer实例时,需要设定播放源(DataSource),这通常通过MediaSource对象来完成。 针对网络视频,可以使用ExtractorMediaSource,并搭配DefaultHttpDataSourceFactory来管理HTTP或HTTPS链接。 4. **播放操作的操控**:ExoPlayer提供了丰富的API用于播放控制,包括play(), pause(), seekTo()等功能。 开发者需要将这些控制接口与UI组件进行关联,以实现便捷的用户交互。 5. **异常管理**:网络视频播放过程中可能遭遇各种挑战,如网络连接中断、服务器响应错误等。 因此,需要编写异常管理代码,确保在问题发生时能够妥善应对,例如...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值