北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
查看: 526|回复: 9

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

[复制链接]
发表于 2024-8-8 15:31:26 | 显示全部楼层 |阅读模式
本帖最后由 doumimi 于 2024-8-20 07:35 PM 编辑

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


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


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

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

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

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





北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 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. 定义比较麻烦。







北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-8-8 15:32:01 | 显示全部楼层
本帖最后由 doumimi 于 2024-8-8 04:21 PM 编辑

三、如何订阅事件

  1. ## 别名 event.Handle <事件名称> <回调钩子> <所属模块> <回调代码>   注册事件回调钩子。参数说明如下:
  2.         - 事件名称: 本钩子要关联的事件的名称,需要事先用 event.Define 声明。
  3.         - 回调钩子: 本次注册的钩子,可以在随后用来取消本钩子,或者当事件发射时,发射方可以用正则表达式指定要触发哪些钩子。
  4.         - 所属模块: 注册钩子所在的代码模块。必须是一个严格的 PaoTin++ 模块描述符。
  5.         - 回调代码: 用来指明钩子被回调时要执行的代码。
复制代码

上面是直接粘贴了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的定义和发射。




北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-8-8 15:32:33 | 显示全部楼层
本帖最后由 doumimi 于 2024-8-8 03:40 PM 编辑

四、如何定义事件


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

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





北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-8-8 15:33:10 | 显示全部楼层
本帖最后由 doumimi 于 2024-8-8 04:08 PM 编辑

五、如何发射事件


  1. ## 别名 event.Emit <事件名称> [<回调钩子通配符>] [<事件参数>]
  2.     发射事件。这将导致与回调钩子通配符相匹配的回调钩子被立即执行。
  3.     默认会触发所有注册在本事件下的事件回调钩子。
  4.     你可以参考 event.Handle 理解什么是事件回调钩子。
复制代码

直接看例子。
  1. event.Emit {move-message} {};
  2. #nop 发送一个名字叫 move-message的消息,我希望被所有监听者接受, 消息没有参数;

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

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

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


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

  7. #alise move2 {
  8.     #if {"%1" != "{e|w|n|s}"} {
  9.         errLog 只能走 东南西北 ewns;
  10.         #return;
  11.     };      
  12.     %1; #nop 执行移动;
  13.     event.Emit {move-message} {};  #nop 发送移动了一步的消息;
  14. };
复制代码






北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-8-8 15:33:45 | 显示全部楼层
本帖最后由 doumimi 于 2024-8-8 04:56 PM 编辑

六、Paotin框架-内置事件

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

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

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

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

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

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


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
 楼主| 发表于 2024-8-8 15:34:17 | 显示全部楼层
本帖最后由 doumimi 于 2024-8-8 04:16 PM 编辑

七、实战分析


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

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

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

  4. #alias job.wei.kill {    #undelay walk.Resume;    kill %1 %2;};
复制代码


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

  3. #alias {map.FocusNPC} {
  4.    xxxxxx 省略不重要代码;
  5.    event.Handle {map/GotRoomInfo} {map/utils} {map} {map.check-npc};
  6. };

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

  8. #alias {map.Room.getInfo.done} {
  9.    xxxxxx 省略不重要代码;
  10.    #var gMapRoom[allDone] {true};  #nop 房间信息已收集完毕? yes!
  11.    event.Emit {map/GotRoomInfo};
  12. };
  13. #alias map.Room.Watch {
  14.     xxxxxx 省略不重要代码;
  15.     #action {{*UTF8}{?:^}{.{1,9}?\S}{$dungeon} -{$nation}{$pkzone}{$terrain}{$save}{$store}{$group}{$mark}{$undef}{| \[副本\]}$} {
  16.         收集一些房间信息;
  17.         map.Room.getInfo;
  18.         #if { @isFalse{$gMapRoom[allDone]} } {
  19.             map.Room.getInfo.done;
  20.         };
  21.     };
  22. };

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

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



此贴到此完结



北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-8-8 15:46:58 | 显示全部楼层
感谢扫盲,朦朦胧胧有了一点感觉。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
发表于 2024-8-8 15:53:15 | 显示全部楼层

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
回复 支持 1 反对 0

使用道具 举报

发表于 2024-8-8 16:10:44 | 显示全部楼层
写得非常好,已经比我的理解深了。
北大侠客行Mud(pkuxkx.com),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

GMT+8, 2024-9-17 03:39 AM , Processed in 0.010892 second(s), 15 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表