《唐老师基础小框架解析与代码》

一.单例模式基类

1.不继承MonoBehaviour的单例模式基类


(1).为什么要写单例模式基类

  • 面对对象的思想避免代码冗余(多余、重复)

  • 代码(普通单例模式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class TestMgr
    {
    private static TestMgr instance;
    public static TestMgr Instance
    {
    get
    {
    if(instance==null)
    instance=new TestMgr();
    return instance;
    }
    }

    }

    这种单例模式基类我们内容都大差不差,只是类名变了,所以我们要去掉这种代码冗余


(2).不继承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
    public class BaseManager<T> where T:class,new()
    {
    private static T instance;
    //属性的方式
    public static T Instance
    {
    get
    {
    if(instance==null)
    {
    instance=new T();
    }
    return instance;
    }
    }

    //方法的方式
    public static T GetInstance()
    {
    if(instance==null)
    instance=new T();
    return instance;
    }
    }

主要应用知识点:泛型,泛型约束,内容在《C#进阶之重要内容》


(3).潜在的安全问题

* ①构造函数问题:构造函数可在外部调用  可能会破坏唯一性
* ②多线程问题:当多个线程同时访问管理器时,可能会出现共享资源的安全访问问题

这些在后面安全性问题章节再总结。


(4).单例模式的优点

  • 全局数据共享
  • 确保唯一性
  • 方便管理资源
  • 方便管理对象
  • 访问简单化
  • 便于扩展
  • 切换场景时单例模式的对象还在
    • 除非手动滞空,因为整个脚本并没有挂载在物体上,所以在切场景时候不会影响不继承mono的单例


2.继承MonoBehaviour的单例模式基类


(1) 注意事项

  • 继承MonoBehaviour的脚本不能new
  • 继承MOnoBehaviour的脚本一定得依附在GameObject
  • 具体为什么,详见unity重要知识点补充(暂未补充)
  • this 的使用方法详见C#重要知识点补充(已补充)

(2) 实现挂载式的单例模式基类

  • 这种模式不建议大家使用
  • 因为很容易破坏单例模式的唯一性
  • 1.挂载多个脚本
  • 2.切换场景回来时,由于场景放置了挂载脚本的对象,回到该场景时,又会有一个单例模式。(一般都是管理器,过场景不被销毁)
  • 还可以通过代码动态的添加多个该脚本 ,也会破坏唯一性
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
/// <summary>
/// 继承了mono的单例模式基类
/// </summary>
/// <typeparam name="T">需要用单例模型类的名字</typeparam>
public class SingletonMono<T> : MonoBehaviour where T:MonoBehaviour
{

private static T instance;
public static T Instance
{
get
{
return instance;
}
}
//继承了Mono的脚本 不能够直接new
//只能通过拖动到对象上 或者 通过加脚本的api AddComponent去加脚本
//U3D内部帮助我们实例化它
//我们需要自己保证它的唯一性
//缺点:自己可以挂多个,这样就不是单例模式了
//它只会指向最后挂载的这个对象
protected virtual void Awake()
{
//保证子类重写,实现子类的自定义
instance = this as T;
}
}
  • 代码测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class NewBehaviourScript : SingletonMono<NewBehaviourScript>
    {
    protected override void Awake()
    {
    //重写Awake时 这一定不能省略
    base.Awake();
    }

    // Update is called once per frame
    void Update()
    {

    }
    }

    疑问:我们测试的代码并没有继承mono而是继承的基类,为什么符合mono的泛型约束。

    回答:因为父类继承了Mono所以我们的测试类也算Mono 的派生类


3.实现自动挂载式的单例模式基类


  • 推荐使用
  • 无需手动挂载,无需动态添加,无需关系切场景带来的问题
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
/// <summary>
/// 继承这种自动创建的 单例模式的基类 不需要我们手动去拖 或者api去加了,想用直接GetInstance
/// </summary>
/// <typeparam name="T">需要用单例模式类的名字</typeparam>
public class SingleAutoMono<T> : MonoBehaviour where T:MonoBehaviour
{

private static T instance;
public static T Instance
{
get
{
if(instance==null)
{
//创建一个新的对象
GameObject obj = new GameObject();
//得到脚本名,设置对象的名字为脚本名
obj.name = typeof(T).ToString();
//让这个单例模式对象 过场景 不移除
//因为 单例模式对象 往往 是存在整个程序生命周期中的
DontDestroyOnLoad(obj);
//将管理器脚本挂载到对象上
instance=obj.AddComponent<T>();
}
return instance;
}
}
//继承了Mono的脚本 不能够直接new
//只能通过拖动到对象上 或者 通过加脚本的api AddComponent去加脚本
//U3D内部帮助我们实例化它
//我们需要自己保证它的唯一性
//缺点:自己可以挂多个,这样就不是单例模式了
//它只会指向最后挂载的这个对象

}
  • 具体用法,创建T类型的脚本并且继承基类,在里面写东西
  • 之后在其他脚本里面调用Instance就可以了。

4.潜在的安全问题

  • 构造函数问题:

    • 继承MonoBehaviour的函数,不能new,所以不用担心公共构造函数问题。
  • 多线程问题:

    • Unity主线程中相关内容,不允许其他线程直接调用,很少有这样的需求,所以也不用太担心
  • 重复挂载问题

    • 手动重复挂载
    • 代码重复添加
    • 需要人为干涉,定规则,或者通过代码逻辑强制处理

    所有安全性问题会在后面统一处理


5.安全问题(反射,抽象类)

  • 构造函数带来的唯一性问题指的是什么?

  • 对于不继承MonoBehaviour的单例模式基类

    我们要避免在外部 new 单例模式类对象。


  • 对于继承MonoBehaviour的单例模式基类

由于继承MonoBehaviour的脚本不能通过new’创建,因此不用过多考虑。


  • 解决构造函数带来的安全问题

  • (1)父类变成抽象类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public abstract class BaseManager<T> where T : class, new()
    {
    //优点:切场景单例模式的对象还在
    //除非手动滞空,因为整个脚本并没有挂载在物体上,所以在切场景的时候不会影响不继承mono的单例

    //这边调用了泛型的无参构造函数
    //那么必须约束T有一个无参构造函数不然会报错
    private static T instance = new T();
    public static T Instance => instance;
    protected BaseManager()
    {
    //避免小白或者傻逼破坏单例模式
    //可以不用写
    //作用:如果发现有人new了,就可以把他开了
    }
    }
  • (2) 规定继承单例模式基类的类必须显示实现私有无参构造函数(就是把管理器的构造函数私有使其无法new)

    问题:不允许约束new()

    1
    private NewBehaviourScript() { }
    • (3) 为了解决第二个的问题,在基类中通过反射来调用私有构造函数实例化对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //主要知识点:
      利用Type中的 GetConstructor(约束条件,绑定对象,参数类型,参数修饰符)方法来获取私有无参构造函数
      ConstructorInfo constructor =typeof(T).GetConstructor(
      BindingFlags.Instance | BindingFlags.NonPublic, //表示成员私有方法
      null,//表示没有绑定对象
      Type.EmptyTypes,//表示没有参数
      null //表示没有参数修饰符
      );


6. [安全问题]唯一性问题—重复挂载

知识点: Unity入门(特性,Destroy函数)

  • 重复挂载带来的唯一性问题是什么?

    • 对于继承了挂载式单例模式基类
    • (1) 手动挂载多个相同单例模式脚本
    • (2) 代码动态添加多个相同单例模式脚本
    • (3) 会出现没用的脚本组件,只有最后一个是有用的
  • 解决重复挂载带来的安全问题

    • (1)同一个对象的重复挂载

      为脚本添加特性[DisallowMultipleComponent]

      注:治标不治本,还能挂载到其他物体

    • (2) 修改代码逻辑

      判断如果存在对象,移除脚本

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      protected virtual void Awake()
      {
      //如果存在一个对象了,就不需要再有一个了
      if (instance != this)
      {
      Destroy(this);
      return;
      }
      //保证子类重写,实现子类的自定义
      instance = this as T;
      //保证该脚本的依附的对象过场景不移除
      DontDestroyOnLoad(this.gameObject);
      }
    • 对于自动挂载式的单例模式脚本,要制定使用规则,不允许手动挂载或代码添加

7. [安全问题] 线程安全—是否加锁

知识点:C#多线程知识点

  • 是否加锁问题指什么?

如果程序当中存在多线程

我们需要考虑当多个线程同时访问同一个内存空间时出现的问题

如果不加以控制,可能会导致数据出错

我们一般称这种问题为多线程并发问题,指多线程对共享数据的并发访问和操作

而一般解决该问题的方式,就是通过c#中的lock关键字进行加锁。我们需要考虑我们的单例模式对象门是否需要加锁(lock)。

lock原理:保证在任何时刻只有一个线程能够被执行被锁保护的代码块从而防止多个线程同时访问或修改共享资源,确保线程安全。


解决多线程并发带来的问题

(1) 不继承MonoBehaviour的单例模式

建议加锁,避免以后使用多线程时出现并发问题。比如:处理网络通讯模块、复杂算法模块时,经常会进行多线程并发处理。

​ 如果子类数据想加锁的话,直接在方法里面lock()就行。

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
//用于加锁的对象
protected static readonly object lockObj = new object();//其基本含义是指某一项数据或属性在初始化后,便不可再被修改
public static T Instance
{
get
{
if(instance==null)//如果开始进来如果被实例化就直接返回,避免加锁,影响性能
{
//,在多线程中,使用lock后,能使该代码块按照指定的顺序执行,被lock这块代码已经被其中一个线程访问了,那么另外一个线程只能等待。
lock (lockObj)
{
if (instance == null)
{
//instance=new T ();
//利用反射得到私有的无参构造函数,用于对象的实例化
Type type = typeof(T);
ConstructorInfo info = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
if (info != null)
instance = info.Invoke(null) as T;
else
Debug.Log("没有获得对应的无参构造函数");
}
}

}
return instance;
}

}
(2) 继承MonoBehaviour的单例模式

可加可不加,但是建议不加

因为Unity的机制是,Unity主线程中处理的一些对象(如GameObject、Transform等等)是不允许被其他多线程修改访问的,会直接报错。 因此我们一般不会通过多线程去访问继承Mono的相关对象。既然如此,就不会发生多线程并发问题。




二. 公共Mono模块

1. 公共Mono模块主要作用和基本原理

知识点:委托,声明周期函数,协同

  • 主要作用

    Unity游戏开发当中,继承MonoBehaviour的类,可以使用生命周期函数,可以开启协同程序。

    我们可以利用生命周期函数中的`Update、FixedUpdate、LateUpdate`等函数,进行帧更新或定时更新,用来处理游戏逻辑。
    
    可以利用`协同程序分时分步`的处理游戏逻辑,比如异步加载,复杂逻辑分步等。
    

    ​ 但是对于没有继承Monobehaviour的脚本,我们无法使用这些内容来处理逻辑。在不进行任何处理的情况下,我们无法在不继承MonoBehaviour的脚本中进行帧更新或者定时更新逻辑,也无法利用协同程序分时分布执行逻辑

  • 因此公共Mono模块的主要作用是:

​ 让不继承MonoBehaviour的脚本也能:

​ (1) 利用帧更新或定时更新处理逻辑

​ (2) 利用协同程序处理逻辑

除了刚才提到的主要作用

​ 由于Unity当中过多脚本中的过多帧更新或定时更新函数会对性能有一定的影响。在满足主要作用的同时,我们还可以对它们进行集中化管理,减少Unity中多脚本中帧更新或定时更新函数的数量,从而来提升一定的性能。

  • 基本原理

    实现一个继承继承MonoBehaviour单例模式基类的公共Mono管理器脚本在其中

    (1) 通过事件或委托 管理 不继承MonoBehaviour脚本的相关更新函数

    (2) 提供协同程序开启或关闭的方法

    从而让不继承Mono的脚本也能

    (1) 利用帧更新或定时更新处理逻辑

    (2) 利用协同程序处理逻辑

​ (3) 可以统一执行管理帧更新或定时更新相关逻辑

2. 实现公共Mono模块

(1)创建MonoMgr继承 自动挂载式的继承Mono的单例模式基类。

​ (2) 实现Update、FixUpdate、LateUpdate生命周期函数。

​ (3) 声明对应事件或委托用于存储外部函数,并提供添加移除方法,从而达到不继承Mono的脚本可以执行帧更新或定时更新的目的。

​ (4) 声明协同程序开启关闭函数,从而达到不继承Mono也能执行协同程序

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
>/// <summary>
>/// 公共Mono管理器,不需要挂载直接用
>/// </summary>
>public class MonoMgr : SingleAutoMono<MonoMgr>
>{
private event UnityAction updateEvent;
private event UnityAction fixedUpdateEvent;
private event UnityAction lateUpdateEvent;


// Update is called once per frame
private void Update()
{
updateEvent?.Invoke();//如果updateEvent为空就不会调用
}

private void FixedUpdate()//定时物理更新
{
fixedUpdateEvent?.Invoke();
}

private void LateUpdate()//晚于Update的帧更新
{
lateUpdateEvent?.Invoke();
}

//-------------------------------------------------------------------------------------------------------------------------------------------

/// <summary>
/// 添加Update帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void addUpdateListener(UnityAction updateFun)
{
updateEvent += updateFun;
}

/// <summary>
/// 添加FixUpdate帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void addFixedUpdateListener(UnityAction updateFun)
{
fixedUpdateEvent += updateFun;
}

/// <summary>
/// 添加LateUpdate帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void addLateUpdateListener(UnityAction updateFun)
{
lateUpdateEvent += updateFun;
}

//----------------------------------------------------------------------------------------------------------------------------------------------------------------

/// <summary>
/// 移除Update帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void RemoveUpdateListener(UnityAction updateFun)
{
updateEvent -= updateFun;
}

/// <summary>
/// 移除FixUpdate帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void RemoveFixedUpdateListener(UnityAction updateFun)
{
fixedUpdateEvent -= updateFun;
}

/// <summary>
/// 移除LateUpdate帧更新监听函数
/// </summary>
/// <param name="updateFun">需要更新的方法名</param>
public void RemoveLateUpdateListener(UnityAction updateFun)
{
lateUpdateEvent -= updateFun;
}
//------------------------------------------------------------------------------------------------------------------------
//协程直接调这个脚本直接启用就行了,注:启用协同必须要用函数名,字符串只能在mono的脚本中才能找到
//可以统一全用这个脚本进行帧更新,统一管理
>}

  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test4 : BaseManager<Test4>
{
private Test4() { }
public void StartUpdate()
{

MonoMgr.Instance.addUpdateListener(MyUpdate);

}
public void StopUpdate()
{
MonoMgr.Instance.RemoveUpdateListener(MyUpdate);
}
private void MyUpdate()
{

Debug.Log(666);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Test4.Instance.StartUpdate();//
}

// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Test4.Instance.StartUpdate();//注这里 可能会被重复添加
}
if (Input.GetKeyUp(KeyCode.Space))
{
Test4.Instance.StopUpdate();
}
}
}


