refactor: Optimize rendering with chunk canvas caching and performance improvements
This commit is contained in:
parent
d87105baad
commit
bb45c9fba8
44
js/main.js
44
js/main.js
@ -78,18 +78,44 @@ function simulationLoop(timestamp) {
|
|||||||
// Update physics with timestamp for rate limiting
|
// Update physics with timestamp for rate limiting
|
||||||
updatePhysics(timestamp);
|
updatePhysics(timestamp);
|
||||||
|
|
||||||
// Force stone layer chunks to be rendered every frame
|
|
||||||
const visibleChunks = getVisibleChunks();
|
|
||||||
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
|
|
||||||
if (isVisible && chunkY === 1) {
|
|
||||||
// Mark stone layer chunks as dirty to ensure they're always rendered
|
|
||||||
dirtyChunks.add(getChunkKey(chunkX, chunkY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
render();
|
render();
|
||||||
|
|
||||||
|
// Memory management: Clean up chunk cache for chunks that are far away
|
||||||
|
if (timestamp % 5000 < 16) { // Run every ~5 seconds
|
||||||
|
cleanupChunkCache();
|
||||||
|
}
|
||||||
|
|
||||||
// Continue the loop
|
// Continue the loop
|
||||||
requestAnimationFrame(simulationLoop);
|
requestAnimationFrame(simulationLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up chunk cache to prevent memory leaks
|
||||||
|
function cleanupChunkCache() {
|
||||||
|
if (!chunkCanvasCache) return;
|
||||||
|
|
||||||
|
const visibleChunks = getVisibleChunks();
|
||||||
|
const visibleKeys = new Set();
|
||||||
|
|
||||||
|
// Get all visible chunk keys
|
||||||
|
for (const { chunkX, chunkY } of visibleChunks) {
|
||||||
|
visibleKeys.add(getChunkKey(chunkX, chunkY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cached canvases for chunks that are far from view
|
||||||
|
for (const key of chunkCanvasCache.keys()) {
|
||||||
|
if (!visibleKeys.has(key)) {
|
||||||
|
// Keep stone layer chunks in cache longer
|
||||||
|
if (key.split(',')[1] === '1') {
|
||||||
|
// Only remove if it's really far away
|
||||||
|
const [chunkX, chunkY] = key.split(',').map(Number);
|
||||||
|
const centerChunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
|
||||||
|
if (Math.abs(chunkX - centerChunkX) > 10) {
|
||||||
|
chunkCanvasCache.delete(key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chunkCanvasCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
233
js/render.js
233
js/render.js
@ -1,4 +1,7 @@
|
|||||||
// Rendering functions
|
// Rendering functions
|
||||||
|
// Cache for rendered chunks
|
||||||
|
let chunkCanvasCache = new Map();
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
// Clear the canvas
|
// Clear the canvas
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
@ -13,20 +16,27 @@ function render() {
|
|||||||
|
|
||||||
const key = getChunkKey(chunkX, chunkY);
|
const key = getChunkKey(chunkX, chunkY);
|
||||||
|
|
||||||
// Always render stone layer (chunkY = 1) chunks, even if they're not dirty
|
|
||||||
// For other chunks, only render if they're dirty or the world moved
|
|
||||||
if (chunkY !== 1 && !dirtyChunks.has(key) && !worldMoved) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chunks.has(key)) continue;
|
if (!chunks.has(key)) continue;
|
||||||
|
|
||||||
const chunk = chunks.get(key);
|
|
||||||
|
|
||||||
// Calculate screen position of chunk
|
// Calculate screen position of chunk
|
||||||
const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE;
|
const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * PIXEL_SIZE;
|
||||||
const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE;
|
const screenY = (chunkY * CHUNK_SIZE - worldOffsetY) * PIXEL_SIZE;
|
||||||
|
|
||||||
|
// Check if we need to render this chunk
|
||||||
|
const needsRender = dirtyChunks.has(key) || worldMoved || !chunkCanvasCache.has(key);
|
||||||
|
|
||||||
|
// Always render the chunk if it's in the stone layer (for visibility)
|
||||||
|
// or if it needs rendering
|
||||||
|
if (needsRender) {
|
||||||
|
renderChunkToCache(chunkX, chunkY, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the cached chunk to the main canvas
|
||||||
|
const cachedCanvas = chunkCanvasCache.get(key);
|
||||||
|
if (cachedCanvas) {
|
||||||
|
ctx.drawImage(cachedCanvas, screenX, screenY);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw chunk border in debug mode
|
// Draw chunk border in debug mode
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
ctx.strokeStyle = '#ff0000';
|
ctx.strokeStyle = '#ff0000';
|
||||||
@ -44,100 +54,10 @@ function render() {
|
|||||||
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
|
ctx.fillText(`${chunkX},${chunkY}`, screenX + 5, screenY + 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render each pixel in the chunk
|
|
||||||
for (let y = 0; y < CHUNK_SIZE; y++) {
|
|
||||||
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
||||||
const index = y * CHUNK_SIZE + x;
|
|
||||||
const type = chunk[index];
|
|
||||||
|
|
||||||
// Always render stone layer even if it's not directly visible
|
|
||||||
if (type === EMPTY && chunkY !== 1) continue;
|
|
||||||
|
|
||||||
// For the stone layer (chunkY = 1), render a faint background even for empty spaces
|
|
||||||
if (type === EMPTY && chunkY === 1) {
|
|
||||||
// Use a very faint gray for empty spaces in the stone layer
|
|
||||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.2)';
|
|
||||||
ctx.fillRect(
|
|
||||||
screenX + x * PIXEL_SIZE,
|
|
||||||
screenY + y * PIXEL_SIZE,
|
|
||||||
PIXEL_SIZE,
|
|
||||||
PIXEL_SIZE
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set color based on type
|
|
||||||
if (type === SAND) {
|
|
||||||
ctx.fillStyle = SAND_COLOR;
|
|
||||||
} else if (type === WATER) {
|
|
||||||
// Get water color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = WATER_COLORS[colorIndex];
|
|
||||||
} else if (type === WALL) {
|
|
||||||
ctx.fillStyle = WALL_COLOR;
|
|
||||||
} else if (type === DIRT) {
|
|
||||||
// Get dirt color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = DIRT_COLORS[colorIndex];
|
|
||||||
} else if (type === STONE) {
|
|
||||||
// Get stone color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = STONE_COLORS[colorIndex];
|
|
||||||
} else if (type === GRASS) {
|
|
||||||
// Get grass color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = GRASS_COLORS[colorIndex];
|
|
||||||
} else if (type === WOOD) {
|
|
||||||
// Get wood color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = WOOD_COLORS[colorIndex];
|
|
||||||
} else if (type === SEED) {
|
|
||||||
ctx.fillStyle = SEED_COLOR;
|
|
||||||
} else if (type === GRASS_BLADE) {
|
|
||||||
// Use the same color variation as grass
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = GRASS_COLORS[colorIndex];
|
|
||||||
} else if (type === FLOWER) {
|
|
||||||
// Get flower color from metadata or use a default
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
ctx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0];
|
|
||||||
} else if (type === TREE_SEED) {
|
|
||||||
ctx.fillStyle = SEED_COLOR;
|
|
||||||
} else if (type === LEAF) {
|
|
||||||
// Get leaf color from metadata with variation
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = LEAF_COLORS[colorIndex];
|
|
||||||
} else if (type === FIRE) {
|
|
||||||
// Get fire color from metadata
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = FIRE_COLORS[colorIndex];
|
|
||||||
} else if (type === LAVA) {
|
|
||||||
// Get lava color from metadata
|
|
||||||
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
|
||||||
const colorIndex = metadata ? metadata.colorIndex : 0;
|
|
||||||
ctx.fillStyle = LAVA_COLORS[colorIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the pixel
|
|
||||||
ctx.fillRect(
|
|
||||||
screenX + x * PIXEL_SIZE,
|
|
||||||
screenY + y * PIXEL_SIZE,
|
|
||||||
PIXEL_SIZE,
|
|
||||||
PIXEL_SIZE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove this chunk from the dirty list after rendering
|
// Remove this chunk from the dirty list after rendering
|
||||||
dirtyChunks.delete(key);
|
if (dirtyChunks.has(key)) {
|
||||||
|
dirtyChunks.delete(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset world moved flag after rendering
|
// Reset world moved flag after rendering
|
||||||
@ -175,3 +95,114 @@ function render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render a chunk to an offscreen canvas and cache it
|
||||||
|
function renderChunkToCache(chunkX, chunkY, key) {
|
||||||
|
const chunk = chunks.get(key);
|
||||||
|
|
||||||
|
// Create a new canvas for this chunk if it doesn't exist
|
||||||
|
if (!chunkCanvasCache.has(key)) {
|
||||||
|
const chunkCanvas = document.createElement('canvas');
|
||||||
|
chunkCanvas.width = CHUNK_SIZE * PIXEL_SIZE;
|
||||||
|
chunkCanvas.height = CHUNK_SIZE * PIXEL_SIZE;
|
||||||
|
chunkCanvasCache.set(key, chunkCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkCanvas = chunkCanvasCache.get(key);
|
||||||
|
const chunkCtx = chunkCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Clear the chunk canvas
|
||||||
|
chunkCtx.clearRect(0, 0, chunkCanvas.width, chunkCanvas.height);
|
||||||
|
|
||||||
|
// Render each pixel in the chunk
|
||||||
|
for (let y = 0; y < CHUNK_SIZE; y++) {
|
||||||
|
for (let x = 0; x < CHUNK_SIZE; x++) {
|
||||||
|
const index = y * CHUNK_SIZE + x;
|
||||||
|
const type = chunk[index];
|
||||||
|
|
||||||
|
// Always render stone layer even if it's not directly visible
|
||||||
|
if (type === EMPTY && chunkY !== 1) continue;
|
||||||
|
|
||||||
|
// For the stone layer (chunkY = 1), render a faint background even for empty spaces
|
||||||
|
if (type === EMPTY && chunkY === 1) {
|
||||||
|
// Use a very faint gray for empty spaces in the stone layer
|
||||||
|
chunkCtx.fillStyle = 'rgba(100, 100, 100, 0.2)';
|
||||||
|
chunkCtx.fillRect(
|
||||||
|
x * PIXEL_SIZE,
|
||||||
|
y * PIXEL_SIZE,
|
||||||
|
PIXEL_SIZE,
|
||||||
|
PIXEL_SIZE
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set color based on type
|
||||||
|
if (type === SAND) {
|
||||||
|
chunkCtx.fillStyle = SAND_COLOR;
|
||||||
|
} else if (type === WATER) {
|
||||||
|
// Get water color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = WATER_COLORS[colorIndex];
|
||||||
|
} else if (type === WALL) {
|
||||||
|
chunkCtx.fillStyle = WALL_COLOR;
|
||||||
|
} else if (type === DIRT) {
|
||||||
|
// Get dirt color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = DIRT_COLORS[colorIndex];
|
||||||
|
} else if (type === STONE) {
|
||||||
|
// Get stone color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = STONE_COLORS[colorIndex];
|
||||||
|
} else if (type === GRASS) {
|
||||||
|
// Get grass color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = GRASS_COLORS[colorIndex];
|
||||||
|
} else if (type === WOOD) {
|
||||||
|
// Get wood color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = WOOD_COLORS[colorIndex];
|
||||||
|
} else if (type === SEED) {
|
||||||
|
chunkCtx.fillStyle = SEED_COLOR;
|
||||||
|
} else if (type === GRASS_BLADE) {
|
||||||
|
// Use the same color variation as grass
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = GRASS_COLORS[colorIndex];
|
||||||
|
} else if (type === FLOWER) {
|
||||||
|
// Get flower color from metadata or use a default
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
chunkCtx.fillStyle = metadata && metadata.color ? metadata.color : FLOWER_COLORS[0];
|
||||||
|
} else if (type === TREE_SEED) {
|
||||||
|
chunkCtx.fillStyle = SEED_COLOR;
|
||||||
|
} else if (type === LEAF) {
|
||||||
|
// Get leaf color from metadata with variation
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata && metadata.colorIndex !== undefined ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = LEAF_COLORS[colorIndex];
|
||||||
|
} else if (type === FIRE) {
|
||||||
|
// Get fire color from metadata
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = FIRE_COLORS[colorIndex];
|
||||||
|
} else if (type === LAVA) {
|
||||||
|
// Get lava color from metadata
|
||||||
|
const metadata = getMetadata(chunkX * CHUNK_SIZE + x, chunkY * CHUNK_SIZE + y);
|
||||||
|
const colorIndex = metadata ? metadata.colorIndex : 0;
|
||||||
|
chunkCtx.fillStyle = LAVA_COLORS[colorIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the pixel
|
||||||
|
chunkCtx.fillRect(
|
||||||
|
x * PIXEL_SIZE,
|
||||||
|
y * PIXEL_SIZE,
|
||||||
|
PIXEL_SIZE,
|
||||||
|
PIXEL_SIZE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -528,11 +528,11 @@ function generateChunksAroundPlayer() {
|
|||||||
const key = getChunkKey(chunkX, chunkY);
|
const key = getChunkKey(chunkX, chunkY);
|
||||||
|
|
||||||
// Always generate stone layer chunks
|
// Always generate stone layer chunks
|
||||||
|
const isNewChunk = !chunks.has(key);
|
||||||
getOrCreateChunk(chunkX, chunkY);
|
getOrCreateChunk(chunkX, chunkY);
|
||||||
|
|
||||||
// Mark as dirty only if it's a new chunk or if it's visible
|
// Mark as dirty only if it's a new chunk
|
||||||
if (!chunks.has(key) || visibleChunks.some(chunk =>
|
if (isNewChunk) {
|
||||||
chunk.chunkX === chunkX && chunk.chunkY === chunkY && chunk.isVisible)) {
|
|
||||||
dirtyChunks.add(key);
|
dirtyChunks.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user