北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
楼主: jarlyyn

一步一步在北侠做机器人[进阶]状态篇

[复制链接]
 楼主| 发表于 2021-10-25 00:45:04 | 显示全部楼层
再解释一下事件机制

事件机制是设计模式中观察者模式很常见的一种应用。

在网页制作中有极广泛的应用


关于观察者模式
https://design-patterns.readthed ... terns/observer.html

本质来说,利用事件模式是为了将事件发生的触发和实际的处理逻辑解耦合

分析事件的触发,不需要知道那些代码会处理事件

处理时间的代码,不需要知道哪些代码会触发事件。

这样的有点是容易做模块化的处理,容易更新。

当然,也有缺点,比如性能问题,比如无法容易的发现事件的变更会影响那些代码。

所有的方案都一种tradeoff,使用这个模式,只是我觉得在机器人这个场景里优点更明显,而且我能驾驭。


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-10-25 00:56:44 | 显示全部楼层
本帖最后由 jarlyyn 于 2021-10-25 12:57 AM 编辑

然后是责任链模式

https://refactoringguru.cn/desig ... n-of-responsibility

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。


具体来说,我们的责任链模式最主要就是体现在 状态的代码里的OnEvent里


OnEvent中,正常的使用方式是,当前状态,根据本状态下特殊的需要处理事件,再根据情况,将上下文和时间传递到下一个状态(当前状态的父类)上。


比如伪代码,战斗状态处的事件处理


  1.     StateFight.prototype.OnEvent=function(context,event,data){
  2.         switch (event){
  3.             case "能出pfm了":
  4.                 do_pfm()
  5.                 return
  6.             case "需要治疗了":
  7.                 do_heal()
  8.                 return
  9.             case "需要逃跑了"
  10.                 do_flee()
  11.                 return
  12.         }
  13.         statebacic.prototype.OnEvent.call(context,event,data)
  14.     }
复制代码


战斗状体只处理pfm,治疗,逃跑三个事件。

其他的抛给别人处理。


直到任何一个状态终止了这个传递为止。

在状态中使用责任链的模式优点是可以方便的复用代码,以及专注于状态本身的功能。



北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
发表于 2021-10-25 17:58:59 | 显示全部楼层
高手,高手,这是高手...
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-10 19:20:57 | 显示全部楼层
本帖最后由 jarlyyn 于 2021-11-10 07:22 PM 编辑

好,现在基础的代码架构我们确认了。

下一个问题来了。

我们为什么要引入状态?

为了显得高大上吗?

自然不是。要看上去牛逼还不如先来一套抽象工厂和控制反转,看上去专业多了。

引入状态是为了解决实际问题的。

归根到底,状态是为了实现两个问题

  • 以合适维度来管理触发的反馈,对代码进行解耦
  • 通过状态流转图来规划机器

什么是状态流转图

让我们随便搜一下



代表一个状态在不同条件下进行切换的图

那么,让我们随便搜一个北侠的任务,画一下状态流转图

看了下,胡一刀的任务大概是最复杂的,我画了下,大概是这样的


那理论上,按这个流程图,把各个流程分别做好,分别测试,联通之后,这个机器人就做好了。其中战斗状态,移动状态,搜寻状态都是通用状态,因此,只要单独制作

  • 接任务状态
  • 任务分析状态
  • 等用户输入图片内容状态
  • ask并获取藏宝图状态
  • 收集战利品阶段
  • combine藏宝图状态
  • 交给胡一刀状态
  • 查看藏宝图定位状态
  • 放弃任务状态
  • 结束任务状态


这几个状态就行了。
其中大部分状态只是执行某个命令,执行完后判断是否身上有某个道具就行,本质也是通用状态。



把任务分解为解耦,可分分解,可处理的单位,就是状态模式的根本用途



本帖子中包含更多资源

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

x
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-10 19:37:15 | 显示全部楼层
插一句,上文中的图我是用开源软件draw.io做的,有兴趣可以下载用用看。

