本文为博主原创文章,欢迎转载。请保留博主链接http://blog.csdn.net/andrewfan


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<T> where T:class,Pool_Unit
{
protected object m_template;
protected List<T> m_idleList;
protected List<T> m_workList;
protected int m_createdNum = ;
public Pool_UnitList()
{
m_idleList = new List<T>();
m_workList = new List<T>();
} /// <summary>
/// 获取一个闲置的单元,如果不存在则创建一个新的
/// </summary>
/// <returns>闲置单元</returns>
public virtual T takeUnit<UT>() where UT:T
{
T unit;
if (m_idleList.Count > )
{
unit = m_idleList[];
m_idleList.RemoveAt();
}
else
{
unit = createNewUnit<UT>();
unit.setParentList(this);
m_createdNum++;
}
m_workList.Add(unit);
unit.state().InPool = Pool_Type.Work;
OnUnitChangePool(unit);
return unit;
}
/// <summary>
/// 归还某个单元
/// </summary>
/// <param name="unit">单元</param>
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);
}
}
/// <summary>
/// 设置模板
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="template"></param>
public void setTemplate(object template)
{
m_template = template;
}
protected abstract void OnUnitChangePool(T unit);
protected abstract T createNewUnit<UT>() 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<UnitType, UnitList> : BaseBehavior
where UnitType : class,Pool_Unit
where UnitList : Pool_UnitList<UnitType>, new()
{
/// <summary>
/// 缓冲池,按类型存放各自分类列表
/// </summary>
private Dictionary<Type, UnitList> m_poolTale = new Dictionary<Type, UnitList>(); protected override void OnInitFirst()
{
} protected override void OnInitSecond()
{ } protected override void OnUpdate()
{ } /// <summary>
/// 获取一个空闲的单元
/// </summary>
public T takeUnit<T>() where T : class,UnitType
{
UnitList list = getList<T>();
return list.takeUnit<T>() as T;
} /// <summary>
/// 在缓冲池中获取指定单元类型的列表,
/// 如果该单元类型不存在,则立刻创建。
/// </summary>
/// <typeparam name="T">单元类型</typeparam>
/// <returns>单元列表</returns>
public UnitList getList<T>() where T : UnitType
{
var t = typeof(T);
UnitList list = null;
m_poolTale.TryGetValue(t, out list);
if (list == null)
{
list = createNewUnitList<T>();
m_poolTale.Add(t, list);
}
return list;
}
protected abstract UnitList createNewUnitList<UT>() 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<Pooled_BehaviorUnit,Pool_UnitList_Comp>
{
[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<UT>()
{
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<Pooled_BehaviorUnit>
{
protected Pool_Comp m_pool;
public void setPool(Pool_Comp pool)
{
m_pool = pool;
}
protected override Pooled_BehaviorUnit createNewUnit<UT>()
{
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<UT>();
if (comp == null)
{
comp = result_go.AddComponent<UT>();
}
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<Pooled_BehaviorUnit> m_parentList;
/// <summary>
/// 返回一个单元状态,用于控制当前单元的闲置、工作状态
/// </summary>
/// <returns>单元状态</returns>
public virtual Pool_UnitState state()
{
return m_unitState;
}
/// <summary>
/// 接受父列表对象的设置
/// </summary>
/// <param name="parentList">父列表对象</param>
public virtual void setParentList(object parentList)
{
m_parentList = parentList as Pool_UnitList<Pooled_BehaviorUnit>;
}
/// <summary>
/// 归还自己,即将自己回收以便再利用
/// </summary>
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=;
[SerializeField][Tooltip("移动时长")]
private float m_moveTime=;
[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 = ;
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_Comp>("pool_comps");
m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);
} protected override void OnInitSecond()
{ } protected override void OnUpdate()
{
m_fireTick -= Time.deltaTime;
if (m_fireTick < )
{
m_fireTick += m_fireRate;
fire();
}
}
protected void fire()
{
Bullet bullet = m_compPool.takeUnit<Bullet>();
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
{
/// <summary>
/// 单例单元管理器
/// 你可以创建单例组件,每个单例组件对应一个GameObject。
/// 你可以为单例命名,名字同时也会作为GameObject的名字。
/// 这些产生的单例一般用作管理器。
/// </summary>
public static class Singletons
{
private static Dictionary<string, BaseBehavior> m_singletons = new Dictionary<string, BaseBehavior>();
public static T Get<T>(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<T>();
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<string> keys = new List<string>();
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_Comp>("pool_comps");//创建内存池
m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);//设置模板
Bullet bullet = m_compPool.takeUnit<Bullet>();//索取单元
bullet.restore(); //回收单元

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

对象池

本例完整项目资源请参见我的CSDN博客:http://blog.csdn.net/andrewfan

本文为博主原创文章,欢迎转载。请保留博主链接:http://blog.csdn.net/andrewfan

Unity中的万能对象池的更多相关文章

  1. c++实现游戏开发中常用的对象池(含源码)

    c++实现游戏开发中常用的对象池(含源码) little_stupid_child2017-01-06上传   对象池的五要素: 1.对象集合 2.未使用对象索引集合 3.已使用对象索引集合 4.当前 ...

  2. Unity实现简单的对象池

    一.简介 先说说为什么要使用对象池 在Unity游戏运行时,经常需要生成一些物体,例如子弹.敌人等.虽然Unity中有Instantiate()方法可以使用,但是在某些情况下并不高效.特别是对于那些需 ...

  3. unity游戏开发_对象池

    现在假设游戏中我们需要实现一个这样功能,按下鼠标左键,发射一颗子弹,3秒之后消失.在这个功能中,我们发射了上百上千发子弹,就需要实例化生成上百上千次.这时候,我们就需要使用对象池这个概念,每次实例化生 ...

  4. 【Fungus入门】10分钟快速构建Unity中的万能对话系统 / 叙事系统 / 剧情系统

    我真的很久没有写过一个完整的攻略了(笑),咸鱼了很久之后还是想来写一个好玩的.这次主要是梳理一下Unity的小众插件Fungus的核心功能,并且快速掌握其使用方法. 官方文档:http://fungu ...

  5. Unity中销毁游戏对象的方式

    销毁方式 销毁物体的方式有两种:Destroy和DestroyImmediate两种,那两者有什么区别呢?细听分说. 两种方式都能达到销毁物体的目的,有以下区别: Destroy销毁场景中的物体但是内 ...

  6. 在C#中实现简单的对象池

    当我们频繁创建删除大量对象的时候,对象的创建删除所造成的开销就不容小觑了.为了提高性能,我们往往需要实现一个对象池作为Cache:使用对象时,它从池中提取.用完对象时,它放回池中.从而减少创建对象的开 ...

  7. 游戏设计模式——Unity对象池

    对象池这个名字听起来很玄乎,其实就是将一系列需要反复创建和销毁的对象存储在一个看不到的地方,下次用同样的东西时往这里取,类似于一个存放备用物质的仓库. 它的好处就是避免了反复实例化个体的运算,能减少大 ...

  8. 对象池在 .NET (Core)中的应用[1]: 编程体验

    借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC.并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还 ...

  9. 对象池在 .NET (Core)中的应用[2]: 设计篇

    <编程篇>已经涉及到了对象池模型的大部分核心接口和类型.对象池模型其实是很简单的,不过其中有一些为了提升性能而刻意为之的实现细节倒是值得我们关注.总的来说,对象池模型由三个核心对象构成,它 ...

随机推荐

  1. SQL Server 2012 - 数据表的操作

     unicode:双字节编码      variable:可变的    character:字符 T-SQL:  Transact Structured Query Language unique:唯 ...

  2. Backbone视图渲染React组件

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title&g ...

  3. 【推荐】PHP中格式化时间函数date与gmdate的区别 | 修改PHP的默认时区

    PHP中的时间有2个格式化函数:date()和gmdate(),在官方的文档中的描述为: date -- 格式化一个本地时间/日期 gmdate -- 格式化一个 GMT/UTC 日期/时间,返回的是 ...

  4. StackExchange.Redis 官方文档(二) Configuration

    配置 有多种方式可以配置redis,StackExchange.Redis提供了一个丰富的配置模型,在执行Connect (or ConnectAsync) 时被调用: var conn = Conn ...

  5. 深入理解SQL的四种连接

    SQL标准 select table1.column,table2.column from table1 [inner | left | right | full ] join table2 on t ...

  6. 在windows上搭建ipv6代理

    事出有因,学校每天12:00之后断网,断网之后怎么办?难道直接睡了?我不甘心.     幸好学校还是留有余地,在断网之后,还是能够上ipv6的,只是现阶段互联网对ipv6支持很不理想,怎么办?刚刚发现 ...

  7. jQuery图片轮播的具体实现

    先看一看html代码,以及对应的css代码: <div id="scrollPics">    <ul class="slider" > ...

  8. Linux用户和用户组管理总结

    Linux下和用户和用户组管理有关的配置文件: /etc/group Group account information. /etc/gshadow Secure group account info ...

  9. IDEA Show Line Number

    刚开始用IDEA,经常发现右侧没有显示行号,然后去右键选一下,就显现了 一直没有留意这个现象,刚用vim想删几行数据代码,突然发现没有行号了 明明记得刚刚才右键显示了的 好吧,有行号用着比较顺心了.. ...

  10. spring 里面的StringUtils,先放这儿,有时间研究吧

    /* * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Vers ...