注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

重新出发的阿赵

阿赵的博客

 
 
 

日志

 
 

关于事件机制的一些分享  

2017-11-18 11:26:46|  分类: Unity教程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
这里说的事件机制,并不是指Unity的SendMessage,是指观察者模式的基础的事件机制。最简单的思想就是,不同的逻辑之间,通过抛事件去通知需要关心事件的其他逻辑去做相应的事情,而不是直接调用某个类的某个方法。这样做就把代码直接的耦合关系解开。事件可以用于mvc的不同层之间的数据穿梭,也可以用于模块与模块之间的通讯。
接下来先通过例子来说明事件机制的使用方式,在文章的最后会以伪代码的形式说明一些代码实现的方式。
一、举例说明事件使用的情况
举一个简单的例子,比如说在一个ARPG游戏里面,服务器通过下发协议来通知玩家血量变化了。这时候,首先可以把血量变化的数据存到数据层去,然后抛出一个血量变化的事件。对于数据的model层来说,其实血量变化了之后,究竟谁会去用到这个数据,是不需要关心的,只需要抛出事件就够了。然后对于这个血量变化的数据,用到的地方可能很多,比如一开始的时候,只有主界面上面的血量条需要关心。于是主界面上面加一个监听血量变化事件的方法,当接收到血量变化事件时,血量条做相应的显示变化。再后来,需求变化了,需要在血量变化的时候在人物上面也需要做一点改变,比如根据不同的血量显示不同的颜色之类。那么角色控制层上面再加一个监听血量变化的事件,在收到事件之后,给人物做出相应的表现。如此类推,不管之后需求怎样变化,数据model层是完全不需要做出任何的修改,只需要在关心数据变化的业务层里面加对应的监听,然后做出相应的处理就可以了。
对于刚才那个例子,还有一些补充的说明。这个血量变化的消息,一般人会有2种做法。第一种,是抛血量变化的时候,把真实血量的数据当做事件的参数一起抛出,接收事件的地方直接读取这个事件的参数。第二种做法,只是抛出血量变化的消息,不带任何的参数,然后接收事件的地方根据需要,再从数据model层去取出当前的血量来做正确的显示。这两种做法我觉得使用的场合会有区分。如果只是需要显示当前的血量,不需要细分到每一步变化的具体表现,其实用第二种是最准确的。如果需要到每一次血量改变都有相应的细微表现,那么可能就必须使用第一种方式了。

二、同步事件和异步事件
这个地方需要考虑的是同步事件和异步事件的应用。实际上一般来说的事件,很多时候都是指同步事件,有就是抛出事件的瞬间,所有监听事件的地方都会立刻收到事件并执行。这样的时效性是最高的,也没有区分数据是否准确,存在model层的数据和抛出去的数据肯定是一致的。但同步事件有一个弊端,因为是在同一帧里面抛出的事件会全部一起执行完毕,所以如果这么不凑巧这个事件的监听者很多,而且事件里面又继续抛出了其他的同步事件,那样调用链就会很长而导致不可控制。这样的后果就是你的游戏会在抛出这个事件的那一帧里面变得非常的卡。解决这个问题的方法是异步事件。异步事件指的是,抛出事件之后,不是直接分发给接收者,而是存在一个队列里面。我们可以根据每帧的cpu耗时情况,来决定当前帧可以分发多少条事件消息。比如说同时产生了10条异步事件消息,但处理到第三条消息的时候,当前帧的耗时已经超过33毫秒了,这时候如果你的游戏fps是定在30的,就已经有掉帧的危险了,必须把剩下的7条消息放到下一帧去执行。
异步事件使消息的分发变得可控制,但问题就是消息的时效性就变得弱了,很可能当接收者接到消息的时候,数据层已经发生了新的变化。所以还是刚才那个血量变化的例子,如果只是需要显示当前的真是血量,那么收到消息的时候去model层取一次最新的血量数据来显示,就没有任何问题。如果需要做血量变化飘字,必须准确到每一次血量的变化,那么就必须在事件抛出的时候把当前血量作为参数抛出去,然后接收的业务逻辑根据需要取每一次抛出事件的参数来显示。
异步事件可以保证抛出去的事件是按顺序逐条的执行,但由于不是即时生效,所以某些时候还是必须使用同步事件的。比如有些数据你就算冒着掉帧的风险还是必须保证他的时效性的,或者你的整个项目框架的设计不一定完全都是观察者模式,还有一些是直接互相调用的时候,为了保证方法调用时的数据准确,你也只能抛同步事件去实现。

