Skip to main content

特性驱动架构:从拖拽引用到声明式 Udon 开发

本文档系统分析 VVMW 工程中的 C# 特性体系,通过源码追踪揭示复杂特性如何从简单声明解析而来,最终实现声明式、高可维护性的 Udon 工程。

冷知识:VVMW工程也就是VizVid播放器~~~~


第1层:设计哲学层

1.1 VVMW 的设计目标与 Udon 约束

VVMW 是一个视频播放器系统,面临三个 Udon 核心约束:

┌─────────────────────────────────────────────────────────────────────┐
│                        Udon 约束三角                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│         约束1:拖拽引用繁琐                                           │
│         "Core 需要被 PlayerHandler、FrontendHandler、UIHandler... 引用" │
│         → 手动拖拽 10+ 个组件?                                       │
│                                                                      │
│              ↓                                                       │
│                                                                      │
│         约束2:事件订阅重复                                           │
│         "每个组件都要监听 OnVideoPlay、OnVideoPause、OnVideoEnd..."    │
│         → 每个组件写 3 遍相同的订阅代码?                              │
│                                                                      │
│              ↓                                                       │
│                                                                      │
│         约束3:序列化逻辑分散                                         │
│         "Loop 属性改变时需要同步、UI 需要更新、AudioLink 需要同步..."   │
│         → 在 N 个地方写相同的同步逻辑?                                │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

VVMW 的解法

约束 传统方案 VVMW 方案
拖拽引用 手动拖拽 10+ 次 [Locatable] + [Resolve] 自动定位
事件订阅 每个组件重复订阅 [BindUdonSharpEvent] 声明式绑定
序列化逻辑 散落在各处 [FieldChangeCallback] 属性化

核心思路:用声明替代命令,用标记替代调用。


1.2 声明式 vs 命令式:设计范式转变

命令式风格(传统 Udon)

public class MyUI : UdonSharpBehaviour {
    public Core core;  // 需要手动拖拽赋值
    
    void Start() {
        // 手动订阅事件
        core.OnVideoPlay += OnVideoPlay;
        core.OnVideoPause += OnVideoPause;
        core.OnVideoEnd += OnVideoEnd;
    }
    
    public void OnVideoPlay() { /* 更新 UI */ }
    public void OnVideoPause() { /* 更新 UI */ }
    public void OnVideoEnd() { /* 更新 UI */ }
}

问题

  • 每增加一个监听事件,需要修改 Start 方法
  • 组件未初始化时事件可能为 null
  • 拖拽引用容易遗漏

声明式风格(VVMW)

public class MyUI : UdonSharpBehaviour {
    [SerializeField, LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")]
    [Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]
    [Locatable, BindUdonSharpEvent(
        nameof(OnVideoPlay),
        nameof(OnVideoPause),
        nameof(OnVideoEnd)
    )] Core core;
    
    // 事件方法声明即可,编辑器自动完成订阅
    public void OnVideoPlay() { /* 更新 UI */ }
    public void OnVideoPause() { /* 更新 UI */ }
    public void OnVideoEnd() { /* 更新 UI */ }
}

优势

  • 引用赋值由编辑器自动完成
  • 事件订阅由特性声明指定
  • 代码意图清晰,易于维护

1.3 特性系统的三层架构

VVMW 的特性系统分为三个层次:

┌─────────────────────────────────────────────────────────────────────┐
│                      特性系统三层架构                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  第1层:声明层(开发者编写)                                          │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ [Locatable]              ← "我需要被自动定位"                   │  │
│  │ [Resolve(...)]           ← "帮我解析这个引用"                   │  │
│  │ [BindUdonSharpEvent(...)]← "帮我订阅这些事件"                   │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              ↓                                       │
│  第2层:编辑器层(VRC.Foundation 实现)                             │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ LocatableDrawer          ← 检测拖拽 → 自动解析 → 填充引用        │  │
│  │ ResolveDrawer            ← 查找 GameObject → 填充 _GO 字段       │  │
│  │ BindUdonSharpEventDrawer ← 反射方法 → 订阅事件                   │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              ↓                                       │
│  第3层:运行时层(Udon VM 执行)                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ [UdonSynced] + Property   ← 网络同步 + 业务逻辑                 │  │
│  │ FieldChangeCallback       ← 值变化 → 回调触发                   │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

关键洞察:开发者的声明只是"意图",真正的绑定工作由编辑器在第2层完成。


第2层:核心特性详解与源码追踪

2.1 引用解析三剑客

2.1.1 Locatable — 自动定位

设计动机:解决预制体实例化后引用丢失的问题。

源码追踪:特性声明

来自 AutoPlayOnNearV2.cs 第 16-18 行:

[SerializeField, LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")]
[Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]
[Locatable] Core core;

源码追踪:特性定义

VRC.Foundation 中的 LocatableAttribute 定义:

// JLChnToZ.VRC.Foundation/LocatableAttribute.cs(VRC.Foundation 源码)
namespace JLChnToZ.VRC.Foundation {
    public enum InstaniatePrefabHierachyPosition { Before, After }

    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class LocatableAttribute : Attribute {
        /// <summary>缺失时实例化的预制体路径</summary>
        public string InstaniatePrefabPath { get; set; }
        /// <summary>预制体实例化位置</summary>
        public InstaniatePrefabHierachyPosition InstaniatePrefabPosition { get; set; }
    }
}

参数解析

参数 类型 说明
InstaniatePrefabPath string 缺失时实例化的预制体路径
InstaniatePrefabPosition enum 预制体实例化位置(Before/After)

Udon 适配

  • UdonSharp 编译时字段自动解析
  • 支持场景中定位和预制体实例化两种模式

2.1.2 Resolve — 引用解析

设计动机:替代手动拖拽引用,支持条件隐藏。

源码追踪:特性声明

来自 VideoPlayerHandler.cs 第 34、40-42 行:

// 解析自身组件
[SerializeField, HideInInspector, Resolve(".")] Animator animator;
[SerializeField, HideInInspector, Resolve(".")] BaseVRCVideoPlayer videoPlayer;
[SerializeField, HideInInspector, Resolve(".")] new Renderer renderer;

// 解析其他字段引用的 GameObject
[SerializeField, HideInInspector, Resolve(nameof(primaryAudioSource))] GameObject primaryAudioSourceGO;

源码追踪:复杂表达式解析

来自 AutoPlayOnNearV2.cs 第 17 行:

[Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]

这段代码解析了链式引用:

  • handlerFrontendHandler 字段
  • .coreFrontendHandlercore 属性
  • 最终找到 Core 实例

源码追踪:特性定义

// JLChnToZ.VRC.Foundation/ResolveAttribute.cs
namespace JLChnToZ.VRC.Foundation {
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class ResolveAttribute : Attribute {
        /// <summary>要解析的路径,如 "." 表示自身,"nameof(core)" 表示某字段</summary>
        public string Path { get; }
        /// <summary>默认为 true,仅在字段为空时解析</summary>
        public bool NullOnly { get; set; } = true;
        /// <summary>解析成功后隐藏字段</summary>
        public bool HideInInspectorIfResolvable { get; set; }

        public ResolveAttribute(string path) => Path = path;
    }
}

参数解析

参数 类型 说明
Path string 解析路径
NullOnly bool 默认为 true,仅在字段为空时解析
HideInInspectorIfResolvable bool 解析成功后隐藏字段

Udon 适配

  • Path 支持 nameof() 表达式和字符串拼接
  • 自动查找引用的 GameObject 并填充

2.1.3 BindUdonSharpEvent — UdonSharp 事件绑定

设计动机:自动化 UdonSharpBehaviour 事件订阅。

源码追踪:特性声明

来自 FrontendHandler.cs 第 20-34 行:

[LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core"), Locatable, BindUdonSharpEvent(
    nameof(_OnRangeLoopToggled),
    nameof(_OnScreenSharedPropertiesChanged),
    nameof(_OnSpeedChange),
    nameof(_OnSyncOffsetChange),
    nameof(_OnTitleData),
    nameof(_OnVideoBeginLoad),
    nameof(_OnVideoError),
    nameof(_OnVolumeChange),
    nameof(OnVideoReady),
    nameof(OnVideoStart),
    nameof(OnVideoPlay),
    nameof(OnVideoPause),
    nameof(OnVideoEnd)
), SingletonCoreControl] public Core core;

这个声明绑定了 13 个事件!如果用传统方式,需要写 13 次手动订阅。

源码追踪:特性定义

// JLChnToZ.VRC.Foundation/BindUdonSharpEventAttribute.cs
namespace JLChnToZ.VRC.Foundation {
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class BindUdonSharpEventAttribute : Attribute {
        public string[] EventNames { get; }
        public BindUdonSharpEventAttribute(params string[] eventNames) => EventNames = eventNames;
    }
}

源码追踪:编辑器解析逻辑

VRC.Foundation 的编辑器会在运行时通过反射找到这些方法:

// 伪代码:VRC.Foundation 编辑器的 BindUdonSharpEvent 解析逻辑
void ProcessBindUdonSharpEvent(FieldInfo field, BindUdonSharpEventAttribute attr) {
    var target = field.GetValue(component) as UdonSharpBehaviour;
    foreach (var eventName in attr.EventNames) {
        // 通过反射找到事件方法
        var method = target.GetType().GetMethod(eventName, 
            BindingFlags.Public | BindingFlags.Instance);
        if (method != null) {
            // 自动订阅(通过 VRC.Foundation 的内部机制)
            SubscribeToEvent(target, eventName);
        }
    }
}

Udon 适配