那么,我们是否要完全按这样的图来开发和定制系统呢?图怎么转化为代码呢?

很简单。对于任务的设计和机器人的制作,我们往往不是发散式的制作,往往是分组按顺序执行。

对于一个组来说,就是几个按顺序切换的状态,一个意外状态

任何一个状态失败,就进入意外状态,否者进入下一个状态。这样状态只需要维护数据,也不用关心具体状态的切换了。

那么,很明显,胡一刀这个任务的分组是

取任务组

[移动状态,接任务状态,任务分析状态]

找npc组 模式一,模式二,模式3

战斗组

[战斗装备状态,战斗状态,收集战利品状态,战后整备状态]

奖励组

[combine藏宝图状态,移动状态,交胡一刀状态,查看藏宝图定位状态,移动状态,xunbao状态,结束状态]

所有组的失败都是一个 放弃任务状态

直接进入了逻辑流模式,不同的状态组的最后一个状态负责维护状态队列,就能很快速的进行开发了。

当然,胡一刀是主流任务,我这里就讲解下思路,不会做具体的代码编写



北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-11 10:25:10 | 显示全部楼层
接着,我们接下去还要实现一个状态堆栈

参考 https://gpp.tkchu.me/state.html 中的下推自动机

这个解决了状态中的状态的问题,降低了维护状态的复杂度。

比如说,常见的需求是,我维护了一个主任务的状态列表,其中需要获取某个道具。

在获取这个道具的时候,其实还会分为几个状态,分别是

[移动状态,到达状态,向npc下指令状态,检查是否获取到道具状态。]

如果这个都需要在主流程中实现,这个代码几乎是不可维护的

我们需要在执行主流程到准备道具时,压入一层准备道具的流程,做完后,回到原来调用点继续执行 。

北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-17 11:37:23 | 显示全部楼层
好,我们接下去的问题很简单,如何创建一个机制,能够

  • 很好的进行状态顺序管理
  • 能够有多个层次
  • 避免使用状态模式带来的弊端。

写代码里,大部分方案都是一个tradeoff,只看是不是能调整到最佳市场的模式。

状态模式/有限状态机的确有非常多的优点,但同时也会有问题,就是当状态数量上升时,状态机的规模会呈指数上升。

有限状态机的本质是一个状态迁移表的程序体现。

状态迁移表参看
https://zhuanlan.zhihu.com/p/55432214


从图上很明显的能看到,状态数量和表的指数关系。

为了解决这个问题,我们必须对状态进行一定的限制。

即状态有一个基准状态(我使用了ready状态)

大部分状态都是从ready进入,退出到ready,只有特殊情况才在状态间直接切换。

这样,能保证正常情况喜爱状态迁移表的规模和状态数量呈线性关系(接近1:1)而不是指数关系

同时,ready状态还需要负责调用真正的决策引擎,执行机器人的下一步操作

于是,ready状态的代码就是这样的

  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateReady=function(){
  4.         basicstate.call(this)
  5.         this.ID="ready"
  6.         this.Handler=null
  7.     }
  8.     StateReady.prototype = Object.create(basicstate.prototype)
  9.     StateReady.prototype.Enter=function(context,newstatue){
  10.         basicstate.prototype.Enter.call(this,context,newstatue)
  11.         this.Handler()
  12.     }
  13.     return StateReady
  14. })(App)
复制代码

https://github.com/hellclient-sc ... state/stateready.js

其中 StateReady.Handler就是程序中负责处理驱动逻辑的部分



本帖子中包含更多资源

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

x
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-17 11:40:58 | 显示全部楼层
本帖最后由 jarlyyn 于 2021-11-17 11:47 AM 编辑

接着我们需要定义个驱动机器人的数据的模型,我将其称之为自动机 automaton


很简单,一个automaton包含以下内容

  • FinalState 最终的结束状态
  • Context 数据上下文(运行期间的数据)
  • Transitions 所有的过度状态(在进入结束状态前应该经过的状态)
  • FialState失败状态
