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脚本以便于后续控制。注意这里使用泛型,也就是说它应该可以兼容任意的脚本类型。

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内存不会再无限制增长,它将出现类似的下图循环利用。

对象池

博客传送门 :http://blog.csdn.net/andrewfan

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 本文为博主原创文章,欢迎转载。请保留博主链接http://blog.csdn.net/andrewfan Unit...
    AndrewFan阅读 4,077评论 5 39
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 有朋友还在单例的问题上纠结,如何写才算完备,今天在这里写一写。 注意:单例一但创建,整个App的使用过程中都不会被...
    攻克乃还_阅读 293评论 0 0
  • 喜欢吃的菜总是习惯放到最后去吃 可是到最后发现都被别人吃了 喜欢的人总是想慢慢去追 可是到最后发现已被别人追走了 ...
    简书播客阅读 17,243评论 174 400