如果单例是神,那依赖注入就是弑神者,而我李老师,亲手把神推下了悬崖
开端:单例的引入
单例模式是一种常用的设计模式,但还是不乏有一些小鸟(包括一年前的李老师)被各种组件间的引用折磨的头皮发麻,从而写出了大分一样的code,为了照顾萌新体验,李老师在这里特意提一嘴单例
单例在unity中主要充当管理器的作用,全局只保持存在一个,其他地方可以直接调用公共变量和函数,省去了引用的麻烦,接下来我们看一段例子
public class PlayerManager
{
private static PlayerManager _instance;
public static PlayerManager Instance=>instance;//通过表达式获取私有静态实例,可以防止修改
private void Awake()
{
if(instance==null)
{
instance=this;//单例指向自己
DontDestory(gameObject);//跨场景不销毁,看你自己咯
}
else
{
Destory(gameObject);//已经存在一个了(instance指向别的了,把自己删了)
}
}
public void SayHello()
{
Debug.Log("你好");
}
}
public class Test
{
void Start()
{
PlayerManager.Instance.SayHello();//可以通过这个类的Instance的实例直接调用
}
}
这段例子生动的告诉了我们单例模式的妙用,如果你还没有搞懂单例,那么请你离开我们,对不起跑题了,新的问题接踵而至,我每一个管理器都要写一个Instance,好麻烦哦,老师老师,有没有一劳永逸的方法,有的兄弟,c#里有很多让程序员偷懒的方法,比如List<(int,int)>可以生成一个元组,你就不用自己写一个类了
进阶:通过继承只写一次
刚刚扯远了,在面向对象中,继承是非常重要的一个概念,那么,我们如何让单例模式与继承相结合呢
public class Singleton<T>:Mono where T:Sinleton<T>//需要满足泛型的约束,可以不加,但是不加有麻烦
{
protected static T _instance;//注意这里保护级别变成protected让儿子能用
public static T Instance=>instance;
protected virtual void Awake()
{
if(instance==null)
{
instance=this as T;
DontDestory(gameObject);//跨场景不销毁,看你自己咯,不要这一句就是单场景单例
}
else
{
Destory(gameObject);//已经存在一个了(instance指向别的了,把自己删了)
}
}
}
有了上面的单例类,我们扩展起来就很容易了
public class PlayerManager:Singleton<PlayerManager>
{
protected ovrride Awake()
{
base.Awake();//用爸爸的方法
//填自己想要做的事
}
}
简单讲解一下,PlayerManager继承自Singleton,这里的PlayerManager实际上是泛型T,而父类中持有一个T的单例,在最开始是就会通过类型转化变成T,可能有同学会问为什么能转过来,欢迎评论区交流
到这里已经很好用了,老师老师,还有没有更大的
有的兄弟,有的
高潮:接口的引入与依赖注入的思考
终于聊到依赖注入的问题了,依赖注入,简单来说就是我不要你的钱,我要你自己把钱给我,比如我现在是B型血缺血,而你是B型血,你自己给我输血就是依赖注入,而我直接拿走你的血就违背了依赖注入,其实很多小鸟早就已经会了依赖输入并使用了很多次了,其实,每次你Serizefield(忘了怎么拼了哈哈)或者公共字段(不准用)的类,然后再在unity检查器中把需要的拖进去,都是一次依赖注入的过程,好好想想这个过程吧
至于接口,这里就简单讲讲,接口就相当于是一个人的能力,当一个工作需要你这种人时,不管你是谁都可以去应聘,简单给个例子
public interface IEat//吃的接口,接口用interface,命名规范是前面加一个大写的I
{
void Eat();
}
public class Person:IEat//继承接口(实际上是实现)
{
void Eat()//实现接口方法
{
Console.WriteLine("我要吃饭");
}
}
class Test
{
public void Test1(IEat everyone)
{
everyone.Eat();
}
}
上面的Person类实现了IEat接口,可以被Test1方法使用,换言之,任何一个类只要实现了IEat接口,都可以被Test1方法使用,好像有点扯远了,没事,就当丰富知识了,至于这俩东西有什么关系呢,列位看官且听下回分解
不会真划走了吧,简单来说就是让一个接口控制单例的所有方法和属性(接口中可以放属性,方法和事件),然后需要用到单例的地方统统换成接口,最后由该单例把自己依赖注入进需要的地方(感觉不太对呢)
依旧先给大家一个例子开开胃
public class Test1
{
void Start()
{
PlayerManager.Instance.Eat();//没有接口和依赖注入的
}
public class Test2
{
IEat _eat;//这里有一个接口
void Start()
{
_eat.Eat();//用接口而不是单例
}
void SetEat(IEat eat)//依赖注入的入口
{
_eat=eat;
}
}
}
public class PlayerManager:Singleton<PlayerManager>,IEat
{
protected ovrride Awake()
{
base.Awake();//用爸爸的方法
test2.SetEat(this) //把自己给注进去
}
public void Eat()
{
}
}
这只是一个很简单的例子,你看懂了就会用,到现在,你已经掌握的差不多了,你可能会问,还有吗,哈哈,还有
结尾:可直接使用的框架
现在常用的都是Extenject框架,人家直接把上面的过程帮你处理完了,你直接拿来用就好了,由于李老师今天才接触这个框架,下面附上一段AI的帮助(今天老师就开始用)
一、安装(二选一)
Package Manager
- ▼ → Add package from git URL
https://github.com/Mathijs-Bakker/Extenject.git?path=UnityProject/Assets/Plugins/Zenject
或者 Asset Store 搜 Extenject 直接 Import 。
二、场景里放“容器”
新建空物体 → 改名 SceneContext
Add Component → Scene Context(Zenject 提供)
这个物体就是 DI 容器入口,整个场景只留一个。
三、写绑定脚本(Installer)
using Zenject;
public class GameInstaller : MonoInstaller // 必须继承 MonoInstaller
{
// 把 WeaponManager 直接做成“单例”
public override void InstallBindings()
{
// 1. 先绑定输入(这里演示用你自己实现的 PlayerInput)
Container.Bind<IInput>().To<PlayerInput>().AsSingle();
// 2. 再绑定武器管理器本身
Container.Bind<WeaponManager>().AsSingle().NonLazy(); // NonLazy=场景启动就创建
}
}
把 GameInstaller 拖到 SceneContext → Installers 列表里
四、删掉手动 SetInput,改成自动注入
public class WeaponManager : SingletonDestroy<WeaponManager>
{
// 不需要 SetInput 了,Zenject 会自动把实现填进来
[Inject] private IInput _input; // 字段注入
private void OnEnable() => /* 跟之前一样绑事件 */ ;
private void OnDisable() => /* 跟之前一样卸事件 */ ;
}
五、运行
Ctrl+Shift+R(Zenject 自带 Validate And Run)
控制台没红字就说明注入成功,_input 里已经是 PlayerInput 实例
。
六、常见坑速查
“NullReferenceException: _input”
忘了在 Installer 里 Bind() 或场景没放 SceneContext
重复事件注册
用 OnEnable/OnDisable 即可,Zenject 会在对象 Enable 前注入好
想换 Mock 输入做单元测试
再写一个 TestInstaller : MonoInstaller 把 IInput 绑到 MockInput 即可
一句话总结
SceneContext + Installer + [Inject] 三步走完,Zenject 就把所有依赖串好了;以后加新系统只管写绑定,再也不用到处 FindObjectOfType 或手动 SetInput
这里老师提一嘴不用自己写单例了,如果这篇文章帮到了你,或者有什么不懂的,欢迎评论区讨论,锐评我的文章

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



