游戏随笔之游戏资源池的设计

  很久没有更新了,今天给大家写一篇游戏资源池的相关文章,就当作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年新年快乐!

原文地址:https://www.cnblogs.com/zblade/p/8134121.html