Unity单例走到尽头:我用一个Awake()把代码烧干净,再让依赖注入重生

如果单例是神,那依赖注入就是弑神者,而我李老师,亲手把神推下了悬崖

开端:单例的引入

单例模式是一种常用的设计模式,但还是不乏有一些小鸟(包括一年前的李老师)被各种组件间的引用折磨的头皮发麻,从而写出了大分一样的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

这里老师提一嘴不用自己写单例了,如果这篇文章帮到了你,或者有什么不懂的,欢迎评论区讨论,锐评我的文章

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值