三、使用事件的风险和处理
同步事件有一个风险,就是在收到事件之后又抛出同步事件,让事件形成一个环,造成死循环。这种情况其实很容易出现的。因为事件消息的抛出者不关心接收者做了什么逻辑,比如A方法抛出了事件之后,B方法接收了,然后又抛了个同步事件,C方法接收了,它又抛出一个同步事件,这个事件这么不巧被A方法关心并接收到了。这样,就形成了一个环,出现死循环了。所以同步事件在用的时候需要加一个同级调用的层数,在事件消息开始分发的时候加1,在消息结束执行的时候减1。这样如果是消息里面又抛了消息的情况出现,这个调用层级数就会累加。从经验上来说,这样的层级调用一般都不可能超过10个的,超过5层同时调用,就应该去检查逻辑写的是不是有问题了。如果有几十层或者100层以上,已经可以认为是出现死循环了。所以可以对这个层级调用数做一个判断,如果超过一定值时,就给出警告打印,再超过一定值时,直接中断逻辑并打印错误信息。
另外一个使用事件会产生的风险,就是事件的重复监听和忘记移除。由于分发消息的地方不会关心谁去监听了这个事件,所以如果当注册事件的地方注册了多次同一个消息,就会产生收到多次消息而重复调用的风险。另一种情况,就是某些事件的消息监听是具有时效性,在某种情况下需要监听,但在另一种情况下是不需要监听的。所以必须有事件的移除监听方法。
举个例子。还是刚才那个血量变化的例子,如果游戏里面除了主界面,还有一个角色界面需要显示血量的。那么当角色界面打开的时候,它需要添加血量变化的事件监听,但当角色界面关掉的时候,这个监听其实就不再需要了,那么在角色界面关闭的同时(一般是走UI的生命周期,比如OnClose之类),就必须把这个血量变化的事件监听移除掉。

四、最后,以伪代码的形式说明一下实现事件的写法:
//事件消息管理器类
class Messenger
{
//注册监听事件的方法,由关心事件的地方去监听,使用事件类型作为Key,然后传入回调方法
static public int AddListen(string key,MsgCallback fun)
{
//把对应监听的Key和回调委托fun存到字典里面
//这里需要产生对应这次监听的一个唯一的id,用于反注册事件监听。
return 唯一的id
}
//抛出同步事件的方法
static public void SendMsg(string key,params object[] args)
{
//从字典里查找有没有人监听过对应key的事件,如果有就把参数传进回调方法执行
//这里的参数用了不定长参数,类型是object,是因为每个事件需要的参数是不一样的,类型也不确定
}
//抛出异步事件的方法
static public void SendMsgAsyn(string key,params object[] args)
{
//把消息的key和参数存在一个可以固定顺序的队列里面,比如List之类。
//再在Update的时候检查队列,逐个事件抛出
}
//移除监听事件
static public void RemoveListen(string key,int id)
{
//当之前监听过事件的业务逻辑现在已经不需要再继续监听的时候,可以用之前监听时返回的id,作为反注册的用途
//根据事件类型的key和具体的id,从字典里面删除已经存在的事件
}

public void Update()
{
//具体怎样调起update就看个人需要了,比如Unity可以用自带的Update生命周期,或者用时间间隔来Tick之类。
//检查有没有需要抛出的异步事件,然后通过分帧条件来逐个抛出
}
}

//抛出事件的举例
class SendMsgDemo
{
public void SetHPData(HPData data)
{
this.hpData = data;
Messenger.SendMsgAsyn(EventType.HP_CHANGE,data);
}
}

//注册监听的举例
class AddEventDemo
{
private void AddListener()
{
listenId = Messenger.AddListen(EventType.HP_CHANGE,OnHPChange);
}
private void RemoveListener()
{
Messenger.RemoveListen(EventType.HP_CHANGE,listenId);
}

private void OnHPChange(object[] args)
{
HPData data = (HPData)args[0];
//当收到事件时需要做的处理
}
}


  评论这张
 
阅读(106)| 评论(1)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018