207 lines
No EOL
8.6 KiB
HTML
207 lines
No EOL
8.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Infinite Chunk System - Architecture Manual</title>
|
|
<style>
|
|
:root {
|
|
--bg-color: #f4f4f9;
|
|
--text-color: #333;
|
|
--sidebar-bg: #2c3e50;
|
|
--sidebar-text: #ecf0f1;
|
|
--link-color: #3498db;
|
|
--code-bg: #e2e2e8;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
line-height: 1.6;
|
|
}
|
|
/* Sidebar Styles */
|
|
nav {
|
|
width: 280px;
|
|
background-color: var(--sidebar-bg);
|
|
color: var(--sidebar-text);
|
|
height: 100vh;
|
|
position: fixed;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
}
|
|
nav h2 {
|
|
margin-top: 0;
|
|
font-size: 1.2rem;
|
|
border-bottom: 1px solid #455a64;
|
|
padding-bottom: 10px;
|
|
}
|
|
nav ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
nav ul li {
|
|
margin-bottom: 10px;
|
|
}
|
|
nav ul li a {
|
|
color: var(--sidebar-text);
|
|
text-decoration: none;
|
|
font-size: 0.95rem;
|
|
display: block;
|
|
padding: 5px;
|
|
border-radius: 4px;
|
|
transition: background 0.2s;
|
|
}
|
|
nav ul li a:hover {
|
|
background-color: #34495e;
|
|
}
|
|
/* Main Content Styles */
|
|
main {
|
|
margin-left: 280px;
|
|
padding: 40px;
|
|
max-width: 900px;
|
|
box-sizing: border-box;
|
|
}
|
|
section {
|
|
margin-bottom: 50px;
|
|
background: #fff;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
}
|
|
h1 {
|
|
border-bottom: 2px solid var(--link-color);
|
|
padding-bottom: 10px;
|
|
margin-top: 0;
|
|
}
|
|
h2 {
|
|
color: #2c3e50;
|
|
margin-top: 0;
|
|
}
|
|
h3 {
|
|
color: #34495e;
|
|
}
|
|
code {
|
|
background-color: var(--code-bg);
|
|
padding: 2px 5px;
|
|
border-radius: 4px;
|
|
font-family: "Courier New", Courier, monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
pre {
|
|
background-color: #2d2d2d;
|
|
color: #ccc;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
}
|
|
pre code {
|
|
background-color: transparent;
|
|
color: inherit;
|
|
padding: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav>
|
|
<h2>Chunk System Manual</h2>
|
|
<ul>
|
|
<li><a href="#react-pixi-bridge">1. The React-Pixi Bridge</a></li>
|
|
<li><a href="#chunk-management">2. Managing the Infinite Grid</a></li>
|
|
<li><a href="#object-pooling">3. Rendering & Object Pools</a></li>
|
|
<li><a href="#plugin-system">4. Plugins for an Infinite Grid</a></li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<main>
|
|
<h1>Infinite Chunk & Integration Architecture</h1>
|
|
<p>A focused guide on coordinating a React-based UI with a PixiJS rendering engine to manage and render an infinite 32x32 grid-based world, alongside a lifecycle-hooked plugin system.</p>
|
|
|
|
<section id="react-pixi-bridge">
|
|
<h2>1. The React-Pixi Bridge: Separating UI State from Engine State</h2>
|
|
<p>To keep things modular and performant, PixiJS and React must be treated as two completely separate applications that talk across a bridge.</p>
|
|
<ul>
|
|
<li><strong>The Engine (PixiJS)</strong> runs on a <code>requestAnimationFrame</code> loop. It controls the game loop and renders the canvas.</li>
|
|
<li><strong>The Editor UI (React)</strong> runs on state updates. It should <em>not</em> try to control or re-render Pixi objects directly via React state, as that will tank performance.</li>
|
|
</ul>
|
|
|
|
<h3>The Communication Model</h3>
|
|
<p>Use a lightweight <strong>Event Bus</strong> or a <strong>Command Pattern</strong> to bridge the gap.</p>
|
|
<ul>
|
|
<li>When a user selects a tile in the React Asset Browser and clicks on the Pixi Canvas, React emits a global event: <code>EDITOR_BRUSH_CHANGED { type: 'grass_tile' }</code>.</li>
|
|
<li>The Pixi-based editor controller listens to this event, updates its internal brush state, and handles the actual vertex manipulation on the canvas.</li>
|
|
<li>Conversely, when Pixi updates the camera position, it emits a throttled <code>CAMERA_MOVED { x, y }</code> event so React can update the coordinate numbers in the status bar.</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section id="chunk-management">
|
|
<h2>2. Managing the Infinite Grid (32x32 Chunks)</h2>
|
|
<p>In an infinite world, you cannot keep every tile in memory. You need a <strong>Chunk Manager</strong> that dynamically loads and unloads data based on where the camera is looking.</p>
|
|
|
|
<h3>Data Structure: Hash Map of Coordinates</h3>
|
|
<p>Instead of a giant multi-dimensional array, store your chunks in a standard JavaScript <code>Map</code> using a string coordinate hash as the key:</p>
|
|
<pre><code>// Key format: "chunkX,chunkY" -> e.g., "0,0", "-1,4"
|
|
const chunkMap = new Map();
|
|
|
|
function getChunkKey(worldX, worldY) {
|
|
const chunkX = Math.floor(worldX / (32 * 32)); // 32 tiles * 32 pixels
|
|
const chunkY = Math.floor(worldY / (32 * 32));
|
|
return `${chunkX},${chunkY}`;
|
|
}
|
|
</code></pre>
|
|
</section>
|
|
|
|
<section id="object-pooling">
|
|
<h2>3. Rendering: PixiJS Container Pools</h2>
|
|
<p>Do not create and destroy Pixi <code>Container</code> or <code>Sprite</code> objects every time a chunk moves out of view. That will trigger the JavaScript Garbage Collector and cause massive frame drops.</p>
|
|
|
|
<p>Instead, use an <strong>Object Pool</strong>:</p>
|
|
<ol>
|
|
<li>Create a fixed number of Pixi <code>Container</code> objects (enough to cover the screen plus a 1-chunk padding buffer).</li>
|
|
<li>When a chunk leaves the screen, <em>do not delete it</em>. Instead, clear its children, return the container to the pool, and save its raw tile data (just integers representing tile IDs) back to your JavaScript <code>chunkMap</code>.</li>
|
|
<li>When a new chunk enters the screen, grab an idle container from the pool, populate it with the correct sprites from your Pixi Texture Atlas, and reposition it.</li>
|
|
</ol>
|
|
</section>
|
|
|
|
<section id="plugin-system">
|
|
<h2>4. Designing the "Pop-In" Plugin System for an Infinite Grid</h2>
|
|
<p>To make libraries "pop-in" seamlessly to an infinite chunk system, your plugins need to hook directly into the <strong>Chunk Lifecycle</strong>. Your plugin interface should expose events that trigger when chunks are created, loaded, simulated, or saved.</p>
|
|
|
|
<h3>Example: A Procedural Generation Plugin</h3>
|
|
<p>Imagine you want to "pop in" a Perlin Noise terrain generator library. The plugin code would look like this:</p>
|
|
<pre><code>// BiomeGeneratorPlugin.js
|
|
export const plugin = {
|
|
id: "procedural-biomes",
|
|
|
|
init(engine) {
|
|
// Register hooks into the engine's chunk lifecycle
|
|
engine.chunks.on('chunk_created', (chunk) => {
|
|
this.generateTerrain(chunk);
|
|
});
|
|
},
|
|
|
|
generateTerrain(chunk) {
|
|
// chunk.x and chunk.y tell us where we are in the infinite world
|
|
for (let x = 0; x < 32; x++) {
|
|
for (let y = 0; y < 32; y++) {
|
|
const globalX = (chunk.x * 32) + x;
|
|
const globalY = (chunk.y * 32) + y;
|
|
|
|
// Determine tile type using your library's math
|
|
const tileId = noise(globalX * 0.05, globalY * 0.05) > 0.5 ? 1 : 2;
|
|
chunk.setTile(x, y, tileId);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</code></pre>
|
|
<p>Because the Core Engine simply fires a <code>chunk_created</code> event every time the camera moves into ungenerated territory, any plugin can listen to that event, modify the raw tile data array, and let PixiJS handle the rendering automatically. The editor can expose toggle switches in React to enable or disable these loaded generation plugins on the fly.</p>
|
|
</section>
|
|
</main>
|
|
|
|
</body>
|
|
</html> |