  • 事件方法必须声明为 public
  • 编辑器通过反射找到同名字方法并自动订阅
  • 支持多个事件一次性绑定

2.2 属性化同步

FieldChangeCallback — 字段变化回调

设计动机:将业务逻辑封装在属性中,而非散落在各处。

源码追踪:特性声明

来自 Core.cs 第 49-50 行:

[UdonSynced, FieldChangeCallback(nameof(Loop))]
bool loop;

源码追踪:属性实现

来自 Core.cs 第 116-128 行:

public bool Loop {
    get => loop;
    set {
        bool wasLoop = loop;
        loop = value;
        // 业务逻辑 1:同步到 Handler
        if (Utilities.IsValid(activeHandler)) activeHandler.Loop = value;
        // 业务逻辑 2:网络同步
        if (synced && wasLoop != value && Networking.IsOwner(gameObject))
            RequestSerialization();
        // 业务逻辑 3:AudioLink 同步
#if AUDIOLINK_V1
        if (IsAudioLinked()) audioLink.SetMediaLoop(value ? MediaLoop.LoopOne : MediaLoop.None);
#endif
    }
}

特性定义

// JLChnToZ.VRC.Foundation/FieldChangeCallbackAttribute.cs
namespace JLChnToZ.VRC.Foundation {
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class FieldChangeCallbackAttribute : Attribute {
        public string CallbackPropertyName { get; }
        public FieldChangeCallbackAttribute(string callbackPropertyName) => 
            CallbackPropertyName = callbackPropertyName;
    }
}

原理图解

┌─────────────────────────────────────────────────────────────────────┐
│                      FieldChangeCallback 原理                        │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  传统方式:                                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ loop = true;           ← 直接赋值,无业务逻辑                │   │
│  │ SyncToHandler();        ← 手动调用业务逻辑                   │   │
│  │ RequestSerialization(); ← 手动网络同步                       │   │
│  │ SyncAudioLink();        ← 手动 AudioLink 同步               │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                       │
│  FieldChangeCallback 方式:                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ Loop = true;             ← 通过属性赋值                     │   │
│  │       ↓                                                           │   │
│  │ 属性 setter 被调用                                               │   │
│  │       ↓                                                           │   │
│  │ 所有业务逻辑自动执行(同步 Handler、同步网络、同步 AudioLink)    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  源码映射:                                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ [UdonSynced, FieldChangeCallback(nameof(Loop))]              │   │
│  │ bool loop;  ← backing field,存储实际值                       │   │
│  │                                                              │   │
│  │ public bool Loop {   ← 属性,触发 [FieldChangeCallback]     │   │
│  │     get => loop;     ← 读取 backing field                   │   │
│  │     set { ... }      ← 赋值触发业务逻辑                      │   │
│  │ }                                                              │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Udon 适配

