前言
最近在学习Unity游戏设计模式,看到两本比较适合入门的书,一本是unity官方的 《Level up your programming with game programming patterns》 ,另一本是 《游戏编程模式》
这两本书介绍了大部分会使用到的设计模式,因此很值得学习
本专栏暂时先记录一些教程部分,深入阅读后续再更新
文章目录
- 前言
- 什么是对象池
- 关于生命周期管理
什么是对象池
对象池技术是老生常谈的一个概念了,很简单,无论是软件开发还是编程语言的学习中,我们都经常接触到池化技术。池化技术指的是将一些常用的变量或对象存储到一个公共池中,当我们需要调用这些变量或对象的时候,直接引用公共池中的对象而无需new一个新对象。例如管理socket的连接池,管理string的Intern字符串池,还有管理GameObject的对象池等等等等。
在Unity中,如果你想要实现发射子弹的功能,那么就需要在发射时实例化子弹预制件。但是如果使用创建Instantiate()
和销毁Destory()
方法来控制子弹在场景中的生命周期,这无疑是一个很浪费性能的策略,会导致游戏运行卡顿。
所以我们引入了对象池,我们在场景初始化的时候提前生成好指定数量的子弹实例,当我们发射子弹时,从对象池中取出物体,而销毁子弹时则将他们放回对象池。这样对性能的消耗就小多了。
优点是能够更好的管理物体,减少Cpu压力,减少对象生成占用堆空间导致的GC
对象池代码挺简单的,主要由三个部分组成:
1.对象池初始化代码
2.从对象池取出物体的代码
3.将物体放回对象池的代码
public class ObjPool<T> where T : new()
{
private Stack<T> ObjStack;
public ObjPool (int maxCount)
{
ObjStack = new Stack<T>(maxCount);
for (int i = 0; i < maxCount; i++)
{
var obj = new T();
ObjStack.Push(obj);
}
}
public void InPool (T obj)
{
ObjStack.Push(obj);
}
public T OutPool()
{
return ObjStack.Pop();
}
}
很简单就能手搓一个对象池,当然上述代码只是随手写的,也可也根据自己需要来写一个对象池
为什么我不介绍unity的对象池呢,因为unity太幽默了,大部分版本不支持UnityEngine.Pool这个命名空间。不过可以贴出它的代码,相比于我们的代码,就是在对象池函数更完善了,且在执行时添加了一些委托方法以供触发。
using System;
using System.Collections.Generic;
namespace UnityEngine.Pool
{
/// <summary>
/// <para>A stack based Pool.IObjectPool_1.</para>
/// </summary>
public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
{
internal readonly List<T> m_List;
private readonly Func<T> m_CreateFunc;
private readonly Action<T> m_ActionOnGet;
private readonly Action<T> m_ActionOnRelease;
private readonly Action<T> m_ActionOnDestroy;
private readonly int m_MaxSize;
internal bool m_CollectionCheck;
public int CountAll { get; private set; }
public int CountActive => this.CountAll - this.CountInactive;
public int CountInactive => this.m_List.Count;
public ObjectPool(
Func<T> createFunc,
Action<T> actionOnGet = null,
Action<T> actionOnRelease = null,
Action<T> actionOnDestroy = null,
bool collectionCheck = true,
int defaultCapacity = 10,
int maxSize = 10000)
{
if (createFunc == null)
throw new ArgumentNullException(nameof (createFunc));
if (maxSize <= 0)
throw new ArgumentException("Max Size must be greater than 0", nameof (maxSize));
this.m_List = new List<T>(defaultCapacity);
this.m_CreateFunc = createFunc;
this.m_MaxSize = maxSize;
this.m_ActionOnGet = actionOnGet;
this.m_ActionOnRelease = actionOnRelease;
this.m_ActionOnDestroy = actionOnDestroy;
this.m_CollectionCheck = collectionCheck;
}
public T Get()
{
T obj;
if (this.m_List.Count == 0)
{
obj = this.m_CreateFunc();
++this.CountAll;
}
else
{
int index = this.m_List.Count - 1;
obj = this.m_List[index];
this.m_List.RemoveAt(index);
}
Action<T> actionOnGet = this.m_ActionOnGet;
if (actionOnGet != null)
actionOnGet(obj);
return obj;
}
public PooledObject<T> Get(out T v) => new PooledObject<T>(v = this.Get(), (IObjectPool<T>) this);
public void Release(T element)
{
if (this.m_CollectionCheck && this.m_List.Count > 0)
{
for (int index = 0; index < this.m_List.Count; ++index)
{
if ((object) element == (object) this.m_List[index])
throw new InvalidOperationException("Trying to release an object that has already been released to the pool.");
}
}
Action<T> actionOnRelease = this.m_ActionOnRelease;
if (actionOnRelease != null)
actionOnRelease(element);
if (this.CountInactive < this.m_MaxSize)
{
this.m_List.Add(element);
}
else
{
Action<T> actionOnDestroy = this.m_ActionOnDestroy;
if (actionOnDestroy != null)
actionOnDestroy(element);
}
}
public void Clear()
{
if (this.m_ActionOnDestroy != null)
{
foreach (T obj in this.m_List)
this.m_ActionOnDestroy(obj);
}
this.m_List.Clear();
this.CountAll = 0;
}
public void Dispose() => this.Clear();
}
}
关于生命周期管理
还是射击游戏的例子,现在我们要枪射出子弹,代码设计如下:
1.创建一个枪类
2.在枪类中创建一个接受子弹类的对象池
3.创建n个子弹类的预制体
那么我们该如何管理它们的生命周期?或者说上述三个类哪些需要执行MonoBehaviour的Update方法?
首先,对象池类肯定不执行Update方法,它就不该继承MonoBehaviour,我们只需要让它实现管理物体进出池的方法,对物体本身生命周期的执行不该由他执行。
子弹类可以实现Update方法吗?由于子弹类是GameObject,所以通常需要继承MonoBehaviour,但不代表生命周期函数一定由子弹类自身执行。很简单的道理,因为每个Update的存在都是对Unity 的性能消耗,假设场景里有十把枪,每把发射100发子弹,那么总计1000发子弹就得执行1000个Update方法!显然这是不合适的。
所以正常的方式是让枪类实现Update方法,并在枪类中对子弹应做的Update事件进行处理。这样10把枪就只需要10个update就能管理1000发子弹。
using UnityEngine.Pool;
public class RevisedGun : MonoBehaviour
{
…
// stack-based ObjectPool available with Unity 2021 and above
private IObjectPool<RevisedProjectile> objectPool;
// throw an exception if we try to return an existing item, already in the pool
[SerializeField] private bool collectionCheck = true;
// extra options to control the pool capacity and maximum size
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private void Awake()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
collectionCheck, defaultCapacity, maxSize);
}
// invoked when creating an item to populate the object pool
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}
// invoked when returning an item to the object pool
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}
// invoked when retrieving the next item from the object pool
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}
// invoked when we exceed the maximum number of pooled items (i.e. destroy the pooled object)
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
private void FixedUpdate()
{
…
}
}
还有一点,就是如果多个对象引用同一个对象池,那么我们在实现对象池代码的时候还需要注意类型安全,线程安全等特性。