因此,代码如下

  1. (function(){
  2.     let Automaton=function(final,states){
  3.         this.FinalState=final
  4.         this.Context={}
  5.         this.Transitions=states|[]
  6.         this.FailState=final
  7.     }
  8.     Automaton.prototype.WithFinalState=function(final){
  9.         this.FinalState=final
  10.         return this
  11.     }
  12.     Automaton.prototype.WithFailState=function(final){
  13.         this.FailState=final
  14.         return this
  15.     }
  16.     Automaton.prototype.WithTransitions=function(states){
  17.         this.Transitions=states|[]
  18.         return this
  19.     }
  20.     Automaton.prototype.WithData=function(key,value){
  21.         this.Context[key]=value
  22.         return this
  23.     }
  24.     return Automaton
  25. })()
复制代码
https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/include/automaton.js

然后,我们需要创建一个使用自动机的机制

  • 维护一个自动机的栈,代表不同级别的运行状态
  • 可以通过New或者Push来创建一个顶级的自动机或者次级的自动机
  • 可以通过Finish或者Fail来终止自动机的运行,并分别进入成功或失败状态,并注册到全局
  • 失败但没有定义失败状态的自动机会出发类似throw的机制,一路向上抛,知道有某一层的自动机的FailState进行处理,或者报错进入手动模式
  • 定义GetContext,SetContext进行当期自动机的快速数据处理,并注册到全局

所以代码如下

  1. (function(app){
  2.     let automaton=Include("include/automaton.js")
  3.     app.Data.Automata=[]
  4.     app.Automaton={}
  5.     app.Automaton.Current=function(){
  6.         if (app.Data.Automata.length==0){
  7.             throw "自动机为空"
  8.         }
  9.         return app.Data.Automata[app.Data.Automata.length-1]
  10.     }
  11.     app.Automaton.New=function(final,states){
  12.         let a=new automaton(final,states)
  13.         app.Data.Automata=[t]
  14.         return a
  15.     }
  16.     app.Automaton.Push=function(final,states){
  17.         let a=new automaton(final,states)
  18.         app.Data.Automata.push(a)
  19.         return a
  20.     }
  21.     app.Automaton.GetContext=function(key){
  22.         return app.Automaton.Current().Context[key]
  23.     }
  24.     app.Automaton.SetContext=function(key,value){
  25.         return app.Automaton.Current().Context[key]=value
  26.     }
  27.     app.Automaton.Pop=function(){
  28.         if (app.Data.Automata.length==0){
  29.             throw "自动机为空"
  30.         }
  31.         return app.Data.Automata.pop()
  32.     }
  33.     app.Automaton.Finish=function(){
  34.         let final=app.Automaton.Current().Final
  35.         app.Data.Automata.pop()
  36.         app.ChangeState(final?final:"ready")
  37.     }
  38.     app.Automaton.Fail=function(){
  39.         if (app.Data.Automata.length==0){
  40.             world.Note("自动任务失败")
  41.             app.ChangeState("manual")
  42.             return
  43.         }
  44.         let fail=app.Automaton.Current().Fail
  45.         app.Data.Automata.pop()
  46.         if (!fail){
  47.             app.Automaton.Fail()
  48.         }else{
  49.             app.ChangeState(fail)
  50.         }
  51.         
  52.     }
  53.     app.Automaton.Flush=function(){
  54.         app.Data.Automata=[]
  55.     }
  56.     let auto=function(){
  57.         if (app.Data.Automata.length==0){
  58.             world.Note("自动任务结束")
  59.             app.ChangeState("manual")
  60.             return
  61.         }
  62.         let a=app.Automaton.Current()
  63.         if (a.Transitions.length){
  64.             app.ChangeState(t.Transitions.shift())
  65.             return
  66.         }
  67.         a.Automaton.Finish()
  68.     }
  69.     app.GetContext=app.Automaton.GetContext
  70.     app.SetContext=app.Automaton.SetContext
  71.     app.GetState("ready").Handler=auto
  72.     app.Finish=app.Automaton.Finish
  73.     app.Fail=app.Automaton.Fail
  74. })(App)
