jarlyyn 发表于 2021-10-25 00:45:04

再解释一下事件机制

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

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


关于观察者模式
https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/observer.html

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

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

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

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

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

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


jarlyyn 发表于 2021-10-25 00:56:44

本帖最后由 jarlyyn 于 2021-10-25 12:57 AM 编辑

然后是责任链模式

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

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


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


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


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


    StateFight.prototype.OnEvent=function(context,event,data){
      switch (event){
            case "能出pfm了":
                do_pfm()
                return
            case "需要治疗了":
                do_heal()
                return
            case "需要逃跑了"
                do_flee()
                return
      }
      statebacic.prototype.OnEvent.call(context,event,data)
    }


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

其他的抛给别人处理。


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

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



pufan 发表于 2021-10-25 17:58:59

高手,高手,这是高手...

jarlyyn 发表于 2021-11-10 19:20:57

本帖最后由 jarlyyn 于 2021-11-10 07:22 PM 编辑

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

下一个问题来了。

我们为什么要引入状态?

为了显得高大上吗?

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

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

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


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

什么是状态流转图

让我们随便搜一下



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

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

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


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


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


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



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



jarlyyn 发表于 2021-11-10 19:37:15

插一句,上文中的图我是用开源软件draw.io做的,有兴趣可以下载用用看。

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

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

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

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

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

取任务组

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

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

战斗组

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

奖励组



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

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

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



jarlyyn 发表于 2021-11-11 10:25:10

接着,我们接下去还要实现一个状态堆栈

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

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

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

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

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

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

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

jarlyyn 发表于 2021-11-17 11:37:23

好,我们接下去的问题很简单,如何创建一个机制,能够


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

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

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

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

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


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

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

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

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

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

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

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

(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateReady=function(){
      basicstate.call(this)
      this.ID="ready"
      this.Handler=null
    }
    StateReady.prototype = Object.create(basicstate.prototype)
    StateReady.prototype.Enter=function(context,newstatue){
      basicstate.prototype.Enter.call(this,context,newstatue)
      this.Handler()
    }
    return StateReady
})(App)

https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/stateready.js

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



jarlyyn 发表于 2021-11-17 11:40:58

本帖最后由 jarlyyn 于 2021-11-17 11:47 AM 编辑

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


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


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

(function(){
    let Automaton=function(final,states){
      this.FinalState=final
      this.Context={}
      this.Transitions=states|[]
      this.FailState=final
    }
    Automaton.prototype.WithFinalState=function(final){
      this.FinalState=final
      return this
    }
    Automaton.prototype.WithFailState=function(final){
      this.FailState=final
      return this
    }
    Automaton.prototype.WithTransitions=function(states){
      this.Transitions=states|[]
      return this
    }
    Automaton.prototype.WithData=function(key,value){
      this.Context=value
      return this
    }
    return Automaton
})()https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/include/automaton.js

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


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

所以代码如下

(function(app){
    let automaton=Include("include/automaton.js")
    app.Data.Automata=[]
    app.Automaton={}
    app.Automaton.Current=function(){
      if (app.Data.Automata.length==0){
            throw "自动机为空"
      }
      return app.Data.Automata
    }
    app.Automaton.New=function(final,states){
      let a=new automaton(final,states)
      app.Data.Automata=
      return a
    }
    app.Automaton.Push=function(final,states){
      let a=new automaton(final,states)
      app.Data.Automata.push(a)
      return a
    }
    app.Automaton.GetContext=function(key){
      return app.Automaton.Current().Context
    }
    app.Automaton.SetContext=function(key,value){
      return app.Automaton.Current().Context=value
    }
    app.Automaton.Pop=function(){
      if (app.Data.Automata.length==0){
            throw "自动机为空"
      }
      return app.Data.Automata.pop()
    }
    app.Automaton.Finish=function(){
      let final=app.Automaton.Current().Final
      app.Data.Automata.pop()
      app.ChangeState(final?final:"ready")
    }
    app.Automaton.Fail=function(){
      if (app.Data.Automata.length==0){
            world.Note("自动任务失败")
            app.ChangeState("manual")
            return
      }
      let fail=app.Automaton.Current().Fail
      app.Data.Automata.pop()
      if (!fail){
            app.Automaton.Fail()
      }else{
            app.ChangeState(fail)
      }
      
    }
    app.Automaton.Flush=function(){
      app.Data.Automata=[]
    }
    let auto=function(){
      if (app.Data.Automata.length==0){
            world.Note("自动任务结束")
            app.ChangeState("manual")
            return
      }
      let a=app.Automaton.Current()
      if (a.Transitions.length){
            app.ChangeState(t.Transitions.shift())
            return
      }
      a.Automaton.Finish()
    }
    app.GetContext=app.Automaton.GetContext
    app.SetContext=app.Automaton.SetContext
    app.GetState("ready").Handler=auto
    app.Finish=app.Automaton.Finish
    app.Fail=app.Automaton.Fail
})(App)https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/automaton.js



jarlyyn 发表于 2021-11-17 12:09:15

接着对移动模块进行改写

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

(function(app){
    let Move=function(mode,target,data){
      this.Mode=mode
      this.Target=target
      this.Current=null
      this.Data=data?data:{}
      this.Context=null
      this.StateOnStep=""
      this.Stopped=false
      this.OnRoom=""
      this.StartCmd=""
    }
    Move.prototype.Start=function(){
      app.Automaton.Push()
      app.SetContext("Move",this)
      app.ChangeState(this.Mode)
    }
    Move.prototype.Stop=function(){
      this.Stopped=true
    }
    return Move
})(App)https://github.com/hellclient-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/include/move.js

