doumimi 发表于 2024-8-8 15:31:26

【Paotin++】入门系列之五: 事件驱动编程

本帖最后由 doumimi 于 2024-8-20 07:35 PM 编辑

【Paotin++】入门系列之一: 客户端基础 - 技术园地 - 北大侠客行MUD论坛 - Powered by Discuz! (pkuxkx.com)


请多看看 HELP event! HELP event! HELP event!

一、基本概念
什么是事件驱动? 什么是回调函数?   
和事件驱动编程对应的通常是轮询编程, 这里拿现实生活中的一个事情举例说明,

2023过年春运,小明自己手动抢票没有抢到, 情急之下在xx抢票软件上下了抢票的订单,完事儿之后心急如焚想要知道是否抢到了票。
小明就每隔5分钟看一眼结果,1:50 1:55 2:00 怎么还没抢到..... 5:35 5:40 都好几个小时了....9:30 妈妈呀,怎么回家呀 ..... 10:20 打开再一看,哇抢到了!太好了,小明可以回家了。

2024过年春运,小明自己手动抢票没有抢到, 这次小明有经验了,没那么着急了,在xx抢票软件上下了抢票的订单的时候,发现有个完成后通知选项,<font color="#9932cc">可以勾选完成后电话通知 ,完成后短信通知,完成后邮箱通知,完成后app通知,完成后qq通知等各种通知方式, 完成之后,小明就去放心玩北大侠客行了</font>, 等到 10:20 听到手机叮铃铃一声响, 小明拿手机一看, 呵,票到手了。上面的不停的看时间就是 轮询编程。 下面的被动通知,是事件驱动编程。

事件驱动:当一个事件发生后,触发下一步的行动逻辑, 这种编程的思维模式叫做事件驱动编程。 不用想的太难, #action 触发器。 就是很典型的事件驱动编程的使用方式。 至于 action和event 各自的使用场景,会在后面单独分析。
回调函数:上面提到了【完成后进行短信/电话等方式通知】, 这个就相当于注册到平台的一个回调函数。 当事件发生(抢到票)后,会执行的动作,就叫回调函数。 依然很简单, #action {事件} {command} 其中 command 就是回调函数。path.Walk 扬州的中央广场-扬州的武庙 {command} 。 在这个地方的command也是回调函数。





doumimi 发表于 2024-8-8 15:31:27

本帖最后由 doumimi 于 2024-8-8 03:55 PM 编辑

二、event和action的不同


上面介绍了例子,也通过action的说明,应该让大家知道了,事件驱动编程不是什么太高深的概念,那为啥了有了action ,还需要有 event呢?
大家可以先看一下,有个印象,然后再了解完event如何使用后, 在来看这个对比会更加的有体会。


proscons
action1. 简单,非常好理解。1. 对于同一行的内容,无法共同生效。也就是无法通过一个action,来干多个事情,除非硬要写到一起。
2. 有时候写的action会被其他的优先级更高的覆盖。 通过ACTS 命令可以看到现在一共注册了哪些action,但是想找到其中有冲突的很难。
event1. 事件可以被多个接收方处理。
2. 事件可以灵活的选择哪些订阅方。
3. 通过event.List命令可以非常方便的看到当前的事件订阅树。1. 不太好理解。
2. 定义比较麻烦。







doumimi 发表于 2024-8-8 15:32:01

本帖最后由 doumimi 于 2024-8-8 04:21 PM 编辑

三、如何订阅事件

## 别名 event.Handle <事件名称> <回调钩子> <所属模块> <回调代码>   注册事件回调钩子。参数说明如下:
      - 事件名称: 本钩子要关联的事件的名称,需要事先用 event.Define 声明。
      - 回调钩子: 本次注册的钩子,可以在随后用来取消本钩子,或者当事件发射时,发射方可以用正则表达式指定要触发哪些钩子。
      - 所属模块: 注册钩子所在的代码模块。必须是一个严格的 PaoTin++ 模块描述符。
      - 回调代码: 用来指明钩子被回调时要执行的代码。