三. 缓存池(对象池)模块

  • 知识点:

C#GC机制,C#数据结构,UnityGameObject,Resources(GC补)

  • 抛出问题

  • 对象的频繁创建

    频繁的实例化对象会带来一定的性能开销

  • 对象的频繁销毁

    对象的频繁销毁会造成大量的内存垃圾,会造成GC(垃圾回收)的频繁触发。GC的触发,内存的释放,可能会带来卡顿感,影响玩家体验。

  • 缓存池主要作用

优化资源管理,提高程序性能。

主要通过重复利用已经创建的对象,避免频繁的创建和销毁过程,从而减少系统的内存分配和垃圾回收带来的开销

  • 目标:

通过重复利用已经创建的对象,避免频繁的创建和销毁过程

  • 思路:

用一个柜子中的各种抽屉来装东西,用时去拿(没有就创造,存在就获取),不用就还(将东西分门别类的放入抽屉中)。

image-20250215190352816

缓存池具体实现

(1) 创建PoolMgr继承 不继承MonoBehaviour的单例模式基类。

(2). 声明柜子(Dictionary)和抽屉(List、Stack、Queue等)容器

(3). 拿东西方法

​ 有抽屉并且抽屉里有东西 直接获取

​ 没有抽屉或者抽屉里没有东西 创造