接着建立一个基础的move状态
(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateMove=function(){
      basicstate.call(this)
      this.ID="move"
    }
    StateMove.prototype = Object.create(basicstate.prototype)
    StateMove.prototype.Enter=function(context,newstatue){
      world.EnableTriggerGroup("move",true)
      basicstate.prototype.Enter.call(this,context,newstatue)
    }
    StateMove.prototype.Leave=function(context,newstatue){
      world.EnableTimer("steptimeout",false)
      world.EnableTriggerGroup("move",false)
      basicstate.prototype.Leave.call(this,context,newstatue)
    }
    StateMove.prototype.OnEvent=function(context,event,data){
      switch(event){
            case "move.ignore":
                this.Ignore()
            break
      }
    }
    StateMove.prototype.Ignore=function(){
      let move=app.GetContext("Move")
      move.Ignore=true
    }
    StateMove.prototype.Go=function(command){
      app.Go(command)
    }
    StateMove.prototype.TryMove=function(step){
      if (!step){
            let move=app.GetContext("Move")
            step=move.Current
      }
      this.Go(step.Command)
    }
    return StateMove
})(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-scripts/pkuxkx.noob/blob/3063723c5541c574890e1e310e857a014de98a33/script/core/state/move/find.js




jarlyyn 发表于 2021-11-17 17:31:50

本帖最后由 jarlyyn 于 2021-11-17 05:39 PM 编辑

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

购买商品的流程大体如下



那么,我们需要的是


[*]道具移动
[*]Nobusy
[*]Execute
[*]Check
这几个额外状态

首先是全局的入口函数

(function(app){
    app.Produce=function(id){
      let item=App.API.GetItem(id)
      if (item==null){
            throw "item "+id +" not found"
      }
      let a=app.Automaton.Push("core.state.produce.check")
      a.WithTransitions()
      a.WithData("Item",item)
      app.ChangeState("ready")
    }
})(App)很家单,获取注册过的道具信息,将item.Type作为初始状态,将item作为Item上下文,,将检查物品作为终点,进入ready开始运行

然后是购买的初始状态 goods
(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateGoods=function(){
      basicstate.call(this)
      this.ID="goods"
    }
    StateGoods.prototype = Object.create(basicstate.prototype)
    StateGoods.prototype.Enter=function(context,oldstatue){
      basicstate.prototype.Enter.call(this,context,oldstatue)
      let item=app.GetContext("Item")
      let a=app.Automaton.Push()
      a.WithTransitions(["core.state.produce.move","nobusy","core.state.produce.execute","nobusy"])
      a.WithData("Item",item)
      app.ChangeState("ready")
    }
    return StateGoods
})(App)
很明显,定义里系列的过渡,["core.state.produce.move","nobusy","core.state.produce.execute","nobusy"],和上述的图一致。

状态core.state.produce.move代码

(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateProduceMove=function(){
      basicstate.call(this)
      this.ID="core.state.produce.move"
    }
    StateProduceMove.prototype = Object.create(basicstate.prototype)
    StateProduceMove.prototype.Enter=function(context,oldstatue){
      basicstate.prototype.Enter.call(this,context,oldstatue)
      let item=app.GetContext("Item")
      app.NewMove("walk",item.Location).Start()
    }
    return StateProduceMove
})(App)
很简单的直接Start一个move并Start,完全不管下一个状态是什么

nobusy状态

(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateNoBusy=function(){
      basicstate.call(this)
      this.ID="nobusy"
      this.Callback=""
    }
    StateNoBusy.prototype = Object.create(basicstate.prototype)
    StateNoBusy.prototype.Enter=function(context,oldstatue){
      basicstate.prototype.Enter.call(this,context,oldstatue)
      app.CheckBusy(this.Callback)
    }
    return StateNoBusy
})(App)这是一个通用模块,检查忙,不忙进入ready状态

core.state.produce.execute状态
(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateProduceExecute=function(){
      basicstate.call(this)
      this.ID="core.state.produce.execute"
    }
    StateProduceExecute.prototype = Object.create(basicstate.prototype)
    StateProduceExecute.prototype.Enter=function(context,oldstatue){
      basicstate.prototype.Enter.call(this,context,oldstatue)
      let item=app.GetContext("Item")
      app.Send(item.Command)
      app.ChangeState("ready")
    }
    return StateProduceExecute
})(App)发送上下文变量Item里的Command,因为是使用上下文变量,所以不会和全局冲突。同样不检测下一步是什么。

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

core.state.produce.check状态:

(function (app) {
    let basicstate = Include("core/state/basicstate.js")
    let StateProduceExecute=function(){
      basicstate.call(this)
      this.ID="core.state.produce.check"
    }
    StateProduceExecute.prototype = Object.create(basicstate.prototype)
    StateProduceExecute.prototype.Enter=function(context,oldstatue){
      basicstate.prototype.Enter.call(this,context,oldstatue)
      app.Send("i2")
      app.ResponseReady()
    }
    return StateProduceExecute
})(App)
发个i2触发更新道具信息,然后发送一个Response等服务器响应后进入Ready状态

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

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



页: 1 [2] 3
查看完整版本: 一步一步在北侠做机器人[进阶]状态篇