文章目录
- 设计思路
- 总体设计
- 从生命周期考虑
- 一些代码
对象池这个东西老生常谈了,使用它的好处在于:当我们需要重复创建或者销毁一些物体,例如限制子弹数量上限为10发,当射出第11发就需要使第10发消失,第11出现。销毁10号子弹和创建11号子弹虽然能实现,但是由于创建和销毁比较消耗性能,因此不应该这样实现。如果使用对象池技术就可以避免创建销毁,改为隐藏和显示。也就是显示11号,隐藏10号子弹实现一样的功能。通过对象池,如果要发射1000发子弹,我们不必实现1000次创建和销毁,只需11次创建和销毁(产生11个子弹并依次显示即可了。
设计思路
在设计对象池的时候,我们会考虑到使用设计模式的问题。像吃鸡游戏里有口径不同的子弹,那么不同枪也有不同的弹容,不同的射速也会造成同一时间内不同枪支的对象池中可以同时存在的子弹数量不同,也就意味着不同的枪有不同的对象池,且不同的对象池使用的子弹,子弹的最大容量都有不同。在之前的笔记中学习到了Monobehavior的生命周期,每个生命周期都是要在每帧消耗性能的。因此我们要仔细考虑整个对象池的设计模式:
应当怎样实现对象池?首先从设计需求来看,一个对象池中存放N发子弹(N应当是可变的),且对象池中子弹也不能固定。其次,如果在游戏中切换枪支,由于不同枪支各有自己的对象池,因此使用的对象池也应当改变。最后,射击游戏中枪支的使用一般是贯穿全程的,所以无论场景变换,对象池始终应该是DDOL的。
总体设计
基于上述的想法,我们可以确定一个设计模式:首先用一个对象池管理器,这个对象池管理器是DDOL的,并且它可以管理对象池的切换,相当于切枪时也要切换对应的对象池。对于这个功能,我们可以用字典来实现,通过键值对以不同的键值来对应不同的对象池。对象池管理器可以方便的进行对象池(枪支)管理,例如有的枪支子弹用完了就要丢弃,我们就卸载对应的对象池,捡到新枪就加载对应的对象池。由于它是DDOL的,我们可以把上个场景的枪也保留到下一个场景,而不是销毁后重新加载。而基于这样的性质,对象池在游戏中也应当是全局唯一的,这就要求我们为它继承一个单例模式。
其次,对象池的设计,一般而言,我们应当在Awake时就为对象池生成最大数量N的子弹,而不是需要时生成,因为生成子弹的卡顿可能会摧毁玩家的游戏体验。我们宁可把1s的生成的时间放在加载界面,也不能让玩家在游玩时有0.1秒的卡顿。对于子弹在对象池中的存取,先发射的肯定先消失,那么队列就是最适合的数据结构,我们将其出队,当射程结束后再入队。这样就能保证持续射击时能不断生成子弹。
最后是子弹,由于同一个对象池类应当能发射不同的子弹,我们可以把子弹作为预制件来保存。有些属性是可以挂载在子弹物体本身上的。例如设计游戏时枪支属性和子弹属性分别计算的时候。枪支可以设计基类,那我们也可以设计一个子弹的基类来让其余子弹继承。
从生命周期考虑
决定这些系统的设计模式之后,那么应当考虑哪些脚本需要继承Monobehavior的生命周期。首先子弹是绝对不能继承的,如果对象池最多有100发子弹,并且同时拥有10个对象池。如果子弹有生命周期就会有1000个生命周期,反之则只有10个。所以子弹不能有生命周期,因此对于子弹的运动,我们应当把子弹的功能设计统筹到它的对象池中去,在对象池中用Update或者协程为这个GameObject进行一些需要更新的处理。
其次就是对象池,这个对象应该继承Mono behavior,这使得不同的对象池能够独立的处理它们自身不同的情况,更好的设计是写一个对象池的父类,并让子类来继承并重写一些可自定义的方法。
最后就是对象池管理器,如果我们想要它DDOL,那就需要继承Mono;如果它不用和生命周期交互,只是简单的管理对象池,就不需要,可继承也可不继承。而通常我们都是在单例模式设计时设计该单例是否继承Mono的。
一些代码
单例模式
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
}
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
obj.AddComponent<T>();
}
return _instance;
}
}
}
继承单例的对象池管理器
public class ObjPoolManager : Singleton<ObjPoolManager>
{
[SerializeField]
public Dictionary<string, ObjPool> m_poolDic;
/// <summary>
/// 读取挂载的子物体中的对象池并加入字典
/// </summary>
/// <param name="obj">用于统一管理对象池的父物体</param>
public void initPool(GameObject obj)
{
m_poolDic = new Dictionary<string, ObjPool>();
for (int i = 0; i < obj.transform.childCount; i++)
{
var thisChild = obj.transform.GetChild(i);
m_poolDic.Add(thisChild.name, thisChild.GetComponent<ObjPool>());
}
}
///其他管理对象池的代码
}
对象池代码
public class ObjPool : MonoBehaviour
{
[SerializeField]
GameObject obj;
[SerializeField]
GameObject QiangKou;
[SerializeField]
int Maxnum = 0;
void Start()
{
Initiate();
}
protected Queue m_queue;
void Initiate()
{
m_queue = new Queue(Maxnum);
for (int i = 0; i < Maxnum; i++)
{
var newobj = Instantiate(obj);
newobj.transform.position = Vector3.zero;
newobj.name = "bullet" + i.ToString();
newobj.transform.SetParent(this.transform);
m_queue.Enqueue(newobj);
newobj.SetActive(false);
}
}
void Shot(GameObject bullet)
{
Debug.Log("发射子弹:" + bullet.name);
StartCoroutine(Fire(bullet));
}
IEnumerator Fire(GameObject bullet)
{
Debug.Log("发射:" + bullet.name);
while (bullet.transform.position.x < 1500)
{
bullet.transform.position = new Vector3(bullet.transform.position.x + 10f, bullet.transform.position.y, bullet.transform.position.z);
yield return new WaitForSeconds(0.02f);
}
InPool(bullet);
}
void OutPool()
{
GameObject bullet = (GameObject)m_queue.Dequeue();
bullet.transform.position = QiangKou.transform.position;
bullet.SetActive(true);
Shot(bullet);
}
void InPool(GameObject bullet)
{
bullet.SetActive(false);
m_queue.Enqueue(bullet);
}
public void OnShotClick()
{
Debug.Log("准备发射");
OutPool();
}
}
调用对象池管理器单例的其他类
public class GunsManager : MonoBehaviour
{
void Start()
{
ObjPoolManager.Instance.initPool(this.gameObject);
}
}
单对象池效果展示,其中白色块代表枪口,黑色块代表子弹
多对象池效果展示,三色按钮代表了三种不同的枪,点击按钮发射子弹
想要完善系统,则需要拓展对象池管理器类,然后通过其他类进行调用对象池管理器类。
在上述例子中,我只是将对象池类挂载到了每个按钮上,然后根据按钮触发事件来发射子弹。那么同样发射的代码就需要重复三次。更好的方式应当是统一使用一个按钮来执行发射对象池事件,可以直接定义在对象池管理器上,然后根据索引值来选择操作的对象池。
例如切枪操作,也可以通过修改对象池管理器的字典,通过对象池管理器,可以更灵活地管理对象池。