KNOWLEDGE MODULE 04

移动、寻路与视野

逐一解决距离、疲劳、无路径、跨房间路线、CostMatrix、房间可见性与远程观察问题。

专题内进度10 / 12查看完整学习顺序 →

Screeps 常见问题

Game.rooms[roomName] 为什么是 undefined:Screeps 房间视野与 Observer 排查

解释 Game.rooms[roomName] 为什么会是 undefined,区分当前视野与 Memory 历史状态,并用 Game.getObjectById()、lastSeenAt 和 Observer 的下一 tick 时序安全恢复远程房间任务。

难度
高级
适用阶段
视野与可视化
模块位置
第 10 / 12 篇
前置知识
Screeps Creep.pull():两只 Creep 同 tick 协同移动

CURATED PATH

这些链接按前置知识、具体故障、排错动作或下一课人工维护,不按泛标签机械推荐。

Debugging / Observability

推荐下一步

需要远程视野时使用 Observer获取视野区分对象 ID 与当前可见对象对象恢复跨房移动时结合房间路线Movement 分支

KNOWLEDGE GRAPH

把这篇文章连接到 API、错误码和工具

这里由现有 API Reference、Object Hub 与知识库顺序自动生成,用来继续排查和学习;它不替代正文中的具体前置条件与返回值说明。

RELATED API

相关 API 与对象入口

Game.rooms[roomName]APIRoom API HubHub
POSSIBLE ERRORS

相关错误码

ERR_NOT_OWNER-1ERR_INVALID_TARGET-7ERR_INVALID_ARGS-10ERR_RCL_NOT_ENOUGH-14
USEFUL TOOLS

相关诊断与规划工具

房间运行诊断Tool
NEXT TOPIC

继续这个知识模块

当前属于“移动、寻路与视野”第 10 / 12 篇。

下一篇:StructureObserver.observeRoom() 怎么安全获取远方房间视野 →
VERIFICATION文档已核对 · 语法已检查 · Console待测试 · 主循环待验证查看验证详情
官方文档
已核对
JavaScript 语法
已检查
Screeps Console
待测试
真实主循环
待验证
最后核对
2026年8月15日
本文目录15 个主要章节
  1. 常见误区

Game.rooms[roomName] 是 undefined 时,最重要的结论不是“这个房间不存在”,而是:你的脚本在当前 tick 没有拿到这个房间的 live Room 对象。

官方 API 把 Game.rooms 定义为当前对你可用的房间集合。自己的 Creep、自己的结构以及 Observer 等视野来源都可能让房间进入当前可见集合;当视野消失后,之前写进 Memory 的房间数据仍可能存在,但它不能替代当前 Room 对象。

因此,远程采集、侦察、预订、跨房移动和目标缓存代码都应该先回答两个问题:

  1. 这个房间当前是否可见?
  2. 如果不可见,我应该等待视野、请求 Observer,还是只保留历史状态?

本文只处理这条“可见性边界”。如果问题是 moveTo() 找不到路线,应继续看 ERR_NO_PATH 排查;如果已经有 Observer,需要完整的请求与下一 tick 状态机,可继续看 Observer 远程视野指南。

快速结论

可靠的处理顺序可以压缩成下面这条链:

读取 Game.rooms[roomName]
→ 有 Room:读取当前 live 对象
→ 没有 Room:停止依赖 Room 的逻辑
→ 保留自己写入 Memory 的历史状态
→ 如有必要,提交 Observer 请求
→ 下一 tick 再检查 Game.rooms[roomName]
→ 视野恢复后重新取得对象,不复用旧快照

最小安全判空只需要这样:

function getVisibleRoom(roomName) {
  const room = Game.rooms[roomName];

  if (!room) {
    return null;
  }

  return room;
}

不要把 undefined 直接解释成:

  • 房间被删除;
  • Controller 消失;
  • Source 消失;
  • 保存的对象 ID 一定失效;
  • 远程任务应该立即清空。

这些结论都需要更多证据。

Game.rooms 到底代表什么

Game 会在每个 tick 重新创建并填入当前游戏状态。Game.rooms 不是一份永久房间数据库,而是当前 tick 可以直接访问的 Room 对象集合。

const room = Game.rooms['W2N2'];

这里可能得到:

Room 对象

也可能得到:

undefined

undefined 只说明当前索引里没有这个 Room 对象。

这也是为什么下面的写法存在风险:

const controller = Game.rooms['W2N2'].controller;

