概述 入门系列中,我们习惯了直接在脚本中用 GetComponent<T>() 或 FindObjectOfType<T>() 获取依赖。这种方式的问题在于 硬编码的依赖查找 ——被依赖对象的位置、创建方式发生变化时,所有引用处都要改。
依赖注入(Dependency Injection, DI) 将依赖的创建和查找从使用者中剥离,由外部统一注入。而 可编写脚本对象(ScriptableObject) 是 Unity 原生提供的一种轻量级 DI 和数据驱动工具。
1. 为什么需要依赖注入 1.1 紧耦合的问题 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Player : MonoBehaviour { private AudioManager _audio; private ScoreManager _score; private void Awake () { _audio = FindObjectOfType<AudioManager>(); _score = FindObjectOfType<ScoreManager>(); } }
这些隐式依赖导致:
测试时需要在场景中手动搭建完整环境
重构时修改查找逻辑影响所有使用者
依赖关系不明确,新人难以理解
1.2 DI 的核心原则 依赖倒置原则 ——高层模块不应依赖低层模块,两者都应依赖抽象。
不要 new :让外部传入依赖
不要找 :让外部传入依赖
不要问 :让外部传入依赖
2. 构造器注入(Constructor Injection) 纯 C# 类可以通过构造器注入依赖,最适合非 MonoBehaviour 类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public interface IInputService { Vector2 GetMovement () ; bool IsJumpPressed () ; } public interface ISaveService { void Save (string key, object data ) ; T Load <T >(string key ) ; } public class PlayerController { private readonly IInputService _input; private readonly ISaveService _save; public PlayerController (IInputService input, ISaveService save ) { _input = input; _save = save; } public void Update () { var movement = _input.GetMovement(); } }
2.1 在 Unity 中手动装配 1 2 3 4 5 6 7 8 9 public class GameBootstrap : MonoBehaviour { private void Awake () { var input = new UnityInputService(); var save = new JsonSaveService(); var controller = new PlayerController(input, save); } }
Bootstrapper 类作为组合根,负责创建和组装所有依赖。这是最简单的 DI 形式,无需任何框架。
3. 使用 VContainer 框架 当项目复杂后,手动装配变得繁琐。VContainer 是目前 Unity 生态中最主流的 DI 框架(轻量、高性能、支持多种注入方式)。
3.1 安装 通过 Package Manager 添加 VContainer:
1 https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer
3.2 注册依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using VContainer;using VContainer.Unity;public class GameLifetimeScope : LifetimeScope { protected override void Configure (IContainerBuilder builder ) { builder.Register<IInputService, UnityInputService>(Lifetime.Singleton); builder.Register<ISaveService, JsonSaveService>(Lifetime.Singleton); builder.RegisterComponentInHierarchy<Player>(); builder.Register<PlayerController>(Lifetime.Transient); } }
3.3 注入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class PlayerController { public PlayerController (IInputService input, ISaveService save ) { } } public class Player : MonoBehaviour { [Inject ] private IInputService _input; [Inject ] private ISaveService _save; } public class Player : MonoBehaviour { [Inject ] public void Construct (IInputService input, ISaveService save ) { } }
3.4 生命周期管理 1 2 3 builder.Register<IAudioService, AudioService>(Lifetime.Singleton); builder.Register<IBullet, Bullet>(Lifetime.Transient); builder.Register<IPooledObject, Bullet>(Lifetime.Scoped);
VContainer 在场景切换时会自动释放 MonoBehaviour 的注入,无需手动清理。
4. 可编写脚本对象(ScriptableObject)作为 DI 工具 ScriptableObject 是 Unity 提供的数据容器,但它的能力远不止存数据。利用 SO 可以实现 数据驱动的架构 ,这是 Unity 中最实用的轻量 DI 方案。
4.1 作为事件通道 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 [CreateAssetMenu(menuName = "Events/IntEventChannel" ) ] public class IntEventChannelSO : ScriptableObject { public event System.Action<int > OnEventRaised; public void Raise (int value ) { OnEventRaised?.Invoke(value ); } } public class ScoreManager : MonoBehaviour { [SerializeField ] private IntEventChannelSO _scoreChangedChannel; public void AddScore (int amount ) { _scoreChangedChannel.Raise(currentScore); } } public class ScoreUI : MonoBehaviour { [SerializeField ] private IntEventChannelSO _scoreChangedChannel; private void OnEnable () => _scoreChangedChannel.OnEventRaised += UpdateUI; private void OnDisable () => _scoreChangedChannel.OnEventRaised -= UpdateUI; private void UpdateUI (int score ) { _scoreText.text = score.ToString(); } }
发布者和订阅者在 Inspector 中通过拖拽绑定,无需写代码关联。这是 Unity 的事件总线简化版,特别适合中小项目。
4.2 作为游戏配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [CreateAssetMenu(menuName = "Game/EnemyConfig" ) ] public class EnemyConfigSO : ScriptableObject { public string EnemyName; public float MoveSpeed = 3f ; public int MaxHealth = 100 ; public int Damage = 10 ; public GameObject Prefab; public AudioClip SpawnSound; } public class EnemySpawner : MonoBehaviour { [SerializeField ] private EnemyConfigSO _goblinConfig; [SerializeField ] private EnemyConfigSO _orcConfig; public void SpawnGoblin () { var enemy = Instantiate(_goblinConfig.Prefab); enemy.GetComponent<Enemy>().Init(_goblinConfig); } }
优势:
策划可直接在 Inspector 中调整数值,无需修改代码
支持变体(继承 SO),方便做不同类型的配置
运行时热更新数值(编辑器模式下)
4.3 作为工厂 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [CreateAssetMenu(menuName = "Game/EffectFactory" ) ] public class EffectFactorySO : ScriptableObject { [SerializeField ] private GameObject _explosionPrefab; [SerializeField ] private GameObject _sparkPrefab; [SerializeField ] private GameObject _smokePrefab; public GameObject Create (EffectType type, Vector3 position ) { var prefab = type switch { EffectType.Explosion => _explosionPrefab, EffectType.Spark => _sparkPrefab, EffectType.Smoke => _smokePrefab, _ => null }; return prefab ? Instantiate(prefab, position, Quaternion.identity) : null ; } }
4.4 作为运行时数据仓库 1 2 3 4 5 6 7 8 9 10 11 [CreateAssetMenu(menuName = "Game/PlayerInventory" ) ] public class PlayerInventorySO : ScriptableObject { public int Gold { get ; private set ; } public List<Item> Items { get ; private set ; } = new (); public void AddGold (int amount ) { Gold += amount; } }
SO 作为数据仓库的好处是:在编辑器中运行游戏时可以实时查看和修改数据,方便调试。
5. 三种 DI 方式对比
方式
适用场景
优势
劣势
手动构造器注入
小项目、纯 C# 库
无额外依赖,简单直接
项目大后管理复杂
VContainer
中大型项目
自动管理生命周期、高性能、支持多种注入
需要学习框架 API
ScriptableObject
数据驱动、配置管理
Unity 原生、图形化配置、可继承
运行时修改需序列化
混合使用建议 1 2 3 4 5 VContainer 负责:系统级依赖(服务、管理器、工厂) ScriptableObject 负责:配置数据、事件通道、资源引用 // VContainer 注入 SO 配置 builder.RegisterInstance(_enemyConfig);
总结 依赖注入的核心思想是 控制反转 ——让使用者不再负责依赖的创建和查找。
手动注入 :适合小项目,理解 DI 本质
VContainer :中大型项目的标配,自动管理生命周期
ScriptableObject :Unity 原生的数据驱动方案,适合配置与事件通道
无论用哪种方式,目标是让代码变成这样:
1 2 3 4 5 6 7 8 public class PlayerController { public PlayerController (IInputService input, IMovementService movement, IAnimationService animation ) { } }
下一篇将探讨 分层状态机与行为树 ,深入了解游戏 AI 架构。