上次我们在这篇文章中简要提到了Screep的状态栈概念,这次我们来详细盘一下这个状态栈的意义以及如何实现。
状态栈可以理解是状态机的高级模式。
状态机
我们在玩Screeps的时候,为了处理Screeps的行为通常会为每个Creep对象加一个状态属性,这个属性可以指明当前Creep该干嘛。
例如
Creep 的状态 是 harvest 就表示这个creep应该去harvest
当Creep的Storge满了之后,我们通过判断修改Creep的状态变成 upgrade ,那么这个时候Creep就会跑去撸Controller了,等到Creep 的 Storge 空了时候,又修改成 harvest 状态。
如此反复,Creep就能自习完成升级Controller的任务了。
那么对于跨Tick任务来说,这样的方式就略有不足。因为creep每个tick只知道自己是什么状态,然后去执行对应该状态的方法,然后进行下一步。我把这样的Creep形容为蛤蟆...自行意会
现在该干嘛呀?走一步
现在呢??再走一步
now?harvest!
满了满了?! 滚
状态栈
它描述了Creep的一系列的线性状态,让Creep在N个tick的任意一刻都知道(或者叫记得)自己下一步该干什么,该怎么干。并且当你删除驱动Creep的方法之后,Creep依然能独自完成它的工作。
当然这要求你的代码不再为每个tick服务,而是为事务服务。
事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
简单来说,为事务就是告诉你的creep这个事情该怎么做而不是这个tick该怎么做,并包含了每个tick的具体执行方法。
而状态栈,在事务中,你可以理解它就是作为一种类似流程图的存在,指导Creep处理中断或者异常情况(例如creep突然忘记自己要干嘛了,这里通常是global清空,数据被修改,或者遭到攻击了,或者本该成功执行的api没有成功执行导致后面的流程错位或者偏移)。
每个流程图中有可能包含另一个流程图,最终所有流程图的终点都指向简单的一个API操作。你也可以想成它把一个复杂的问题自动拆分成几个小的问题。
它看起来是这样的
去升级Controller(附上一张说明书)
好,升级Controller需要能量,我没有能量,那我应该先去harvest能量,再去升级Controller,可是怎么去harvest呢
(附上一个导航以及交通规则)
第一步应该...,第二步应该...,第三步应该...
好了,知道怎么做了。
...淦...
完成了!
简单实现跨tick寻路的思路
这里不再拿TickStream做例子,写这篇文章的目的是为了分享这样一种思维模式,所以我们从0开始。
状态模拟技术
在处理Creep之前,我们需要使用HashMap来储存和计算某个tick的游戏状态。Creep如果需要知道接下来n个tick该干嘛,那么为了成功调用api,它同样也需要知道那个tick发生了什么,前面有没有creep挡着路等等。
之前我发布过一篇状态模拟技术的文章,如果你没有了解可以适当阅读一下。
状态模拟技术就是一个为了模拟creep在第n个tick下的位置以及在那个位置上能否成功执行相应的api并返回正确的调用结果的技术。
当然你可以选择不这样做,因为个个模块之间需要耦合,篇幅限制下我无法描述过多只能引述。
所以我们假设creep所在的房间没有任何其他creep,只有静态的地形,所以我们不需要担心creep走不走的过去,也就占时不需要进行状态模拟。
实现状态栈
现在我们需要给每个Creep设置自己的栈用来储存状态来实现状态栈及其方法。
这里使用Hoho的原型拓展方法。
实现intent
Intent是一个链表,里面每一个数据都包含了调用一个基础api的信息。
基础api就是最基本的操作游戏的api,例如creep.move;creep.upgrade,查看这些api的源代码你会发现这些api中不会再去调用其他api。反之,像creep.moveTo这种api最终调用的都是move,为了在跨tick执行和状态模拟中节省不必要的cpu消耗,我们只需将这些基础api封装到intent,并不惜头发为这些非基础api重写方法(至少TickStream是这样做的:)。
现在每个Creep都具有下面的属性和方法,我们随便抓一只creep来看看
let creep=Game.creeps[0];
creep.stateStack;//状态栈
creep.stateStack.push('hello');//进栈方法
creep.stateStack.pop();//出栈方法
creep.intent.set(...);
creep.intent.clean();
intent的数据在global上
stateStack的数据在Memory上
global.intent.data;//数组数据
creep.Memory.stateStack;//状态栈序列化数据
每个tick我们固定要做的事情
- Tick开始
- 读取所有Creep的Memory状态栈序列化数据并将其反序列化到Creep.stateStrack
- 检查global有没有被清空,如有触发相应事件
- 遍历global上本tick的intent的数据并调用对应API
- 若有API没有报OK状态,则触发相应事件
- 处理新的intent请求
- 序列化Creep.stateStrack到Memory
- Tick结束
我们来看一张结构图,方便大家理解
准备工作完毕
现在我们来完成事务代码
let upgradeController = {
name:'升级controller',
call:function(data){
let creep = data.creep;
//移动creep
drive(creep,moveTo,{creep:creep,target:data.target});
//抵达后,进行haevest
drive(creep,harvest,{creep:creep,target:data.target});
}
}
let moveTo = {
name:'导航',
call:function(data){
let creep = data.creep;
const path = creep.pos.findPathTo(data.target);//搜索路径
for(let i = 0;i<path.length;i++){
//用move让creep过去,这里看上去就像是跨tick执行的事情了
drive(creep,move,{creep:creep,direction:path[i].direction});
}
}
}
let move ={
name:'移动',
call:function(data){
let creep = data.creep;
intent(creep.id,'move',data.direction);
}
}
let harvest ={
name:'采集',
call:function(data){
let creep = data.creep;
intent(creep.id,'harvest',target);
}
}
网友评论