jarlyyn 发表于 2021-10-24 01:53:56

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

http://pkuxkx.net/forum/thread-46326-1-1.html


之前的帖子里,我们整理了最基础的机器人制作

包括了数据收集与维护,行走,简单决策。

并制作了一个简单的能命令序列,完成了最基本的机器人雏形。

接下去我们就开始一个相对正经的机器人的制作了。

机器人的制作,核心其实是几块


[*]数据维护
[*]事件触发
[*]决策
可以认为是一个简单的游戏AI的雏形。

那让我们看看,怎么开始做一个基础的游戏AI。


jarlyyn 发表于 2021-10-24 01:59:57

在进阶篇里,我希望写的东西不只局限于北侠,或者是某个MUD。

我希望能分享一些写代码的经验,快乐,共同提升。

我本身并不是学计算机的,可以说入写代码这行也是受当年做mud机器人影响。

虽然现在还在玩mud的人基本都已经人近中年,人生规划已经固定,不太可能转行。

但写代码,特别是写机器人这种事情,实在是太有意思了。

希望我的经验能让你更好的领略到这份快乐。


jarlyyn 发表于 2021-10-24 02:17:00

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

一些基础

关于语言,客户端

看过上一篇的应该知道,我选用的语言是Javascript。但是,这篇东西和语言关系不大。作为图灵完备的语言,基本上抽象出了基本接口后,后续部分应该是共同的。

而客户端的不同,更应该在上一篇已经被抽象掉了。

关于设计模式

这一篇会逐渐引入一些设计模式的内容。如果你学得就是计算机,那基本直接可以理解。

如果不是的话,问题也不大,你只需要理解,所谓设计模式,相当于常见病的治疗方法。是对一些常见的问题的解决方案的归纳,常用套路。

熟悉套路能让你少踩很多坑,但也不要应套套路,要知道套路解决的问题是什么,相应的优势是什么。

关于面向对象OOP

为了代码复用和提高效率,我会适当的使用一些面向对象的内容。

但本质上我是一个过渡OOP的人,希望不会给你带来误导。

参考资料

按我写文档的习惯,参考资料应该是丢附录的。但是论坛没有附录这种东西,所以先放这里

有兴趣的可以先看看,但本质上来说,这个应该是根据实际的代码来参考的,一开始不知道是什么意思也不必在意。


