Creep 死亡后留下的资源不一定需要放弃,建筑被摧毁后留在 Ruin 中的资源也可以回收。但这两类对象都不是地面 Resource:
- 地面资源使用
creep.pickup(resource); - Tombstone 和 Ruin 使用
creep.withdraw(target, resourceType, amount); - Container、Storage 等建筑也使用
withdraw(),但它们的生命周期和目标选择逻辑不同。
本文只解决一个具体问题:怎样让回收 Creep 在当前可见房间中发现 Tombstone 与 Ruin,按过期风险和资源价值选择目标,并对 withdraw() 的提交结果进行可追踪验证。
Tombstone 与 Ruin 有什么区别
Tombstone 是死亡 Creep 的遗留对象
Tombstone 代表已经死亡的 Creep。它可以被穿过,并带有:
store:死亡时遗留的资源;ticksToDecay:距离消失还有多少 tick;deathTime:Creep 死亡的游戏时间;creep:死亡 Creep 的只读信息。
普通 Tombstone 的存在时间与原 Creep 的身体部件数量相关。实际业务代码不必自己重新计算,直接读取当前对象的 ticksToDecay 更可靠。
Ruin 是被摧毁建筑的遗留对象
Ruin 代表已经被摧毁的 Structure。它同样可以被穿过,并带有:
store:建筑被摧毁时遗留的资源;ticksToDecay:距离消失还有多少 tick;destroyTime:建筑被摧毁的游戏时间;structure:原建筑的只读信息。
普通 Ruin 通常会保留一段时间,但特殊对象可能不同。因此不要把过期时间写死,应始终以对象当前的 ticksToDecay 为准。
为什么不能只选择最近目标
最近的 Tombstone 不一定最值得回收。例如:
- 近处只有少量 Energy,远处有 Power 或 Ghodium;
- 一个目标将在 8 tick 后消失,另一个还有 300 tick;
- Creep 只剩 50 容量,目标中有多种资源;
- 多只回收 Creep 同时选择了同一个目标;
- 目标被不可通行的敌对 Rampart 覆盖。
候选排序至少应该考虑:
- 是否仍有可取资源;
- 是否即将过期;
- 资源类型的业务优先级;
- 当前最多能取多少;
- 距离;
- 完全同分时的稳定排序。
资源优先级不是固定规则
下面只是一个示例。你应根据自己的房间经济修改。
const RESOURCE_PRIORITY = [
RESOURCE_POWER,
RESOURCE_OPS,
RESOURCE_GHODIUM,
RESOURCE_CATALYST,
RESOURCE_ZYNTHIUM,
RESOURCE_UTRIUM,
RESOURCE_LEMERGIUM,
RESOURCE_KEANIUM,
RESOURCE_OXYGEN,
RESOURCE_HYDROGEN,
RESOURCE_ENERGY
];
function getResourceRank(resourceType) {
const index = RESOURCE_PRIORITY.indexOf(resourceType);
// 未列出的矿物、化合物或商品排在 Energy 前面。
return index === -1
? RESOURCE_PRIORITY.length - 1
: index;
}
战争现场、远程房间或低 RCL 房间可能更缺 Energy,此时可以提高 Energy 的优先级。
检查目标所在格的 Rampart
可见目标不代表一定适合回收。一个简单过滤方法是检查目标位置上的 Rampart:
function isBlockedByHostileRampart(target) {
const structures = target.pos.lookFor(LOOK_STRUCTURES);
return structures.some(structure =>
structure.structureType === STRUCTURE_RAMPART
&& structure.my !== true
&& structure.isPublic !== true
);
}
这个过滤只检查目标所在格。真实路径上仍可能有墙体、关闭的 Rampart、敌对 Creep 或 CostMatrix 限制,因此移动结果仍需单独记录。
从 Store 中选择资源类型
Tombstone 与 Ruin 的 store 可能包含 Energy、矿物、化合物、Power 或商品。不要直接使用 Object.keys(target.store)[0] 作为业务优先级。
function selectResourceType(target, freeCapacity) {
if (!target?.store || freeCapacity <= 0) {
return null;
}
return Object.keys(target.store)
.filter(resourceType =>
target.store.getUsedCapacity(resourceType) > 0
)
.sort((left, right) => {
const rankDifference =
getResourceRank(left) - getResourceRank(right);
if (rankDifference !== 0) {
return rankDifference;
}
const amountDifference =
target.store.getUsedCapacity(right)
- target.store.getUsedCapacity(left);
return amountDifference !== 0
? amountDifference
: left.localeCompare(right);
})[0] ?? null;
}
完整示例:扫描、排序、持久化与验证
下面的版本包含:
- 同时扫描
FIND_TOMBSTONES与FIND_RUINS; - 按
ticksToDecay、资源优先级、数量和距离排序; - 跨 tick 只保存目标 ID 和资源类型;
- 分开记录
withdraw()与moveTo()的返回值; - 下一 tick 使用 Store 差值进行有限验证;
- Memory 历史只保留最近 20 条。
const RESOURCE_PRIORITY = [
RESOURCE_POWER,
RESOURCE_OPS,
RESOURCE_GHODIUM,
RESOURCE_CATALYST,
RESOURCE_ZYNTHIUM,
RESOURCE_UTRIUM,
RESOURCE_LEMERGIUM,
RESOURCE_KEANIUM,
RESOURCE_OXYGEN,
RESOURCE_HYDROGEN,
RESOURCE_ENERGY
];
function getResourceRank(resourceType) {
const index = RESOURCE_PRIORITY.indexOf(resourceType);
return index === -1
? RESOURCE_PRIORITY.length - 1
: index;
}
function getRecoveryKind(target) {
if ('deathTime' in target) return 'tombstone';
if ('destroyTime' in target) return 'ruin';
return 'unknown';
}
function isBlockedByHostileRampart(target) {
return target.pos.lookFor(LOOK_STRUCTURES)
.some(structure =>
structure.structureType === STRUCTURE_RAMPART
&& structure.my !== true
&& structure.isPublic !== true
);
}
function selectResourceType(target, freeCapacity) {
if (!target?.store || freeCapacity <= 0) {
return null;
}
return Object.keys(target.store)
.filter(resourceType =>
target.store.getUsedCapacity(resourceType) > 0
)
.sort((left, right) => {
const rankDifference =
getResourceRank(left) - getResourceRank(right);
if (rankDifference !== 0) {
return rankDifference;
}
const amountDifference =
target.store.getUsedCapacity(right)
- target.store.getUsedCapacity(left);
return amountDifference !== 0
? amountDifference
: left.localeCompare(right);
})[0] ?? null;
}
function describeCandidate(creep, target) {
const freeCapacity = creep.store.getFreeCapacity();
if (
freeCapacity <= 0
|| !target?.id
|| !target.store
|| !Number.isFinite(target.ticksToDecay)
|| target.ticksToDecay <= 0
|| isBlockedByHostileRampart(target)
) {
return null;
}
const resourceType = selectResourceType(
target,
freeCapacity
);
if (!resourceType) {
return null;
}
const available = target.store.getUsedCapacity(resourceType);
const amount = Math.min(available, freeCapacity);
if (!Number.isFinite(amount) || amount <= 0) {
return null;
}
return {
target,
targetId: target.id,
kind: getRecoveryKind(target),
resourceType,
amount,
ticksToDecay: target.ticksToDecay,
resourceRank: getResourceRank(resourceType),
range: creep.pos.getRangeTo(target)
};
}
function compareCandidates(left, right) {
return (
left.ticksToDecay - right.ticksToDecay
|| left.resourceRank - right.resourceRank
|| right.amount - left.amount
|| left.range - right.range
|| left.targetId.localeCompare(right.targetId)
);
}
function selectCandidate(creep) {
return [
...creep.room.find(FIND_TOMBSTONES),
...creep.room.find(FIND_RUINS)
]
.map(target => describeCandidate(creep, target))
.filter(Boolean)
.sort(compareCandidates)[0] ?? null;
}
function saveTarget(creep, candidate) {
creep.memory.recoveryTarget = candidate
? {
id: candidate.targetId,
resourceType: candidate.resourceType
}
: null;
}
function restoreCandidate(creep) {
const saved = creep.memory.recoveryTarget;
if (!saved?.id || !saved.resourceType) {
return null;
}
const target = Game.getObjectById(saved.id);
if (!target) {
saveTarget(creep, null);
return null;
}
const candidate = describeCandidate(creep, target);
if (
!candidate
|| candidate.resourceType !== saved.resourceType
) {
saveTarget(creep, null);
return null;
}
return candidate;
}
function getRecoveryMemory() {
Memory.recovery ??= {
pending: {},
history: []
};
return Memory.recovery;
}
function verifyPreviousRecovery(creep) {
const memory = getRecoveryMemory();
const pending = memory.pending[creep.name];
if (!pending || pending.tick >= Game.time) {
return null;
}
const currentCreepAmount =
creep.store.getUsedCapacity(pending.resourceType);
const target = Game.getObjectById(pending.targetId);
const currentTargetAmount = target?.store
? target.store.getUsedCapacity(pending.resourceType)
: null;
const creepGain =
currentCreepAmount - pending.creepAmountBefore;
const targetLoss = currentTargetAmount === null
? null
: pending.targetAmountBefore - currentTargetAmount;
let status = 'not-observed';
if (
creepGain > 0
&& targetLoss !== null
&& targetLoss > 0
) {
status = 'matching-delta-observed';
} else if (creepGain > 0) {
status = 'creep-gain-observed';
} else if (target === null) {
status = 'target-unavailable';
} else if (Game.time > pending.tick + 1) {
status = 'late-observation';
}
const record = {
verifiedAt: Game.time,
creepName: creep.name,
...pending,
currentCreepAmount,
currentTargetAmount,
creepGain,
targetLoss,
status
};
memory.history.push(record);
memory.history = memory.history.slice(-20);
delete memory.pending[creep.name];
return record;
}
function submitRecovery(creep, candidate) {
if (creep.memory.recoveryDecisionTick === Game.time) {
return {
status: 'already-decided-this-tick'
};
}
creep.memory.recoveryDecisionTick = Game.time;
if (!creep.pos.isNearTo(candidate.target)) {
const moveResult = creep.moveTo(candidate.target, {
range: 1,
reusePath: 5
});
return {
status: 'moving-to-recovery-target',
targetId: candidate.targetId,
resourceType: candidate.resourceType,
moveResult
};
}
const creepAmountBefore =
creep.store.getUsedCapacity(candidate.resourceType);
const targetAmountBefore =
candidate.target.store.getUsedCapacity(
candidate.resourceType
);
const result = creep.withdraw(
candidate.target,
candidate.resourceType,
candidate.amount
);
if (result === OK) {
const memory = getRecoveryMemory();
memory.pending[creep.name] = {
tick: Game.time,
targetId: candidate.targetId,
resourceType: candidate.resourceType,
requestedAmount: candidate.amount,
creepAmountBefore,
targetAmountBefore
};
} else {
saveTarget(creep, null);
}
return {
status: result === OK
? 'withdraw-submitted'
: 'withdraw-failed',
targetId: candidate.targetId,
kind: candidate.kind,
resourceType: candidate.resourceType,
requestedAmount: candidate.amount,
result
};
}
function runRecoveryCreep(creep) {
const verification = verifyPreviousRecovery(creep);
if (creep.spawning === true) {
return {
status: 'creep-spawning',
verification
};
}
if (creep.getActiveBodyparts(CARRY) <= 0) {
return {
status: 'no-active-carry-part',
verification
};
}
if (creep.store.getFreeCapacity() <= 0) {
return {
status: 'creep-full',
verification
};
}
let candidate = restoreCandidate(creep);
if (!candidate) {
candidate = selectCandidate(creep);
saveTarget(creep, candidate);
}
if (!candidate) {
return {
status: 'recovery-target-not-found',
verification
};
}
return {
...submitRecovery(creep, candidate),
verification
};
}
module.exports.loop = function () {
const creep = Game.creeps.Recovery1;
if (!creep) {
return;
}
const outcome = runRecoveryCreep(creep);
if (
outcome.status === 'withdraw-failed'
|| outcome.status === 'no-active-carry-part'
) {
console.log(JSON.stringify({
type: 'resource-recovery-problem',
tick: Game.time,
creepName: creep.name,
...outcome
}));
}
};
为什么 OK 不等于资源已经到账
Screeps 在 tick 中收集玩家提交的动作,并在后续阶段处理。withdraw() 返回 OK 表示命令已被接受安排,不应把同一行代码后读取到的 Store 变化当成最终结算证明。
更稳妥的方法是:
- 当前 tick 保存 Creep 与目标的资源数量;
- 下一 tick 再读取两边 Store;
- 记录 Creep 增量与目标减量;
- 同时保留目标消失、观察过晚和未观察到变化等状态。
即使下一 tick 观察到差值,也只能说明状态变化与该动作一致。若同 tick 有其他 Creep、transfer、drop 或其他脚本动作参与,差值可能被并发行为干扰。因此本文使用的是有限验证,不是绝对因果证明。
withdraw() 常见返回值
| 返回值 | 常见原因 | 处理方向 |
|---|---|---|
OK |
动作已经提交 | 下一 tick 验证 Store 变化 |
ERR_NOT_OWNER |
调用者不是自己的 Creep | 检查对象来源 |
ERR_BUSY |
Creep 仍在生成 | 等待生成结束 |
ERR_NOT_ENOUGH_RESOURCES |
目标资源不足或被其他 Creep 先取走 | 清除目标并重新选择 |
ERR_INVALID_TARGET |
目标失效或不是可 withdraw 对象 | 重新通过 ID 恢复和判空 |
ERR_FULL |
Creep 没有剩余容量 | 先配送或卸载资源 |
ERR_NOT_IN_RANGE |
距离超过 1 格 | 调用 moveTo() 并单独检查结果 |
ERR_INVALID_ARGS |
资源类型或 amount 无效 | 重新计算资源类型与数量 |
多只 Creep 可以在同一 tick 对同一个对象提交 withdraw()。这也是为什么目标预订和下一 tick 验证不能省略。
常见错误
用 pickup() 处理 Tombstone 或 Ruin
pickup() 只接受地面 Resource。Tombstone 与 Ruin 有 store,应使用 withdraw()。
只找 Tombstone,漏掉 Ruin
战斗后被摧毁的 Storage、Terminal、Lab、Factory 或其他建筑可能留下高价值资源。应根据任务范围同时扫描两类对象。
把过期时间写死
不同对象的生命周期可能不同。排序时直接读取 ticksToDecay,不要根据对象类型猜测剩余时间。
把完整对象写入 Memory
跨 tick 只保存 ID、资源类型和必要业务字段。下一 tick 使用 Game.getObjectById() 恢复对象,并再次检查资源和访问条件。
只记录 withdraw(),不记录 moveTo()
距离不足时,真正失败的可能是移动:无路径、fatigue、MOVE 部件失效或路径被阻挡。两个动作的结果应分开记录。
多只回收 Creep 同时冲向同一目标
本文只提供单 Creep 示例。多 Creep 系统应增加目标预订表,并排除已经被其他 Creep 预订的对象或资源份额。
回收后没有配送状态
当 Creep 满载时,回收任务应切换到 Storage、Terminal、Spawn 或指定容器。本文故意不混入配送逻辑,以免一个示例承担过多职责。
离线验证覆盖
候选选择测试覆盖:
- Creep 容量为 0;
- 目标 Store 为空;
ticksToDecay为 0;- 目标被不可通行的敌对 Rampart 覆盖;
- 多资源目标按业务优先级选择;
- 可取数量不超过 Creep 剩余容量;
- 更早过期的目标优先;
- 同过期时间下按资源和数量排序;
- 完全同分时使用 ID 稳定排序。
完整示例已通过 JavaScript 语法检查。离线测试不能证明:
- 官方 shard 中的实际移动结果;
- Tombstone 与 Ruin 的真实衰减过程;
- 多玩家或多 Creep 同 tick 竞争;
- hostile Rampart 与复杂 CostMatrix 的完整交互;
withdraw()在真实服务器上的最终结算;- 该策略在你的房间中具有最低 CPU 成本。
这些项目在没有真实 Console 或 shard 记录前保持未验证状态。
适用边界
本文不覆盖:
- 地面 Resource 的
pickup(); - Container、Storage 的常规运输循环;
- 跨房间回收;
- 多 Creep 目标预订实现;
- 战斗区域威胁评分;
- 回收完成后的配送与仓储策略;
- 自动出售或加工回收资源。
相关站内内容
- Creep.pickup() 怎么安全捡取地上的 Energy
- Creep.withdraw() 如何从 Container 取 Energy
- Game.getObjectById() 怎么恢复跨 tick 目标
- 多个 Source 怎样按路径选择目标
- Creep 如何切换采集与工作状态
- 进入资源采集与房间经济专题
官方资料
资料核对日期:2026-08-04。候选选择与完整示例语法已离线检查;真实 Console、官方 shard 和多 Creep 竞争仍待验证。