上面是直接粘贴了HELP event里面的内容。但是这个描述太专业了, 我用我个人的理解先给大家翻译一下这几个参数。
事件名称: 这个没什么可说的,我要订阅一个事件, 这个就是事件的名称。 这个事件可以是 "走了一步" "获取到一颗宝石" "完成了韩世忠任务" "角色busy了" "进入副本" 等。
回调钩子: 订阅方的名称或者标识。 这个有两个作用,最主要的是取消监听的时候用,其次就是能让消息发送方来控制消息发给谁。 你要是不理解,就随便写一个名字就行,因为大多数情况下,就是用来取消时候用的。
所属模块: 你就直接写这个机器对应的文件, 比如 var/plugins/jobjxf.tin 那你就写 jobjxf 。var/plugins/job/jobhsz.tin 那你就写 job/jobhsz.tin。(至于$MODULE,萌新可以不用掌握,因为据炮爷说这个只能在顶级块生效。)
回调代码:这个就跟action 中的command一样,不多解释了。

#alias getjob {....};
event.Handle {move-message} {jxf} {job/jxf} {getjob};
#nop 我是jxf,我所在的文件是 plugins/job/jxf, 我要监听移动的消息, 每当收到消息的时候,我要getjob接取任务;

#alias sellweapon {...};
event.Handle {move-message} {sell} {sell} {sellweapon};
#nop 我是sell 我所在的文件是 plugins/sell,我要监听移动的消息, 每当收到消息的时候,我要sellweapon 卖武器;

event.Handle {move-message} {haha1} {haha2} {haha};
#nop 我是haha1, 我所在的文件是 plugins/haha2, 我要监听移动的消息, 每当收到消息的时候,我要haha;

event.UnHandle {move-message} sell;
#nop 我是sell, 我不再监听 移动的消息了(此时还有jxf和sell 在监听);

event.UnHandle {move-message} haha1;
#nop 我是jxf,我也不监听移动的消息了,此时只有jxf在监听);
看上面的5个例子,很简单吧。 其实跟action是差不多的, 只是从监听某个文字,变成了监听某个事件。 并且要带上自己的一些信息。
此时还有个问题没有解决, 就是 move-message 到底是什么, 以及什么时候会发送这个东西呢? 下面来介绍一下event的定义和发射。




doumimi 发表于 2024-8-8 15:32:33

本帖最后由 doumimi 于 2024-8-8 03:40 PM 编辑

四、如何定义事件


## 别名 event.Define <名称> <类型> <模块> <说明>    定义事件。事件在使用前必须先定义。事件经过定义后,可以用 event.List 查看。
    参数列表:
      - 名称:标识事件的唯一名称,只能以拉丁字母或下划线开头,后面跟若干个字母、数字、下划线(_)、斜线(/)、小数点(.) 组成。其中三个标点符号不能出现在末尾,只能出现在中间。
      - 类型:枚举值,{有参} 或者 {无参} 二选一。如果事件被定义为有参,则允许发射事件时携带参数,事件驱动会将参数传递给事件处理句柄。
      - 模块:标识事件所属模块,一般来说事件发射方为事件所属模块。这里要用标准的 PaoTin++ 模块描述符。
      - 说明:事件的简短说明。会出现在类似于 event.List 的用户交互界面。直接结合 example 来看会比较清晰。

event.Define {move-message} {无参} {move} {这是一个移动的消息, 每当进行一次移动,就会发送一个消息};
event.Define {task-finish} {有参} {task} {这是任何一个任务完成时候发送的消息};
event.Define {baowei} {无参} {baowei} {这是保卫开始时候发送的消息};现在我定义了3个消息如上所示。
名称:就是给事件命名, 当发送方的事件名和接收方的事件名一样,就完成了配对,接收方就有权利接收到发送方发的事件,相当于是 消费者和生产者链接的通道。
参数:是否有参数很好理解,就是发送这个事件的时候,是否带有参数。 这个地方只是声明是否有参数,但具体参数是什么,是在发射的时候指定的。
- 如果没有,那么最后被接受方处理的时候,调用的就是 command;
- 如果有参数, 那么最后被接收方处理的时候, 调用的就是 command args;
模块:上面也介绍过了,这是是预计是哪个模块发送(发送机器所在的文件), 就写对应的文件名称。
说明:给自己看的,随便写。