一旦房间当前不可见,代码会在访问 .controller 之前就因为 Game.rooms['W2N2'] 是 undefined 而抛出异常。

应该先建立可见性边界:

const roomName = 'W2N2';
const room = Game.rooms[roomName];

if (!room) {
  console.log(roomName, '当前不可见');
  return;
}

const controller = room.controller;

“房间不可见”和“房间里没有 Controller”不是一回事

即使 Game.rooms[roomName] 存在,也不能继续假设 room.controller 一定存在。

更可靠的状态分类是:

function describeRoomState(roomName) {
  const room = Game.rooms[roomName];

  if (!room) {
    return {
      roomName,
      visible: false,
      state: 'NOT_VISIBLE'
    };
  }

  const controller = room.controller;

  if (!controller) {
    return {
      roomName,
      visible: true,
      state: 'VISIBLE_NO_CONTROLLER'
    };
  }

  if (controller.my) {
    return {
      roomName,
      visible: true,
      state: 'VISIBLE_OWNED'
    };
  }

  if (controller.owner) {
    return {
      roomName,
      visible: true,
      state: 'VISIBLE_OTHER_OWNER',
      username: controller.owner.username
    };
  }

  if (controller.reservation) {
    return {
      roomName,
      visible: true,
      state: 'VISIBLE_RESERVED',
      username: controller.reservation.username
    };
  }

  return {
    roomName,
    visible: true,
    state: 'VISIBLE_NEUTRAL'
  };
}

这段分类的价值不是给所有房间贴标签,而是避免把不同问题混成一句“房间不存在”。

例如:

NOT_VISIBLE

表示当前没有 live Room。

而:

VISIBLE_NO_CONTROLLER

表示房间已经可见,只是当前房间本身没有 Controller。此时继续等待 Observer 并不能“生成”一个 Controller。

Memory.rooms 不是当前视野证明

Memory 会跨 tick 保存玩家脚本写入的数据。Game 则会在每个 tick 根据当前状态重新创建。

因此下面两个结果可以同时成立:

Boolean(Memory.rooms?.W2N2) === true;
Boolean(Game.rooms.W2N2) === false;

这并不矛盾。

Memory.rooms.W2N2 可能只是你之前通过 room.memory 或自己的代码保存过的数据。它可以帮助恢复任务上下文,但不能证明当前还能读取房间里的 Source、Creep、Structure 或 Controller。

不要写成:

const room = Game.rooms[roomName]
  || Memory.rooms[roomName];

room.find(FIND_SOURCES);

Memory.rooms[roomName] 是普通持久化数据,不是 Room 实例,也没有 room.find() 这样的 live API。

更安全的职责划分是:

const room = Game.rooms[roomName];
const remembered = Memory.rooms
  ? Memory.rooms[roomName]
  : undefined;

if (!room) {
  return {
    visible: false,
    remembered: Boolean(remembered)
  };
}

return {
  visible: true,
  room
};

给历史数据加一个 lastSeenAt

如果远程任务需要知道“最后一次看到这个房间是什么时候”,不要从 Memory.rooms 是否存在来猜。

可以自己保存一个明确时间:

function recordRoomVisibility(roomName) {
  if (!Memory.roomVisibility) {
    Memory.roomVisibility = {};
  }

  const room = Game.rooms[roomName];

  if (!room) {
    return false;
  }

  Memory.roomVisibility[roomName] = {
    lastSeenAt: Game.time
  };

  return true;
}

读取时:

function getVisibilityAge(roomName) {
  const record = Memory.roomVisibility
    ? Memory.roomVisibility[roomName]
    : undefined;

  if (!record || !Number.isInteger(record.lastSeenAt)) {
    return null;
  }

  return Game.time - record.lastSeenAt;
}

这样可以明确区分:

当前可见
最后 3 tick 前看见
最后 500 tick 前看见
从未记录过视野

lastSeenAt 是本站示例字段,不是 Screeps 内置属性。

保存对象 ID 时,还要保存房间名

远程任务经常先保存一个 Source、Structure 或其他对象的 ID:

Memory.remoteTarget = {
  roomName: 'W2N2',
  id: '0123456789abcdef01234567'
};

下一 tick 再恢复:

const saved = Memory.remoteTarget;
const target = saved
  ? Game.getObjectById(saved.id)
  : null;

这里有一个很重要的边界:官方文档说明 Game.getObjectById() 只能访问当前对你可见房间里的对象。

因此:

target === null

不能单独证明目标已经被摧毁或消失。

先检查目标房间有没有视野:

function loadRemoteTarget(saved) {
  if (!saved || !saved.id || !saved.roomName) {
    return {
      state: 'NO_SAVED_TARGET',
      target: null
    };
  }

  const room = Game.rooms[saved.roomName];

  if (!room) {
    return {
      state: 'ROOM_NOT_VISIBLE',
      target: null
    };
  }

  const target = Game.getObjectById(saved.id);

  if (!target) {
    return {
      state: 'TARGET_NOT_FOUND_WHILE_VISIBLE',
      target: null
    };
  }

  return {
    state: 'TARGET_VISIBLE',
    target
  };
}

只有在房间已经可见时仍然无法恢复对象,才获得了更强的“这个旧 ID 当前已经找不到”的证据。

这类目标缓存的完整设计可继续看 Game.getObjectById() 与 Memory 目标恢复。

需要主动视野时,用 Observer,但不要在同一 tick 验证结果

StructureObserver.observeRoom(roomName) 会安排一次远程观察。官方 API 明确说明:目标 Room 对象会在下一 tick 可用。

因此下面的逻辑是错误的验证方式:

const result = observer.observeRoom('W2N2');
const room = Game.rooms.W2N2;

if (result === OK && room) {
  console.log('Observer 已经在当前 tick 提供视野');
}

OK 只表示观察操作已经被安排。不要把同一 tick 的 Game.rooms 状态当成这次请求的结果。

如果你已经通过配置保存 Observer ID,可以把请求和后续读取分开:

function requestObservation(observerId, roomName) {
  const observer = Game.getObjectById(observerId);

  if (!observer
      || observer.structureType !== STRUCTURE_OBSERVER
      || !observer.my
      || !observer.isActive()) {
    return {
      state: 'OBSERVER_UNAVAILABLE',
      code: null
    };
  }

  const code = observer.observeRoom(roomName);

  if (code === OK) {
    if (!Memory.observationRequests) {
      Memory.observationRequests = {};
    }

    Memory.observationRequests[roomName] = {
      requestedAt: Game.time,
      observerId: observer.id
    };
  }

  return {
    state: code === OK
      ? 'OBSERVATION_SCHEDULED'
      : 'OBSERVATION_FAILED',
    code
  };
}

后续 tick 再检查:

function readObservedRoom(roomName) {
  const request = Memory.observationRequests
    ? Memory.observationRequests[roomName]
    : undefined;

  if (!request) {
    return {
      state: 'NO_REQUEST',
      room: null
    };
  }

  if (Game.time <= request.requestedAt) {
    return {
      state: 'WAIT_NEXT_TICK',
      room: null
    };
  }

  const room = Game.rooms[roomName];

  return room
    ? { state: 'VISIBLE_AFTER_REQUEST', room }
    : { state: 'NOT_VISIBLE_AFTER_REQUEST', room: null };
}

这仍然不是“Observer 一定成功”的真实服务器证据。要验证实际运行结果,需要在自己的 Screeps 环境中记录请求 tick、返回码和后续 tick 的 Game.rooms[roomName] 状态。

完整的 Observer 范围、返回码和调度边界请看 StructureObserver.observeRoom() 指南。

把“需要 live Room”写进函数契约

一个常见架构问题是:业务函数内部到处重复猜“房间可能看不见”。

更简单的方式是让调用方先过可见性门槛:

function runVisibleRoomJob(room) {
  const sources = room.find(FIND_SOURCES);
  const controller = room.controller;

  return {
    roomName: room.name,
    sourceCount: sources.length,
    hasController: Boolean(controller)
  };
}

function runRemoteRoomJob(roomName) {
  const room = Game.rooms[roomName];

  if (!room) {
    return {
      state: 'WAITING_FOR_VISION'
    };
  }

  return {
    state: 'ROOM_VISIBLE',
    result: runVisibleRoomJob(room)
  };
}

这样 runVisibleRoomJob() 的输入契约很明确:它只接收当前 tick 已经存在的 live Room。

对远程房间来说,“等待视野”是一个正常状态,而不是异常。

视野恢复后,要重新读取当前对象

假设 1000 tick 时你看见远程房间并保存了:

{
  roomName: 'W2N2',
  sourceId: '0123456789abcdef01234567'
}

随后房间失去视野。

当视野再次恢复时,不应该继续使用以前缓存的完整对象快照。应重新从当前 Game 状态取得对象:

const room = Game.rooms[remote.roomName];

if (!room) {
  return;
}

const source = Game.getObjectById(remote.sourceId);

