1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-09 02:54:01 +00:00
tobspr_shapez.io/src/js/game/map_view.js
PFedak 6f56d77535
Aggregate map chunks in overlay. (#1247)
Overlay rendering performance seemed bottlenecked by drawImage calls. To
reduce both the number of calls and the number of different source
buffers, cache overlay buffers for squares of chunks. This adds a very
small extra cost for updates (one additional drawImage) and some cost
for drawing chunks outside of view, but this is more than made up for by
the savings.

By default, the aggregate are 4x4 squares of chunks.
2021-08-25 13:04:52 +02:00

244 lines
8.8 KiB
JavaScript

import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters";
import { BaseMap } from "./map";
import { freeCanvas, makeOffscreenBuffer } from "../core/buffer_utils";
import { Entity } from "./entity";
import { THEME } from "./theme";
import { MapChunkView } from "./map_chunk_view";
import { MapChunkAggregate } from "./map_chunk_aggregate";
/**
* This is the view of the map, it extends the map which is the raw model and allows
* to draw it
*/
export class MapView extends BaseMap {
constructor(root) {
super(root);
/**
* DPI of the background cache images, required in some places
*/
this.backgroundCacheDPI = 2;
/**
* The cached background sprite, containing the flat background
* @type {HTMLCanvasElement} */
this.cachedBackgroundCanvas = null;
/** @type {CanvasRenderingContext2D} */
this.cachedBackgroundContext = null;
this.internalInitializeCachedBackgroundCanvases();
this.root.signals.aboutToDestruct.add(this.cleanup, this);
this.root.signals.entityAdded.add(this.onEntityChanged, this);
this.root.signals.entityDestroyed.add(this.onEntityChanged, this);
this.root.signals.entityChanged.add(this.onEntityChanged, this);
}
cleanup() {
freeCanvas(this.cachedBackgroundCanvas);
this.cachedBackgroundCanvas = null;
}
/**
* Called when an entity was added, removed or changed
* @param {Entity} entity
*/
onEntityChanged(entity) {
const staticComp = entity.components.StaticMapEntity;
if (staticComp) {
const rect = staticComp.getTileSpaceBounds();
for (let x = rect.x; x <= rect.right(); ++x) {
for (let y = rect.y; y <= rect.bottom(); ++y) {
this.root.map.getOrCreateChunkAtTile(x, y).markDirty();
}
}
}
}
/**
* Draws all static entities like buildings etc.
* @param {DrawParameters} drawParameters
*/
drawStaticEntityDebugOverlays(drawParameters) {
if (G_IS_DEV && (globalConfig.debug.showAcceptorEjectors || globalConfig.debug.showEntityBounds)) {
const cullRange = drawParameters.visibleRect.toTileCullRectangle();
const top = cullRange.top();
const right = cullRange.right();
const bottom = cullRange.bottom();
const left = cullRange.left();
const border = 1;
const minY = top - border;
const maxY = bottom + border;
const minX = left - border;
const maxX = right + border - 1;
// Render y from top down for proper blending
for (let y = minY; y <= maxY; ++y) {
for (let x = minX; x <= maxX; ++x) {
// const content = this.tiles[x][y];
const chunk = this.getChunkAtTileOrNull(x, y);
if (!chunk) {
continue;
}
const content = chunk.getTileContentFromWorldCoords(x, y);
if (content) {
let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1;
if (!isBorder) {
content.drawDebugOverlays(drawParameters);
}
}
}
}
}
}
/**
* Initializes all canvases used for background rendering
*/
internalInitializeCachedBackgroundCanvases() {
// Background canvas
const dims = globalConfig.tileSize;
const dpi = this.backgroundCacheDPI;
const [canvas, context] = makeOffscreenBuffer(dims * dpi, dims * dpi, {
smooth: false,
label: "map-cached-bg",
});
context.scale(dpi, dpi);
context.fillStyle = THEME.map.background;
context.fillRect(0, 0, dims, dims);
const borderWidth = THEME.map.gridLineWidth;
context.fillStyle = THEME.map.grid;
context.fillRect(0, 0, dims, borderWidth);
context.fillRect(0, borderWidth, borderWidth, dims);
context.fillRect(dims - borderWidth, borderWidth, borderWidth, dims - 2 * borderWidth);
context.fillRect(borderWidth, dims - borderWidth, dims, borderWidth);
this.cachedBackgroundCanvas = canvas;
this.cachedBackgroundContext = context;
}
/**
* Draws the maps foreground
* @param {DrawParameters} parameters
*/
drawForeground(parameters) {
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawForegroundDynamicLayer);
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawForegroundStaticLayer);
}
/**
* Calls a given method on all given chunks
* @param {DrawParameters} parameters
* @param {function} method
*/
drawVisibleChunks(parameters, method) {
const cullRange = parameters.visibleRect.allScaled(1 / globalConfig.tileSize);
const top = cullRange.top();
const right = cullRange.right();
const bottom = cullRange.bottom();
const left = cullRange.left();
const border = 0;
const minY = top - border;
const maxY = bottom + border;
const minX = left - border;
const maxX = right + border;
const chunkStartX = Math.floor(minX / globalConfig.mapChunkSize);
const chunkStartY = Math.floor(minY / globalConfig.mapChunkSize);
const chunkEndX = Math.floor(maxX / globalConfig.mapChunkSize);
const chunkEndY = Math.floor(maxY / globalConfig.mapChunkSize);
// Render y from top down for proper blending
for (let chunkX = chunkStartX; chunkX <= chunkEndX; ++chunkX) {
for (let chunkY = chunkStartY; chunkY <= chunkEndY; ++chunkY) {
const chunk = this.root.map.getChunk(chunkX, chunkY, true);
method.call(chunk, parameters);
}
}
}
/**
* Calls a given method on all given chunks
* @param {DrawParameters} parameters
* @param {function} method
*/
drawVisibleAggregates(parameters, method) {
const cullRange = parameters.visibleRect.allScaled(1 / globalConfig.tileSize);
const top = cullRange.top();
const right = cullRange.right();
const bottom = cullRange.bottom();
const left = cullRange.left();
const border = 0;
const minY = top - border;
const maxY = bottom + border;
const minX = left - border;
const maxX = right + border;
const aggregateTiles = globalConfig.chunkAggregateSize * globalConfig.mapChunkSize;
const aggStartX = Math.floor(minX / aggregateTiles);
const aggStartY = Math.floor(minY / aggregateTiles);
const aggEndX = Math.floor(maxX / aggregateTiles);
const aggEndY = Math.floor(maxY / aggregateTiles);
// Render y from top down for proper blending
for (let aggX = aggStartX; aggX <= aggEndX; ++aggX) {
for (let aggY = aggStartY; aggY <= aggEndY; ++aggY) {
const aggregate = this.root.map.getAggregate(aggX, aggY, true);
method.call(aggregate, parameters);
}
}
}
/**
* Draws the wires foreground
* @param {DrawParameters} parameters
*/
drawWiresForegroundLayer(parameters) {
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawWiresForegroundLayer);
}
/**
* Draws the map overlay
* @param {DrawParameters} parameters
*/
drawOverlay(parameters) {
this.drawVisibleAggregates(parameters, MapChunkAggregate.prototype.drawOverlay);
}
/**
* Draws the map background
* @param {DrawParameters} parameters
*/
drawBackground(parameters) {
// Render tile grid
if (!this.root.app.settings.getAllSettings().disableTileGrid || !this.root.gameMode.hasResources()) {
const dpi = this.backgroundCacheDPI;
parameters.context.scale(1 / dpi, 1 / dpi);
parameters.context.fillStyle = parameters.context.createPattern(
this.cachedBackgroundCanvas,
"repeat"
);
parameters.context.fillRect(
parameters.visibleRect.x * dpi,
parameters.visibleRect.y * dpi,
parameters.visibleRect.w * dpi,
parameters.visibleRect.h * dpi
);
parameters.context.scale(dpi, dpi);
}
this.drawVisibleChunks(parameters, MapChunkView.prototype.drawBackgroundLayer);
}
}