用对象池管理游戏物体

对象池要实现的是对对象的复用,就好像是把一堆东西放在一个地方,用的时候就拿一个出去,再用就再拿一个,用完了再放回来。在Unity中可以用SetActive方法将游戏物体关闭与开启来代替Instantiate实例化与Destroy销毁,拿出去的东西既是激活的游戏物体,放回来的东西既是关闭的游戏物体。这种方法性能消耗小,可以把不同地点,不同事件,不同角色调用的同一种游戏物体放在一个列表中(对象池)管理,减少了指令中的内存相关操作。

下面一个简单的例子:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;public class ObjectPooler : MonoBehaviour {//之后会通过这个静态变量来访问下面的获取对象池中游戏物体的方法public static ObjectPooler current;//可以在Editor界面中通过给此变量赋值设置需要被管理的游戏物体public GameObject pooledObject;//对象池初始时实例化游戏物体的数量的上限,如果下面的poolGrow变量为false。那么场景中激活的对象池物体不会超过此数量。public int pooledAmount=20;//对象池在游戏进程中是否可动态扩展public bool poolGrow=true;//一个对象池。可以是list,stack或其他集合。public List<GameObject> pool=new List<GameObject>();void Awake(){current=this;}void Start () {//实例化游戏物体,暂时关闭游戏物体,加入游戏物体到列表中for(int i=0;i<pooledAmount;i++){GameObject obj=(GameObject)Instantiate(pooledObject);obj.SetActive(false);pool.Add(obj);}}//其他类通过此方法获取对象池中的游戏物体,此方法只会将未使用的(既是未激活的)游戏物体返回。如果列表中的游戏物体已经全部在场景中,就实例化新的游戏对象,并开始动态扩展列表。public GameObject getPooledObject(){for(int i=0;i<pool.Count;i++){if(!pool[i].activeInHierarchy){pool[i].SetActive(true);return pool[i];}}if(poolGrow){GameObject obj=(GameObject)Instantiate(pooledObject);pool.Add(obj);return obj;}return null;}
}


图1:场景中设置两个几何物体。假设立方体为飞机,球体为子弹。

球体子弹上的脚本:

using UnityEngine;
using System.Collections;public class SpereTest : MonoBehaviour {//假设子弹的生命周期为两秒,把Invoke命令放在了OnEnable方法内,开启两秒后消失。void OnEnable(){Invoke("Destroy",2f);}//用setActive(false)代替销毁,以使它可以重复使用void Destroy(){gameObject.SetActive(false);}//销毁或关闭一个游戏物体并不会自动取消Invoke方法,所以如果有其他途径关闭了这个子弹,可以在这里手动取消Invoke方法。void OnDisable(){CancelInvoke();}void Update () {//子弹移动 transform.Translate(Vector3.forward*Time.deltaTime*50f);}
}

Cube飞机上的脚本:

using UnityEngine;
using System.Collections;public class CubeTest : MonoBehaviour {// Use this for initializationvoid Start () {}// Update is called once per framevoid Update () {if(Input.GetKey(KeyCode.Q)){Fire();}}void Fire(){//通过对象池类中的静态变量获取对象池中的游戏物体GameObject obj=ObjectPooler.current.getPooledObject();//如果对象池类中的动态扩展变量为false,则只会获取pooledAmount个游戏物体。有可能在某个时间点对象池中暂无可用的子弹。if(obj==null){return;}//将子弹移动到出现的地点。obj.transform.position=transform.position;obj.transform.rotation=transform.rotation;}
}


图2:飞机发射子弹截图

对象池类可以继续扩展一些功能,例如可以在某个方法内销毁(真正的Destroy)所有对象池中游戏物体并清空列表,并在游戏空闲场景中调用这个方法以释放堆空间。

单例模式

单线程场景解决方案

某些类的设计初衷是不适合出现两个实例的,例如上文的对象池类,如果出现了两个子弹对象池,势必出现某些混乱现象。单例模式既是对类的实例数量进行限制,保证在系统中只会有一个实例对象,下面就会将上文的对象池类改造成单例模式。