doumimi 发表于 2024-8-8 15:33:10

本帖最后由 doumimi 于 2024-8-8 04:08 PM 编辑

五、如何发射事件


## 别名 event.Emit <事件名称> [<回调钩子通配符>] [<事件参数>]
    发射事件。这将导致与回调钩子通配符相匹配的回调钩子被立即执行。
    默认会触发所有注册在本事件下的事件回调钩子。
    你可以参考 event.Handle 理解什么是事件回调钩子。
直接看例子。
event.Emit {move-message} {};
#nop 发送一个名字叫 move-message的消息,我希望被所有监听者接受, 消息没有参数;

event.Emit {task-finish} {} {韩世忠};
#nop 发送一个名字叫 task-finish的消息,我希望被所有监听者接受, 消息带有一个参数: 韩世忠;

event.Emit {move-message} {jxf};
#nop 发送一个名字叫 move-message的消息,我只希望 名字叫jxf的监听者 能够听到这个消息;

event.Emit {move-message} {job%*};
#nop 发送一个名字叫 move-message的消息,我希望 所有名字以job开头的监听者 能够听到这个消息;

前期大家刚开始理解的时候, 就只用第一种方式就行,也就是发送一个无参数的消息, 那么信息如何传递呢? 通过#var 就可以, 因为#var 定义完成之后, 是全局的,任何地方都可以访问到。 Paotin 自己本身的事件,绝大部分都是无参的。
发送消息通常是要结合场景的,通常是执行了某个命令,或者触发了一些action,或者接收到gmcp信息后,发送event。下面给出一些example。
#action {韩世忠说:恭喜你完成%*, 你获取的了%*经验,%*声望} {
    event.Emit {task-finish} {} {韩世忠}; #nop 发送任务完成的消息,参数是韩世忠;      
};
#action {纪晓芙说:恭喜你完成%*, 你获取的了%*经验,%*声望} {
    event.Emit {task-finish} {} {jxf}; #nop 发送任务完成的消息,参数是jxf;      
};

#alise move2 {
    #if {"%1" != "{e|w|n|s}"} {
      errLog 只能走 东南西北 ewns;
      #return;
    };      
    %1; #nop 执行移动;
    event.Emit {move-message} {};#nop 发送移动了一步的消息;
};





doumimi 发表于 2024-8-8 15:33:45

本帖最后由 doumimi 于 2024-8-8 04:56 PM 编辑

六、Paotin框架-内置事件

Paotin框架已经定义了非常多的事件,其实大家用好这些事件,就已经能实现很多功能了。大家可以输入 event.List 来看看当前有哪些事件定义 , 我介绍几个最常用的。

GMCP.Move: 接收到 GMCP 移动信息,已更新 gGMCP。这个是真的每移动一步,就发送的一个消息。随便举例两个用法
- 找凶手,比如我提前录制好了一段路径, 我可以每走一步,检查一下当前房间里面是否有要找的人。
- 押镖时,每前进一格,判断一下当前是否需要战斗,是否需要恢复。

GMCP.Buff: 接收到 GMCP BUFF状态,已更新 gGMCP, 当使用了某些buff的时候,比如yun powerup ,就会发送这个消息。

GMCP.Status: 接收到 GMCP 角色状态,已更新 gGMCP。 比如血量降低了,内力减少了,都会发送这个事件,监听这个事件就可以第一时间做出反应。

map/GotRoomInfo: 已经获取到房间信息,并储存到 gMapRoom 全局变量。gMapRoom是非常常用的一个全局变量, 那么每次在收到map/GotRoomInfo 时,就可以使用gMapRoom变量里面的内容。

map/walk/continue: 走路机器人结束运行时,可以发射本事件以驱动后续动作继续运行。 配合path.Walk中的botStep使用。
map/walk/failed:走路机器人运行失败时,可以发射本事件以通知调用方。 配合path.Walk中的botStep使用。


