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
| Measurement | What it means |
|---|---|
visual.getSize() | Serialized visual bytes in that room during the current tick |
| 512,000 bytes | Official 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
| Status | Meaning |
|---|---|
drawn | Creep drawn without a target relationship |
drawn-with-target | Same-room target line drawn |
target-unavailable | An ID exists but no current object can be resolved |
cross-room-target | The target is in another room; a label replaces an impossible cross-room line |
byte-limit | Rendering 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.