Unity依赖注入实战:基于Zenject的架构设计与性能优化

📅 2026/8/10 20:28:17
Unity依赖注入实战:基于Zenject的架构设计与性能优化
1. 项目概述为什么Unity开发者需要依赖注入如果你在Unity项目里写过超过1000行代码大概率遇到过这样的场景一个PlayerController脚本需要引用GameManager而GameManager又需要AudioManager和UIManagerUIManager又依赖InventorySystem……最后你的代码里塞满了FindObjectOfType、GetComponent或者在Inspector面板上拖拽几十个引用。项目初期还能应付一旦功能迭代、团队协作或者需要做单元测试这种紧耦合的代码结构就会变成一场噩梦。每次修改一个类都可能引发一连串的连锁反应调试起来像在走迷宫。这正是依赖注入Dependency Injection DI要解决的问题。它不是什么高深莫测的“架构宇航学”而是一种朴实无华的设计模式核心思想是“别自己造问别人要”。一个类不应该自己动手去创建或查找它依赖的其他服务比如上面的GameManager而是应该由外部“注入”给它。这样做的好处立竿见影代码解耦了可测试性飙升你可以轻松地用Mock对象替换真实依赖进行测试模块的复用和替换也变得异常简单。在Unity生态里提到依赖注入Zenject以及它的继任者Extenject是绕不开的名字。它不仅仅是一个DI容器更是一套为Unity量身定制的完整架构解决方案。它深度整合了Unity的生命周期、场景加载、MonoBehaviour系统让你能用声明式的方式管理游戏中所有对象的创建、依赖关系和生命周期。网上有很多零散的教程但往往只讲“怎么绑定”很少深入剖析“为什么这么绑定”以及在实际项目中如何体系化地应用。这篇指南的目标就是为你补全这块拼图从核心概念到高级模式从项目搭建到性能优化提供一个能直接用于生产环境的完整解决方案。2. Zenject核心概念与项目初始化2.1 理解Zenject的四大基石在深入代码之前必须理解Zenject运作所依赖的四个核心概念这能帮你避免后期架构上的混乱。绑定Binding这是DI的起点你告诉容器“当有人需要类型A时请提供实例B”。在Zenject中绑定通常在安装器Installer中完成。绑定关系决定了对象的创建方式和生命周期。注入Injection容器根据绑定关系自动将依赖项提供给需要它的类。Zenject支持三种主要的注入方式构造函数注入最推荐的方式通过类的构造函数参数声明依赖。这强制要求依赖在对象创建时就明确保证了对象的可用性。字段/属性注入在字段或属性上标记[Inject]特性。这种方式更灵活但可能隐藏依赖关系使类在未注入时处于无效状态。方法注入在方法上标记[Inject]特性容器会在注入完字段/属性后调用该方法可用于复杂的初始化逻辑。作用域Scope与生命周期这是Zenject管理对象生存期的关键。不当的作用域设置是内存泄漏和Bug的主要来源。常见的作用域有Transient每次请求都创建一个新实例。适用于无状态的工具类。Singleton整个容器生命周期内只有一个实例。适用于全局管理器如GameManager。Scene在同一场景上下文内保持单例。场景加载时创建场景卸载时销毁。这是Unity开发中最常用的作用域之一。GameObject绑定到特定GameObject随该GameObject的销毁而销毁。上下文Context这是Zenject与Unity场景结合的灵魂。上下文决定了绑定的生效范围。主要有SceneContext每个场景的根管理该场景内的所有绑定和依赖。ProjectContext跨场景的全局上下文通常用于绑定那些从游戏启动到结束都需要存在的服务如存档系统、音频池。一个项目只有一个ProjectContext。GameObjectContext允许一个GameObject及其子物体拥有自己独立的DI容器适合复杂的UI面板或子系统模块。2.2 项目初始化与第一个安装器理论说再多不如动手。我们开始搭建一个标准的Zenject项目。首先通过Unity的Package Manager或Asset Store安装ExtenjectZenject的官方维护分支。安装后你的项目会出现Plugins/Extenject目录。第一步创建ProjectContext这是全局入口。在项目初期就应创建。右键点击Hierarchy或Project窗口 - Extenject - Project Context。这会创建一个名为ProjectContext的预制体。我通常将其放在Resources文件夹外的一个固定目录如_Project/Prefabs/Systems/然后通过[RuntimeInitializeOnLoadMethod]确保它在游戏开始时被实例化或者更常见的做法是直接在第一个场景中放置它并勾选其SceneContext上的Auto Run和Parent New Objects Under Project Context。第二步创建场景上下文SceneContext每个需要DI的场景都需要一个。在场景中创建一个空GameObject命名为SceneContext。为其添加SceneContext组件。关键设置将Parent Containers指向你的ProjectContext。这样场景容器就能访问到全局容器中绑定的服务。第三步编写第一个安装器Installer安装器是组织绑定逻辑的单元。我们从全局服务开始。创建C#脚本CoreInstaller。让它继承自MonoInstaller如果绑定不依赖MonoBehaviour也可以用Installer。重写InstallBindings方法。using Zenject; public class CoreInstaller : MonoInstaller { public override void InstallBindings() { // 绑定一个全局游戏状态管理器以单例形式存在 Container.BindIGameStateManager().ToGameStateManager().AsSingle().NonLazy(); // NonLazy表示立即创建而不是第一次被注入时才创建。 // 绑定一个音频播放服务。这里假设AudioService是一个MonoBehaviour。 // 我们需要告诉容器如何创建它从一个预制体实例化并使其在项目上下文中常驻。 Container.BindIAudioService().ToAudioService().FromComponentInNewPrefabResource(Prefabs/AudioService).AsSingle().NonLazy(); } }第四步关联安装器与上下文将CoreInstaller脚本挂载到ProjectContext游戏对象上。在ProjectContext组件的Installers列表中添加这个CoreInstaller。对于场景特定的绑定如LevelManager,EnemySpawner重复第三步和第四步创建GameplayInstaller并添加到SceneContext的安装器列表中。注意安装器的执行顺序很重要。ProjectContext的安装器先执行然后是SceneContext的。你可以通过调整列表顺序来控制绑定和覆盖的优先级。一般来说基础服务在前具体业务在后。3. 绑定详解从基础到高级模式绑定是Zenject配置的核心。理解每一种绑定方式的适用场景是写出清晰、可维护DI代码的关键。3.1 基础绑定方式与生命周期管理Container.BindT()是起点后面跟的一系列链式调用决定了具体行为。To指定具体实现类型。Container.BindIMovement().ToPlayerMovement().AsSingle();From指定实例的来源。这是功能最丰富的部分。FromNew()默认选项使用new关键字创建对于MonoBehaviour无效。FromInstance()绑定一个已经存在的实例。适用于第三方库提供的单例或手动创建的对象。var myConfig new GameConfig(); Container.BindGameConfig().FromInstance(myConfig);FromFactory()使用一个自定义的工厂来创建实例。用于复杂的创建逻辑。FromComponentInNewPrefab()/FromComponentInNewPrefabResource()针对MonoBehaviour从预制体实例化。这是Unity开发中最常用的方式之一。// 从Resources文件夹加载预制体 Container.BindEnemyController().FromComponentInNewPrefabResource(Prefabs/Enemy).AsTransient(); // 从项目资产引用预制体需要public字段或属性 [SerializeField] private EnemyController _enemyPrefab; Container.BindEnemyController().FromComponentInNewPrefab(_enemyPrefab).AsTransient();FromComponentInHierarchy()/FromComponentInChildren()/FromComponentInParents()在指定的上下文或GameObjectContext中查找已存在的组件。慎用因为它依赖于场景状态可能引入隐藏的依赖。FromMethod()通过一个委托方法来获取实例。非常灵活。Container.BindComplexObject().FromMethod(ctx CreateComplexObject(ctx.Container)).AsSingle();As定义作用域生命周期。AsTransient()瞬态。每次注入都是新实例。用于无状态服务。AsSingle()单例。整个绑定所在的容器内唯一。注意如果在父容器和子容器都绑定了AsSingle()它们会是不同的实例。AsCached()类似单例但作用域更灵活常与FromMethod等结合使用。.FromComponentInNewPrefab(...).AsSingle()对于MonoBehaviour这通常意味着该预制体只会被实例化一次且实例常驻。条件绑定使用When、WhenInjectedInto等条件子句进行精细控制。// 只有当注入到Player类时才使用KeyboardInput否则使用AIInput Container.BindIInput().ToKeyboardInput().WhenInjectedIntoPlayer(); Container.BindIInput().ToAIInput().AsSingle(); // 默认绑定3.2 工厂模式解耦复杂对象创建当对象的创建过程非常复杂需要多步初始化、依赖其他运行时数据时直接绑定会显得笨重。这时就该工厂模式登场了。Zenject内置了对工厂的优雅支持。1. 普通工厂Factory适用于创建非MonoBehaviour的纯C#对象。public class WeaponFactory : PlaceholderFactoryWeaponConfig, IWeapon { // Zenject会自动生成这个类你只需要绑定它。 } // 在安装器中绑定 Container.BindFactoryWeaponConfig, IWeapon, WeaponFactory().ToWeapon(); // 在使用处注入工厂并创建 public class PlayerArsenal { private readonly WeaponFactory _weaponFactory; public PlayerArsenal(WeaponFactory weaponFactory) { _weaponFactory weaponFactory; } public IWeapon CreateWeapon(WeaponConfig config) { return _weaponFactory.Create(config); // 参数传递给Weapon的构造函数 } }2. 预制体工厂PrefabFactory这是Unity开发中的神器用于动态创建带有MonoBehaviour的预制体。public class EnemyFacade : MonoBehaviour { // 这是一个“门面”组件挂在敌人预制体根节点用于注入依赖到预制体内部。 } public class EnemyFactory : PlaceholderFactoryVector3, Quaternion, EnemyFacade { } // 绑定 - 注意FromComponentInNewPrefab Container.BindFactoryVector3, Quaternion, EnemyFacade, EnemyFactory() .FromComponentInNewPrefab(_enemyPrefab); // 使用 var enemy _enemyFactory.Create(spawnPosition, Quaternion.identity); // 创建后enemy GameObject上的所有依赖都已被注入3. 内存池MemoryPool对于需要频繁创建和销毁的对象如子弹、特效、敌人使用内存池能极大提升性能。Zenject的MemoryPool是工厂的增强版。public class BulletPool : MemoryPoolBulletFacade { protected override void OnCreated(BulletFacade bullet) { // 当池中创建新实例时调用首次或池空时 bullet.gameObject.SetActive(false); } protected override void OnSpawned(BulletFacade bullet) { // 从池中取出对象时调用 bullet.gameObject.SetActive(true); bullet.ResetState(); } protected override void OnDespawned(BulletFacade bullet) { // 对象回池时调用 bullet.gameObject.SetActive(false); } } // 绑定 Container.BindMemoryPoolBulletFacade, BulletPool() .WithInitialSize(20) // 初始池大小 .FromComponentInNewPrefab(_bulletPrefab); // 使用 var bullet _bulletPool.Spawn(); // ... 使用后 _bulletPool.Despawn(bullet);实操心得对于游戏中的动态实体我几乎总是优先考虑使用MemoryPool而不是普通的Factory。即使初始规模很小它也为你处理了对象复用逻辑避免频繁的Instantiate和Destroy调用这对性能有显著好处。记得在OnSpawned和OnDespawned中处理好对象的初始化和重置状态。3.3 信号Signals轻量级的解耦通信除了通过接口依赖进行通信模块间有时需要更松散的、一对多的通知机制。这就是Signals的用武之地。它可以替代一部分UnityEvent或C#事件并且与DI容器集成能自动解析接收者的依赖。声明信号// 这是一个无参信号 public struct PlayerDiedSignal { } // 带参数的信号 public struct EnemyKilledSignal { public readonly int Score; public readonly Vector3 Position; public EnemyKilledSignal(int score, Vector3 position) { Score score; Position position; } }触发信号在需要触发信号的地方注入SignalBus并调用Fire。public class PlayerHealth : MonoBehaviour { [Inject] private readonly SignalBus _signalBus; public void TakeDamage(int damage) { // ... 扣血逻辑 if(currentHealth 0) { _signalBus.Fire(new PlayerDiedSignal()); } } }接收信号在接收类中声明一个以信号为参数的方法并标记[Inject]和参数类型。public class GameOverUI : MonoBehaviour { [Inject] private void OnPlayerDied(PlayerDiedSignal signal) { // 当PlayerDiedSignal触发时此方法会被自动调用 ShowGameOverScreen(); } }绑定信号需要在安装器中声明对信号系统的使用和具体信号的绑定。Container.DeclareSignalPlayerDiedSignal(); Container.DeclareSignalEnemyKilledSignal(); // 如果需要配置信号如异步触发、生命周期可以链式调用 // .OptionalSubscriber() // 允许没有接收者 // .RunAsync() // 异步触发注意事项信号虽然方便但滥用会导致“信号 spaghetti”让程序流难以追踪。我的经验法则是用于跨模块的、非核心逻辑的、一对多的通知如UI更新、成就触发、音效播放。对于有明确返回值或需要严格顺序的核心业务逻辑仍然优先使用接口依赖。4. 高级架构模式与项目组织当一个小型项目逐渐成长为中大型项目时简单的绑定列表会变得难以维护。我们需要更清晰的组织架构。4.1 使用安装器进行模块化不要把所有绑定都塞进一个巨大的GameInstaller。应该按功能模块划分安装器。CoreInstaller绑定最基础、最稳定的服务如存档ISaveSystem、日志ILogger、配置IGameConfig。放在ProjectContext。AudioInstaller绑定所有音频相关的服务IAudioService,AudioPool, 音效配置。可以放在ProjectContext或根据需要按场景加载。UIModuleInstaller绑定UI相关的工厂、管理器、ViewModel。如果UI是全局的放ProjectContext如果是场景特定的放SceneContext。GameplayInstaller绑定当前关卡或游戏模式特定的逻辑如LevelManager、WaveSystem、EnemyRegistry。放在SceneContext。这样当你需要修改或替换某个模块时只需关注对应的安装器代码的可读性和可维护性大大提升。4.2 场景生命周期与子容器理解Zenject如何与Unity场景加载协同工作至关重要。场景加载流程典型情况游戏启动ProjectContext初始化执行其所有安装器创建全局容器。加载场景A场景中的SceneContextAwake。SceneContext将其父容器设置为ProjectContext的容器。SceneContext执行其安装器列表创建场景子容器。场景子容器会继承父容器全局容器的所有绑定并可覆盖它们如果绑定了相同的类型。容器开始向场景中所有标记了[Inject]的MonoBehaviour或非MonoBehaviour类注入依赖。场景卸载时SceneContextOnDestroy被调用其子容器被销毁容器中作用域为Scene或Transient的对象会被清理如果实现了IDisposable会调用Dispose。GameObjectContext对于复杂的子系统比如一个包含多个交互组件的商店UI面板你可以为这个面板的根GameObject添加GameObjectContext。这个面板会拥有自己独立的子容器可以有自己的安装器来管理面板内部的依赖。当面板被销毁时其容器和所有Transient/Scene作用域的对象也会被清理。这实现了依赖关系的模块化和隔离。4.3 集成Unity的ScriptableObject与Addressables现代Unity项目大量使用ScriptableObject作为数据载体用Addressables系统管理资源。Zenject可以很好地与它们集成。1. 绑定ScriptableObject数据// 假设有一个WeaponConfig ScriptableObject [CreateAssetMenu(fileName WeaponConfig, menuName Configs/Weapon)] public class WeaponConfig : ScriptableObject { public int Damage; public float FireRate; } // 在安装器中绑定通常从Resources加载或通过Addressables [SerializeField] private WeaponConfig _defaultWeaponConfig; // 在Inspector中拖拽 Container.BindWeaponConfig().FromInstance(_defaultWeaponConfig).AsSingle(); // 或者绑定一个接口允许多种配置 Container.BindIWeaponConfig().ToWeaponConfig().FromInstance(_defaultWeaponConfig).AsSingle();2. 与Addressables异步加载结合这是生产环境中的常见模式。我们希望在游戏初始化时异步加载关键配置然后再进行绑定。public class AddressablesInstaller : MonoInstaller { public override void InstallBindings() { // 不能在这里直接进行异步绑定因为InstallBindings需要同步完成。 // 我们需要一个协程或异步初始化流程。 } } // 更常见的做法创建一个异步初始化服务 public interface IAddressablesLoader { TaskT LoadAssetAsyncT(string key); } public class AddressablesLoader : IAddressablesLoader { public async TaskT LoadAssetAsyncT(string key) { var handle Addressables.LoadAssetAsyncT(key); await handle.Task; return handle.Result; } } // 在GameBootstrapper一个MonoBehaviour中 public class GameBootstrapper : MonoBehaviour { [Inject] private IAddressablesLoader _loader; [Inject] private DiContainer _container; private async void Start() { // 1. 加载关键配置 var weaponConfig await _loader.LoadAssetAsyncWeaponConfig(DefaultWeapon); // 2. 动态绑定到容器使用Container.Rebind _container.RebindWeaponConfig().FromInstance(weaponConfig).AsSingle(); // 3. 通知其他服务初始化完成 _container.ResolveIGameStateManager().OnConfigLoaded(); } }踩坑记录直接在InstallBindings中执行await是无效的因为容器安装过程必须是同步的。正确的模式是将“资源加载”和“依赖绑定”分离。先同步绑定必要的服务如加载器本身然后在一个启动协程或异步方法中完成资源加载和动态绑定。Container.Rebind方法在这里非常有用它允许你在运行时替换已有的绑定。5. 测试驱动开发与调试技巧依赖注入最大的优势之一就是极大地提升了代码的可测试性。Zenject也提供了对单元测试和集成测试的原生支持。5.1 为MonoBehaviour编写单元测试假设我们要测试一个依赖IWeapon的PlayerShooting组件。public class PlayerShooting : MonoBehaviour { [Inject] private IWeapon _weapon; public void Shoot() { if(_weapon.CanShoot()) { _weapon.Fire(); } } }测试步骤在Unity中创建测试程序集如Assembly-CSharp-Editor.Tests。安装NUnit和Unity Test Framework包。编写测试using NUnit.Framework; using Zenject; using UnityEngine.TestTools; using System.Collections; public class PlayerShootingTest { private DiContainer _container; [SetUp] public void Setup() { // 每个测试前创建一个新的容器 _container new DiContainer(); } [UnityTest] public IEnumerator Shoot_CallsWeaponFire_WhenWeaponCanShoot() { // 1. 创建Mock使用Moq等框架或手动创建 var mockWeapon new MockWeapon(canShoot: true); _container.BindIWeapon().FromInstance(mockWeapon); // 2. 创建被测试的GameObject和组件 var go new GameObject(TestPlayer); var shooting go.AddComponentPlayerShooting(); // 3. 手动注入依赖在测试中替代Zenject的自动注入 _container.Inject(shooting); // 4. 执行测试动作 shooting.Shoot(); // 5. 验证断言 Assert.IsTrue(mockWeapon.WasFired); yield return null; // 对于UnityTest可能需要 } // 一个简单的手动Mock类 public class MockWeapon : IWeapon { public bool WasFired { get; private set; } false; private bool _canShoot; public MockWeapon(bool canShoot) { _canShoot canShoot; } public bool CanShoot() _canShoot; public void Fire() { WasFired true; } } }通过这种方式我们完全隔离了PlayerShooting与真实的武器系统可以精准测试其逻辑。5.2 集成测试与场景测试对于涉及多个系统交互的测试可以使用Zenject的SceneTestFixture。public class GameplayIntegrationTest : SceneTestFixture { [UnityTest] public IEnumerator EnemySpawner_SpawnsEnemies_OnGameStart() { // 加载包含完整游戏逻辑的场景 yield return LoadScene(TestGameplayScene); // 通过容器解析关键系统 var enemySpawner SceneContainer.ResolveIEnemySpawner(); var initialCount GameObject.FindObjectsOfTypeEnemy().Length; enemySpawner.StartSpawning(); yield return new WaitForSeconds(1.0f); var newCount GameObject.FindObjectsOfTypeEnemy().Length; Assert.Greater(newCount, initialCount); } }5.3 调试与常见问题排查即使有了Zenject依赖问题依然可能出现。以下是一些实用的调试技巧1. 使用ZenjectBinding组件进行可视化调试在编辑器模式下你可以给任何GameObject添加ZenjectBinding组件。它可以显示该对象上所有可注入的成员字段、属性、方法并允许你手动指定绑定的标识符Id对于调试复杂的绑定关系非常直观。2. 处理循环依赖Zenject能检测到构造函数注入的循环依赖并抛出清晰的异常。如果遇到你需要重新设计代码通常的解决方案是引入接口将双向依赖拆解。使用属性注入或方法注入替代构造函数注入打破循环。引入第三个中介类Mediator来协调两者。3. “找不到绑定”异常这是最常见的错误。排查步骤检查作用域你请求的绑定是否在当前容器或其父容器中比如一个在SceneContext中绑定的服务无法在ProjectContext的安装器中解析。检查绑定条件是否使用了WhenInjectedInto等条件绑定导致在当前注入上下文中不匹配使用Container.Resolve进行手动验证在安装器的InstallBindings方法末尾或运行时可以尝试Container.ResolveT()来验证绑定是否有效仅用于调试生产代码应避免直接Resolve。4. 内存泄漏排查Zenject管理的对象如果其生命周期长于预期可能是作用域设置错误。单例持有引用全局单例如果持有了对场景中对象的引用会阻止该对象被垃圾回收。确保单例只持有必要的引用或使用WeakReference。信号订阅未取消如果在一个生命周期较短的对象如UI弹窗中订阅了信号需要在对象销毁时取消订阅否则信号总线会持有该对象的引用导致泄漏。Zenject的信号系统在接收方被销毁时会自动清理但如果你手动调用SignalBus.Subscribe就需要手动Unsubscribe。使用ProfilerUnity的Memory Profiler是终极武器。查看“Managed Memory”部分定位那些意外存活的对象实例再回溯其引用链找到是哪个容器或单例持有了它。6. 性能优化与最佳实践总结在大型项目中不规范的DI使用可能带来性能开销。以下是一些优化建议和最终的最佳实践清单。6.1 性能优化要点避免运行时反射Zenject在启动时安装绑定阶段会使用反射分析类型。这个过程通常只发生一次开销可接受。但要避免在每帧或频繁调用的代码路径中使用Container.Resolve()或Container.Instantiate()这涉及类型查找和依赖解析。最佳实践是在构造函数或[Inject]方法中获取所有依赖之后直接使用字段引用。谨慎使用[Inject]字段字段注入会绕过readonly保护且可能使依赖关系不那么明显。优先使用构造函数注入它强制依赖明确并且允许将依赖字段标记为readonly。优化绑定数量成千上万的绑定会增加容器初始化时间。定期审查安装器移除未使用的绑定。考虑使用模块化安装器并按需加载。合理使用内存池对于高频创建/销毁的GameObject务必使用MemoryPool。调整InitialSize和MaxSize以避免运行时扩容和内存浪费。Lazy初始化对于启动时不立即需要的重型服务可以使用.AsSingle().Lazy()绑定。这样它只在第一次被注入时才会实例化。但要注意这可能会将初始化延迟到游戏运行时造成卡顿需要权衡。6.2 项目最佳实践清单规划清晰的上下文层次在项目初期就设计好ProjectContext全局服务和各个SceneContext场景特定服务的职责。考虑是否需要用GameObjectContext来封装复杂子系统。面向接口编程绑定和注入时尽量使用接口IBindIService.ToConcreteService()。这提高了代码的灵活性和可测试性。构造函数注入优先这是声明依赖最清晰、最安全的方式。模块化安装器按功能划分安装器并使用Installer基类而非MonoInstaller来绑定纯C#类以减少对Unity引擎的依赖。善用工厂和内存池对于任何需要动态创建的对象首先考虑使用Zenject提供的工厂或池。信号用于通知接口用于交互明确通信边界。核心业务流通过接口方法调用跨模块的、松散的事件通知用信号。编写可测试的代码利用DI的特性在编写业务逻辑时就思考如何对其进行单元测试。使用Mock来隔离依赖。保持容器“纯净”尽量避免在业务代码中直接访问DiContainer。依赖应该通过注入获得而不是从容器中抓取。这保证了代码的清晰度和可预测性。文档化绑定对于复杂的绑定条件或非标准的生命周期在安装器代码中添加注释说明为什么这么绑定。性能分析在项目开发中期使用Profiler查看容器初始化时间和关键服务的解析时间。对于热点路径考虑缓存解析结果或调整架构。依赖注入和Zenject不是银弹但它为构建松耦合、可测试、可维护的中大型Unity项目提供了坚实的基石。它要求开发者在前期投入更多时间进行设计但换来的是中后期开发效率的成倍提升和代码质量的显著改善。刚开始可能会觉得繁琐但一旦你习惯了这种“声明式”的依赖管理方式就再也回不去到处FindObjectOfType和拖拽引用的日子了。