复制代码
https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/automaton.js



北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-17 12:09:15 | 显示全部楼层
接着对移动模块进行改写

先把将Move抽象为一个数据为主的结构,剥离动作

  1. (function(app){
  2.     let Move=function(mode,target,data){
  3.         this.Mode=mode
  4.         this.Target=target
  5.         this.Current=null
  6.         this.Data=data?data:{}
  7.         this.Context=null
  8.         this.StateOnStep=""
  9.         this.Stopped=false
  10.         this.OnRoom=""
  11.         this.StartCmd=""
  12.     }
  13.     Move.prototype.Start=function(){
  14.         app.Automaton.Push()
  15.         app.SetContext("Move",this)
  16.         app.ChangeState(this.Mode)
  17.     }
  18.     Move.prototype.Stop=function(){
  19.         this.Stopped=true
  20.     }
  21.     return Move
  22. })(App)
复制代码
https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/include/move.js

接着建立一个基础的move状态
  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateMove=function(){
  4.         basicstate.call(this)
  5.         this.ID="move"
  6.     }
  7.     StateMove.prototype = Object.create(basicstate.prototype)
  8.     StateMove.prototype.Enter=function(context,newstatue){
  9.         world.EnableTriggerGroup("move",true)
  10.         basicstate.prototype.Enter.call(this,context,newstatue)
  11.     }
  12.     StateMove.prototype.Leave=function(context,newstatue){
  13.         world.EnableTimer("steptimeout",false)
  14.         world.EnableTriggerGroup("move",false)
  15.         basicstate.prototype.Leave.call(this,context,newstatue)
  16.     }
  17.     StateMove.prototype.OnEvent=function(context,event,data){
  18.         switch(event){
  19.             case "move.ignore":
  20.                 this.Ignore()
  21.             break
  22.         }
  23.     }
  24.     StateMove.prototype.Ignore=function(){
  25.         let move=app.GetContext("Move")
  26.         move.Ignore=true
  27.     }
  28.     StateMove.prototype.Go=function(command){
  29.         app.Go(command)
  30.     }
  31.     StateMove.prototype.TryMove=function(step){
  32.         if (!step){
  33.             let move=app.GetContext("Move")
  34.             step=move.Current
  35.         }
  36.         this.Go(step.Command)
  37.     }
  38.     return StateMove
  39. })(App)
复制代码
https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/move.js

接着分别建立

  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/walk.js
  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/locate.js
  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/patrol.js

进行walk,locate,patrol三种模式的初始化

以及
  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/walking.js
  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/locating.js
  • https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/patroling.js
这三个状态,继承move状态,进行实际移动的控制

并继承 patrol出一个find状态,用于在巡逻路径时查找制定对象的常用工作。
https://github.com/hellclient-sc ... /state/move/find.js




北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2021-11-17 17:31:50 | 显示全部楼层
本帖最后由 jarlyyn 于 2021-11-17 05:39 PM 编辑

接下来,我们通过购买商品来看下,状态驱动的机器是怎么写的。

购买商品的流程大体如下



那么,我们需要的是

  • 道具移动
  • Nobusy
  • Execute
  • Check
这几个额外状态

首先是全局的入口函数

  1. (function(app){
  2.     app.Produce=function(id){
  3.         let item=App.API.GetItem(id)
  4.         if (item==null){
  5.             throw "item "+id +" not found"
  6.         }
  7.         let a=app.Automaton.Push("core.state.produce.check")
  8.         a.WithTransitions([item.Type])
  9.         a.WithData("Item",item)
  10.         app.ChangeState("ready")
  11.     }
  12. })(App)
复制代码
很家单,获取注册过的道具信息,将item.Type作为初始状态,将item作为Item上下文,,将检查物品作为终点,进入ready开始运行