[*]设计模式[https://design-patterns.readthedocs.io/zh_CN/latest/][https://refactoringguru.cn/design-patterns]
[*]状态模式[https://gpp.tkchu.me/state.html]
[*]行为树[https://zhuanlan.zhihu.com/p/19890016]
[*]人月神话
[*]有限状态机[https://zhuanlan.zhihu.com/p/46347732]
[*]游戏人工智能编程案例精粹[下载地址]


jarlyyn 发表于 2021-10-24 03:22:12

在制作机器人时,我们很容易遇到一个问题。

同样的触发,在不同的情况下,应对的方法应该不一样。

比如,在打坐的时候和做任务的时候,被NPC主动攻击后的应对一样不一样。

比如,在行走,搜索,进入迷宫时,进入新房间要做的时就不一样。

这时候,我们应该怎么处理?


这就是需要我们引入最适合处理这个场景的状态模式和有限状态机了。

jarlyyn 发表于 2021-10-24 03:25:19

有限状态机是一个很常见的名词,很适合用在AI/机器人制作里。

但是你如果直接搜资料,会发现很多都是介绍文字处理之类的。

所以,在这里先整理一下

有限状态机是一个抽象意义上的模型,指导你一种写代码的思路。


他并不能直接解决问题。

有限状态机的本质是指

通过维护当前状态,由状态本身来复杂处理相应的输入,改变状态并进行相应的输出。

怎么样,这个和我们mud机器人的本质差不多吧?


jarlyyn 发表于 2021-10-24 03:32:00

有限状态机的实际应用形式,就是设计模式里的状态模式了

https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/state.html

在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

实际来看,这篇文章更接近于我们的用法

https://gpp.tkchu.me/state.html

总之,就是


[*]永远维护一个当前状态
[*]当前状态负责处理所有的触发引起的事件,对mud服务器进行输出
[*]在合适的时候切换进入新的状态
[*]在进入新状态或退出状态的时候执行代码,比如打开或关闭触发组

jarlyyn 发表于 2021-10-24 03:39:07

接下来是代码部分了

首先,让我们做一个State状态类

(function(){
    var State=function(){
      this.ID=""
      this.Tags={}
    }
    State.prototype.Enter=function(context,oldstatue){
    }
    State.prototype.Leave=function(context,newstatue){
    }
    State.prototype.OnEvent=function(context,event,data){
    }
    return State
})()很简单,两个属性,三个方法

属性ID是注册用哦ID

属性Tags是用来给状态打标签,比如是战斗型状态,准备性状态等,可以用来做特殊处理

三个方法分别是


[*]Enter 进入状态时执行的方法,会传入老状态,一般用于初始化触发,数据,或者作为中间状态,执行判断进入心状态
[*]Leave 退出状态时执行的方法,一般用于处理触发和数据
[*]OnEvent是核心代码,用于处理各种事件和相应的数据。一般的触发器会在触发后,整理数据并调用这一块

https://github.com/hellclient-scripts/pkuxkx.noob/blob/ec7127f02f58c6fecf1fd2572f37fe2242d02d3d/script/include/state.js

jarlyyn 发表于 2021-10-24 03:42:45

接着是状态上下文,也就是真正持有状态的对象,比如我们机器人里控制的角色

(function(){
    var StateContext=function(){
      this.State=null
    }
    StateContext.prototype.ChangeState=function(newstatue){
      let old=this.State
      if (this.State){
            this.State.Leave(this,newstatue)
      }
      this.State=newstatue
      this.State.Enter(this,old)
    }
    StateContext.prototype.OnEvent=function(event,data){
      if (this.State){
            this.State.OnEvent(this,event,data)
      }
    }
   
    return StateContext
})()
一个属性,两个方法


[*]属性State是当前状态
[*]方法ChangeState 切换状态,会自动执行Leave和Enter逻辑
[*]OnEvent 将事件和数据传给当前状态进行处理

https://github.com/hellclient-scripts/pkuxkx.noob/blob/ec7127f02f58c6fecf1fd2572f37fe2242d02d3d/script/include/statecontext.js


jarlyyn 发表于 2021-10-24 03:50:47

然后是针对mud的基础类

(function (app) {
    let state = Include("include/state.js")
    let BasicState=function(){
      state.call(this)
    }
    BasicState.prototype = Object.create(state.prototype)
    BasicState.prototype.OnEvent=function(context,event,data){

    }
    return BasicState
})(App)
https://github.com/hellclient-scripts/pkuxkx.noob/blob/ec7127f02f58c6fecf1fd2572f37fe2242d02d3d/script/core/state/basicstate.js


这个负责默认的事件处理,用于被所有实际的状态触发

(function (app) {
    let statecontext = Include("include/statecontext.js")
    let GlobalStateContext=function(){
      statecontext.call(this)
    }
    GlobalStateContext.prototype = Object.create(statecontext.prototype)
    GlobalStateContext.prototype.ChangeState=function(newstatue){
      app.Data.State=newstatue.ID
      statecontext.prototype.ChangeState.call(this,newstatue)
      app.UpdateState()
    }
    return GlobalStateContext
})(App)

https://github.com/hellclient-scripts/pkuxkx.noob/blob/ec7127f02f58c6fecf1fd2572f37fe2242d02d3d/script/core/state/globalstatecontext.js

这是一个全局(用户控制的惧色)状态,并和实际的App.Data做交互

然后是全局的管理和注册状态的代码

(function(app){
    let globalstatecontext=Include("core/state/globalstatecontext.js")
    app.Data.State=""
    app.States={}
    app.StateContext=new globalstatecontext()
    app.RegisterState=function(state){
      if (state.ID==""){
            throw "State id不可为空"
      }
      app.States=state
    }
    app.GetState=function(id){
      let state=app.States
      if (!state){
            throw "未知的state id"+id
      }
      return state
    }
    app.ChangeState=function(id){
      let state=app.GetState(id)
      app.StateContext.ChangeState(state)
    }
    app.OnStateEnvent=function(event,data){
      app.StateContext.OnEvent(event,data)
    }
    app.UpdateState=function(){
      let statuetext=app.Data.State
      world.SetStatus(statuetext);
    }
    app.RegisterState(new (Include("core/state/stateinit.js"))())
    app.RegisterState(new (Include("core/state/stateready.js"))())
    app.RegisterState(new (Include("core/state/statemanual.js"))())
    app.ChangeState("init")
})(App)
https://github.com/hellclient-scripts/pkuxkx.noob/blob/ec7127f02f58c6fecf1fd2572f37fe2242d02d3d/script/core/state.js

我添加了全局的全局的注册获取状态的方法,全局的切换状态和相应状态方法的快捷方式

同时,引入了 init,ready,manual三个状态,并在加载程序后进入了init状态



jarlyyn 发表于 2021-10-24 17:02:59



上一个机器人的架构图。可以发现。机器的架构是分层的

第一层原始输入,指最原始接受的到的数据。如果mud有小修改,基本应该在这一层。同时,除了mud的输出外,一般机器人还会接受到用户输入和广播信息(不通过mud,在不同游戏之间进行广播)

第二层是脚本设置,就是对于原始处理,机器人需要写相应的触发器,定时,别名等进行响应

第三层是核心层,进行数据更新,并触发相应的事件和回调。同时,这里也需要维护一定的数据,比如每个角色的配置,地图/路径/道具信息等

第四层就是实际的决策层了。一般可以通过状态机/行为树/指令序列中的一个或多个,来决定机器下一步的行为。如果mud有大的流程修改,就需要修改这一层

第五层是输出处理,将程序的输出的指令转换为合适的实际指令进行输出。主要用于控制输出的频率,类型(是否分组)

第六层是实际的输出,一般没什么花样,mud的输出自由文字和断线控制,最多再多个一个不经过mud 的广播输出


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