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状态
虽然整个购物不复杂,但很明显的体现了状态模式写代码的流程
分阶段,画图,分别写触发,维护上下文变量,代码互相解耦。