美文网首页unityGameDev
游戏开发 - 事件系统的使用原则

游戏开发 - 事件系统的使用原则

作者: vectorZ | 来源:发表于2023-03-22 19:22 被阅读0次

    1. 什么是事件系统?

    事件系统是游戏开发中最常用的基础模块,通常采用订阅发布模式实现。通过事件系统,我们可以在多个不同的模块在互不引用的情况下,实现模块间的交互。

    所以事件系统是用来处理模块间解耦的主要手段

    一个基础的事件系统主要提供3个功能,注册注销发送消息。注册:在事件中心中添加对某个消息的监听;注销:在事件中心中取消掉对某个消息的监听;发送消息:在需要的时机发送某个消息,触发所有对其的监听的回调。

    下面是一个事件系统的伪代码范例:

    // 添加事件监听
    EventCtrl.Instance.AddListener("MyEventName", EventCallBack);
    // 注销事件监听
    EventCtrl.Instance.RemoveListener("MyEventName", EventCallBack);
    // 派发事件
    EventCtrl.Instance.Send("MyEventName", SomeArgs);
    


    2. 被滥用的事件系统

    事件系统是游戏开发中最常用的基础模块,但同时也是最多被滥用的。

    2.1 被滥用的表现

    如果发现项目中的代码有这种情况,表示事件系统可能不被正确的使用了。

    1. 同一条事件在项目内被很多地方派发和很多地方注册;

    2. 同一条事件用来处理不同的业务逻辑;

    3. 多个地方派发的同一条事件的参数不同;

    4. 一个函数方法的片段内,包含很多次事件派发;

    5. 当代码出bug时,不是只专注于在业务代码逻辑中查找bug,还需要去检查事件消息的注册/注销逻辑是否正确;

    6. 某一条事件触发频率过高,导致其响应占用性能过高;

    7. 同一条事件的多个响应会相互影响,多个响应必须保持一定的顺序触发才可以正常运行;

    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的关系时,讨论的数据是泛指所有系统的状态而非仅仅指持久化数据,系统的状态有变化也代表着数据的变化。这个对于理解下文中<u>3.2事件定义的2种类型</u> 非常重要。

    例如向下滚动网页这个操作,使用者并未实际上修改了数据库或本地的网页数据,但是其对应的显示区域状态发生了变化,也算是数据的变化。

    视图(View)依托于数据,是数据变化的呈现方式。所有的视图都是为了2个作用:读取数据并呈现,触发对数据的写入

    下面以一次淘宝购物的流程来说明,视图层在过程中的这2个作用:

    点击搜索商品,展示商品列表=》读取商品列表数据并呈现;

    点击某个商品,进入商品详情页=》读取商品详情数据并呈现;

    点击加入购物车并跳转=》触发对购物车数据的写入,然后读取购物车数据并呈现;

    在购物车内支付购买=》触发对购物车/购物数据的写入,读取购物车/购物数据并呈现;

    综上所述,M数据是核心和基础,V视图是依托于数据的呈现,C控制是M和V交互的方式。所有的软件都是基于数据,提供数据修改的方式和对数据的呈现。

    2. 事件定义的2种类型

    是当思考清晰Model和View的关系后,我们就可以对如何定义事件下结论了:

    image.png

    我们只需要定义2种类型的事件:

    1. 开始数据的写入的事件

    2. 数据修改后通知的事件

    3. 常见的事件范例

    1. 开始数据的写入的事件

    常见的范例:

    Event_Begin_LoadScene: 开始切换新场景的事件;

    Event_Begin_GetLoginRewrad: 开始领取登录奖励的事件;

    1. 数据修改后通知的事件

    常见的范例:

    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. 事件发送/响应的几条原则

    那么事件发送/响应的原则是:

    1. 同一事件的多个响应之间不应该有时域耦合;

    2. 事件响应不能与事件派发后的逻辑有时域耦合;


    原创声明
    作者:vectorZ
    出处:https://www.jianshu.com/u/01450ce9ecbf
    版权:本文版权归作者所有
    转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

    相关文章

      网友评论

        本文标题:游戏开发 - 事件系统的使用原则

        本文链接:https://www.haomeiwen.com/subject/skdbrdtx.html