游戏随笔之游戏资源池的设计
很久没有更新了,今天给大家写一篇游戏资源池的相关文章,就当作2017年的最后一篇文章吧。 转载请标明出处:http://www.cnblogs.com/zblade/
一、游戏项目中的资源池
在一款游戏中,随着游戏的进行,我们会不断的创建和销毁一些角色,比如我们玩一款射击游戏,我们需要不断的发射子弹,一般的情况下,我们会不断的创建子弹,然后发射出去,在击中物体后销毁。分析整个设计的过程,我们会不断的创建子弹,然后发射出去,最后销毁它。这儿,其实就可以引入资源池的概念来解决子弹的反复创建和销毁。
如果只是反复的创建和销毁子弹,那么每次的性能点主要在子弹的创建上,在场景中有较多角色频繁操作射击的时候,不断执行创建,会带来性能的较大消耗,从而让游戏卡帧。如果我们把这些子弹预先创建出来,塞入到一个弹夹,然后每次在射击发射子弹的时候,从预先创建的弹夹中取出来发射,每次在销毁的时候,又将其还原到弹夹中,这样循环反复的利用,可以避免每次射击子弹的时候的创建操作带来的性能消耗。将弹夹拓展一步,就是资源池的概念了。
二、不同设计下的资源池
1、简单lua版本的资源池
基于前文子弹的阐述,我就写一个简单版本的资源池的lua版本,首先,是资源池的定义:
function BulletPool:initialize()
--缓存子弹的table
self.mBulletPool = {}
end
是不是觉得很简单,是的,lua版本的资源池可以只需要用一个table就简单的表示,接下来,我们只需要维护好这个table即可。首先是取子弹的接口:
function BulletPool:GetBullet()
--如果没有定义,则执行一次兜底定义
if not self.mBulletPool then
self.mBulletPool = {}
end
-- 如果池子里面没有可以取的了,则返回nil
if next(self.mBulletPool) == nil then
return nil
end
local bulletObj = self.mBulletPool[#self.mBulletPool]
table.remove(self.mBulletPool, #self.mBulletPool)
return bulletObj
end
有了设计的接口,接下来,我们可以继续设计归还的接口,所谓有借有还再借不难,不能只从池子里面取,不归还,那池子早晚会干涸的 :D
function BulletPool:InsertBullet(bullet)
if not bullet then return end
if not self.mBulletPool then
self.mBulletPool = {}
end
--子弹归还前的释放操作,可以不在意这一步操作
bullet:Release()
table.insert(self.mBulletPool, bullet)
end
好了,有了整体的获取和归还的操作,池子的基本接口就有了,有的同学会说,如果我们想重置一遍池子怎么办?那就再写一个清除池子的操作吧 :b
function BulletPool:Release()
for k, v in pairs(self.mBulletPool) do
v:Release()
end
for k, v in pairs(self.mBulletPool) do
self.mBulletPool[k] = nil
end
end
这下接口都有了,让我们来应用这些接口吧 :D
首先给角色挂载一个子弹的资源池的获取接口吧:
--获取接口
function Character:GetBullet()
return BulletPool:GetBullet()
end
--塞入接口
function Character:RemoveBullet(bullet)
BulletPool:InsertBullet(bullet)
end
因为每个角色都会射出一堆的子弹,所以我们是直接挂在角色身上,就不在整个场景管理器中去管理子弹了,可以通过场景管理器的更新来执行角色的更新,从而执行所有子弹的更新,这样每个角色的子弹更新和角色更新一致。这种设计模式下,不会出现先更新角色,然后再更新子弹的带来的一些问题。
有了这两个接口,下面就是让角色调用这2个接口:
...
--获取子弹
local bullet = mChar:GetBullet()
--没有则新建,有则重新初始化相关参数
if bullet == nil then
bullet = Bullet:new(...)
else
bullet:initialize(...)
end
--塞入到角色身上的一个table中维护
mChar:AddBullet()
...
--移除子弹,比较简单
if bullet:Update() then
mChar:RemoveBullet(bullet)
end
到这儿,我们完成了一个简单的lua版本的资源池的设计和实现,通过这几个接口,对资源池有一个简单的入门理解了。接下来,我们进一步编写一个c#版本的资源池吧。
2、C#版本的资源池
在有了lua版本的资源池入门之后,接下来我们可以进一步的设计一个c#版本的资源池了。在unity的c#中,会有各种各样的资源需要资源池来进行管理,所以我们不能单独的做某个类的资源池了,我们需要引入泛型来指代各种类型的资源池。
先写一个简单的资源池,就实现一个获取和归还接口吧:
using System.Collections; using System.Collections.Generic; public class ObjectPool<T> where T:class { //用一个列表来代替lua中的table,用作资源池 LinkedList<T> objs = new LinkedList<T>(); public T GetObject() { if(objs.Count > 0) { T obj = objs.Last.Value; objs.RemoveLast(); return obj; } return null; } public void ReturnObj(T obj) { if(obj != null) { obj.AddLast(obj); } } }
有了简化版本的资源池,我们可以进一步的拓展这个池子的设计。首先,我们可以将链表改为堆栈,用一个栈来代替链表,相对会比较容易控制,只需要管理入栈和出栈即可。其次,在池子已经被榨干,取完的时候,前面是直接返回一个null,我们可以继续拓展,在没有的时候,就进行一次创建操作,这个可以通过委托来实现,在池子的初始化的时候就注册相关的委托。同理,进一步的拓展出取完后的操作和归还释放时的操作委托,这样就把我们前面lua中归还池子时候释放子弹的操作封装为一个事件。说完思路,下面让我们开始吧:
using System; using System.Collections.Generic; public class ObjectPool<T> where T:class { //堆栈 private readonly Stack<T> m_stack; //事件 private readonly Func<T> m_ActionOnCreate; private readonly Action<T> m_ActionOnGet; private readonly Action<T> m_ActionOnRelease; //构造函数 public ObjectPool(Func<T> actionOnCreate, Action<T> actionOnGet, Action<T> actionOnRelease) { m_stack = new Stack<T>(); m_ActionOnCreate = actionOnCreate; m_ActionOnGet = actionOnGet; m_ActionOnRelease = actionOnRelease; } //获取接口 public T Get() { T obj; if(m_stack.Count == 0) { //执行构建操作 obj = m_ActionOnCreate(); } else { obj = m_stack.Pop(); } //执行回调 if(m_AcitonOnGet != null) { m_ActionOnGet(obj); } return obj; } //释放接口 public void Release(T obj) { if(m_ActionOnRelease != null) { m_ActionOnRelease(obj); } m_stack.Push(obj); } //clear接口 public void Clear() { m_stack.Clear(); } }
写到这儿,一个基本的资源池的构建算是完成了,大家可以在这个版本的基础上进一步的衍生出资源池的使用,比如给资源池的对象添加一个计时的功能,当资源计时超过一定的时间后,就将其从资源池中去除,避免资源池不断扩大。诸如此类种种,都是后续可以操作的,好了,这篇文章就写到这儿,也祝提前祝大家2018年新年快乐!