doumimi 发表于 2024-8-8 15:34:17

本帖最后由 doumimi 于 2024-8-8 04:16 PM 编辑

七、实战分析


分享一个10w经验的明教小萌新PaoTin++成长之路 - 技术园地 - 北大侠客行MUD论坛 - Powered by Discuz! (pkuxkx.com)
我直接引用踢你头同学文章中的使用案例,将代码贴在下面分析一下。 这段代码完成的功能是 "在寻路过程中 【如果遇到了刘三蛋,那么就停下来,揍他】"
#alias walk.cycle.time {    path.Walk.Stop;    cry;    #delay walk.Resume {path.Walk.Resume} 1;};

event.Handle {GMCP.Move} {path.Walk} {$MODULE} {walk.cycle.time};

map.FocusNPC {    {name}{刘三蛋}} command {job.wei.kill};

#alias job.wei.kill {    #undelay walk.Resume;    kill %1 %2;};

第二行可以看到, 我这个监听者的名字叫path.Walk, 我所处的模块是$MODULE, 我监听了GMCP.Move这个移动的消息。 每次移动后,我都调用 walk.cycle.time, 实现了每走一步,我都等1秒再走,方便我进行npc的查找。
========================= 下面是map.FocusNPC 源码分析 ==========================
那这个 map.Focus是怎么实现的呢? 哦,看源码知道, <b>是每次接收到 map/GotRoomInfo 这个消息后,要执行map.check-npc </b>。 收到这个消息就代表gMapRoom这个变量已经有房间信息了,包含描述,出口,物品,npc等. 那么 map.check-npc不用看,也知道就是解析gMapRoom里面是否有特定的npc了。

#alias {map.FocusNPC} {
   xxxxxx 省略不重要代码;
   <font color="#ff0000">event.Handle {map/GotRoomInfo} {map/utils} {map} {map.check-npc};</font>
};

接下来的问题是, <b>那 map/GotRoomInfo是谁发送的呢</b>? 继续看下去。

#alias {map.Room.getInfo.done} {
   xxxxxx 省略不重要代码;
   #var gMapRoom {true};#nop 房间信息已收集完毕? yes!
   <font color="#ff0000">event.Emit {map/GotRoomInfo};</font>
};
#alias map.Room.Watch {
    xxxxxx 省略不重要代码;
    <font color="#ff0000">#action </font>{{*UTF8}{?:^}{.{1,9}?\S}{$dungeon} -{$nation}{$pkzone}{$terrain}{$save}{$store}{$group}{$mark}{$undef}{| \[副本\]}$} {
      收集一些房间信息;
      map.Room.getInfo;
      #if { @isFalse{$gMapRoom} } {
            <font color="#ff0000">map.Room.getInfo.done;</font>
      };
    };
};

在游戏刚启动的时候,就执行了map.Room.Watch. 这个别名中间声明了一个 action, 这个action 监听的是 look命令返回的第一行内容 【醉仙楼二楼 - [大宋国] [城市] ★】那么玩家<b>每走一步</b>,就会 触发这个action, 就会抓一些房间的信息, <b>然后调用 map.Room.getInfo.done 来发送一个map/GotRoomInfo 的 event的消息。</b>

那么整体串起来就是:
生产者: 每走一步, 通过action获取一些房间信息, 获取完毕之后,发送map/GotRoomInfo消息.
监听者: 每当接收到 map/GotRoomInfo消息的时候,就进行 map.check-npc, 如果发现了特定的npc, 就执行 map.FocusNPC的回调函数.


此贴到此完结



mzywq 发表于 2024-8-8 15:46:58

感谢扫盲,朦朦胧胧有了一点感觉。

kickuhead 发表于 2024-8-8 15:53:15

jinger 发表于 2024-8-8 16:10:44

写得非常好,已经比我的理解深了。{:93:}
页: [1]
查看完整版本: 【Paotin++】入门系列之五: 事件驱动编程