游戏开发 |
您所在的位置:网站首页 › 常见的事件监听方式有几种 › 游戏开发 |
1. 什么是事件系统?
事件系统是游戏开发中最常用的基础模块,通常采用订阅发布模式实现。通过事件系统,我们可以在多个不同的模块在互不引用的情况下,实现模块间的交互。 所以事件系统是用来处理模块间解耦的主要手段。 一个基础的事件系统主要提供3个功能,注册,注销,发送消息。注册:在事件中心中添加对某个消息的监听;注销:在事件中心中取消掉对某个消息的监听;发送消息:在需要的时机发送某个消息,触发所有对其的监听的回调。 下面是一个事件系统的伪代码范例: // 添加事件监听 EventCtrl.Instance.AddListener("MyEventName", EventCallBack); // 注销事件监听 EventCtrl.Instance.RemoveListener("MyEventName", EventCallBack); // 派发事件 EventCtrl.Instance.Send("MyEventName", SomeArgs); 2. 被滥用的事件系统事件系统是游戏开发中最常用的基础模块,但同时也是最多被滥用的。 2.1 被滥用的表现如果发现项目中的代码有这种情况,表示事件系统可能不被正确的使用了。 同一条事件在项目内被很多地方派发和很多地方注册; 同一条事件用来处理不同的业务逻辑; 多个地方派发的同一条事件的参数不同; 一个函数方法的片段内,包含很多次事件派发; 当代码出bug时,不是只专注于在业务代码逻辑中查找bug,还需要去检查事件消息的注册/注销逻辑是否正确; 某一条事件触发频率过高,导致其响应占用性能过高; 同一条事件的多个响应会相互影响,多个响应必须保持一定的顺序触发才可以正常运行; 2.2 为什么会被滥用?首先是未能梳理好各个独立业务模块的关系,未做好各模块的接口设计。 工作中接手过太多代码混乱的项目,也算有点心得。这些混乱的项目大部分未作好模块独立,各模块间的调用直来直去混为一团。同时事件系统作为解耦的得力助手也无能为力。要么是完全没人用,所有地方都是不考虑解耦直接调用。要么是每个地方都在用,不论是模块内还是模块外。 其次是没有做好事件定义的规范,错误得把所有模块间的相互调用都定义为事件。 例如在A和B两个独立的模块中,有很多相互直接调用。为了解耦,A模块中不能直接调用B模块的方法,那么很简单,把原来A中对B的所有直接调用换成事件。 这种方式是未经思考的乱用事件系统,不仅解耦的效果仅仅浮于形式,代码还会非常丑陋,事件调用事件的循环链会导致逻辑混乱,很难捋清楚。 对于如何正确定义事件和派发事件,在第3,5节中具体讨论。 最后是注册/注销事件的时机不统一,在业务逻辑中根据其他判断条件进行注册和注销。 这种方式下,一旦出了bug很难排查。如果注册的事件内的逻辑有问题,需要去排查3种情况:1.派发事件时该回调没有注册;2.派发事件时该回调已经注销了;3.回调内部代码有bug。 对于如何正确注册和注销事件,在第4节中具体讨论。 3. 事件定义的原则 1. MVC框架下的事件定义游戏开发常用的MVC架构,包括很多基于MVC的变种和拓展如MVVM,MVP等等。无论怎么变化,其中的Model(数据层)和View(视图层)都是其中不变的核心,其他变化的都是对M和V交互的不同处理方式。 数据(Model)是所有软件程序的基础。所有软件究其本源,最终都是对数据的读取和写入。 我们点击网页上的一个链接,打开微信查看好友的讯息,打开抖音观看搞笑视频,所有这些操作都是对数据的读取,只不过是通过不同的媒介呈现。 我们在某宝上买了一台电脑,编辑早安消息发送给朋友,发布一条跳舞视频,所有这些操作都是对数据的写入。 注意:上面举例子中的数据大多指存放在持久性数据,如服务器的数据库中的数据,本地文件夹下保存的数据等。但是我们在考虑事件系统与MVC的关系时,讨论的数据是泛指所有系统的状态而非仅仅指持久化数据,系统的状态有变化也代表着数据的变化。这个对于理解下文中3.2事件定义的2种类型 非常重要。 例如向下滚动网页这个操作,使用者并未实际上修改了数据库或本地的网页数据,但是其对应的显示区域状态发生了变化,也算是数据的变化。 视图(View)依托于数据,是数据变化的呈现方式。所有的视图都是为了2个作用:读取数据并呈现,触发对数据的写入; 下面以一次淘宝购物的流程来说明,视图层在过程中的这2个作用: 点击搜索商品,展示商品列表=》读取商品列表数据并呈现; 点击某个商品,进入商品详情页=》读取商品详情数据并呈现; 点击加入购物车并跳转=》触发对购物车数据的写入,然后读取购物车数据并呈现; 在购物车内支付购买=》触发对购物车/购物数据的写入,读取购物车/购物数据并呈现; 综上所述,M数据是核心和基础,V视图是依托于数据的呈现,C控制是M和V交互的方式。所有的软件都是基于数据,提供数据修改的方式和对数据的呈现。 2. 事件定义的2种类型是当思考清晰Model和View的关系后,我们就可以对如何定义事件下结论了: image.png我们只需要定义2种类型的事件: 开始数据的写入的事件; 数据修改后通知的事件; 3. 常见的事件范例 开始数据的写入的事件;常见的范例: Event_Begin_LoadScene: 开始切换新场景的事件; Event_Begin_GetLoginRewrad: 开始领取登录奖励的事件; 数据修改后通知的事件;常见的范例: Event_After_PropsChange: 金币/钻石/道具数量变化的事件; Event_After_GetLoginReward: 登录奖励领取成功后的事件; Event_After_PurchaseSuccess: 购买商品成功后的事件; 4. 事件注册/注销的原则 1. 注册/注销的逻辑应当保持一致,即在注册对象的生命周期开始阶段注册,注册对象的生命周期结束阶段注销。例如在MonoBehavior的Awake中注册,OnDestroy中注销。 在类的构造函数中注册,在类的析构函数中注销。 大家可能有疑问,我如果要在某些情况下才触发响应,而某些情况下不触发,也需要在整个生命周期开始注册结束注销吗? 答案:是的,”某些条件下才触发,某些情况下不触发“这种逻辑应当放在事件响应中,事件响应后检测判断条件即可。为了保证代码逻辑统一性,多消耗一点检测的性能是可以接受的。如果事件触发太过频繁导致检测消耗过大,则需要考虑事件派发逻辑和检测逻辑是否可以优化。 当然这个答案很有争议,仅代表个人之言。在很多项目中,如果非常有必要去动态注册/注销也是可以的,当然前提必须是代码逻辑清晰。 2. 以添加事件次数最少的方式开发;在一个模块中,应该是在主模块注册事件而非每个子模块单独注册;在一个有很多重复item的界面中,应当是在主界面注册事件,而非每个item单独注册事件; 常见的不好的使用方式是:一个展示很多格子的背包ui,里面每个格子都注册了事件。 5. 事件发送/响应的原则 1. 先谈谈耦合常说的耦合更多是指代码层面的不同模块的相互调用,即内容耦合。 耦合度的高低可以查看A模块中对于B模块的代码引用得到,我们通过查看代码很容易发现问题。 这种耦合可以理解为显性的耦合。 相对于显性的耦合,那么便是隐性的耦合。隐形的耦合常见于代码在时间维度的执行顺序上的耦合。 例如下面一个关于饥饿的人吃食物的代码例子: class HungryMan{ SomeFood food = new SomeFood(); public void FuncA(){ Cook(food); .... } public void FuncB(){ Eat(food); } }上面代码中,SomeFood类需要先烹饪(调用Cook)后才可以吃(调用Eat),否则会导致吃了生的食物拉肚子(出bug)。 在这种代码书写下,FuncB和FuncA的调用顺序必须固定:先调用FuncA再调用FuncB。这种情况下我们可以说FuncA和FuncB产生了隐形的耦合:在代码之间没有直接引用时,却在时间调用顺序上受到其影响的耦合,称其为时域耦合。 2. 事件发送/响应的几条原则那么事件发送/响应的原则是: 同一事件的多个响应之间不应该有时域耦合; 事件响应不能与事件派发后的逻辑有时域耦合; 原创声明 作者:vectorZ 出处:https://www.jianshu.com/u/01450ce9ecbf 版权:本文版权归作者所有 转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |