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

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
版权:本文版权归作者所有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容