  • [UdonSynced] 字段 + [FieldChangeCallback] 实现网络同步与业务逻辑联动
  • 赋值通过属性触发,确保业务逻辑不遗漏

2.3 BindEvent — Unity 事件绑定

设计动机:自动绑定 Unity 组件的事件(如 Button.onClick)到方法。

源码追踪:特性声明

来自 ButtonEntry.cs 第 16 行:

[RequireComponent(typeof(Button))]
[BindEvent(typeof(Button), nameof(Button.onClick), nameof(_OnClick))]
public class ButtonEntry : UdonSharpBehaviour {
    public void _OnClick() { ... }
}

源码追踪:完整使用实例

来自 ButtonEntry.cs 第 1-109 行:

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using VRC.SDKBase;
using UdonSharp;
using JLChnToZ.VRC.Foundation;
using JLChnToZ.VRC.Foundation.I18N;

namespace JLChnToZ.VRC.VVMW {
    [UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
    [RequireComponent(typeof(Button))]
    [DisallowMultipleComponent]
    [BindEvent(typeof(Button), nameof(Button.onClick), nameof(_OnClick))]
    [AddComponentMenu("VizVid/Components/Button Entry")]
    [DefaultExecutionOrder(3)]
    public class ButtonEntry : UdonSharpBehaviour {
        LanguageManager manager;
        [SerializeField, LocalizedLabel] GameObject buttonGO;
        [SerializeField, HideInInspector, Resolve(nameof(buttonGO), NullOnly = false)] Text buttonText;
        [SerializeField, HideInInspector, Resolve(nameof(buttonGO), NullOnly = false)] TextMeshProUGUI buttonTMPro;
        
        /// <summary>回调目标</summary>
        [LocalizedLabel] public UdonSharpBehaviour callbackTarget;
        /// <summary>回调事件名</summary>
        [LocalizedLabel] public string callbackEventName;
        /// <summary>变量名</summary>
        [LocalizedLabel] public string callbackVariableName;
        /// <summary>自定义数据</summary>
        [LocalizedLabel] public object callbackUserData;

        /// <summary>本地化文本参数</summary>
        public object[] Args {
            get => args;
            set { args = value; _OnLanguageChanged(); }
        }

        /// <summary>本地化文本键</summary>
        public string Key {
            get => key;
            set { key = value; _OnLanguageChanged(); }
        }

        /// <summary>本地化文本</summary>
        public string Text {
            get {
                if (Utilities.IsValid(buttonText)) return buttonText.text;
                if (Utilities.IsValid(buttonTMPro)) return buttonTMPro.text;
                return "";
            }
        }

        /// <summary>语言管理器</summary>
        public LanguageManager LanguageManager {
            get => manager;
            set {
                manager = value;
                if (Utilities.IsValid(manager)) manager._AddListener(this);
                _OnLanguageChanged();
            }
        }

        void _OnLanguageChanged() {
            var result = manager.GetLocale(key);
            if (Utilities.IsValid(args) && args.Length > 0)
                result = string.Format(result, args);
            if (Utilities.IsValid(buttonText)) buttonText.text = result;
            if (Utilities.IsValid(buttonTMPro)) buttonTMPro.text = result;
        }

        /// <summary>按钮点击回调入口</summary>
        public void _OnClick() {
            if (!Utilities.IsValid(callbackTarget)) return;
            if (!string.IsNullOrEmpty(callbackVariableName))
                callbackTarget.SetProgramVariable(callbackVariableName, callbackUserData);
            if (!string.IsNullOrEmpty(callbackEventName))
                callbackTarget.SendCustomEvent(callbackEventName);
        }
    }
}

特性定义

// JLChnToZ.VRC.Foundation/BindEventAttribute.cs
namespace JLChnToZ.VRC.Foundation {
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class BindEventAttribute : Attribute {
        public Type ComponentType { get; }
        public string EventMemberName { get; }
        public string CallbackMethodName { get; }

        public BindEventAttribute(Type componentType, string eventMemberName, string callbackMethodName) {
            ComponentType = componentType;
            EventMemberName = eventMemberName;
            CallbackMethodName = callbackMethodName;
        }
    }
}

参数解析

参数 类型 说明
第1参数 Type 目标组件类型(如 typeof(Button)
第2参数 string 事件成员名称(如 nameof(Button.onClick)
第3参数 string 回调方法名称

2.4 本地化支持

LocalizedLabel — 本地化标签

设计动机:支持多语言 UI 显示。

源码追踪:简单用法

来自 AbstractMediaPlayerHandler.cs 第 29-34 行:

[LocalizedLabel]
#if COMPILER_UDONSHARP
public
#else
[SerializeField] internal
#endif
string playerName = "";

源码追踪:指定 key

来自 FrontendHandler.cs 第 20 行:

[LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")] public Core core;

编译期条件(核心技巧):

[LocalizedLabel]
#if COMPILER_UDONSHARP
public                    // UdonSharp:公开访问
#else
[SerializeField] internal // 编辑器:可序列化
#endif
string playerName = "";

这确保了:

  • UdonSharp 编译时:字段是 public,供其他 UdonSharpBehaviour 访问
  • 编辑器编译时:字段是 [SerializeField] internal,可在 Inspector 中编辑

LocalizedEnum — 本地化枚举

源码实例(FrontendHandler.cs):

[SerializeField, LocalizedLabel, LocalizedEnum] 
internal CoreMatchingStrategy coreControlStrategy = CoreMatchingStrategy.All;

2.5 编辑器增强

SingletonCoreControl — 单例核心控制

设计动机:编辑器据此提供特殊交互(如一键定位 Core)。

源码追踪:特性声明

来自 FrontendHandler.cs 第 34 行:

[SingletonCoreControl] public Core core;

源码追踪:编辑器类型发现

来自 EditorBase.cs 第 84-94 行:

if (type.IsSubclassOf(typeof(UdonSharpBehaviour))) {
    var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    if (fields.Length == 0) continue;
    foreach (var field in fields) {
        // 查找:Core 类型 + SingletonCoreControl 特性
        if (field.FieldType == typeof(Core) && 
            field.GetCustomAttribute<SingletonCoreControlAttribute>() != null) {
            // 建立映射:脚本类型 → (Core字段, 编辑器类型)
            controllableTypes[type] = (field, editorType);
            break;
        }
    }
}

特性定义

// JLChnToZ.VRC.Foundation/SingletonCoreControlAttribute.cs
namespace JLChnToZ.VRC.Foundation {
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class SingletonCoreControlAttribute : Attribute { }
}

HelpURL — 帮助链接

设计动机:在 Inspector 中添加文档链接。

源码实例(Core.cs 第 28 行):

[HelpURL("https://xtlcdn.github.io/VizVid/docs/#vvmw-game-object")]
public partial class Core : UdonSharpBehaviour { }

第3层:编译器条件架构

这是 VVMW 的精髓:同一份源码,两个身份

3.1 COMPILER_UDONSHARP 的双模式编译

#if COMPILER_UDONSHARP
    public           // UdonSharp:运行时公开访问
#else
    internal protected // 编辑器工具:程序集内访问
#endif
    Core core;

编译目标

┌─────────────────────────────────────────────────────────────────────┐
│                        双模式编译架构                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  源码文件                                                            │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ #if COMPILER_UDONSHARP                                        │  │
│  │     public Core core;           ← 编译为 Udon Bytecode        │  │
│  │ #else                                                         │  │
│  │     internal Core core;         ← 编译为 .NET 程序集          │  │
│  │ #endif                                                        │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                        ↓                         ↓                   │
│         ┌─────────────────────┐     ┌─────────────────────┐        │
│         │ Udon Bytecode       │     │ .NET 程序集          │        │
│         │ (VRChat 运行时)      │     │ (Unity 编辑器)       │        │
│         └─────────────────────┘     └─────────────────────┘        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

3.2 条件可见性模式

VVMW 定义了四种条件可见性模式:

// 模式 1:简单 public/internal
#if COMPILER_UDONSHARP
    public
#else
    internal
#endif
    void MyMethod() { }

// 模式 2:带 protected
#if COMPILER_UDONSHARP
    public
#else
    internal protected
#endif
    Core core;

// 模式 3:完全相反
#if COMPILER_UDONSHARP
    internal
#else
    public
#endif
    Core core;

3.3 跨模式设计实例

VizVidBehaviour — 行为基类

来自 VizVidBehaviour.cs

namespace JLChnToZ.VRC.VVMW {
    // 运行时:普通 UdonSharpBehaviour 基类
    public abstract class VizVidBehaviour : UdonSharpBehaviour {
        public virtual void OnVideoStart() { }
        public virtual void OnVideoPlay() { }
        public virtual void OnVideoPause() { }
        public virtual void OnVideoEnd() { }
    }
    
#if UNITY_EDITOR && !COMPILER_UDONSHARP
    // 编辑器:额外实现接口
    public abstract partial class VizVidBehaviour : IVizVidCompoonent {
        Core IVizVidCompoonent.Core => core;
    }
#endif
}

AbstractMediaPlayerHandler — 媒体处理器

来自 AbstractMediaPlayerHandler.cs

// 运行时:声明属性
#if COMPILER_UDONSHARP
    public
#else
    internal protected
#endif
    override bool IsActive { get => isActive; set => isActive = value; }

// 编辑器:声明字段
#if COMPILER_UDONSHARP
    [NonSerialized] public
#else
    internal protected
#endif
    Core core;

第4层:编辑器工具链

4.1 IPreprocessor — 场景预处理器

设计动机:在场景加载前执行数据预处理。

接口定义

// JLChnToZ.VRC.Foundation/IPreprocessor.cs
namespace JLChnToZ.VRC.Foundation {
    public interface IPreprocessor {
        int Priority { get; }
        void OnPreprocess(Scene scene);
    }
}

源码追踪:VVMW 实现

来自 CoreConfigPreprocessor.cs 第 1-41 行:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UdonSharpEditor;
using JLChnToZ.VRC.Foundation.Editors;

namespace JLChnToZ.VRC.VVMW.Editors {
    internal class CoreConfigPreprocessor : IPreprocessor {
        public int Priority => 99;  // 执行优先级

        public void OnPreprocess(Scene scene) {
            // 遍历场景中所有 Core 组件
            foreach (var core in scene.IterateAllComponents<Core>()) {
                
                // === 预处理 1:音量持久化 ===
#if VRC_ENABLE_PLAYER_PERSISTENCE
                if (core.enablePersistence) {
                    var pathStack = new Stack<string>();
                    for (var transform = core.transform; transform; transform = transform.parent)
                        pathStack.Push(transform.name);
                    var path = string.Join("/", pathStack);
                    core.volumePersistenceKey = $"VVMW:{path}:Volume";
                    core.mutedPersistenceKey = $"VVMW:{path}:Muted";
                }
#endif
                
                // === 预处理 2:音频控制器收集 ===
                var audioControllers = new List<AbstractAudioController>();
                var audioSources = new HashSet<AudioSource>(core.audioSources);
                foreach (var audioSource in core.audioSources) {
                    if (audioSource == null || !audioSource.TryGetComponent(out AbstractAudioController controller))
                        continue;
                    // 自动注入 Core 引用
                    controller.core = core;
                    audioSources.Remove(audioSource);
                    audioControllers.Add(controller);
                    // 同步到 Udon
                    UdonSharpEditorUtility.CopyProxyToUdon(controller);
                }
                core.audioSources = new AudioSource[audioSources.Count];
                audioSources.CopyTo(core.audioSources);
                core.audioControllers = audioControllers.ToArray();
                
                // === 预处理 3:区域配置检测 ===
                core.hasRegion = ActiveRegionConfig.GetRegionConfigs(core).Count > 0;
                
                // === 预处理 4:默认音量处理 ===
                if (!core.muteOnOutOfRange) core.outOfRangeVolume = 1f;
                
                // === 预处理 5:同步到 Udon ===
                UdonSharpEditorUtility.CopyProxyToUdon(core);
            }
        }
    }
}

预处理流程图解

┌─────────────────────────────────────────────────────────────────────┐
│                    IPreprocessor 执行流程                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  场景加载前                                                          │
│       │                                                             │
│       ↓                                                             │
│  VRCFoundation.Preprocessors.OnPreprocess(scene)                     │
│       │                                                             │
│       ├── CoreConfigPreprocessor.Priority = 99                      │
│       │       │                                                     │
│       │       ↓                                                     │
│       │   1. 音量持久化路径生成                                       │
│       │   2. 音频控制器收集 + Core 注入                              │
│       │   3. 区域配置检测                                            │
│       │   4. 默认值处理                                              │
│       │   5. CopyProxyToUdon 同步                                    │
│       │                                                             │
│       └── [其他 Preprocessor...]                                     │
│                                                                      │
│       ↓                                                             │
│  场景加载完成(数据已预处理)                                          │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

使用场景

  • 引用填充(查找依赖组件)
  • 数据校验(确保配置完整)
  • 性能优化(预计算、缓存)

4.2 CustomEditor — 自定义编辑器

设计动机:提供品牌化的 Inspector UI。

源码追踪:编辑器基类

来自 EditorBase.cs 第 14-224 行(核心部分):

namespace JLChnToZ.VRC.VVMW.Editors {
    public abstract class VVMWEditorBase : Editor {
        // === 静态资源 ===
        const string bannerTextureGUID = "e8354bc2ac14e86498c0983daf484661";
        const string iconGUID = "a24ecd1d23cca9e46871bc17dfe3bd46";
        const string fontGUID = "088cf7162d0a81c46ad54028cfdcb382";
        
        // === 类型映射 ===
        protected static readonly Dictionary<Type, (FieldInfo fieldInfo, Type editorType)> controllableTypes = new Dictionary<Type, (FieldInfo, Type)>();
        protected static readonly Dictionary<Type, Type> editorTypes = new Dictionary<Type, Type>();
        
        // === 启动时类型收集 ===
        [InitializeOnLoadMethod]
        static void Init() {
            AssemblyReloadEvents.afterAssemblyReload += GatherControlledTypes;
            GatherControlledTypes();
        }
        
        // === 类型发现核心逻辑 ===
        static void GatherControlledTypes() {
            controllableTypes.Clear();
            const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            
            // 遍历所有程序集中的所有类型
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
                foreach (var type in assembly.GetTypes()) {
                    if (type.IsAbstract) continue;
                    
                    // === 查找 UdonSharpBehaviour ===
                    if (type.IsSubclassOf(typeof(UdonSharpBehaviour))) {
                        var fields = type.GetFields(flags);
                        foreach (var field in fields) {
                            // 查找:Core 类型 + SingletonCoreControl 特性
                            if (field.FieldType == typeof(Core) && 
                                field.GetCustomAttribute<SingletonCoreControlAttribute>() != null) {
                                editorTypes.TryGetValue(type, out var editorType);
                                controllableTypes[type] = (field, editorType);
                                break;
                            }
                        }
                    }
                }
        }
        
        // === Banner 绘制 ===
        protected static void DrawBanner() {
            // 绘制 Logo
            var bannerRect = new Rect(...);
            GUI.DrawTexture(bannerRect, bannerTexture);
            
            // 绘制版本号
            var versionStyle = new GUIStyle(EditorStyles.whiteLargeLabel) { ... };
            GUI.Label(new Rect(...), $"v{selfUpdater.CurrentVersion}", versionStyle);
        }
        
        // === Inspector GUI ===
        public override void OnInspectorGUI() {
            // 绘制 Banner
            DrawBanner();
            
            // UdonSharp 特殊处理
            if (isUdonSharp) {
                if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target, true, false))
                    return;
            }
            
            DrawInspectorGUI();
        }
    }
}

源码追踪:Core 编辑器实现

来自 CoreEditor.cs 第 138-150 行:

public override void DrawEmbeddedInspectorGUI() {
    if (HandleDragDrop()) return;
    
    var autoPlayControllerEditor = GetAutoPlayControllerEditor();
    if (autoPlayControllerEditor != null)
        autoPlayControllerEditor.serializedObject.Update();
        
    DrawCommonSettings(autoPlayControllerEditor);      // 通用设置
    HorizontalLine();
    DrawDefaultBehaviourSettings(autoPlayControllerEditor);  // 默认行为
    HorizontalLine();
    DrawAdvancedSettings(autoPlayControllerEditor);   // 高级设置
    
    if (autoPlayControllerEditor != null)
        autoPlayControllerEditor.serializedObject.ApplyModifiedProperties();
}

4.3 启动钩子

InitializeOnLoadMethod

设计动机:编辑器启动时执行初始化。

源码追踪

来自 EditorBase.cs 第 30-34 行:

[InitializeOnLoadMethod]
static void Init() {
    // 订阅程序集重载事件
    AssemblyReloadEvents.afterAssemblyReload += GatherControlledTypes;
    // 执行类型收集
    GatherControlledTypes();
}

DidReloadScripts

设计动机:脚本重新编译后执行刷新。

源码追踪(来自其他 VVMW 组件):

[DidReloadScripts]
public static void RefreshAll() {
    regionConfigTable.Clear();
    foreach (var config in FindObjectsOfType<ActiveRegionConfig>(true))
        config.Register();
}

第6层:代码流程解析

本章通过完整流程图和伪代码,详细解析 VRC.Foundation 特性系统如何从"声明"变成"功能"。

6.1 完整生命周期总览

┌─────────────────────────────────────────────────────────────────────────────┐
│                    VRC.Foundation 特性系统完整生命周期                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  阶段 1:编辑器启动(一次性)                                                 │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ [InitializeOnLoadMethod]                                              │   │
│  │        ↓                                                              │   │
│  │ GatherControlledTypes()                                              │   │
│  │        ↓                                                              │   │
│  │ 遍历所有程序集 → 找到 UdonSharpBehaviour → 查找 [SingletonCoreControl] │   │
│  │        ↓                                                              │   │
│  │ 建立映射:controllableTypes[type] = (fieldInfo, editorType)           │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                              ↓                                               │
│  阶段 2:场景加载前(每个场景一次)                                           │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ IPreprocessor.OnPreprocess(scene)                                    │   │
│  │        ↓                                                              │   │
│  │ CoreConfigPreprocessor.OnPreprocess()                                │   │
│  │        ↓                                                              │   │
│  │ 遍历场景中所有 Core → 填充 audioControllers → CopyProxyToUdon()      │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                              ↓                                               │
│  阶段 3:组件检视(持续)                                                    │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ CustomEditor.OnInspectorGUI()                                        │   │
│  │        ↓                                                              │   │
│  │ DrawEmbeddedInspectorGUI()                                           │   │
│  │        ↓                                                              │   │
│  │ PropertyDrawer 处理特性字段                                            │   │
│  │   ├── LocatableDrawer → 拖拽检测 → 自动解析                           │   │
│  │   ├── ResolveDrawer   → 引用解析 → 填充 GameObject                    │   │
│  │   └── BindUdonSharpEventDrawer → 反射方法 → 订阅事件                  │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                              ↓                                               │
│  阶段 4:运行时执行(Udon VM)                                               │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ Udon Bytecode 执行                                                    │   │
│  │        ↓                                                              │   │
│  │ [UdonSynced] + [FieldChangeCallback] → 属性赋值触发业务逻辑            │   │
│  │ BindUdonSharpEvent 订阅的方法 → 事件触发时自动调用                    │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

6.2 GatherControlledTypes — 类型发现流程

6.2.1 源码追踪

来自 EditorBase.cs 第 30-34、55-94 行:

// === 启动时注册 ===
[InitializeOnLoadMethod]
static void Init() {
    // 订阅程序集重载事件(脚本修改后自动重新收集)
    AssemblyReloadEvents.afterAssemblyReload += GatherControlledTypes;
    // 立即执行一次
    GatherControlledTypes();
}

// === 类型收集核心逻辑 ===
static void GatherControlledTypes() {
    controllableTypes.Clear();
    const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    
    // 遍历所有已加载的程序集
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        foreach (var type in assembly.GetTypes()) {
            if (type.IsAbstract) continue;
            
            // === 步骤 1:查找 UdonSharpBehaviour ===
            if (type.IsSubclassOf(typeof(UdonSharpBehaviour))) {
                var fields = type.GetFields(flags);
                if (fields.Length == 0) continue;
                
                // === 步骤 2:遍历字段查找 [SingletonCoreControl] ===
                foreach (var field in fields) {
                    // 查找条件:Core 类型 + SingletonCoreControl 特性
                    if (field.FieldType == typeof(Core) && 
                        field.GetCustomAttribute<SingletonCoreControlAttribute>() != null) {
                        
                        // === 步骤 3:建立映射 ===
                        editorTypes.TryGetValue(type, out var editorType);
                        controllableTypes[type] = (field, editorType);
                        break;
                    }
                }
            }
            
            // === 步骤 4:收集 CustomEditor 类型 ===
            if (type.IsSubclassOf(typeof(VVMWEditorBase))) {
                var customEditorAttribute = type.GetCustomAttribute<CustomEditor>();
                if (customEditorAttribute == null) continue;
                
                var inspectedType = inspectedTypeField.GetValue(customEditorAttribute) as Type;
                
                // 更新映射
                if (controllableTypes.TryGetValue(inspectedType, out var value))
                    controllableTypes[inspectedType] = (value.fieldInfo, type);
                editorTypes[inspectedType] = type;
                
                // 处理继承关系
                if ((bool)editorForChildClassesField.GetValue(customEditorAttribute)) {
                    inheritedEditors[inspectedType] = type;
                    // 为所有子类建立映射...
                }
            }
        }
}

6.2.2 流程图解

┌─────────────────────────────────────────────────────────────────────┐
│                  GatherControlledTypes 执行流程                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  启动时触发                                                           │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ for each assembly in AppDomain.CurrentDomain.GetAssemblies() │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   for each type in assembly.GetTypes()                       │   │
│  │       │                                                       │   │
│  │       ├───[否]─ type.IsSubclassOf(UdonSharpBehaviour)? ──┐   │   │
│  │       │                                                       │   │
│  │       │   [是]                                                │   │
│  │       │       │                                               │   │
│  │       │       ↓                                               │   │
│  │       │   for each field in type.GetFields()                 │   │
│  │       │       │                                               │   │
│  │       │       ├───[否]─ field 是 Core 类型? ──┐              │   │
│  │       │       │                               │              │   │
│  │       │       │   [是]                          │              │   │
│  │       │       │       │                       │              │   │
│  │       │       │       ↓                       │              │   │
│  │       │       │   [否]─ 有 SingletonCoreControl? ─→ 跳过       │   │
│  │       │       │       │                                       │   │
│  │       │       │   [是]                                        │   │
│  │       │       │       │                                       │   │
│  │       │       │       ↓                                       │   │
│  │       │       │   建立映射: controllableTypes[type] = ...    │   │
│  │       │       │                                                       │   │
│  │       └───[是]─ 是 VVMWEditorBase?                               │   │
│  │               │                                                       │   │
│  │               ↓                                                       │   │
│  │           收集 CustomEditor 类型                                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  完成:controllableTypes 和 editorTypes 已填充                       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.3 Locatable + Resolve — 引用解析完整流程

6.3.1 从声明到解析的完整链路

AutoPlayOnNearV2.cs 第 16-21 行为例:

// 开发者编写的声明
[SerializeField, LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")]
[Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]
[Locatable] Core core;

// 隐式声明(编辑器自动生成)
[SerializeField, HideInInspector, Resolve(nameof(core))] GameObject coreGO;
[SerializeField, HideInInspector, Resolve(nameof(handler))] GameObject handlerGO;
[SerializeField, HideInInspector, BindUdonSharpEvent(nameof(_OnMatchingCoresChanged))] 
ActiveRegionManager activeRegionManager;

6.3.2 ResolveDrawer 解析流程

┌─────────────────────────────────────────────────────────────────────┐
│                    ResolveDrawer 解析流程                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. 属性绘制器被调用                                                   │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ public override void OnGUI(Rect position, SerializedProperty│   │
│  │                              property, GUIContent label)     │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   var attribute = fieldInfo.GetCustomAttribute<ResolveAttribute>();│
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   // 获取 Path                                                │   │
│  │   var path = attribute.Path;  // 如 "handler.core" 或 "."    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  2. Path 解析                                                        │
│       │                                                             │
│       ├─── "." ───────────────────────────→ 解析自身组件              │
│       │                                                             │
│       ├─── "nameof(core)" ────────────────→ 解析 core 字段           │
│       │                                                             │
│       └─── "handler.core" ────────────────→ 链式解析(见下方)        │
│                                                                      │
│  3. 链式解析(handler.core)                                          │
│       │                                                             │
│       ┌─────────────────────────────────────────────────────────┐   │
│       │ 步骤 1: 解析 "handler" → FrontendHandler 实例            │   │
│       │           ↓                                              │   │
│       │ 步骤 2: 访问 ".core" → FrontendHandler.core → Core 实例  │   │
│       │           ↓                                              │   │
│       │ 步骤 3: 获取 Core 的 GameObject                          │   │
│       │           ↓                                              │   │
│       │ 步骤 4: 赋值给 coreGO 字段                                │   │
│       └─────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  4. 条件隐藏                                                          │
│       │                                                             │
│       if (attribute.HideInInspectorIfResolvable && resolved)        │
│           editorGUI.BeginDisabledGroup(true);                       │
│       │                                                             │
│       ↓                                                             │
│  5. 绘制 UI 并返回                                                    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.3.3 LocatableDrawer 拖拽检测流程

┌─────────────────────────────────────────────────────────────────────┐
│                    LocatableDrawer 拖拽检测流程                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  OnGUI() 被调用                                                      │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ var e = Event.current;                                       │   │
│  │                                                               │   │
│  │ switch (e.type) {                                            │   │
│  │   case EventType.DragUpdated:                                 │   │
│  │       // 拖拽进入                                               │   │
│  │       if (IsValidDrag(e)) {                                   │   │
│  │           DragAndDrop.visualMode = DragAndDropVisualMode.Link;│   │
│  │           e.Use();                                            │   │
│  │       }                                                        │   │
│  │       break;                                                   │   │
│  │                                                               │   │
│  │   case EventType.DragPerform:                                 │   │
│  │       // 拖拽释放                                               │   │
│  │       if (IsValidDrag(e)) {                                   │   │
│  │           var target = DragAndDrop.objectReferences[0];       │   │
│  │           ResolveAndAssign(target);  // 解析并赋值              │   │
│  │           e.Use();                                            │   │
│  │       }                                                        │   │
│  │       break;                                                   │   │
│  │ }                                                              │   │
│  └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  ResolveAndAssign():                                                 │
│       │                                                             │
│       ┌─────────────────────────────────────────────────────────┐   │
│       │ if (NullOnly && currentValue != null) return;          │   │
│       │                                                           │   │
│       │ var resolved = Resolve(Path);  // 解析引用                │   │
│       │ if (resolved != null) {                                  │   │
│       │     serializedObject.Update();                            │   │
│       │     property.objectReferenceValue = resolved;            │   │
│       │     serializedObject.ApplyModifiedProperties();           │   │
│       │ }                                                         │   │
│       └─────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.4 BindUdonSharpEvent — 事件订阅完整流程

6.4.1 源码追踪:特性处理逻辑

// 伪代码:VRC.Foundation 编辑器的 BindUdonSharpEvent 处理逻辑
void ProcessBindUdonSharpEvent(UdonSharpBehaviour component, FieldInfo field, 
                                BindUdonSharpEventAttribute attr) {
    // === 步骤 1:获取字段值(被绑定的 Core 实例) ===
    var targetComponent = field.GetValue(component) as UdonSharpBehaviour;
    if (targetComponent == null) return;
    
    // === 步骤 2:获取事件发送者(Core 或其基类)===
    var eventSender = GetEventSender(targetComponent);
    if (eventSender == null) return;
    
    // === 步骤 3:遍历特性中声明的所有事件名 ===
    foreach (var eventName in attr.EventNames) {
        // === 步骤 4:通过反射查找回调方法 ===
        var callbackMethod = component.GetType().GetMethod(
            eventName,
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
        );
        
        if (callbackMethod == null) {
            Debug.LogWarning($"Method '{eventName}' not found on {component.GetType().Name}");
            continue;
        }
        
        // === 步骤 5:注册到事件系统 ===
        RegisterEventListener(eventSender, eventName, component, callbackMethod);
    }
}

6.4.2 流程图解

┌─────────────────────────────────────────────────────────────────────┐
│              BindUdonSharpEvent 事件订阅完整流程                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  场景加载 / 组件添加                                                  │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ for each UdonSharpBehaviour in scene                         │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   for each field in behaviour.GetType().GetFields()          │   │
│  │       │                                                       │   │
│  │       ├───[否]─ 有 BindUdonSharpEvent? ──┐                  │   │
│  │       │                                   │                  │   │
│  │       │   [是]                            │                  │   │
│  │       │       │                           │                  │   │
│  │       │       ↓                           │                  │   │
│  │       │   获取 attr.EventNames[]                                 │   │
│  │       │       │                           │                  │   │
│  │       │       ↓                           │                  │   │
│  │       │   for each eventName in attr.EventNames                │   │
│  │       │       │                           │                  │   │
│  │       │       ↓                           │                  │   │
│  │       │   var method = component.GetMethod(eventName)         │   │
│  │       │       │                           │                  │   │
│  │       │       ├───[失败]─ 跳过              │                  │   │
│  │       │       │                           │                  │   │
│  │       │       [成功]                        │                  │   │
│  │       │       │                           │                  │   │
│  │       │       ↓                           │                  │   │
│  │       │   // 注册到 VRC.Foundation 事件系统                    │   │
│  │       │   EventRegistry.Register(component, eventName, method) │   │
│  └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  事件触发时(Core.SendEvent("_OnMatchingCoresChanged"))             │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ var listeners = EventRegistry.GetListeners("_OnMatching...") │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ for each listener in listeners                               │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   listener.method.Invoke(listener.component, null);         │   │
│  │   // → AutoPlayOnNearV2._OnMatchingCoresChanged() 被调用        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.5 FieldChangeCallback — 属性化同步流程

6.5.1 编译时 vs 运行时

┌─────────────────────────────────────────────────────────────────────┐
│              FieldChangeCallback 编译时 vs 运行时                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 编译时(UdonSharp 编译器)                                      │   │
│  │                                                              │   │
│  │ 源码:                                                         │   │
│  │ ┌─────────────────────────────────────────────────────────┐ │   │
│  │ │ [UdonSynced, FieldChangeCallback(nameof(Loop))]         │ │   │
│  │ │ bool loop;                                              │ │   │
│  │ │                                                         │ │   │
│  │ │ public bool Loop {                                      │ │   │
│  │ │     get => loop;                                        │ │   │
│  │ │     set {                                               │ │   │
│  │ │         loop = value;                                   │ │   │
│  │ │         if (Utilities.IsValid(activeHandler))            │ │   │
│  │ │             activeHandler.Loop = value;                 │ │   │
│  │ │         if (synced && ... && Networking.IsOwner(...))    │ │   │
│  │ │             RequestSerialization();                      │ │   │
│  │ │     }                                                    │ │   │
│  │ │ }                                                        │ │   │
│  │ └─────────────────────────────────────────────────────────┘ │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ UdonSharp 编译器:                                             │   │
│  │ - 识别 [UdonSynced] → 生成同步逻辑                           │   │
│  │ - 识别 [FieldChangeCallback] → 生成 setter 包装              │   │
│  │ - 将所有对 loop 的赋值改为 Loop = value                       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 运行时(Udon VM)                                             │   │
│  │                                                              │   │
│  │ 玩家 A 调用: Loop = true;                                     │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ Udon Bytecode:                                               │   │
│  │ ┌─────────────────────────────────────────────────────────┐ │   │
│  │ │ // setter 被调用                                         │ │   │
│  │ │ set_Loop(true);                                          │ │   │
│  │ │ // setter 内部:                                          │ │   │
│  │ │     loop = true;                                         │ │   │
│  │ │     activeHandler.Loop = true;  // 同步到视频处理器        │ │   │
│  │ │     if (owner) RequestSerialization();  // 网络同步       │ │   │
│  │ └─────────────────────────────────────────────────────────┘ │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ RequestSerialization() → 序列化数据 → 发送给其他客户端          │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ 玩家 B 收到同步 → OnDeserialization() → Loop 属性被赋值      │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │ B 的 activeHandler.Loop 也被设置为 true                        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.5.2 核心代码映射

来自 Core.cs 第 49-50、116-128 行:

// === 声明 ===
[UdonSynced, FieldChangeCallback(nameof(Loop))]
bool loop;

// === 属性实现 ===
public bool Loop {
    get => loop;
    set {
        bool wasLoop = loop;
        loop = value;
        
        // 业务逻辑 1:同步到视频处理器
        if (Utilities.IsValid(activeHandler)) activeHandler.Loop = value;
        
        // 业务逻辑 2:网络同步
        if (synced && wasLoop != value && Networking.IsOwner(gameObject))
            RequestSerialization();
        
        // 业务逻辑 3:AudioLink 同步
#if AUDIOLINK_V1
        if (IsAudioLinked()) audioLink.SetMediaLoop(value ? MediaLoop.LoopOne : MediaLoop.None);
#endif
    }
}

编译后(伪代码)

// UdonSharp 生成的 setter
void set_Loop(bool value) {
    // 调用属性 setter
    this.Loop.set(value);  // 触发完整业务逻辑
}

// 当需要同步 loop 时,直接调用 setter
void SyncLoopToNetwork() {
    set_Loop(true);  // 自动触发 RequestSerialization()
}

6.6 完整示例:AutoPlayOnNearV2 的特性解析全过程

6.6.1 原始声明

来自 AutoPlayOnNearV2.cs 第 16-22 行:

[SerializeField, LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")]
[Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]
[Locatable] Core core;

[SerializeField, HideInInspector, Resolve(nameof(core))] GameObject coreGO;
[SerializeField, HideInInspector, Resolve(nameof(handler))] GameObject handlerGO;
[SerializeField, HideInInspector, BindUdonSharpEvent(nameof(_OnMatchingCoresChanged))] 
ActiveRegionManager activeRegionManager;

6.6.2 解析时间线

┌─────────────────────────────────────────────────────────────────────────────┐
│                  AutoPlayOnNearV2 特性解析完整时间线                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  T0: 编辑器启动                                                              │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ [InitializeOnLoadMethod] Init()                                        │   │
│  │       ↓                                                              │   │
│  │ GatherControlledTypes()                                               │   │
│  │       ↓                                                              │   │
│  │ 发现 AutoPlayOnNearV2 是 UdonSharpBehaviour                           │   │
│  │       ↓                                                              │   │
│  │ 查找字段:                                                              │   │
│  │   - handler (FrontendHandler) + 无特性 → 跳过                         │   │
│  │   - core (Core) + [Locatable] → 建立映射                               │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  T1: 场景加载                                                                │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ IPreprocessor.OnPreprocess(scene)                                     │   │
│  │       ↓                                                              │   │
│  │ CoreConfigPreprocessor.OnPreprocess()                                 │   │
│  │       ↓                                                              │   │
│  │ 为场景中每个 Core:                                                     │   │
│  │   - 收集 audioControllers                                             │   │
│  │   - 检测 hasRegion                                                     │   │
│  │   - CopyProxyToUdon()                                                 │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  T2: AutoPlayOnNearV2 被添加到 GameObject                                    │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ ResolveDrawer 处理 core 字段:                                          │   │
│  │   - Path = "handler.core"                                              │   │
│  │   - 链式解析: handler → FrontendHandler.core → Core                   │   │
│  │   - 赋值: core = resolvedCore                                         │   │
│  │       ↓                                                              │   │
│  │ ResolveDrawer 处理 coreGO 字段:                                        │   │
│  │   - Path = "core"                                                     │   │
│  │   - 解析: core.gameObject                                              │   │
│  │   - 赋值: coreGO = core.gameObject                                     │   │
│  │       ↓                                                              │   │
│  │ ResolveDrawer 处理 handlerGO 字段:                                     │   │
│  │   - Path = "handler"                                                   │   │
│  │   - 解析: handler.gameObject                                           │   │
│  │   - 赋值: handlerGO = handler.gameObject                             │   │
│  │       ↓                                                              │   │
│  │ BindUdonSharpEventDrawer 处理 activeRegionManager:                    │   │
│  │   - eventName = "_OnMatchingCoresChanged"                              │   │
│  │   - 反射查找方法: AutoPlayOnNearV2._OnMatchingCoresChanged             │   │
│  │   - 注册到事件系统                                                     │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
│  T3: 运行时执行                                                              │
│  ┌──────────────────────────────────────────────────────────────────────┐   │
│  │ AutoPlayOnNearV2.OnEnable()                                           │   │
│  │       ↓                                                              │   │
│  │ isSynced = core.IsSynced;  // 使用自动解析的 core 引用                 │   │
│  │       ↓                                                              │   │
│  │ ...                                                                   │   │
│  │       ↓                                                              │   │
│  │ ActiveRegionManager._UpdateMatching()                                 │   │
│  │       ↓                                                              │   │
│  │ SendEvent("_OnMatchingCoresChanged");                                │   │
│  │       ↓                                                              │   │
│  │ VRC.Foundation 事件系统                                               │   │
│  │       ↓                                                              │   │
│  │ AutoPlayOnNearV2._OnMatchingCoresChanged() 被调用                      │   │
│  └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

6.6.3 隐式字段的生成逻辑

// 开发者只需声明带特性的字段
[Locatable] Core core;

// 编辑器自动"生成"(或理解为处理)隐式字段
[SerializeField, HideInInspector, Resolve(nameof(core))] GameObject coreGO;

// 这个"生成"是在什么时机发生的?
// 
// 答案:在 PropertyDrawer 处理时动态创建,而不是真正修改源文件。
// 
// 机制:
// 1. EditorBase.GatherControlledTypes() 扫描所有类型
// 2. 发现 AutoPlayOnNearV2 有 [Locatable] Core core
// 3. 在编辑器中为其创建对应的 _GO 字段引用
// 4. PropertyDrawer 检测到 [Resolve(nameof(core))] GameObject coreGO
// 5. 解析 core 并赋值给 coreGO

6.7 场景预处理完整流程

6.7.1 源码追踪

来自 CoreConfigPreprocessor.cs 第 11-39 行:

public void OnPreprocess(Scene scene) {
    // === 遍历场景中所有 Core 组件 ===
    foreach (var core in scene.IterateAllComponents<Core>()) {
        
        // === 预处理 1:音量持久化路径 ===
#if VRC_ENABLE_PLAYER_PERSISTENCE
        if (core.enablePersistence) {
            // 生成层级路径作为持久化 key 前缀
            var pathStack = new Stack<string>();
            for (var transform = core.transform; transform; transform = transform.parent)
                pathStack.Push(transform.name);
            var path = string.Join("/", pathStack);
            core.volumePersistenceKey = $"VVMW:{path}:Volume";
            core.mutedPersistenceKey = $"VVMW:{path}:Muted";
        }
#endif
        
        // === 预处理 2:音频控制器收集 ===
        var audioControllers = new List<AbstractAudioController>();
        var audioSources = new HashSet<AudioSource>(core.audioSources);
        
        foreach (var audioSource in core.audioSources) {
            if (audioSource == null || 
                !audioSource.TryGetComponent(out AbstractAudioController controller))
                continue;
            
            // 自动注入 Core 引用
            controller.core = core;
            audioSources.Remove(audioSource);
            audioControllers.Add(controller);
            
            // 关键:将编辑器数据同步到 Udon
            UdonSharpEditorUtility.CopyProxyToUdon(controller);
        }
        
        core.audioSources = new AudioSource[audioSources.Count];
        audioSources.CopyTo(core.audioSources);
        core.audioControllers = audioControllers.ToArray();
        
        // === 预处理 3:区域配置检测 ===
        core.hasRegion = ActiveRegionConfig.GetRegionConfigs(core).Count > 0;
        
        // === 预处理 4:默认值处理 ===
        if (!core.muteOnOutOfRange) core.outOfRangeVolume = 1f;
        
        // === 预处理 5:同步到 Udon ===
        UdonSharpEditorUtility.CopyProxyToUdon(core);
    }
}

6.7.2 流程图解

┌─────────────────────────────────────────────────────────────────────┐
│                    场景预处理执行流程                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  场景加载前(Build 或 Play)                                         │
│       │                                                             │
│       ↓                                                             │
│  VRCFoundation.Preprocessors.OnPreprocess(scene)                    │
│       │                                                             │
│       ↓                                                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ CoreConfigPreprocessor.OnPreprocess(scene)                   │   │
│  │       │                                                       │   │
│  │       ↓                                                       │   │
│  │   for each core in scene.IterateAllComponents<Core>()        │   │
│  │       │                                                       │   │
│  │       ┌─────────────────────────────────────────────────┐   │   │
│  │       │ 步骤 1: 持久化路径生成                           │   │   │
│  │       │   if (enablePersistence)                        │   │   │
│  │       │       path = "Parent/Child/Core"                │   │   │
│  │       │       volumePersistenceKey = "VVMW:path:Volume" │   │   │
│  │       └─────────────────────────────────────────────────┘   │   │
│  │       │                                                       │   │
│  │       ┌─────────────────────────────────────────────────┐   │   │
│  │       │ 步骤 2: 音频控制器收集                           │   │   │
│  │       │   audioControllers = []                        │   │   │
│  │       │   for each audioSource in core.audioSources     │   │   │
│  │       │       if (has AbstractAudioController) {        │   │   │
│  │       │           controller.core = core               │   │   │
│  │       │           audioControllers.Add(controller)    │   │   │
│  │       │           CopyProxyToUdon(controller)         │   │   │
│  │       │       }                                        │   │   │
│  │       └─────────────────────────────────────────────────┘   │   │
│  │       │                                                       │   │
│  │       ┌─────────────────────────────────────────────────┐   │   │
│  │       │ 步骤 3: 区域配置检测                             │   │   │
│  │       │   hasRegion = GetRegionConfigs(core).Count > 0  │   │   │
│  │       └─────────────────────────────────────────────────┘   │   │
│  │       │                                                       │   │
│  │       ┌─────────────────────────────────────────────────┐   │   │
│  │       │ 步骤 4: 同步到 Udon                              │   │   │
│  │       │   CopyProxyToUdon(core)                        │   │   │
│  │       └─────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────┘   │
│       │                                                             │
│       ↓                                                             │
│  场景加载完成(所有数据已预处理)                                      │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

6.8 CopyProxyToUdon — 数据同步机制

6.8.1 机制解释

┌─────────────────────────────────────────────────────────────────────┐
│                    CopyProxyToUdon 机制                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  UdonSharp 的双对象模型:                                             │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     Editor 中的对象                           │   │
│  │                                                               │   │
│  │  ┌──────────────────┐     ┌──────────────────┐             │   │
│  │  │ ProxyObject     │ ←──→ │ UdonBehaviour    │             │   │
│  │  │ (MonoBehaviour)  │     │ (UdonSharp)       │             │   │
│  │  │                  │     │                  │             │   │
│  │  │ core (字段)      │     │ _core (Udon变量)  │             │   │
│  │  │ audioControllers │     │ _audioControllers│             │   │
│  │  │ hasRegion        │     │ _hasRegion        │             │   │
│  │  └──────────────────┘     └──────────────────┘             │   │
│  │               ↑                   ↑                          │   │
│  │               │                   │                          │   │
│  │               └──── CopyProxyToUdon() ────┘                 │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  为什么需要这个?                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 1. UdonSharp 字段在编辑器中显示为 Proxy 对象                 │   │
│  │ 2. 预处理修改的是 Editor 中的数据                             │   │
│  │ 3. 需要同步到 UdonBehaviour 的内部变量                        │   │
│  │ 4. CopyProxyToUdon() 就是做这个同步                          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

第5层:工程实践

5.1 特性组合公式

基础组合:Core 引用(完整版)

来自 AutoPlayOnNearV2.cs 第 16-21 行:

// 声明层:开发者编写的特性组合
[SerializeField, LocalizedLabel(Key = "JLChnToZ.VRC.VVMW.Core")]
[Resolve(nameof(handler) + "." + nameof(FrontendHandler.core), HideInInspectorIfResolvable = true)]
[Locatable] Core core;

// 隐藏的 GameObject 引用(自动生成)
[SerializeField, HideInInspector, Resolve(nameof(core))] GameObject coreGO;
[SerializeField, HideInInspector, Resolve(nameof(handler))] GameObject handlerGO;
[SerializeField, HideInInspector, BindUdonSharpEvent(nameof(_OnMatchingCoresChanged))] 
ActiveRegionManager activeRegionManager;

解析:这个组合实际上是 5 个特性的协同工作:

  1. [LocalizedLabel] — 本地化显示名称
  2. [Resolve(...)] — 解析链式引用 handler.core
  3. [HideInInspectorIfResolvable = true] — 解析成功后隐藏
  4. [Locatable] — 支持自动定位
  5. [BindUdonSharpEvent(...)] — 自动订阅事件

简化组合:仅自动定位

[Locatable] public Core core;

扩展组合:带事件和配置

[SerializeField, LocalizedLabel]
[Locatable, BindUdonSharpEvent(
    nameof(OnVideoPlay),
    nameof(OnVideoPause),
    nameof(OnVideoEnd)
)]
public Core core;

public void OnVideoPlay() { /* 处理播放 */ }
public void OnVideoPause() { /* 处理暂停 */ }
public void OnVideoEnd() { /* 处理结束 */ }

5.2 避坑指南

坑1:Udon 不支持反射

问题BindUdonSharpEvent 依赖反射,但 Udon VM 不支持。

解决:特性仅在编辑器编译时生效,运行时事件通过 UdonSharp 生成的代码处理。

坑2:特性顺序敏感

问题[Resolve] 必须在 [HideInInspector] 之后才能正确隐藏。

正确

[SerializeField, HideInInspector, Resolve(nameof(core))] GameObject coreGO;

坑3:编译器符号不一致

问题COMPILER_UDONSHARPUDONSHARP 可能不同。

VVMW 用法

#if COMPILER_UDONSHARP
    public
#else
    internal
#endif

坑4:编辑器代码无法访问 Udon 字段

问题:编辑器程序集看不到 Udon 字段。

解决:使用双模式声明,或通过 UdonSharpEditorUtility 访问。

坑5:Locatable 预制体路径错误

问题InstaniatePrefabPath 路径不正确导致实例化失败。

检查:确保路径与 VPM 包中的实际路径匹配。


5.3 最简实现模板

模板1:声明式 UdonSharpBehaviour

using UnityEngine;
using UdonSharp;
using VRC.SDKBase;
using JLChnToZ.VRC.Foundation;

public class MyComponent : UdonSharpBehaviour {
    // 声明:需要自动定位的 Core
    [SerializeField, LocalizedLabel(Key = "MyProject.Core")]
    [Locatable, BindUdonSharpEvent(
        nameof(OnVideoPlay),
        nameof(OnVideoPause)
    )]
    public Core core;
    
    // 事件处理方法(声明即可,编辑器自动订阅)
    public void OnVideoPlay() {
        Debug.Log("Video playing");
    }
    
    public void OnVideoPause() {
        Debug.Log("Video paused");
    }
}

模板2:属性化同步字段

using UnityEngine;
using VRC.SDKBase;
using UdonSharp;
using JLChnToZ.VRC.Foundation;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class MySyncComponent : UdonSharpBehaviour {
    // 同步字段 + 回调
    [UdonSynced, FieldChangeCallback(nameof(MyValue))]
    private int myValue = 0;
    
    public int MyValue {
        get => myValue;
        set {
            if (myValue == value) return;
            myValue = value;
            OnValueChanged();
            if (Networking.IsOwner(gameObject))
                RequestSerialization();
        }
    }
    
    void OnValueChanged() {
        Debug.Log($"Value changed to: {myValue}");
    }
}

模板3:Unity 事件绑定

using UnityEngine;
using UnityEngine.UI;
using UdonSharp;
using JLChnToZ.VRC.Foundation;

[RequireComponent(typeof(Button))]
[BindEvent(typeof(Button), nameof(Button.onClick), nameof(_OnClick))]
public class MyButtonHandler : UdonSharpBehaviour {
    public void _OnClick() {
        Debug.Log("Button clicked!");
    }
}

模板4:编辑器工具类

#if UNITY_EDITOR
using UnityEditor;

[InitializeOnLoadMethod]
static class MyEditorInit {
    static MyEditorInit() {
        // 编辑器启动时执行
        Debug.Log("Editor initialized");
    }
}
#endif

模板5:场景预处理器

#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.SceneManagement;
using JLChnToZ.VRC.Foundation;

public class MyPreprocessor : IPreprocessor {
    public int Priority => 50; // 执行优先级,越小越早

    public void OnPreprocess(Scene scene) {
        foreach (var component in scene.IterateAllComponents<MyComponent>()) {
            // 预处理逻辑
            component.Initialize();
        }
    }
}
#endif

5.4 特性速查表

┌─────────────────────────────────────────────────────────────────────────┐
│                           特性速查表                                     │
├──────────────────┬──────────────────────────────────────────────────────┤
│ 特性              │ 用法                                                │
├──────────────────┼──────────────────────────────────────────────────────┤
│ LocalizedLabel   │ [LocalizedLabel] / [LocalizedLabel(Key = "...")]      │
│ LocalizedEnum    │ [LocalizedEnum]                                      │
│ Locatable        │ [Locatable] / [Locatable(InstaniatePrefabPath=...)]  │
│ Resolve          │ [Resolve(".")] / [Resolve(nameof(field))]            │
│ BindUdonSharpEvent│ [BindUdonSharpEvent(nameof(Method1), nameof(Method2))]│
│ BindEvent        │ [BindEvent(typeof(Button), nameof(Button.onClick), │
│                  │   nameof(OnClick))]                                 │
│ FieldChangeCallback│ [FieldChangeCallback(nameof(Property))]            │
│ SingletonCoreControl│ [SingletonCoreControl]                            │
│ HideInInspector  │ [HideInInspector]                                     │
│ HelpURL          │ [HelpURL("https://...")]                             │
│ DefaultExecutionOrder│ [DefaultExecutionOrder(0)]                       │
├──────────────────┼──────────────────────────────────────────────────────┤
│ 编辑器特性        │ 用法                                                │
├──────────────────┼──────────────────────────────────────────────────────┤
│ InitializeOnLoadMethod│ [InitializeOnLoadMethod]                        │
│ DidReloadScripts │ [DidReloadScripts]                                   │
│ CustomEditor     │ [CustomEditor(typeof(Component))]                   │
│ IPreprocessor    │ 实现接口 + 注册                                       │
└──────────────────┴──────────────────────────────────────────────────────┘

附录:VVMW 特性系统全景图

┌─────────────────────────────────────────────────────────────────────────┐
│                     VRC.Foundation 特性系统                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  命名空间                                                               │
│  ├── JLChnToZ.VRC.Foundation          → 特性定义 + 核心功能              │
│  ├── JLChnToZ.VRC.Foundation.I18N      → 本地化功能                     │
│  └── JLChnToZ.VRC.Foundation.Editors   → 编辑器工具                     │
│                                                                          │
│  特性分类                                                               │
│  ├── 引用绑定                                                           │
│  │   ├── [Locatable]           自动定位与实例化                          │
│  │   ├── [Resolve]             引用解析                                 │
│  │   └── [BindUdonSharpEvent]  UdonSharp 事件绑定                        │
│  ├── 字段回调                                                           │
│  │   └── [FieldChangeCallback] 字段变化回调                             │
│  ├── 本地化                                                             │
│  │   ├── [LocalizedLabel]      本地化标签                               │
│  │   └── [LocalizedEnum]       本地化枚举                               │
│  ├── 组件标记                                                           │
│  │   └── [SingletonCoreControl] 单例核心控制                           │
│  └── 编辑器工具                                                         │
│      ├── IPreprocessor           场景预处理器                           │
│      ├── InitializeOnLoadMethod  编辑器启动钩子                         │
│      └── DidReloadScripts        脚本重载钩子                           │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

附录:源码索引

本文档引用的 VVMW 源码文件:

文件 行号 引用内容
AutoPlayOnNearV2.cs 16-22 Locatable + Resolve + BindUdonSharpEvent 组合
VideoPlayerHandler.cs 34, 40-42 Resolve 解析自身组件
FrontendHandler.cs 20-34 BindUdonSharpEvent 13 事件绑定
Core.cs 49-50, 116-128 FieldChangeCallback 属性化同步
ButtonEntry.cs 1-109 BindEvent Unity 事件绑定完整示例
AbstractMediaPlayerHandler.cs 29-34 LocalizedLabel 编译期条件
EditorBase.cs 14-224 VVMWEditorBase 编辑器基类
CoreEditor.cs 1-983 Core 自定义编辑器完整实现
CoreConfigPreprocessor.cs 1-41 IPreprocessor 场景预处理

参考来源:VVMW 源码分析(Core.cs、FrontendHandler.cs、VideoPlayerHandler.cs、AbstractMediaPlayerHandler.cs、VizVidBehaviour.cs、ButtonEntry.cs、EditorBase.cs、CoreEditor.cs、CoreConfigPreprocessor.cs、AutoPlayOnNearV2.cs 等)