Unity3D使用对象池高效管理内存

2017年03月15日 16:04 0 点赞 0 评论 更新于 2025-11-21 21:04
Unity3D使用对象池高效管理内存

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,用于处理同一类的对象。

考虑到内存池单元要尽可能容易扩展,能够兼容任意数据类型,我们选择使用接口来定义内存池单元。因为一旦使用类,就无法兼容 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 = 0;

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 > 0)
{
unit = m_idleList[0];
m_idleList.RemoveAt(0);
}
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 组件化

前面实现的结构不受限于 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 = 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_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 < 0)
{
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 内存将不会再无限制增长,而是会实现循环利用。

资源下载

本页完整资源下载地址:http://download.csdn.net/detail/andrewfan/9764702

原文链接

http://blog.csdn.net/andrewfan

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章