博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Unity编程标准导引-3.4 Unity中的对象池
阅读量:4648 次
发布时间:2019-06-09

本文共 12861 字,大约阅读时间需要 42 分钟。

本文为博主原创文章,欢迎转载。请保留博主链接


Unity编程标准导引-3.4 Unity中的对象池

  本节通过一个简单的射击子弹的示例来介绍Transform的用法。子弹射击本身很容易制作,只要制作一个子弹Prefab,再做一个发生器,使用发生器按频率产生子弹,即克隆子弹Prefab,然后为每个子弹写上运动逻辑就可以了。这本该是很简单的事情。不过问题来了,发射出去后的子弹如何处理?直接Destroy吗?这太浪费了,要知道Unity的Mono内存是不断增长的。就是说除了Unity内部的那些网格、贴图等等资源内存(简单说就是继承自UnityEngine下的Object的那些类),而我们自己写的C#代码继承自System下的Object,这些代码产生的内存即是Mono内存,它只增不减。同样,你不断Destroy你的Unity对象也是要消耗性能去进行回收,而子弹这种消耗品实在产生的太快了,我们必需加以控制。

  那么,我们如何控制使得不至于不断产生新的内存呢?答案就是自己写内存池。自己回收利用之前创建过的对象。所以这个章节的内容,我们将重点放在写一个比较好的内存池上。就我自己来讲,在写一份较为系统的功能代码之前,我考虑的首先不是这个框架是该如何的,而是从使用者的角度去考虑,这个代码如何写使用起来才会比较方便,同样也要考虑容易扩展、通用性强、比较安全、减少耦合等等。

本文最后结果显示如下:

3.4.1、从使用者视角给出需求

  首先,我所希望的这个内存池的代码最后使用应该是这样的。

  • Bullet a = Pool.Take<Bullet>(); //从池中立刻获取一个单元,如果单元不存在,则它需要为我立刻创建出来。返回一个Bullet脚本以便于后续控制。注意这里使用泛型,也就是说它应该可以兼容任意的脚本类型。
  • Pool.restore(a);//当使用完成Bullet之后,我可以使用此方法回收这个对象。注意这里实际上我已经把Bullet这个组件的回收等同于某个GameObject(这里是子弹的GameObject)的回收。
      使用上就差不多是这样了,希望可以有极其简单的方法来进行获取和回收操作。

3.4.2、内存池单元结构

  最简单的内存池形式,差不多就是两个List,一个处于工作状态,一个处于闲置状态。工作完毕的对象被移动到闲置状态列表,以便于后续的再次获取和利用,形成一个循环。我们这里也会设计一个结构来管理这两个List,用于处理同一类的对象。

  接下来是考虑内存池单元的形式,我们考虑到内存池单元要尽可能容易扩展,就是可以兼容任意数据类型,也就是说,假设我们的内存池单元定为Pool_Unit,那么它不能影响后续继承它的类型,那我们最好使用接口,一旦使用类,那么就已经无法兼容Unity组件,因为我们自定义的Unity组件全部继承自MonoBehavior。接下来考虑这个内存单元该具有的功能,差不多有两个基本功能要有:

  • restore();//自己主动回收,为了方便后续调用,回收操作最好自己就有。
  • getState();//获取状态,这里是指获取当前是处于工作状态还是闲置状态,也是一个标记,用于后续快速判断。因为接口中无法存储单元,这里使用变通的方法,就是留给实现去处理,接口中要求具体实现需要提供一个状态标记。
      综合内存池单元和状态标记,给出如下代码:
    namespace AndrewBox.Pool{  public interface Pool_Unit  {      Pool_UnitState state();      void setParentList(object parentList);      void restore();  }  public enum Pool_Type  {      Idle,      Work  }  public class Pool_UnitState  {      public Pool_Type InPool      {          get;          set;      }  }}

     

    3.4.3、单元组结构

      接下来考虑单元组,也就是前面所说的针对某一类的单元进行管理的结构。它内部有两个列表,一个工作,一个闲置,单元在工作和闲置之间转换循环。它应该具有以下功能:
  • 创建新单元;使用抽象方法,不限制具体创建方法。对于Unity而言,可能需要从Prefab克隆,那么最好有方法可以从指定的Prefab模板复制创建。
  • 获取单元;从闲置表中查找,找不到则创建。
  • 回收单元;将其子单元进行回收。
      综合单元组结构的功能,给出如下代码:
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace AndrewBox.Pool{    public abstract class Pool_UnitList
where T:class,Pool_Unit { protected object m_template; protected List
m_idleList; protected List
m_workList; protected int m_createdNum = 0; public Pool_UnitList() { m_idleList = new List
(); m_workList = new List
(); } ///
/// 获取一个闲置的单元,如果不存在则创建一个新的 /// ///
闲置单元
public virtual T takeUnit
() where UT:T { T unit; if (m_idleList.Count > 0) { unit = m_idleList[0]; m_idleList.RemoveAt(0); } else { unit = createNewUnit
(); unit.setParentList(this); m_createdNum++; } m_workList.Add(unit); unit.state().InPool = Pool_Type.Work; OnUnitChangePool(unit); return unit; } ///
/// 归还某个单元 /// ///
单元 public virtual void restoreUnit(T unit) { if (unit!=null && unit.state().InPool == Pool_Type.Work) { m_workList.Remove(unit); m_idleList.Add(unit); unit.state().InPool = Pool_Type.Idle; OnUnitChangePool(unit); } } ///
/// 设置模板 /// ///
///
public void setTemplate(object template) { m_template = template; } protected abstract void OnUnitChangePool(T unit); protected abstract T createNewUnit
() where UT : T; }}

 

