目录
2.11 避免运行时使用Find()和SendMessage()方法 (P46)
2.11 避免运行时使用Find()和SendMessage()方法 (P46)
众所周知,SendMessage()方法和GameObject.Find()方法非常昂贵,应该不惜一切代码避免使用。
找到一个可靠和快速的方法,让新对象查找场景中的现有的对象,而不需要使用find()方法,以便最小化复杂性和性能成本。
将引用分配给预先存在的对象
静态类
单例组件
全局信息传递系统
2.11.1 将引用分配给预先存在的对象 (P49)
使用SerializeField属性将私有成员变量显示给Inspector窗口。
对于Inspector窗口,该值现在将表现为一个公共字段,允许通过编辑器方便地更改它,但将数据安全的封装在代码库的其他部分中。
这其实也是Unity中的最常用的关联其他对象的方式,学到的教程第一步就是教这个的。
这种组件引用技术的一个常见用法是获取对附加到GameObject上的其他组件的引用。这是一种零成本缓存组件的替代方法。
有一定的危险,预制组件和GameObject字段可能弄错。
对预置的任何意外更改都是永久性的,因为无论Play模式是否激活,预置都会占用相同的内存空间。
2.11.2 静态类 (P51)
在任何时候创建一个对整个代码库全局可访问的类。任何类型的全局管理类在软件工程圈中都是不受欢迎的。
主要是因为很难调试,更改可以在运行期间的任何位置和任何点发生,并且此种类倾向于维护其他系统所依赖的状态信息。
它是迄今为止最容易理解和实现的解决方案。
单例设计模式是确保某个对象类型的实例在内存中只存在一个常用方法。单例模式确保了有一个入口点来进行这些活动,而不是让大量不同的子系统来竞争共享资源,并可能找出彼此的瓶颈。
可以为静态类实现一个伙伴组件,以保持代码恰当地解耦。
2.11.3 单例组件 (P54)
静态类很难和Unity相关的功能交互,不能直接利用MonoBehaviour特性,如事件回调、协程、分层设计和预制块。
解决方案是实现一个类似于单例的组件——它提供静态方法来授予全局访问权。
using UnityEngine;
public class SingletonComponent<T> : MonoBehaviour where T :SingletonComponent<T>
{
private static T _instance;
protected static SingletonComponent<T> _Instance {
get{
if(!_instance){
T[] managers=GameObject.FindObjectsOfType(typeof(T)) as T[];
if(managers!=null){
if(managers.Length==1){
_instance=managers[0];
return _instance;
}
else if(managers.Length>1){
Debug.LogError("You have more than one "+ typeof(T).Name+" in the Scene. You only need one - it's a singleton!");
for(int i=0;i<managers.Length;++i){
T manager=managers[i];
Destroy(manager.gameObject);
}
}
}
GameObject go=new GameObject(typeof(T).Name,typeof(T));
_instance=go.GetComponent<T>();
DontDestroyOnLoad(_instance.gameObject);
}
return _instance;
}
set{
_instance=value as T;
}
}
}
DontDestroyOnLoad(),这是一个特殊的函数,希望对象在场景之间持久存在。加载新的场景时,对象不会被破坏,并保留它的所有数据。
派生类
using UnityEngine;
public class EnemyManagerSingletonComponent : SingletonComponent<EnemyManagerSingletonComponent>
{
public static EnemyManagerSingletonComponent Instance{
get{
return ((EnemyManagerSingletonComponent)_Instance);
}
set { _Instance =value;}
}
private void Awake(){
DontDestroyOnLoad(this.gameObject);
}
public void CreateEnemy(GameObject prefab){
//same as StaticEnemyManager
Debug.Log("CreateEnemy:"+prefab);
}
public void KillAll(){
//same as StaticEnemyManager
Debug.Log("KillAll");
}
}
可能遇到的问题,在应用程序关闭期间意外地创建了SingletonComponent的新GameObject。
为了解决这个问题需要做3个改变。
1.添加一个额外的标记,该标记跟踪其活动状态,并在适当的时候禁用它。
private bool _alive=true;
void OnDestroy(){
_alive=false;
}
void OnApplicationQuit(){
_alive=false;
}
2.实现一种外部对象验证单例当前状态的方法
public static bool IsAlive{
get{
if(_instance==null){
return false;
}
return _instance._alive;
}
}
3.任何对象在其自身的OnDestroy()方法中尝试调用单例对象时,必须首先使用IsAlive属性来验证状态,然后再调用实例。
void OnDestroy(){
if(EnemyManagerSingletonComponent.IsAlive){
EnemyManagerSingletonComponent.Instance.KillAll();
}
}
这将确保销毁期间没有人试图访问单例实例。
2.11.4 全局消息传递系统(P58)
任何对象都可以访问该系统,并将消息通过该系统发送给任何可能对侦听特定类型的消息感兴趣的对象。
这种方法最复杂,可能需要一些努力来实现和维护,但它是一个优秀的长期解决方案,可以在应用程序变得越来越复杂时保持对象通信的模块化、解耦和快速。
Message对象
public class Message
{
public string type;
public Message(){
type=this.GetType().Name;
}
}
MessagingSystem类的特性:
- 它可以全局访问。
- 任何对象都应该能够注册/注销为侦听器,来接收特定的消息类型(即Observer设计模式)。
- 当从其他地方广播给定的消息时,注册对象应该提供一个调用方法。
- 系统应该在合理的时间范围内将消息发送给所有侦听器,但不要同时处理太多的请求。
1.全局可访问的对象
2.注册
public delegate bool MessageHandlerDelegate(Message message);
3.消息的处理
内置某种基于时间的机制,以防止它同时处理过多的消息。
4.实现消息传递系统
public class MessagingSystem : SingletonComponent<MessagingSystem>
{
public delegate bool MessageHandlerDelegate(Message message);
public static MessagingSystem Instance{
get{
return ((MessagingSystem)_Instance);
}
set{
_Instance=value;
}
}
private Dictionary<string,List<MessageHandlerDelegate>> _listenerDict=new Dictionary<string, List<MessageHandlerDelegate>>();
public bool AttachListener(System.Type type,MessageHandlerDelegate handler){
if(type==null){
Debug.LogError("MessagingSystem:AttachListener failed due to having no message type specified");
return failed;
}
string msgType=type.Name;
if(!_listenerDict.ContainsKey(msgType)){
_listenerDict.Add(msgType,new List<MessageHandlerDelegate>());
}
List<MessageHandlerDelegate> listenerList=_listenerDict[msgType];
if(listenerList.Contains(handler)){
return false;//already in list
}
listenerList.Add(handler);
return true;
}
}
5.消息的查询和处理
维护一个入站消息对象队列,以便按它们广播的顺序处理。
private Queue<Message> _messageQueue=new Queue<Message>();
public bool QueueMessage(Message msg)
{
if(!_listenerDict.ContainsKey(msg.type)){
return false;//没有处理消息的对象
}
_messageQueue.Enqueue(msg);
return true;
}
在Update()中遍历消息队列的当前内容,每次一个消息,验证自处理以来是否经过太长时间。如果没有,将它们传递到处理的下一阶段。
private const int _maxQueueProcessingTime=16667;
private System.Diagnostics.Stopwatch timer=new System.Diagnostics.Stopwatch();
void Update(){
timer.Start();
while(_messageQueue.Count>0){
if(_maxQueueProcessingTime>0.0f){
if(timer.Elapsed.Milliseconds>_maxQueueProcessingTime)//基于时间的保护措施
{
timer.Stop();
return;
}
}
Message msg=_messageQueue.Dequeue();
if(!TriggerMessage(msg)){
Debug.LogError("Error when processing message:"+msg.type);
}
}
}
将消息分发到侦听器
public bool TriggerMessage(Message msg){
string msgType=msg.type;
if(!_listenerDict.ContainsKey(msgType)){
Debug.Log(" MessagingSystem:Message \""+msgType+"\" has no listeners!");
return false;
}
List<MessageHandlerDelegate> listenerList=_listenerDict[msgType];
for(int i=0;i<listenerList.Count;++i){
if(listenerList[i](msg))
return true;//message consumed by the delegate
}
return true;
}
TriggerMessage()方法的目的是获取给定消息类型的侦听器列表,并为每个侦听器提供处理它的机会。
提供了绕过节流机制的方法。
尽量避免习惯对所有事件使用TriggerMessage(),因为可能会在同一帧中同时处理太多的调用,导致帧率突然下降。确定哪些事件对帧很重要,哪些不重要,并适当地使用QueueMessage()和TriggerMessage()方法。
6.实现自定义消息
public class CreateEnemyMessage : Message
{
}
public class EnemyCreatedMessage : Message
{
public readonly GameObject enemyObject;
public readonly string enemyName;
public EnemyCreatedMessage(GameObject enemyObject,string enemyName)
{
this.enemyObject=enemyObject;
this.enemyName=enemyName;
}
}
设置为public 和readonly,确保数据很容易访问,但是在对象构造之后不能更改。这保护了消息内容不被修改,因为它们在侦听器之间传递。
7.消息发送
public class EnemyCreatorComponent : MonoBehaviour
{
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.Space)){
MessagingSystem.Instance.QueueMessage(new CreateEnemyMessage());
}
}
}
8.消息注册
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyManagerWithMessagesComponent : MonoBehaviour
{
private List<GameObject> _enemies=new List<GameObject>();
[SerializeField] private GameObject _enemyPrefab;
// Start is called before the first frame update
void Start()
{
MessagingSystem.Instance.AttachListener(typeof(CreateEnemyMessage),this.HandleCreateEnemy);
}
bool HandleCreateEnemy(Message msg){
CreateEnemyMessage castMsg=msg as CreateEnemyMessage;
string[] names={"Tom","Dick","Harry"};
GameObject enemy=GameObject.Instantiate(_enemyPrefab,5.0f * Random.insideUnitSphere,Quaternion.identity);
string enemyName=names[Random.Range(0,names.Length)];
enemy.gameObject.name=enemyName;
_enemies.Add(enemy);
MessagingSystem.Instance.QueueMessage(new EnemyCreatedMessage(enemy,enemyName));
return true;
}
}
using UnityEngine;
public class EnemyCreatedListenerComponent : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
MessagingSystem.Instance.AttachListener(typeof(EnemyCreatedMessage),this.HandleEnemyCreated);
}
bool HandleEnemyCreated(Message msg){
EnemyCreatedMessage castMsg=msg as EnemyCreatedMessage;
Debug.Log(string.Format("A new enemy was created ! {0}",castMsg.enemyName));
return true;
}
}
可以随意给委托命名。最明智的做法是用所处理的消息命名方法。可以通过匹配消息和它们的处理委托来跟踪事件链。
在一个子系统变化时通知其他子系统的方式。
9.消息清理 (P67)
内存随着时间会积累大量消息,最终将导致垃圾回收。如果程序运行得足够久,最终将导致偶然的垃圾回收,这是在Unity程序中突发CPU性能峰值的最常见原因。因此,明智的做法是谨慎使用消息传递系统,避免在每次更新时过于频繁地产生消息。
如果需要销毁某个对象,就注销委托。如果处理不当,则消息传递系统将挂起委托引用,从而防止对象被完全销毁并从内存中释放。
与AttachListener配对的DetachListener。
public bool DetachListener(System.Type type,MessageHandlerDelegate handler)
{
if(type==null){
Debug.LogError("MessagingSystem:DetachListener failed due to having no message type specified");
return false;
}
string msgType=type.Name;
if(!_listenerDict.ContainsKey(msgType)){
return false;
}
List<MessageHandlerDelegate> listenerList=_listenerDict[msgType];
if(!listenerList.Contains(handler)){
return false;//not in list
}
listenerList.Remove(handler);
return true;
}
void OnDestroy(){
if(MessagingSystem.IsAlive){
MessagingSystem.Instance.DetachListener(typeof(CreateEnemyMessage),this.HandleCreateEnemy);
}
}
10.总结消息传递系统 (P69)
所有对象都能与之交互,并使用它在彼此之间发送消息。
这种方法的一种有用特性是它与类型无关,这意味着消息发送者和侦听器甚至不需要从任何特定的类派生,来与消息传递系统进行交互。
将来可能需要的更有用的功能:
- 允许消息发送者在消息传递给侦听器之前建议延迟
- 运行消息侦听器为它接收消息的紧急程度定义一个优先级
- 实现一些安全检查:当正在处理特定类型的消息时,添加该类消息
--------------------------------------
暂时用处不大