OBSERVABILITY · ROOMVISUAL DEBUG LAYER

How to Build a Safe RoomVisual Debug Layer in Screeps

Draw current Creep state, target relationships, and task labels with RoomVisual while enforcing per-room switches, stable ordering, item limits, the 512,000-byte ceiling, cross-room boundaries, and a strict separation between visuals and action results.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — current-tick lifecycle, drawing methods, getSize(), 512,000-byte limit, export() and import() · Visibility boundary: A RoomVisual can draw known coordinates without granting live Room visibility

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — current-tick lifecycle, drawing methods, getSize(), 512,000-byte limit, export() and import()
Visibility boundary
A RoomVisual can draw known coordinates without granting live Room visibility
Evidence boundary
A label or line shows the debug plan, not a successful movement or action outcome
JavaScript syntax
Passed
Offline visual-plan review
Passed — disabled, missing object, labels, target availability, cross-room target, stable order, item cap and byte stop states
Screeps Console test
Pending
Live RoomVisual, byte-size and CPU test
Pending
Last verified
July 25, 2026

Quick answer

Use RoomVisual to display what your code currently believes: Creep roles, tasks, target IDs, paths, ranges, and selected positions. Redraw each tick, limit the number of items, stop before 512,000 bytes per room, and measure CPU separately. A drawing is diagnostic evidence about your plan, not proof that an action succeeded.

RoomVisual belongs to the current tick

module.exports.loop = function () {
  const visual = new RoomVisual('W1N1');
  visual.text('running', 25, 25);
};

If the code does not draw or import the visual on the next tick, the previous drawing does not remain automatically.

Drawing does not grant visibility

const visual = new RoomVisual('W10N10');
visual.circle(25, 25, {
  radius: 0.5,
  stroke: '#ffffff',
  fill: 'transparent'
});

console.log({
  roomVisible: Boolean(Game.rooms.W10N10)
});

The circle can be drawn from known coordinates without creating a live Room. Current Creeps, structures, Controller state, and other objects still require visibility.

Visual bytes and CPU are different budgets

MeasurementWhat it means
visual.getSize()Serialized visual bytes in that room during the current tick
512,000 bytesOfficial per-room visual limit for the current tick
Game.cpu.getUsed()CPU consumed so far in the current tick
function measureVisualCpu(draw) {
  const start = Game.cpu.getUsed();
  const result = draw();
  const end = Game.cpu.getUsed();

  return {
    result,
    cpu: Math.max(0, end - start)
  };
}

A visual can be below the byte limit and still cost too much CPU when thousands of objects, paths, labels, or lookups are generated.

Use a per-room debug configuration

function getVisualConfig(roomName) {
  const raw = Memory.visualDebug?.[roomName];

  if (!raw || raw.enabled !== true) {
    return null;
  }

  return {
    showLabels: raw.showLabels !== false,
    showTargets: raw.showTargets !== false,
    showEnergy: raw.showEnergy !== false,
    maximumItems:
      Number.isInteger(raw.maximumItems)
      && raw.maximumItems > 0
        ? raw.maximumItems
        : 30,
    maximumBytes:
      Number.isInteger(raw.maximumBytes)
      && raw.maximumBytes > 0
      && raw.maximumBytes <= 512000
        ? raw.maximumBytes
        : 480000
  };
}

The 480,000-byte fallback is a site policy that leaves margin below the official ceiling.

Build a pure visual plan

function buildCreepVisualPlan(input) {
  if (input.enabled !== true) {
    return {
      ready: false,
      reason: 'disabled',
      items: []
    };
  }

  if (!input.creep?.pos) {
    return {
      ready: false,
      reason: 'creep-missing',
      items: []
    };
  }

  const items = [{
    type: 'circle',
    x: input.creep.pos.x,
    y: input.creep.pos.y
  }];

  if (input.showLabels === true) {
    items.push({
      type: 'text',
      text: input.label,
      x: input.creep.pos.x,
      y: input.creep.pos.y - 0.75
    });
  }

  if (
    input.showTargets === true
    && input.target?.pos
    && input.target.pos.roomName
      === input.creep.pos.roomName
  ) {
    items.push({
      type: 'line',
      x1: input.creep.pos.x,
      y1: input.creep.pos.y,
      x2: input.target.pos.x,
      y2: input.target.pos.y
    });
  }

  return {
    ready: true,
    reason: 'ready',
    items
  };
}

A pure plan can be tested without the visual API. Rendering remains a separate step.

Sort before applying item limits

function selectVisualCreeps(creeps, maximumItems) {
  return [...creeps]
    .sort((left, right) =>
      left.name.localeCompare(right.name)
    )
    .slice(0, maximumItems);
}

Room find order is not a business priority. Stable sorting makes repeated diagnostic output easier to compare.

Complete bounded RoomVisual layer

State impact: this layer draws current-tick visuals and writes a compact summary to Memory. It does not change Creep tasks or target IDs.

function trimVisualLabel(value, maximumLength = 40) {
  const text = String(value);

  if (text.length <= maximumLength) {
    return text;
  }

  return text.slice(0, maximumLength - 3) + '...';
}