然后是购买的初始状态 goods
  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateGoods=function(){
  4.         basicstate.call(this)
  5.         this.ID="goods"
  6.     }
  7.     StateGoods.prototype = Object.create(basicstate.prototype)
  8.     StateGoods.prototype.Enter=function(context,oldstatue){
  9.         basicstate.prototype.Enter.call(this,context,oldstatue)
  10.         let item=app.GetContext("Item")
  11.         let a=app.Automaton.Push()
  12.         a.WithTransitions(["core.state.produce.move","nobusy","core.state.produce.execute","nobusy"])
  13.         a.WithData("Item",item)
  14.         app.ChangeState("ready")
  15.     }
  16.     return StateGoods
  17. })(App)
复制代码

很明显,定义里系列的过渡,["core.state.produce.move","nobusy","core.state.produce.execute","nobusy"],和上述的图一致。

状态core.state.produce.move代码

  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateProduceMove=function(){
  4.         basicstate.call(this)
  5.         this.ID="core.state.produce.move"
  6.     }
  7.     StateProduceMove.prototype = Object.create(basicstate.prototype)
  8.     StateProduceMove.prototype.Enter=function(context,oldstatue){
  9.         basicstate.prototype.Enter.call(this,context,oldstatue)
  10.         let item=app.GetContext("Item")
  11.         app.NewMove("walk",item.Location).Start()
  12.     }
  13.     return StateProduceMove
  14. })(App)
复制代码

很简单的直接Start一个move并Start,完全不管下一个状态是什么

nobusy状态

  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateNoBusy=function(){
  4.         basicstate.call(this)
  5.         this.ID="nobusy"
  6.         this.Callback=""
  7.     }
  8.     StateNoBusy.prototype = Object.create(basicstate.prototype)
  9.     StateNoBusy.prototype.Enter=function(context,oldstatue){
  10.         basicstate.prototype.Enter.call(this,context,oldstatue)
  11.         app.CheckBusy(this.Callback)
  12.     }
  13.     return StateNoBusy
  14. })(App)
复制代码
这是一个通用模块,检查忙,不忙进入ready状态

core.state.produce.execute状态
  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateProduceExecute=function(){
  4.         basicstate.call(this)
  5.         this.ID="core.state.produce.execute"
  6.     }
  7.     StateProduceExecute.prototype = Object.create(basicstate.prototype)
  8.     StateProduceExecute.prototype.Enter=function(context,oldstatue){
  9.         basicstate.prototype.Enter.call(this,context,oldstatue)
  10.         let item=app.GetContext("Item")
  11.         app.Send(item.Command)
  12.         app.ChangeState("ready")
  13.     }
  14.     return StateProduceExecute
  15. })(App)
复制代码
发送上下文变量Item里的Command,因为是使用上下文变量,所以不会和全局冲突。同样不检测下一步是什么。

如果需要做重试处理,也应该放在这里

core.state.produce.check状态:

  1. (function (app) {
  2.     let basicstate = Include("core/state/basicstate.js")
  3.     let StateProduceExecute=function(){
  4.         basicstate.call(this)
  5.         this.ID="core.state.produce.check"
  6.     }
  7.     StateProduceExecute.prototype = Object.create(basicstate.prototype)
  8.     StateProduceExecute.prototype.Enter=function(context,oldstatue){
  9.         basicstate.prototype.Enter.call(this,context,oldstatue)
  10.         app.Send("i2")
  11.         app.ResponseReady()
  12.     }
  13.     return StateProduceExecute
  14. })(App)
复制代码

发个i2触发更新道具信息,然后发送一个Response等服务器响应后进入Ready状态

虽然整个购物不复杂,但很明显的体现了状态模式写代码的流程

分阶段,画图,分别写触发,维护上下文变量,代码互相解耦。



本帖子中包含更多资源

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

x
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2024-11-27 04:26 PM , Processed in 0.011386 second(s), 13 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

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