diff --git a/js/main.js b/js/main.js index 43865b0..311d7ff 100644 --- a/js/main.js +++ b/js/main.js @@ -78,18 +78,44 @@ function simulationLoop(timestamp) { // Update physics with timestamp for rate limiting 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(); + // Memory management: Clean up chunk cache for chunks that are far away + if (timestamp % 5000 < 16) { // Run every ~5 seconds + cleanupChunkCache(); + } + // Continue the loop 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); + } + } + } +} diff --git a/js/render.js b/js/render.js index 935301b..2488a25 100644 --- a/js/render.js +++ b/js/render.js @@ -1,4 +1,7 @@ // Rendering functions +// Cache for rendered chunks +let chunkCanvasCache = new Map(); + function render() { // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -13,20 +16,27 @@ function render() { 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; - const chunk = chunks.get(key); - // Calculate screen position of chunk const screenX = (chunkX * CHUNK_SIZE - worldOffsetX) * 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 if (debugMode) { ctx.strokeStyle = '#ff0000'; @@ -44,100 +54,10 @@ function render() { 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 - dirtyChunks.delete(key); + if (dirtyChunks.has(key)) { + dirtyChunks.delete(key); + } } // 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 + ); + } + } +} diff --git a/js/world.js b/js/world.js index 9a30b8d..c7c320c 100644 --- a/js/world.js +++ b/js/world.js @@ -528,11 +528,11 @@ function generateChunksAroundPlayer() { const key = getChunkKey(chunkX, chunkY); // Always generate stone layer chunks + const isNewChunk = !chunks.has(key); getOrCreateChunk(chunkX, chunkY); - // Mark as dirty only if it's a new chunk or if it's visible - if (!chunks.has(key) || visibleChunks.some(chunk => - chunk.chunkX === chunkX && chunk.chunkY === chunkY && chunk.isVisible)) { + // Mark as dirty only if it's a new chunk + if (isNewChunk) { dirtyChunks.add(key); } }