if (!source) {
  console.log(
    remote.roomName,
    '房间已可见,但旧 sourceId 当前无法恢复'
  );
  return;
}

console.log({
  roomName: room.name,
  sourceId: source.id,
  energy: source.energy,
  ticksToRegeneration: source.ticksToRegeneration
});

原因很简单:Game 是当前 tick 的 live 状态入口;跨 tick 要保存的是可以重新恢复对象的简单数据,而不是假设旧对象引用永久有效。

一个有限、可读的诊断探针

如果只想快速判断问题在哪一层,可以使用这个只读探针:

function probeRoomVisibility(roomName) {
  const room = Game.rooms[roomName];
  const visibilityRecord = Memory.roomVisibility
    ? Memory.roomVisibility[roomName]
    : undefined;

  if (!room) {
    return {
      roomName,
      visible: false,
      lastSeenAt:
        visibilityRecord?.lastSeenAt ?? null,
      age:
        Number.isInteger(visibilityRecord?.lastSeenAt)
          ? Game.time - visibilityRecord.lastSeenAt
          : null
    };
  }

  const controller = room.controller;

  return {
    roomName,
    visible: true,
    hasController: Boolean(controller),
    controllerMy: controller
      ? Boolean(controller.my)
      : null,
    owner: controller?.owner?.username ?? null,
    reservation:
      controller?.reservation?.username ?? null
  };
}

Console 中只读查看:

console.log(
  JSON.stringify(
    probeRoomVisibility('W2N2'),
    null,
    2
  )
);

本文没有在真实 Screeps shard 中替你执行这段探针,因此不能声称某个具体房间已经通过运行验证。

常见误区

Memory.rooms[roomName] 有数据,为什么 Game.rooms[roomName] 还是 undefined?

因为两者生命周期不同。Memory 保存你跨 tick 写入的数据;Game.rooms 只提供当前 tick 可访问的 live Room。Memory 中有历史状态并不会自动提供视野。

Game.getObjectById(id) 返回 null,目标是不是已经没了?

不一定。先检查目标所在房间是否当前可见。官方文档明确限制 Game.getObjectById() 只能取得可见房间中的对象。房间不可见时,不应仅因为返回 null 就删除远程目标记录。

observeRoom() 返回 OK,为什么当前 tick 还是看不到目标房间?

这是正常时序。OK 表示观察已经安排,目标 Room 对象应在下一 tick 检查。

房间可见,但 room.controller 是 undefined,还是视野问题吗?

不一定。此时 Room 已经存在,说明“当前不可见”这个问题已经排除。接下来应判断该房间本身是否有 Controller,而不是继续等待视野。

建议的生产排查顺序

遇到远程房间任务突然失效时,按下面顺序排查:

  1. 直接读取 Game.rooms[roomName],先确认当前是否有 live Room。
  2. 房间不可见时,停止所有依赖 Room、room.find()、Controller 或当前结构状态的逻辑。
  3. 读取自己保存的 lastSeenAt,区分刚失去视野和长期未观察。
  4. 如果任务保存了对象 ID,同时保留 roomName;不可见期间不要仅凭 Game.getObjectById() 的 null 清空目标。
  5. 确实需要主动视野时,提交一次 Observer 请求并保存请求 tick 与返回码。
  6. 下一 tick 再读取 Game.rooms[roomName]。
  7. 视野恢复后重新取得 Source、Structure、Controller 等当前对象,再继续业务逻辑。
  8. 如果房间已经可见但业务仍失败,再进入对应的移动、资源、Controller 或目标类型排查,而不是继续把问题归因于视野。

本文的验证边界

本文在 2026-08-15 重新核对了 Screeps 官方 API 与 Global Objects 文档,确认了以下文档级事实:

  • Game.rooms 是当前可访问房间的集合;
  • Game 每个 tick 重新创建;
  • Memory 用于跨 tick 保存 JSON 数据;
  • Game.getObjectById() 只能访问当前可见房间中的对象;
  • StructureObserver.observeRoom() 成功安排后,目标 Room 在下一 tick 才可用。

本文代码只做静态语法与边界审阅,没有在真实 Screeps Console 或官方 shard 中执行,因此:

Docs checked: yes
Syntax checked: yes
Console tested: no
Live multi-tick tested: no

这一区分很重要:官方文档可以证明 API 契约,但不能替代你自己的房间、Observer 范围、对象 ID 和实际跨 tick 运行结果。

站内继续阅读

官方资料

返回目录