Introduction
资源管理模块的 Introduction
首先,我们需要明确资源管理模块的总的目标是什么。在我看来,资源管理模块调度资源的分配,但并不管理资源的生产!也就是说,假设现在有 300 的 Energy,资源管理模块只负责管理这 300 的 Energy 分配给谁,不分配给谁,而不负责去想怎么再得到 300 的 Energy。
并且,框架内的资源管理模块的接口会相对底层,不支持一些高阶的功能,即例如只支持指定具体资源目标,而不支持抽象资源申请。实际上,我们可以在开发完底层的资源管理接口之后,再提供一些包装的高层资源管理接口,支持一些更复杂的功能。
Okay,明确了总的目标之后,就要明确我们需要达到什么样的功能目标:
在进程中可以申请任意目标,任意种类,任意数量的资源,并且在申请不到时阻塞,申请到时继续执行。其中,数量可以是动态,即设定一个上限或者是下限。
我们管理所有的资源,包括生产者、消费者和既是生产者也是消费者。因为生产者需要我们控制取出的数量,而消费者需要我们控制余量。
这当中有一些 Tradeoff,实际上在这个设计中,放弃了以下的功能:
不指定具体的目标的抽象资源申请。例如,有时候我们只需要 3000 Energy,而并不关心它是从 Storage 中拿,还是 Terminal 中拿。或者,有时候,我们要填充 Tower 的 Energy,但是 Storage 中的能量已经空了,Nuker 中还有。这时候,就从 Nuker 中拿是比较明智的选择。相比而言,我更希望通过额外的一个小的功能模块,来监视房间内 Energy 状况,在不足时,自动生一些 Creep,将能量从 Nuker 搬回 Storage。或者是在应用进程中,增添复杂的取能量的部分,当然这是可以打包成一个小插件的。但是,这又产生一个不可避免的问题,就是如果我们以某个建筑,例如 Storage 为资源中转中心的话。在初期,是没有 Storage 的;并且,如果说被别人揪住了这个代码的 BUG,一发 Nuker 摧毁了你的资源中转中心,那整个机器不就宕机了嘛。所以,我们在申请前需要引入一个步骤,就是测试是否可能申请到资源。
Okay,将以上的设计理念总结一下,我们可以得到这样的模块接口:
/**
* 资源模块的资源定义
* 除了常规的资源外, 还包含 容量 这种资源
*
* 但是, 部分建筑的不同资源的容量是不共通的.
* 其中有:
* - Lab: energy 和 mineral
* - Nuker: energy 和 ghodium
* - PowerSpawn: energy 和 power
*
* 所以, 对于容量的描述需要复杂一些. 这里总共归为三类:
* - capacity: 正常的容量, 适用于一般的建筑
* - capacity_energy: 能量的容量
* - capacity_mineral: mineral / ghodium / power 的容量
*
* 但是, 实际上, 对于 Lab 我们应该对不同的矿物考虑不同的容量.
* 因为, 假如 Lab 中放了矿物 A, 矿物 B就不能放进去了. 但是,
* 这种多重容量的实现过于复杂, 代价很大. 所以我们使用矿物来统一
* 代替, 表示放当前矿物的情况下能放多少. 这种矿物不共通的问题由
* 调用段来承担, 例如可以写出这样的代码:
* if (lab.mineralType !== yourMineral) return OK_STOP_CURRENT
* 而对于只能存放特定资源的建筑来说, 例如 Spawn, 约定是选择最准确的描述
*/
type ResourceType = ResourceConstant | "capacity" | "capacity_energy" | "capacity_mineral"
/**
* 数量描述器
* 包含了 精准数量, 有上界 (<=), 有下界 (>=) 和 有上下界 (>=, <=)
*/
type AmountDescriptor = number | { upperBound: number } | { lowerBound: number } | { lowerBound: number, upperBound: number }
/** 解析数量描述器到统一的有上下界描述 */
function parseAmountDescriptor(amountDescriptor: AmountDescriptor): { lowerBound: number, upperBound: number } {
if ( typeof amountDescriptor === "number" )
return { lowerBound: amountDescriptor, upperBound: amountDescriptor }
else if ( !("lowerBound" in amountDescriptor) )
// 下界默认是 1
return { lowerBound: 1, upperBound: amountDescriptor["upperBound"] }
else if ( !("upperBound" in amountDescriptor) )
// 上界默认是 无穷
return { lowerBound: amountDescriptor["lowerBound"], upperBound: Infinity }
else
return amountDescriptor
}
/** 可存取的建筑, 并不包含 Ruin 和 TombStone */
interface StorableStructure extends OwnedStructure {
/**
* A Store object that contains cargo of this structure.
*/
store: StoreDefinition |
Store<RESOURCE_ENERGY, false> | // Spawn, Extension
Store<RESOURCE_ENERGY | RESOURCE_POWER, false> | // PowerSpawn
Store<RESOURCE_ENERGY | MineralConstant | MineralCompoundConstant, false> | // Lab
Store<RESOURCE_ENERGY | RESOURCE_GHODIUM, false> // Nuker
}
那么这样的函数接口,有没有达到我们既定的目标呢?大部分还是很显然的,但是有一点需要说明一下,就是测试是否可以申请到资源。它的目的是在于比如 Storage 并不存在的情况下,避免从 Storage 申请资源,从而导致永久性阻塞。那么,这里,我们将 target 设为 Id,实际上就是隐形要求了申请方在申请时被申请的资源所在建筑就要存在。
🧱 建筑资源管理
Okay,在当前的情况下,为了方便管理,我们需要进一步实现对于某一个具体建筑的所有资源管理。这里,我们并不会检查建筑是否有可能拥有这个资源,这一切都需要调用方承担,因为它在给 Id 的时候,一定需要知道 Id 指向的是哪个建筑。
/**
* 建筑资源管理
* 不对外公开
*/
class StructureResourceManager {
#id: Id<StorableStructure>
/** 资源到信号量 Id 的映射 */
#resourceDict: {[resourceType in ResourceType]?: string}
/** 获得资源的信号量 */
getSignal(resourceType: ResourceType) {
if (resourceType in this.#resourceDict)
return this.#resourceDict[resourceType]
/** 创建信号量 */
const structure = Game.getObjectById(this.#id)
// 获取资源数值
let value: number = null
if (resourceType === "capacity")
value = structure.store.getFreeCapacity()
else if (resourceType === "capacity_energy")
value = structure.store.getFreeCapacity(RESOURCE_ENERGY)
else if (resourceType === "capacity_mineral") {
if (structure instanceof StructureLab) {
// 在没有矿物的时候, 使用 H 来试探有多少容量
value = structure.store.getFreeCapacity(structure.mineralType || RESOURCE_HYDROGEN)
} else if (structure instanceof StructurePowerSpawn) {
value = structure.store.getFreeCapacity(RESOURCE_POWER)
} else if (structure instanceof StructureNuker) {
value = structure.store.getFreeCapacity(RESOURCE_GHODIUM)
} else
throw `Error: '${this.#id}' 没有专门存储矿物的容量`
} else
value = structure.store.getUsedCapacity(resourceType)
// 校验资源有效性
if (value === null)
throw `Error: '${this.#id}' 不支持 '${resourceType}' 的存储`
return this.#resourceDict[resourceType] = Apollo.proc.signal.createSignal(value)
}
/** 获得资源的具体数值 */
getValue(resourceType: ResourceType) {
return Apollo.proc.signal.getValue(this.getSignal(resourceType))
}
constructor(id: Id<StorableStructure>) {
this.#id = id
this.#resourceDict = {}
}
}
📃 资源管理模块的具体实现
type RequestDescriptor = {
/** 请求的包含资源的建筑 Id */
id: Id<StorableStructure>,
/** 请求的资源种类 */
resourceType: ResourceType,
/** 请求的数量 */
amount: AmountDescriptor,
}
/**
* 资源模块
*/
class ResourceModule {
/** 容量 - 资源常量 */
CAPACITY: typeof CAPACITY = CAPACITY
/** 能量容量 - 资源常量 */
CAPACITY_ENERGY: typeof CAPACITY_ENERGY = CAPACITY_ENERGY
/** 矿物容量 - 资源常量 */
CAPACITY_MINERAL: typeof CAPACITY_MINERAL = CAPACITY_MINERAL
/** 映射建筑 Id 到建筑资源管理 */
#structureDict: {[id: Id<StorableStructure>]: StructureResourceManager} = {}
/** 根据建筑 Id 获得建筑资源管理 */
#getStructureResourceManager(id: Id<StorableStructure>) {
if (id in this.#structureDict)
return this.#structureDict[id]
return this.#structureDict[id] = new StructureResourceManager(id)
}
/**
* 请求资源
* 可以通过包含多个同样 Id 的建筑, 但是不同资源种类来同步申请一个
* 建筑的多种资源.
*
* @atom 只能在进程流程中运行使用
*/
request(target: RequestDescriptor | RequestDescriptor[]): StuckableAtomicFuncReturnCode {
/** 规整参数 */
if (!Array.isArray(target)) target = [ target ]
return Apollo.proc.signal.Swait(
...target.map(v => { return {
signalId: this.#getStructureResourceManager(v.id).getSignal(v.resourceType),
...parseAmountDescriptor(v.amount),
} })
)
}
/**
* 通知资源发生变更
*
* @atom 只能在进程流程中运行使用
*
* 注意: 资源变更只能在进程中通知, 因此可以创建一些监视进程.
*/
signal(target: Id<StorableStructure>, resourceType: ResourceType, amount: number) {
const manager = this.#getStructureResourceManager(target)
const signalId = manager.getSignal(resourceType)
return Apollo.proc.signal.Ssignal({ signalId, request: amount })
}
/**
* 查询资源预期状况
*/
qeury(target: Id<StorableStructure>, resourceType: ResourceType) {
const manager = this.#getStructureResourceManager(target)
return manager.getValue(resourceType)
}
}
注意:在资源管理模块,有一个重要的问题需要考虑,即如何管理 Spawn 和 Extension。本身,Spawn 和 Extension 作为最基础的能量储存和消耗建筑,肯定值得被管理。但是难点在于 Extension 过多,而且消耗能量的时候不容易具体制定。Spawn 本身还有能量自增的问题 (本房间能量小于 300 时,会有能量自增),而能量自增很难判定发生与发生在哪。所以,我选择不通过资源管理模块来管理 Spawn 和 Extension。通过外部的功能模块来完成对 Spawn 和 Extension 的填充管理。
Last updated