前言:
最近在B站看到了唐老师出的课程——学习Unity3D基础框架,我觉得对我会很有用,特写此文章做以记录。
这篇需要你了解一些C#的知识。
单例模式基类模块
我们创建一个新项目,创建标准的目录结构
在Base下面新建脚本——BaseManager.cs
单例模式基类模块的作用主要是——减少单例模式重复代码的书写
我们都知道单例模式是怎么一种设计模式,那么什么是单例模式基类模块呢?其实就是单例模式的基础模板。
代码奉上
public class BaseManager<T> where T:new()
{
private static T _instance;
public static T GetInstance() {
if (_instance == null) {
_instance = new T();
}
return _instance;
}
}
之后只要项目中需要用到单例模式,只要继承此类即可。
public class BaseManager<T> where T:new()
这一句后面的部分可能很多朋友不了解,这是一个C#中泛型的知识点——泛型约束。关于泛型约束的概念可以参考这篇文章
缓存池模块
基本原理与实现
缓存池模块主要是为了节约性能,减少CPU和内存消耗。
Unity每一次创建对象,实际都是C#在内存中申请了一块空间,之后在Unity场景中Destroy这个物体对象,实际上只是断开了实例化对象对于内存的那块空间的一个引用,内存中依然存在那个对象对应的空间。
直到内存占用满了,CPU才会回过头来找内存中没有被引用的空间(一次GC),然后释放该空间,然后建立新的对象,如此往复。
这个GC步骤是比较消耗CPU的,所以在比较不好的机器上可能造成游戏的卡顿,所以,我们需要缓存池。
缓存池可以将你创建的对象在你用完后收录起来,等到你再要调用的时候,就可以直接调用缓存池中收录的对象,如此往复,形成闭环。不必再去申请新的内存。
我们在ProjectBase目录下创建一个新目录——Pool,用来存放缓存池脚本。
public class PoolMgr : BaseManager<PoolMgr>
{
//这里是缓存池模块
//创建字段存储容器
public Dictionary<string,List<GameObject>> pool1Dic
=new Dictionary<string, List<GameObject>>();
//取得游戏物体
public GameObject GetObj(string name) {
GameObject obj = null;
if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0)
{
//取得List中的第一个
obj = pool1Dic[name][0];
//移除第零个(这样才能允许同时创建多个物体)
pool1Dic[name].RemoveAt(0);
}
else {
//缓存池中没有该物体,我们去目录中加载
//外面传一个预设体的路径和名字,我内部就去加载它
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
//创建对象后,将对象的名字与池中名字相符
obj.name = name;
}
//让物体显示出来
obj.SetActive(true);
return obj;
}
//外界返还游戏物体
public void PushObj(string name,GameObject obj) {
//让物体失活
obj.SetActive(false);
//里面有记录这个键
if (pool1Dic.ContainsKey(name))
{
pool1Dic[name].Add(obj);
}
//未曾记录这个键
else {
pool1Dic.Add(name, new List<GameObject>() { obj});
}
}
}
我们来做个试验,给一个场景创建这样的脚本,实现点击鼠标创建物体
public class test : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0)) {
//向缓存池中拿东西
PoolMgr.GetInstance().GetObj("Cube");
}
if (Input.GetMouseButtonDown(1))
{
PoolMgr.GetInstance().GetObj("Sphere");
}
}
}
物体身上挂着另一个脚本
public class DelayPush : MonoBehaviour
{
void OnEnable()
{
Invoke("Push", 1);
}
void Push() {
PoolMgr.GetInstance().PushObj(transform.name, this.gameObject);
}
}
最终效果
这样创造新物体实际就是唤醒旧物体,虽然好像占用了内存,但是却为CPU有很大好处,减少了GC,增加了游戏体验感。
优化
规范化
为了使资源管理更加工整,我们考虑到应该让都存放在一个空物体下,这样会非常便利我们的管理。
public class PoolMgr : BaseManager<PoolMgr>
{
//这里是缓存池模块
//创建字段存储容器
public Dictionary<string,List<GameObject>> pool1Dic
=new Dictionary<string, List<GameObject>>();
private GameObject poolObj;
//取得游戏物体
public GameObject GetObj(string name) {
GameObject obj = null;
if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0)
{
//取得List中的第一个
obj = pool1Dic[name][0];
//移除第零个(这样才能允许同时创建多个物体)
pool1Dic[name].RemoveAt(0);
}
else {
//缓存池中没有该物体,我们去目录中加载
//外面传一个预设体的路径和名字,我内部就去加载它
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
//创建对象后,将对象的名字与池中名字相符
obj.name = name;
}
//让物体显示出来
obj.SetActive(true);
//断开了缓存池物体与poolObj的父子关系
obj.transform.parent = null;
return obj;
}
//外界返还游戏物体
public void PushObj(string name,GameObject obj) {
if (poolObj == null)
{
poolObj = new GameObject("Pool");
}
//将这个物体设置为空物体
obj.transform.parent = poolObj.transform;
//让物体失活
obj.SetActive(false);
//里面有记录这个键
if (pool1Dic.ContainsKey(name))
{
pool1Dic[name].Add(obj);
}
//未曾记录这个键
else {
pool1Dic.Add(name, new List<GameObject>() { obj});
}
}
}
这样,就可以实现所有申请的对象都放在Pool这个空物体下面,当某个对象物体被激活才会回到主目录下。
场景跳转问题
还有一个问题,当场景切换时,缓存池的对象物体都会被销毁,但是引用还存在,这会占用内存又没有用处,
我们可以给PoolMgr添加一个清空方法来应对场景转换的情况。
//清空缓存池的方法
//主要用在场景切换时
public void Clear() {
pool1Dic.Clear();
poolObj = null;
}
这样,跳转场景之前调用这个Clear方法就好了。
更细节的规范化
我们现在虽然可以实现生成的缓存对象全部在Pool这个空物体下,但是却没有明确的划分,如果各式各样的物体很多,就会很杂乱。
所以我们可以修改代码对他们进行分类。
思路是这样的:我们创建了一个字典来保存记录缓存对象,键是字符串、值是Gameobject的集合,我们可以将值替换成一个新的类型——PoolData。
下面是最终的PoolMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//抽屉数据,池子中的一列容器
public class PoolData
{
//抽屉中,对象挂载的父节点
public GameObject fatherObj;
//对象的容器
public List<GameObject> poolList;
public PoolData(GameObject obj, GameObject poolObj)
{
//根据obj创建一个同名父类空物体,它的父物体为总Pool空物体
fatherObj = new GameObject(obj.name);
fatherObj.transform.parent = poolObj.transform;
poolList = new List<GameObject>() { };
PushObj(obj);
}
//像抽屉里面压东西并且设置好父对象
public void PushObj(GameObject obj)
{
//存起来
poolList.Add