(4). 放东西方法

​ 有抽屉 直接放

​ 没抽屉 创建抽屉,再放

(5). 清空柜子方法

​ 我们在切场景时,对象都会被移除,这时应该清空柜子,否则会出现内存泄漏,并且下次取东西会出问题。

1.[缓存池(对象池)优化] 窗口布局优化

  • 窗口布局优化指的是什么

    现在直接失活对象,当之后项目做大了,抽屉多了,对象多了。
    
    游戏中成百上千个对象,在开发测试时不方便从Hierarchy窗口中查看对象获取信息。
    
    因此我们希望能优化一下Hierarchy窗口中的布局,将对象和抽屉的关系可视化
    

  • 制作思路和具体实现

(1) 柜子管理自己的柜子根物体

(2) 抽屉管理自己的抽屉根物体

(3) 失活时建立父子关系,激活时断开父子关系。


  • 具体实现:

(1) 先实现将所有对象放入柜子根物体中。

(2) 再实现将对象放入对应的抽屉根物体中

​ 用面向对象的思想将抽屉相关数据行为封装起来。

  • 将其变为可控制开启的功能

    这样可以避免在真机运行时,由于父子关系的频繁变换,带来一些额外的性能开销。


2.对象上限优化

  • 对象池上限优化指的是什么?

目前我们制作的缓存池模块,理论上来说,当动态创建的对象长时间不放回抽屉,每次从缓存池中动态获取对象时,会不停的新建对象。那么对象的数量是没有上限的,场景上的某种对象可以存在n个。

  • 我们希望控制对象数量有上限,对于不重要的资源我们没必要让其无限加量。而是将使用最久的资源直接抢来用
  • 主要目的:
    • 更加彻底的复用资源,对对象的数量上限加以限制,可以优化内存空间,甚至优化性能(减少数量上限,可以减少渲染压力)

  • 制作思路

  • (1) 在抽屉里声明一个容器用来记录正在使用的资源.

  • (2) 每次获取对象时,传入一个抽屉最大容量值(可以给一个默认值)

  • (3) 从缓存池中获取对象时就需要创建抽屉,用于记录当前使用着的对象

  • (4) 每次取对象时应该分情况

    • ①没有抽屉
    • ② 有抽屉,并且抽屉里有没用的对象或者使用中对象超过上限。
    • ③ 有抽屉,但是抽屉里没有对象,使用中对象也没有超过上限时。
  • 每次放回对象时:

    由于记录了正在使用的资源,因此每次放入抽屉时还需要从记录容器中移除对象

  • 复用一定要初始化