首先要解决的是限制AddComponent方法。由于Unity自身的机制,当我们写一个继承自MonoBehaviour的类,它是可以通过AddComponent()方法在任意一个GameObject身上变为它的组件的,现在通过将它定义在一个C#类之中,或者说对它进行包装,并将签名改为private,这样它就无法再通过AddComponent实例化了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SingletonPool {private class ObjectPooler : MonoBehaviour{......}
}

由于现在只有SingletonPool可以访问到ObjectPooler,所以对于ObjectPooler的单例化问题实际上变为了如何对SingletonPool进行单例限制,由于它是一个纯C#类,对它进行限制相比继承自Unity MonoBehaviour的类要简单一些。

下面通过加入静态自身引用与GetInstance函数实现单线程的单例化。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SingletonPool {//通过instance引用单例private static SingletonPool instance = null;private static GameObject go = null;private static ObjectPooler pool= null;//值可以通过GetInstance访问到instancepublic static SingletonPool GetInstance(){//第一次访问时实例化if(instance==null){instance = new SingletonPool();go = new GameObject();pool = go.AddComponent<ObjectPooler>();return instance;}else{return instance;}}//wrapper方法,接入ObjectPooler的方法并对外开放。public GameObject getPooledObject(){return pool.getPooledObject();}//...也许还会有其他wrapper方法...//private class ObjectPooler : MonoBehaviour{......}
}

多线程场景线程同步解决方案

以上的单例化适用于单线程场景。虽然Unity的设计是单线程的,非主线程调用上面的wrapper方法是会出错的,但是在多线程下是可以对SingletonPool进行实例化的。下面要考虑多线程的场景下的一些问题。

