2D游戏引擎子节点:兼顾数组迭代性能与键值访问的实现方案问询
Great question—game engine performance is non-negotiable, especially when you’re dealing with frequent node updates and tight rendering loops. Let’s walk through a solution that checks all your boxes: fast array-based iteration, clean add()/remove() APIs, no extra properties on child nodes, and avoids the overhead of syncing separate data structures unnecessarily.
Core Idea: Use a WeakMap for Reverse Mapping (No Extra Child Properties)
Instead of adding a renderingIdx property to child nodes, we’ll use a WeakMap to store the relationship between each child and its position in the shader-specific arrays. WeakMaps are perfect here because they don’t prevent garbage collection of unused nodes, and they keep child objects clean of engine-specific metadata.
We’ll also optimize the rendering loop to avoid Object.keys() by tracking active shader program UUIDs in an array—this keeps iteration as fast as possible.
Modified Scene Implementation
Here’s the updated code with these improvements:
export class Scene { constructor() { this.childrenByShaderProgram = {}; // Key: shader UUID, Value: array of children this.childMap = new WeakMap(); // Maps child -> { spUuid: string, index: number } this.activeSpUuids = []; // Tracks shader UUIDs with active children (for fast rendering) } add(child) { // Handle container nodes recursively if (child.children && child.children.length) { for (const nestedChild of child.children) { this.add(nestedChild); } return; } this._addChild(child); } remove(child) { // Handle container nodes recursively if (child.children && child.children.length) { for (const nestedChild of child.children) { this.remove(nestedChild); } return; } this._removeChild(child); } _addChild(child) { const spUuid = child.shaderProgram.uuid; // Check if child is already added if (this.childMap.has(child)) { throw new Error('Could not add child as it is already added to the scene'); } // Initialize the array for this shader if it doesn't exist if (!this.childrenByShaderProgram[spUuid]) { this.childrenByShaderProgram[spUuid] = []; this.activeSpUuids.push(spUuid); // Add to active list } const childrenArray = this.childrenByShaderProgram[spUuid]; const index = childrenArray.length; // Add child to the array and record its position in the WeakMap childrenArray.push(child); this.childMap.set(child, { spUuid, index }); } _removeChild(child) { // Get child's metadata from the WeakMap const childMeta = this.childMap.get(child); if (!childMeta) { throw new Error('Could not remove child which has not been added to the scene'); } const { spUuid, index } = childMeta; const childrenArray = this.childrenByShaderProgram[spUuid]; const lastIndex = childrenArray.length - 1; // Use swap-and-pop for O(1) deletion (far faster than slice/splice) if (index !== lastIndex) { // Swap the child to delete with the last element in the array const swappedChild = childrenArray[lastIndex]; childrenArray[index] = swappedChild; // Update the swapped child's index in the WeakMap this.childMap.set(swappedChild, { spUuid, index }); } // Remove the last element (now the one we wanted to delete) childrenArray.pop(); // Clean up if the array is now empty if (childrenArray.length === 0) { delete this.childrenByShaderProgram[spUuid]; // Remove the shader UUID from active list const uuidIndex = this.activeSpUuids.indexOf(spUuid); if (uuidIndex !== -1) { this.activeSpUuids.splice(uuidIndex, 1); } } // Remove the child from our WeakMap this.childMap.delete(child); } beforeUpdate(dt) {} updateEachChild(child, dt) { child.update(); } afterUpdate(dt) {} } export default Scene;
Optimized Rendering Loop
Update your _render method to use the activeSpUuids array instead of Object.keys()—this eliminates the overhead of object key iteration:
_render(...args) { const [gl, scene, camera] = args; const { childrenByShaderProgram, activeSpUuids } = scene; const dt = 0; gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); camera.updateViewMatrix(); scene.beforeUpdate(dt); // Iterate over active shader UUIDs (fast array iteration) for (let i = 0, len = activeSpUuids.length; i < len; i += 1) { const spUuid = activeSpUuids[i]; const children = childrenByShaderProgram[spUuid]; const sp = children[0].shaderProgram; this._useShaderProgram(gl, sp); sp.updateUniform('u_v', camera.viewMatrix); // Fast array iteration for children for (let j = 0, childLen = children.length; j < childLen; j += 1) { const child = children[j]; scene.updateEachChild(child, dt); sp.bindUniforms(gl); sp.bindAttributes(gl); gl.drawArrays(gl.TRIANGLE_STRIP, 0, Math.floor(child.vertices.length / 2)); } } scene.afterUpdate(dt); window.requestAnimationFrame(() => this._render(...args)); }
Why This Works Better
- No extra child properties: All metadata is stored in the Scene’s WeakMap, keeping child objects clean and decoupled from the engine.
- Blazing-fast iteration: We use pure array loops everywhere—no
Object.keys()orfor-inoverhead, which aligns with your performance requirements. - Efficient deletion: The swap-and-pop technique is O(1) (except for updating the swapped child’s index), which is way faster than creating new arrays with
slice()orsplice(). - Automatic cleanup: WeakMaps handle garbage collection of unused nodes, and we clean up empty shader arrays to keep memory usage lean.
- Clean API: Your
scene.add(child)andscene.remove(child)calls work exactly as expected, with no extra complexity.
Key Optimizations Over Your Previous Approaches
- Better than
renderingIdx: Avoids modifying child objects entirely, keeping your node code clean and reusable. - Better than syncing arrays + key-value pairs: Eliminates the need to manually sync two separate structures—all state is managed through the array + WeakMap combo, reducing bugs and overhead.
内容的提问来源于stack exchange,提问作者user4408933

