如何用PIXI.js复刻agar.io随玩家移动的滚动网格背景(高效版)
Hey there! I’ve messed around with recreating agar.io-style scrolling grids in PIXI.js before, and the key to doing it efficiently is only rendering the grid lines that are actually in (or just slightly outside) the viewport—no wasting resources drawing lines that players can’t see. Let me walk you through the best way to pull this off:
Core Concept
Agar.io’s grid scrolls with the player because the camera follows the player’s position. Instead of drawing an infinite grid (which is impossible and terrible for performance), we calculate exactly which grid lines fall within the current viewport (plus a small buffer to prevent edge flickering) and only render those lines. We’ll update this set of lines as the player moves.
Step-by-Step Implementation
Let’s break this down into working code:
1. Set Up PIXI.js & Basic Player
First, initialize the PIXI app and create a simple player object to simulate movement:
const app = new PIXI.Application({ width: window.innerWidth, height: window.innerHeight, backgroundColor: 0x1a1a1a }); document.body.appendChild(app.view); // Player sprite (simulate agar.io's player blob) const player = new PIXI.Graphics(); player.beginFill(0xff0000); player.drawCircle(0, 0, 20); player.endFill(); player.position.set(app.screen.width / 2, app.screen.height / 2); app.stage.addChild(player); // Grid configuration const gridSize = 32; // Spacing between grid lines const gridColor = 0x333333; const gridThickness = 1; // Store active grid lines to avoid recreating everything every frame let horizontalLines = []; let verticalLines = [];
2. Build the Grid Update Function
This function calculates the visible grid range, cleans up lines that are no longer in view, and creates new lines for the newly visible area:
function updateGrid() { // Make the camera follow the player app.stage.pivot.copy(player.position); app.stage.position.set(app.screen.width / 2, app.screen.height / 2); // Calculate viewport bounds (add a grid-sized buffer to prevent edge flicker) const halfViewportWidth = app.screen.width / 2; const halfViewportHeight = app.screen.height / 2; const viewportLeft = player.x - halfViewportWidth - gridSize; const viewportRight = player.x + halfViewportWidth + gridSize; const viewportTop = player.y - halfViewportHeight - gridSize; const viewportBottom = player.y + halfViewportHeight + gridSize; // --- Handle horizontal lines --- // Calculate the range of Y positions we need to render const startY = Math.floor(viewportTop / gridSize) * gridSize; const endY = Math.ceil(viewportBottom / gridSize) * gridSize; const neededYPositions = new Set(); for (let y = startY; y <= endY; y += gridSize) { neededYPositions.add(y); } // Clean up old lines that are no longer needed const updatedHorizontalLines = []; for (const line of horizontalLines) { if (neededYPositions.has(line.yMarker)) { updatedHorizontalLines.push(line); neededYPositions.delete(line.yMarker); } else { app.stage.removeChild(line); line.destroy(); } } // Create new lines for missing Y positions for (const y of neededYPositions) { const line = new PIXI.Graphics(); line.lineStyle(gridThickness, gridColor); line.moveTo(viewportLeft, y); line.lineTo(viewportRight, y); line.yMarker = y; // Mark the line's Y position for future checks app.stage.addChild(line); updatedHorizontalLines.push(line); } horizontalLines = updatedHorizontalLines; // --- Handle vertical lines --- const startX = Math.floor(viewportLeft / gridSize) * gridSize; const endX = Math.ceil(viewportRight / gridSize) * gridSize; const neededXPositions = new Set(); for (let x = startX; x <= endX; x += gridSize) { neededXPositions.add(x); } const updatedVerticalLines = []; for (const line of verticalLines) { if (neededXPositions.has(line.xMarker)) { updatedVerticalLines.push(line); neededXPositions.delete(line.xMarker); } else { app.stage.removeChild(line); line.destroy(); } } for (const x of neededXPositions) { const line = new PIXI.Graphics(); line.lineStyle(gridThickness, gridColor); line.moveTo(x, viewportTop); line.lineTo(x, viewportBottom); line.xMarker = x; // Mark the line's X position for future checks app.stage.addChild(line); updatedVerticalLines.push(line); } verticalLines = updatedVerticalLines; }
3. Add Player Movement & Grid Updates
Add keyboard controls for the player, and trigger grid updates as the player moves:
// Player movement speed const moveSpeed = 5; window.addEventListener('keydown', (e) => { switch(e.key) { case 'ArrowUp': player.y -= moveSpeed; break; case 'ArrowDown': player.y += moveSpeed; break; case 'ArrowLeft': player.x -= moveSpeed; break; case 'ArrowRight': player.x += moveSpeed; break; } }); // Update grid every frame (we'll optimize this next!) app.ticker.add(updateGrid);
Performance Optimization Tips
To make this even smoother (especially for large viewports or high frame rates), try these tweaks:
- Reduce Update Frequency: You don’t need to update the grid every frame. Only call
updateGrid()when the player has moved more than half thegridSizedistance. Track the player’s last position and compare it to the current position to trigger updates only when necessary. - Use an Object Pool: Instead of destroying unused lines, store them in a pool and reuse them when new lines are needed. This cuts down on garbage collection (GC) pauses, which is critical for smooth performance.
- Cache Line Styles: If your grid style never changes, use
PIXI.LineStyleCacheto avoid re-setting the line style every time you create a new line. - Batch Rendering: For even better performance, use a single
PIXI.Graphicsobject to draw all grid lines at once, or usePIXI.Meshfor batch rendering. This reduces the number of draw calls PIXI has to process.
Final Result
This implementation will give you a smooth, scrolling grid that only renders what’s visible—just like agar.io. It’s lightweight, efficient, and avoids wasting resources on off-screen content.
内容的提问来源于stack exchange,提问作者eeze