3.4.4、内存池结构

  内存池是一些列单元组的集合,它主要使用多个单元组具体实现内存单元的回收利用。同时把接口尽可能包装的简单,以便于用户调用,因为用户只与内存池进行打交道。另外,我们最好把内存池做成一个组件,这样便于方便进行初始化、更新(目前不需要,或许未来你需要执行某种更新操作)等工作的管理。这样,我们把内存池结构继承自上个章节的BaseBehavior。获得如下代码:

using AndrewBox.Comp;using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace AndrewBox.Pool{    public abstract class Pool_Base
: BaseBehavior where UnitType : class,Pool_Unit where UnitList : Pool_UnitList
, new() { ///
/// 缓冲池,按类型存放各自分类列表 /// private Dictionary
m_poolTale = new Dictionary
(); protected override void OnInitFirst() { } protected override void OnInitSecond() { } protected override void OnUpdate() { } ///
/// 获取一个空闲的单元 /// public T takeUnit
() where T : class,UnitType { UnitList list = getList
(); return list.takeUnit
() as T; } ///
/// 在缓冲池中获取指定单元类型的列表, /// 如果该单元类型不存在,则立刻创建。 /// ///
单元类型
///
单元列表
public UnitList getList
() where T : UnitType { var t = typeof(T); UnitList list = null; m_poolTale.TryGetValue(t, out list); if (list == null) { list = createNewUnitList
(); m_poolTale.Add(t, list); } return list; } protected abstract UnitList createNewUnitList
() where UT : UnitType; }}

 

3.4.5、组件化

  目前为止,上述的结构都没有使用到组件,没有使用到UnityEngine,也就是说它们不受限使用于Unity组件或者普通的类。当然使用起来也会比较麻烦。由于我们实际需要的内存池单元常常用于某种具体组件对象,比如子弹,那么我们最好针对组件进一步实现。也就是说,定制一种适用于组件的内存池单元。同时也定制出相应的单元组,组件化的内存池结构。

  另外,由于闲置的单元都需要被隐藏掉,我们在组件化的内存池单元中需要设置两个GameObject节点,一个可见节点,一个隐藏节点。当组件单元工作时,其对应的GameObject被移动到可见节点下方(当然你也可以手动再根据需要修改它的父节点)。当组件单元闲置时,其对应的GameObject也会被移动到隐藏节点下方。
  综合以上,给出以下代码:

using AndrewBox.Comp;using System;using System.Collections.Generic;using System.Linq;using System.Text;using UnityEngine;namespace AndrewBox.Pool{    public class Pool_Comp:Pool_Base
{ [SerializeField][Tooltip("运行父节点")] protected Transform m_work; [SerializeField][Tooltip("闲置父节点")] protected Transform m_idle; protected override void OnInitFirst() { if (m_work == null) { m_work = CompUtil.Create(m_transform, "work"); } if (m_idle == null) { m_idle = CompUtil.Create(m_transform, "idle"); m_idle.gameObject.SetActive(false); } } public void OnUnitChangePool(Pooled_BehaviorUnit unit) { if (unit != null) { var inPool=unit.state().InPool; if (inPool == Pool_Type.Idle) { unit.m_transform.SetParent(m_idle); } else if (inPool == Pool_Type.Work) { unit.m_transform.SetParent(m_work); } } } protected override Pool_UnitList_Comp createNewUnitList
() { Pool_UnitList_Comp list = new Pool_UnitList_Comp(); list.setPool(this); return list; } }}
using System;using System.Collections.Generic;using System.Linq;using System.Text;using UnityEngine;namespace AndrewBox.Pool{    public class Pool_UnitList_Comp : Pool_UnitList
{ protected Pool_Comp m_pool; public void setPool(Pool_Comp pool) { m_pool = pool; } protected override Pooled_BehaviorUnit createNewUnit
() { GameObject result_go = null; if (m_template != null && m_template is GameObject) { result_go = GameObject.Instantiate((GameObject)m_template); } else { result_go = new GameObject(); result_go.name = typeof(UT).Name; } result_go.name =result_go.name + "_"+m_createdNum; UT comp = result_go.GetComponent
(); if (comp == null) { comp = result_go.AddComponent
(); } comp.DoInit(); return comp; } protected override void OnUnitChangePool(Pooled_BehaviorUnit unit) { if (m_pool != null) { m_pool.OnUnitChangePool(unit); } } }}
using AndrewBox.Comp;using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace AndrewBox.Pool{    public abstract class Pooled_BehaviorUnit : BaseBehavior, Pool_Unit    {        //单元状态对象        protected Pool_UnitState m_unitState = new Pool_UnitState();        //父列表对象        Pool_UnitList
m_parentList; ///
/// 返回一个单元状态,用于控制当前单元的闲置、工作状态 /// ///
单元状态
public virtual Pool_UnitState state() { return m_unitState; } ///
/// 接受父列表对象的设置 /// ///
父列表对象 public virtual void setParentList(object parentList) { m_parentList = parentList as Pool_UnitList
; } ///
/// 归还自己,即将自己回收以便再利用 /// public virtual void restore() { if (m_parentList != null) { m_parentList.restoreUnit(this); } } }}

 

3.4.6、内存池单元具体化

接下来,我们将Bullet具体化为一种内存池单元,使得它可以方便从内存池中创建出来。

using UnityEngine;using System.Collections;using AndrewBox.Comp;using AndrewBox.Pool;public class Bullet : Pooled_BehaviorUnit {    [SerializeField][Tooltip("移动速度")]    private float m_moveVelocity=10;    [SerializeField][Tooltip("移动时长")]    private float m_moveTime=3;    [System.NonSerialized][Tooltip("移动计数")]    private float m_moveTimeTick;    protected override void OnInitFirst()    {    }    protected override void OnInitSecond()    {    }    protected override void OnUpdate()    {        float deltaTime = Time.deltaTime;        m_moveTimeTick += deltaTime;        if (m_moveTimeTick >= m_moveTime)        {            m_moveTimeTick = 0;            this.restore();        }        else        {            var pos = m_transform.localPosition;            pos.z += m_moveVelocity * deltaTime;            m_transform.localPosition = pos;        }    }}

 

3.4.7、内存池的使用

最后就是写一把枪来发射子弹了,这个逻辑也相对简单。为了把内存池做成单例模式并存放在单独的GameObject,我们还需要另外一个单例单元管理器的辅助,一并给出。

using UnityEngine;using System.Collections;using AndrewBox.Comp;using AndrewBox.Pool;public class Gun_Simple : BaseBehavior {    [SerializeField][Tooltip("模板对象")]    private GameObject m_bulletTemplate;    [System.NonSerialized][Tooltip("组件对象池")]    private Pool_Comp m_compPool;    [SerializeField][Tooltip("产生间隔")]    private float m_fireRate=0.5f;     [System.NonSerialized][Tooltip("产生计数")]    private float m_fireTick;    protected override void OnInitFirst()    {        m_compPool = Singletons.Get
("pool_comps"); m_compPool.getList
().setTemplate(m_bulletTemplate); } protected override void OnInitSecond() { } protected override void OnUpdate() { m_fireTick -= Time.deltaTime; if (m_fireTick < 0) { m_fireTick += m_fireRate; fire(); } } protected void fire() { Bullet bullet = m_compPool.takeUnit
(); bullet.m_transform.position = m_transform.position; bullet.m_transform.rotation = m_transform.rotation; }}
using AndrewBox.Comp;using System;using System.Collections.Generic;using System.Linq;using System.Text;using UnityEngine;namespace AndrewBox.Comp{    ///     /// 单例单元管理器    /// 你可以创建单例组件,每个单例组件对应一个GameObject。    /// 你可以为单例命名,名字同时也会作为GameObject的名字。    /// 这些产生的单例一般用作管理器。    ///     public static class Singletons    {        private static Dictionary
m_singletons = new Dictionary
(); public static T Get
(string name) where T:BaseBehavior { BaseBehavior singleton = null; m_singletons.TryGetValue(name, out singleton); if (singleton == null) { GameObject newGo = new GameObject(name); singleton = newGo.AddComponent
(); m_singletons.Add(name, singleton); } return singleton as T; } public static void Destroy(string name) { BaseBehavior singleton = null; m_singletons.TryGetValue(name, out singleton); if (singleton != null) { m_singletons.Remove(name); GameObject.DestroyImmediate(singleton.gameObject); } } public static void Clear() { List
keys = new List
(); foreach (var key in m_singletons.Keys) { keys.Add(key); } foreach (var key in keys) { Destroy(key); } } }}

 

3.4.8、总结

最终,我们写出了所有的代码,这个内存池是通用的,而且整个游戏工程,你几乎只需要这样的一个内存池,就可以管理所有的数量众多且种类繁多的活动单元。而调用处只有以下几行代码即可轻松管理。

m_compPool = Singletons.Get
("pool_comps");//创建内存池 m_compPool.getList
().setTemplate(m_bulletTemplate);//设置模板 Bullet bullet = m_compPool.takeUnit
();//索取单元 bullet.restore(); //回收单元

 

最终当你正确使用它时,你的GameObject内存不会再无限制增长,它将出现类似的下图循环利用。

对象池

本例完整项目资源请参见我的CSDN博客:

本文为博主原创文章,欢迎转载。请保留博主链接:

转载于:https://www.cnblogs.com/driftingclouds/p/6421876.html

你可能感兴趣的文章
[C#] 谈谈异步编程async await
查看>>
【转】测试人员职业规划
查看>>
思科交换机的初始配置(使用telnet登录)
查看>>
vim
查看>>
【学习笔记】APP测试基本流程及测试要点
查看>>
区间DP——入门
查看>>
SAS中修改一个表为编辑模式的时候不成功并给出警告的原因及解决办法
查看>>
python数据结构-如何统计序列中元素的频度
查看>>
展开收起js
查看>>
tpcc
查看>>
[ActionScript 3.0] AS3中的位图(BitmapData)应用
查看>>
datagrid在MVC中的运用02-结合搜索
查看>>
我们的目标是安全有效支持业务的信息处理技术平台
查看>>
编程学习方法
查看>>
静态链接库与动态链接库
查看>>
bzoj1180: [CROATIAN2009]OTOCI
查看>>
倒计时问题java
查看>>
某猿的饭局
查看>>
操作集合的工具类Collections
查看>>
一次ajax请求返回状态为Cancled的记录
查看>>