前言
背景知识
什么是异步
举个简单的例子,做早餐需要下面这些步骤:
- 倒一杯咖啡。
- 加热平底锅,然后煎两个鸡蛋。
- 煎三片培根。
- 烤两片面包。
- 在烤面包上加黄油和果酱。
- 倒一杯橙汁。
如果按顺序一项项处理,你可能要花费30分钟。
但是不难注意到,有一些工作开始后,你并不需要呆在边上干预,而是可以同时展开其他工作,这就是异步执行。
异步执行,可以让一些没有依赖关系的工作同时进行,让有依赖关系的工作按顺序执行,从而减少整个过程的耗时。
Screeps中的设计模式
角色+状态机
[Screeps](Screeps Documentation)只提供了对象的同步方法,游戏本身又是轮询的结构,即每个tick调用一次loop(),这种情况下,安排游戏对象进行一些“抽象”的操作,往往通过角色+状态机来实现,比如官方教程中使用到的建造者代码:
var roleBuilder = {
/** @param {Creep} creep **/
run: function(creep) {
if(creep.memory.building && creep.store[RESOURCE_ENERGY] == 0) {
creep.memory.building = false;
creep.say('🔄 harvest');
}
if(!creep.memory.building && creep.store.getFreeCapacity() == 0) {
creep.memory.building = true;
creep.say('🚧 build');
}
if(creep.memory.building) {
var targets = creep.room.find(FIND_CONSTRUCTION_SITES);
if(targets.length) {
if(creep.build(targets[0]) == ERR_NOT_IN_RANGE) {
creep.moveTo(targets[0], {visualizePathStyle: {stroke: '#ffffff'}});
}
}
}
else {
var sources = creep.room.find(FIND_SOURCES);
if(creep.harvest(sources[0]) == ERR_NOT_IN_RANGE) {
creep.moveTo(sources[0], {visualizePathStyle: {stroke: '#ffaa00'}});
}
}
}
};
角色+状态机有不少局限性(严重程度从低到高):
- 状态的数量可能很多
- 直接调用游戏api,颗粒度细,书写繁琐,且不好做全局控制
- 角色一经设定几乎不会更改,很少进行角色间的资源调度
- 单tick下,状态一般只能切换有限次数
- 状态与角色之间高度耦合,难以复用
笔者在很长一段时间内,都使用了这种模式。前三点,可以通过合理的职责划分,方法公用等来缓解,真正让我放弃这种模式的,是第四点和第五点。
任务与任务制
在玩家社群里,常常会听到玩家讨论“任务制”,但是对任务制的理解是各不相同的,笔者在这里先谈谈我对任务制的理解。
任务有以下特征:
- 任务的执行往往是跨tick的
- 任务应该在有限的时间内执行完,因此任务应当由有限的步骤组成
- 步骤的执行往往也是跨tick的
- 任务的执行需要资源(creep,能量等)
- 发布任务,相当于锁定了这部分资源
- 中止,完成任务后,需要释放这部分资源
- 任务可能会涉及到复数的资源,对这些资源的操作,可能同时进行,也可能按顺序进行
针对1,2,3,5特征,为了让任务的书写更便利,笔者设计并实现了一套异步执行框架。在接下来的章节中,笔者将会介绍如何使用这个框架,以及该框架是如何实现的。
第四点会在后续的文章中详细说明,敬请期待。
异步执行框架
对实现细节不感兴趣的话,可以跳至最后一节“编写自定义任务”
概览
语法设计类似于C#的Task,感兴趣的话可以了解下,在设计过程中做了很多简化和妥协。
任务 TaskEntity
任务包含了
- 一系列指令组(步骤)
- 当期执行的步骤
- 每个指令组的执行方式:WaitAll(指令组中的指令全部完成,视作该指令组执行完成), WaitAny(指令组中的任意一个指令全部完成,视作该指令组执行完成)
interface ActionGroup {
waitType: "all" | "any"
actionsIds: string[]
}
type TaskStatus = "running" | "complete" | "fail"
interface TaskEntity extends Entity {
status: TaskStatus
step: number
actionsGroups:ActionGroup[]
}
执行任务时,会依次执行每个指令组(步骤),根据指令组的模式,以及指令组下每个指令的执行情况,决定指令组什么完成,开始执行下一个指令组。
指令 ActionEntity
指令包含了
- 操作者的id (比如:Creep 的 id)
- 一个同步方法的类型
- 序列化后的参数
interface ActionEntity extends Entity {
operatorId: string
type: string
parameters: any[]
}
执行指令时,会通过类型找到对应的方法,反序列化参数,并进行调用
同步方法(末尾不带Async的方法)
这类方法,对游戏API进行了封装,且只能返回 running, complete, fail 三种状态值。
running 意味着该方法下tick还需要继续执行。
Creep.prototype.reach = function (target, range = 1) {
if (!target || !this) return "fail";
const pos: RoomPosition = Convert.ToRoomPosition(target);
if (this.pos.getRangeTo(pos) > range) {
this.moveTo(target);
return "running";
} else {
return "complete";
}
};
异步方法(末尾带Aysnc的方法)
每个同步方法,都对应一个异步方法,且入参完全一致。
异步方法的作用是,将入参序列化,并返回一个指令。
Creep.prototype.reachAsync = function (target, range = 1) {
const posEntity = Convert.ToPosEntity(target);
const reachAction = ActionContext.CreateAndAdd(this.id, _this.type, [posEntity, range]);
return reachAction;
};
任务类 Task
任务类是书写自定义任务时使用的工具类,通过调用Wait(), WaitAny(), WaitAll() 方法,来维护任务中的指令组。
class Task {
public _task: TaskEntity;
constructor() {
this._task = TaskContext.CreateAndAdd();
}
Wait(actionEntity: ActionEntity) {
this.WaitAll(actionEntity);
}
/**
* 有一个执行成功就算成功,失败会立即阻断。
* @param {ActionEntity[]} ...actionEntities
*/
WaitAny(...actionEntities: ActionEntity[]) {
const actionIds = _.map(actionEntities, (entity) => entity.id);
this._task.actionsGroups.push({
waitType: "any",
actionsIds: actionIds
})
TaskContext.Update(this._task);
}
/**
* 全部执行成功才算成功,失败会立即阻断。
* @param {ActionEntity[]} ...actionEntities
*/
WaitAll(...actionEntities: ActionEntity[]) {
const actionIds = _.map(actionEntities, (entity) => entity.id);
this._task.actionsGroups.push({
waitType: "all",
actionsIds: actionIds
})
TaskContext.Update(this._task);
}
}
指令类 Action
指令类中,对同步方法,异步方法进行挂载,并包含了该指令执行的逻辑(主要是参数的反序列化)。
type ActionStatus = "running" | "complete" | "fail"
interface Action {
type:string
// 将同步方法,异步方法挂载到prototype上
mount(): void
// 执行给定的指令
run(actionEntity:ActionEntity): ActionStatus
}
编写自定义任务的例子
点对点运输
export function One2OneTransferTask(creep: Creep, resourceType: ResourceConstant, fromStructure: Structure | Tombstone | Ruin, toStructure: Structure, amount?: number) {
const task = new Task();
task.Wait(creep.reachAsync(fromStructure));
task.Wait(creep.withdrawOnceAsync(fromStructure, resourceType, amount));
task.Wait(creep.reachAsync(toStructure));
task.Wait(creep.transferOnceAsync(toStructure, resourceType, amount));
}
批量填充
export function DistributeTask(creep: Creep, resourceType: ResourceConstant, fromStructure: Structure | Tombstone | Ruin, toStructures: Structure[], totalAmount?: number) {
const task = new Task();
task.Wait(creep.reachAsync(fromStructure));
task.Wait(creep.withdrawOnceAsync(fromStructure, resourceType, totalAmount));
_.map(toStructures, (toStructure) => {
task.Wait(creep.reachAsync(toStructure));
task.Wait(creep.transferOnceAsync(toStructure, resourceType));
})
}
设置任务超时(开发中,仅作参考)
function TimeOutTestTask(creep: Creep, pos: RoomPosition) {
const task = new Task();
const promise1 = creep.reachAsync(pos);
const promise2 = TimeOutAction.TimeOut(1500);
task.WaitAny(promise1, promise2);
}
给资源加锁(开发中,仅供参考)
export function One2OneTransferLockTask(creep: Creep, resourceType: ResourceConstant, fromStructure: Structure | Tombstone | Ruin, toStructure: Structure, amount?: number) {
const task = new Task();
task.LockCreep(creep);
task.LockResources("out", fromStructure, amount);
task.LockResources("in", toStructure, amount);
task.Wait(creep.reachAsync(fromStructure));
task.Wait(creep.withdrawOnceAsync(fromStructure, resourceType, amount));
task.Wait(creep.reachAsync(toStructure));
task.Wait(creep.transferOnceAsync(toStructure, resourceType, amount));
// 不需要手动释放,任务完成/失败/中止时自动释放资源
}
多对象同步操作
export function SpiralMoveTask(center:RoomPosition, edgeLength: number, creep1: Creep, creep2: Creep, creep3: Creep, creep4: Creep) {
const task = new Task();
const diffX = edgeLength / 2;
const diffY = edgeLength / 2;
const leftTopPos = new RoomPosition(center.x - diffX, center.y - diffY, center.roomName);
const rightTopPos = new RoomPosition(center.x + diffX, center.y - diffY, center.roomName);
const leftBottomPos = new RoomPosition(center.x - diffX, center.y + diffY, center.roomName);
const rightBottomPos = new RoomPosition(center.x + diffX, center.y + diffY, center.roomName);
const p1 = creep1.reachAsync(leftTopPos, 0);
const p2 = creep2.reachAsync(rightTopPos, 0);
const p3 = creep3.reachAsync(leftBottomPos, 0);
const p4 = creep4.reachAsync(rightBottomPos, 0);
task.WaitAll(p1, p2, p3, p4);
const p5 = creep1.trackAsync(creep3);
const p6 = creep2.trackAsync(creep1);
const p7 = creep4.trackAsync(creep2);
const p8 = creep3.trackAsync(creep4);
task.WaitAll(p5, p6, p7, p8);
}
后续开发计划
- 任务制相关的资源锁
- 超时锁
- 目前异步方法相当于返回void,后续尝试允许返回值,后续指令可以用前面指令的执行结果作为入参