假设在一个多线程的场景下,if(instance==null) 对于单例的限制是不够鲁棒的。因为一旦在线程A完成instance==null之后且全部完成instance==new SingletonPool()的指令之前发生了线程切换,那么就会有最少两个逻辑流同时进入到if判断之内。一个简单的解决方法是对if(instance==null)加入锁,进行线程同步。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;public class SingletonPool {private static SingletonPool instance = null;private static Object s_lock = new Object();private static GameObject go = null;private static ObjectPooler pool= null;public static SingletonPool GetInstance(){//这里在同步前提前判断,大幅减少同步线程的次数if(instance!=null){return instance;}//线程同步Monitor.Enter(s_lock);if(instance==null){instance = new SingletonPool();go = new GameObject();pool = go.AddComponent<ObjectPooler>();}Monitor.Exit(s_lock);return instance;}public GameObject getPooledObject(){return pool.getPooledObject();}private class ObjectPooler : MonoBehaviour{......}
}

C#的Monitor.Enter/Exit的使用规避了一个有名的双检琐问题。双检琐问题起源于JAVA,是说如果在线程A通过了第一个instance!=null的判断之后且进入线程同步前发生线程切换,线程B进入线程同步并成功实例化,再切换回线程A,那么线程A在第二个instance==null的判断中返回的会是true(虽然实际上instance已经引用了单例),原因是JVM会根据优化思想直接访问cpu缓存的上次比较结果(既是第一次instance!=null后的结果),一个解决方法是在instance前加入volatile关键字(JAVA,C#均可),但是会加大每次对instance的访问时的性能损耗。而CLR的Monitor从底层机制上限制了线程同步后对缓存的访问,既是所有线程进入同步代码后,必须对其中的的变量重新读取,不可以读取缓存。

这样,以上的代码表面上解决了对单例的实例化的线程竞争,但是如果系统在堆中给SingletonPool分配内存之后,Monitor.Exit(s_lock)退出同步之前的期间,其他线程的逻辑流是有可能正好走到第一个instance!=null的判断处的,那样它就会使用一个未完全初始化的instance(已经分配内存,但是没有完成构造函数以及同步块之后的代码)并造成错误。

以下再引入一个Volatile.Write()方法,保证在SingletonPool构造函数完成后才会让其他线程读取instance变量,这样我们还可以利用这个特性将new GameObject和AddComponent方法也放入到构造函数内,保证instance的完全初始化。这个方法与instance声明加入volatile关键字是一样的,但它更优化的地方在于不用牺牲每次访问instance时的性能,只有在第一次实例化以及第一次实例化发生竞争时才会发生阻塞和性能损失。

    private SingletonPool(){go = new GameObject();pool = go.AddComponent<ObjectPooler>();}public static SingletonPool GetInstance(){//这里在同步前提前判断,大幅减少同步线程的次数if(instance!=null){return instance;}//线程同步Monitor.Enter(s_lock);if(instance==null){var temp = new SingletonPool();Volatile.Write(ref instance,temp);}Monitor.Exit(s_lock);return instance;}

但可惜的是在Unity中Threading命名空间中没有Volatile类,看来是不支持。因此只能在instance声明中加入volatile关键字以牺牲性能才能做到100%的鲁棒性。

多线程场景类型构造器解决方案

另一个解决方案是利用CLR的类构造器。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;public class SingletonPool
{private static SingletonPool instance = null;private static GameObject go = null;private static ObjectPooler pool = null;//线程安全的类型构造器static SingletonPool(){instance = new SingletonPool();go = new GameObject();pool = go.AddComponent<ObjectPooler>();}private SingletonPool(){}public static SingletonPool GetInstance(){return instance;}public GameObject getPooledObject(){return pool.getPooledObject();}private class ObjectPooler : MonoBehaviour{......}
}

在CLR中,所有的类在堆中都会有一个唯一的类型对象,对类静态成员的第一次访问会促使CLR对类型对象进行初始化,从而调用类型构造函数,这个调用从底层机制保证了线程安全。

多线程场景利用GC与Interlocked.CompareExchange解决方案

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;public class SingletonPool
{private static SingletonPool instance = null;private static GameObject go = null;private static ObjectPooler pool = null;private SingletonPool(){}public static SingletonPool GetInstance(){if(instance!=null){return instance;}SingletonPool temp = new SingletonPool();Interlocked.CompareExchange(ref instance, temp, null);return instance;}public GameObject getPooledObject(){return pool.getPooledObject();}private class ObjectPooler : MonoBehaviour{......}
}

Interlocked.CompareExchange()方法确保只有instance为null时,才会将temp赋值给instance,并且保证线程安全,这样当发生竞争时,除了被赋值给instance的temp,其他的temp变为了局部变量,当方法结束时就会被释放,对应的堆中实例将会在下一次GC垃圾回收中被回收。这个方法的优点是无阻塞,缺点是在GC回收前有可能会在短时间内浪费堆中内存(多个线程同时new SingletonPool,但是几率很小)。

以上,实现了单例模式的三个最重要的功能:
1,保证在单线程/多线程场景中只有一个实例。单例对象池类无法在系统中通过AddComponent或者是new进行实例化,只能通过GetInstance来访问唯一的instance。
2,自行自动创建。在第一次访问getInstance方法时会自动创建实例。
3,全局访问。游戏中各个场景中的所有类皆随时可通过静态属性Instance来访问对象池类。


参考:
http://unity3d.com/cn/learn/tutorials/topics/scripting/object-pooling --Unity Technology
CLR via C# --Jeffrey Richter

维护:
2020-6-21:review

在Unity内使用对象池并实现线程安全的单例模式相关推荐

  1. Unity中对象池的使用

    unity中用到大量重复的物体,例如发射的子弹,可以引入对象池来管理,优化内存. 对象池使用的基本思路是: 将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用.恰当地使用对象池,可以在 ...

  2. 动态调整线程池_调整线程池的重要性

    动态调整线程池 无论您是否知道,您的Java Web应用程序很可能都使用线程池来处理传入的请求. 这是许多人忽略的实现细节,但是迟早您需要了解如何使用该池以及如何为您的应用程序正确调整池. 本文旨在说 ...

  3. 如何关闭线程池?会创建不会关闭?调用关闭方法时线程池里的线程如何反应?

    前言 相信大家在面试的时候经常会遇到「线程池」相关的问题,比如: 什么是线程池?线程池的优点? 有哪几种创建线程池的方式? 四种创建线程池的使用场景? 线程池的底层原理? 线程池相关的参数,比如Cor ...

  4. Java高并发编程详解系列-线程池原理自定义线程池

    之前博客的所有内容是对单个线程的操作,例如有Thread和Runnable的使用以及ThreadGroup等的使用,但是对于在有些场景下我们需要管理很多的线程,而对于这些线程的管理有一个统一的管理工具 ...

  5. java 线程池 复用机制,java的线程池框架及线程池的原理

    java 线程池详解 什么是线程池? 提供一组线程资源用来复用线程资源的一个池子 为什么要用线程池? 线程的资源是有限的,当处理一组业务的时候,我们需要不断的创建和销毁线程,大多数情况下,我们需要反复 ...

  6. 对警报线程池的警报线程_如何建立更好的警报

    对警报线程池的警报线程 背景 (Background) One of the most popular complaints from developers to DBAs involves aler ...

  7. 对警报线程池的警报线程_检测和警报SQL Server代理丢失的作业

    对警报线程池的警报线程 摘要 (Summary) While alerting on failed SQL Server Agent jobs is straightforward, being no ...

  8. 对警报线程池的警报线程_审核和警报SQL Server作业状态更改(启用或禁用)

    对警报线程池的警报线程 In this article, we will talk about how to track enabled or disabled SQL jobs in SQL Ser ...

  9. 编写高效的C++程序方法之使用对象池

    对象池技术可以避免在程序的生命期中创建和删除大量对象.如果知道程序需要同一类型的大量对象,而且对象的生命周期都很短,就可以为这些对象创建一个池(pool)进行缓存.只要代码中需要一个对象,就可以向对象 ...

最新文章

  1. 自动驾驶车辆何时实现?近期不会实现的五大原因
  2. 信息检索及信息过滤方法概述
  3. java 反射 本类,关于Java反射中基本类型的class有关问题
  4. docker开放的端口_docker-5-解决宿主机没有开放81端口却可以直接访问docker启动的81端口nginx容器的问题...
  5. JVM——内存区域:运行时数据区域详解
  6. html5tab页高德地图,高德地图系列web篇——目的地公交导航
  7. OpenCV4每日一练day12:双目相机标定
  8. 计算机硬件物理设备包含,计算机硬件
  9. [PHP]Phpexcel导入时间格式数据处理
  10. 十大网络安全策略 打造坚固的内网
  11. 《离散数学》题库大全及答案
  12. 扇贝python多少钱_扇贝多少钱一斤?扇贝多少钱一斤2017?
  13. hp-ux 修改系统时间
  14. 【QNX Hypervisor 2.2 用户手册】4 构建QNX Hypervisor系统
  15. 中央电大 c语言程序设计a 试题,中央电大2008年秋C语言程序设计A试题1
  16. 南京高中计算机老师,30个全省第一!南京老师又出名了!
  17. Python max函数
  18. stc单片机如何用C程序将IO口设为强推挽输出
  19. 微信编辑器都有什么功能?
  20. NameValuePair问题

热门文章

  1. H5新标签--语义化标签
  2. yum命令 启用仓库_yum仓库详细解读
  3. android stringbuilder 一次插入多条数据_android开发面试题解析
  4. python绘图颜色深浅代表数值_画图理解Python的深浅拷贝
  5. 无法连接iphone软件更新服务器_NX许可证错误:无法连接至许可证服务器系统。SPLM_LICENSE_SERVER错误[15]...
  6. fpga如何约束走线_FPGA设计约束技巧之XDC约束之I/O篇 (上)
  7. python脚本 游戏赚金币_python捡金币游戏(上)
  8. Linux用管道移动文件夹,常用的Linux上的文件管理类命令讲解及演示
  9. android 返回图标布局,Android 开发BottomNavigationView学习
  10. 基于Websocket草案10协议的升级及基于Netty的握手实现