function drawCreepDebug(visual, creep, config) {
  if (visual.getSize() >= config.maximumBytes) {
    return 'byte-limit';
  }

  const energy = creep.store.getUsedCapacity(
    RESOURCE_ENERGY
  );

  if (config.showLabels) {
    const task = creep.memory?.task || 'no-task';
    const label = trimVisualLabel(
      creep.name + ' ' + task + ' ' + energy + 'E'
    );

    visual.text(
      label,
      creep.pos.x,
      creep.pos.y - 0.75,
      {
        color: '#ffffff',
        font: 0.45,
        opacity: 0.9,
        backgroundColor: '#111111',
        backgroundPadding: 0.15
      }
    );
  }

  visual.circle(creep.pos, {
    radius: 0.43,
    stroke: '#00ff88',
    strokeWidth: 0.08,
    fill: 'transparent',
    opacity: 0.75
  });

  if (!config.showTargets) {
    return 'drawn';
  }

  const targetId = creep.memory?.targetId;
  const target = targetId
    ? Game.getObjectById(targetId)
    : null;

  if (!target?.pos) {
    return targetId
      ? 'target-unavailable'
      : 'drawn';
  }

  if (target.pos.roomName !== creep.pos.roomName) {
    visual.text(
      trimVisualLabel('to ' + target.pos.roomName),
      creep.pos.x,
      creep.pos.y + 0.8,
      {
        color: '#ffaa00',
        font: 0.4
      }
    );
    return 'cross-room-target';
  }

  visual.line(creep.pos, target.pos, {
    color: '#ffaa00',
    width: 0.08,
    opacity: 0.55,
    lineStyle: 'dashed'
  });
  visual.circle(target.pos, {
    radius: 0.32,
    stroke: '#ffaa00',
    fill: 'transparent',
    opacity: 0.7
  });

  return 'drawn-with-target';
}

function drawRoomDebug(room) {
  const config = getVisualConfig(room.name);
  if (!config) {
    return {
      status: 'disabled',
      drawn: 0
    };
  }

  const visual = room.visual;
  const creeps = selectVisualCreeps(
    room.find(FIND_MY_CREEPS),
    config.maximumItems
  );
  const summary = {
    status: 'complete',
    drawn: 0,
    stoppedByBytes: false,
    statuses: {}
  };

  for (const creep of creeps) {
    if (visual.getSize() >= config.maximumBytes) {
      summary.stoppedByBytes = true;
      summary.status = 'byte-limit';
      break;
    }

    const status = drawCreepDebug(
      visual,
      creep,
      config
    );
    summary.drawn += 1;
    summary.statuses[status] =
      (summary.statuses[status] || 0) + 1;
  }

  Memory.visualDebug[room.name].lastSummary = {
    ...summary,
    bytes: visual.getSize(),
    tick: Game.time
  };

  return summary;
}

Handle missing and cross-room targets

StatusMeaning
drawnCreep drawn without a target relationship
drawn-with-targetSame-room target line drawn
target-unavailableAn ID exists but no current object can be resolved
cross-room-targetThe target is in another room; a label replaces an impossible cross-room line
byte-limitRendering stopped before adding more data

The debug layer must not delete a task merely because its target is currently unavailable. Target lifecycle belongs to role or task management.

Visuals do not prove action success

const moveResult = creep.moveTo(target);

creep.room.visual.text(
  'move=' + moveResult,
  creep.pos.x,
  creep.pos.y + 0.75,
  {
    color: moveResult === OK
      ? '#88ff88'
      : '#ff8888',
    font: 0.4
  }
);

Even this label proves only the returned code was displayed. Multi-tick position changes are needed to prove movement progress.

Export and import deliberately

function saveRoomVisual(room) {
  Memory.savedVisuals ??= {};
  Memory.savedVisuals[room.name] = {
    savedAt: Game.time,
    data: room.visual.export()
  };
}

function importRoomVisual(room, maxAge = 100) {
  const saved = Memory.savedVisuals?.[room.name];

  if (
    !saved
    || !Number.isInteger(saved.savedAt)
    || typeof saved.data !== 'string'
    || Game.time - saved.savedAt > maxAge
  ) {
    return false;
  }

  room.visual.import(saved.data);
  return true;
}

Exported strings consume persistent storage and can become misleading when the room changes. Store timestamps and expire old diagrams.

Debugging checklist

  • Require an explicit per-room enable switch.
  • Redraw or import each tick.
  • Do not treat drawing as visibility.
  • Measure bytes with getSize().
  • Measure CPU separately.
  • Sort objects before applying item limits.
  • Trim labels.
  • Stop below the 512,000-byte ceiling.
  • Do not draw one line across room boundaries.
  • Keep target lifecycle outside the visual layer.
  • Record action results and multi-tick outcomes separately.

Scope and next steps

This guide does not implement path heatmaps, terrain overlays, battle replays, Segment-backed visual archives, browser screenshots, or live CPU benchmarks. The visual layer remains a current-tick diagnostic tool.

Frequently asked questions

Can a RoomVisual be drawn in an invisible room?

Known coordinates can be drawn, but no live room data is granted.

Why use 480,000 instead of 512,000?

It is an example safety margin below the official ceiling.

Should visuals run every tick?

Only when the diagnostic value justifies their CPU and byte cost. Use switches and sampling.

Can export create permanent history automatically?

No. Your code must store, timestamp, expire, and re-import the string.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.