All files / utils particlePool.ts

80.23% Statements 69/86
70.73% Branches 29/41
88.88% Functions 16/18
81.01% Lines 64/79

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333                                                                                                                                            20x       20x               20x 20x 20x   20x                                           27x   27x 2x 2x 2x       2x 2x 1x 1x 1x 1x       1x     2x           2x       25x 24x 24x 24x         24x 24x   24x             24x   24x           24x       1x 2x   1x 1x 1x     1x 1x                     1x           1x                       9x 8x 7x   7x         1x                               6x 8x 7x                               19x 16x                                   21x 22x 22x     22x     21x   21x                       2x 5x   2x 2x 2x     2x       5x   2x                                           1x          
/**
 * ParticlePool - Object pooling system for Three.js particle systems
 *
 * Provides efficient memory management for particle effects by reusing
 * Three.js Points objects instead of creating/destroying them repeatedly.
 * Essential for maintaining 60fps during intense combat with many effects.
 *
 * Features:
 * - Object pooling to avoid GC pressure
 * - Automatic pool size management
 * - Lifetime tracking for automatic cleanup
 * - Memory-efficient particle reuse
 *
 * @module utils/particlePool
 * @category Performance
 * @korean 입자풀
 */
 
import * as THREE from "three";
 
/**
 * Pooled particle system instance
 */
interface PooledParticleSystem {
  /** The Three.js Points object */
  points: THREE.Points;
  /** Whether this system is currently in use */
  active: boolean;
  /** How long this system has been active (seconds) */
  lifetime: number;
  /** Unique identifier for tracking */
  id: string;
}
 
/**
 * Configuration for particle pool
 */
export interface ParticlePoolConfig {
  /** Maximum number of particle systems to pool */
  readonly maxSize?: number;
  /** Maximum particles per system */
  readonly particlesPerSystem?: number;
  /** Whether to enable debug logging */
  readonly debug?: boolean;
}
 
/**
 * ParticlePool Class
 *
 * Manages a pool of reusable Three.js Points objects for particle effects.
 * Reduces garbage collection pressure by reusing existing objects.
 *
 * @example
 * ```typescript
 * const pool = new ParticlePool({ maxSize: 50, particlesPerSystem: 100 });
 *
 * // Acquire a particle system
 * const material = new THREE.PointsMaterial({ color: 0xff0000, size: 0.1 });
 * const points = pool.acquire(100, material);
 * scene.add(points);
 *
 * // Later, when effect completes
 * scene.remove(points);
 * pool.release(points);
 *
 * // Cleanup when done
 * pool.dispose();
 * ```
 */
export class ParticlePool {
  private pool: PooledParticleSystem[] = [];
  private readonly maxSize: number;
  private readonly particlesPerSystem: number;
  private readonly debug: boolean;
  private nextId = 0;
 
  /**
   * Create a new particle pool
   *
   * @param config - Pool configuration
   */
  constructor(config: ParticlePoolConfig = {}) {
    this.maxSize = config.maxSize ?? 50;
    this.particlesPerSystem = config.particlesPerSystem ?? 200;
    this.debug = config.debug ?? false;
 
    Iif (this.debug) {
      console.log(
        `[ParticlePool] Created with maxSize=${this.maxSize}, particlesPerSystem=${this.particlesPerSystem}`
      );
    }
  }
 
  /**
   * Acquire a particle system from the pool
   *
   * Returns an existing inactive system if available, otherwise creates a new one.
   * If pool is full, reuses the oldest active system.
   *
   * @param particleCount - Number of particles needed
   * @param material - Material to use for the particles
   * @returns A Three.js Points object ready to use
   */
  acquire(
    particleCount: number,
    material: THREE.PointsMaterial
  ): THREE.Points {
    // Find inactive system
    const inactive = this.pool.find((p) => !p.active);
 
    if (inactive) {
      inactive.active = true;
      inactive.lifetime = 0;
      inactive.points.material = material;
 
      // Resize geometry if needed
      const currentSize =
        inactive.points.geometry.attributes.position.count;
      if (currentSize !== particleCount) {
        inactive.points.geometry.dispose();
        const positions = new Float32Array(particleCount * 3);
        const geometry = new THREE.BufferGeometry();
        geometry.setAttribute(
          "position",
          new THREE.BufferAttribute(positions, 3)
        );
        inactive.points.geometry = geometry;
      }
 
      Iif (this.debug) {
        console.log(
          `[ParticlePool] Acquired inactive system ${inactive.id}`
        );
      }
 
      return inactive.points;
    }
 
    // Create new if pool not full
    if (this.pool.length < this.maxSize) {
      const geometry = new THREE.BufferGeometry();
      const positions = new Float32Array(particleCount * 3);
      geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positions, 3)
      );
 
      const points = new THREE.Points(geometry, material);
      const id = `particle-system-${this.nextId++}`;
 
      const pooled: PooledParticleSystem = {
        points,
        active: true,
        lifetime: 0,
        id,
      };
 
      this.pool.push(pooled);
 
      Iif (this.debug) {
        console.log(
          `[ParticlePool] Created new system ${id} (pool size: ${this.pool.length}/${this.maxSize})`
        );
      }
 
      return points;
    }
 
    // Pool is full, reuse oldest active system
    const oldest = this.pool.reduce((prev, curr) =>
      curr.lifetime > prev.lifetime ? curr : prev
    );
    oldest.active = true;
    oldest.lifetime = 0;
    oldest.points.material = material;
 
    // Resize geometry if needed
    const currentSize = oldest.points.geometry.attributes.position.count;
    Iif (currentSize !== particleCount) {
      oldest.points.geometry.dispose();
      const positions = new Float32Array(particleCount * 3);
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positions, 3)
      );
      oldest.points.geometry = geometry;
    }
 
    Iif (this.debug) {
      console.warn(
        `[ParticlePool] Pool full! Reusing oldest system ${oldest.id} (lifetime: ${oldest.lifetime.toFixed(1)}s)`
      );
    }
 
    return oldest.points;
  }
 
  /**
   * Release a particle system back to the pool
   *
   * Marks the system as inactive so it can be reused. The material
   * is NOT disposed as it may be shared or reused.
   *
   * @param points - The Points object to release
   */
  release(points: THREE.Points): void {
    const pooled = this.pool.find((p) => p.points === points);
    if (pooled) {
      pooled.active = false;
 
      Iif (this.debug) {
        console.log(
          `[ParticlePool] Released system ${pooled.id} (lifetime: ${pooled.lifetime.toFixed(1)}s)`
        );
      }
    I} else if (this.debug) {
      console.warn(
        "[ParticlePool] Attempted to release Points object not in pool"
      );
    }
  }
 
  /**
   * Update all active particle systems
   *
   * Call this once per frame to track lifetimes. Useful for debugging
   * and automatic cleanup.
   *
   * @param delta - Time elapsed since last update (seconds)
   */
  update(delta: number): void {
    this.pool.forEach((p) => {
      if (p.active) {
        p.lifetime += delta;
      }
    });
  }
 
  /**
   * Get pool statistics
   *
   * @returns Object containing pool stats
   */
  getStats(): {
    total: number;
    active: number;
    inactive: number;
    maxSize: number;
  } {
    const active = this.pool.filter((p) => p.active).length;
    return {
      total: this.pool.length,
      active,
      inactive: this.pool.length - active,
      maxSize: this.maxSize,
    };
  }
 
  /**
   * Dispose all pooled particle systems
   *
   * Cleans up all geometries and materials. Call this when the pool
   * is no longer needed.
   *
   * ⚠️ Warning: Materials are disposed! If materials are shared between
   * multiple particle systems outside the pool, dispose them separately.
   */
  dispose(): void {
    this.pool.forEach((p) => {
      p.points.geometry.dispose();
      Iif (Array.isArray(p.points.material)) {
        p.points.material.forEach((m) => m.dispose());
      } else {
        p.points.material.dispose();
      }
    });
    this.pool = [];
 
    Iif (this.debug) {
      console.log("[ParticlePool] Disposed all particle systems");
    }
  }
 
  /**
   * Clear inactive particle systems
   *
   * Removes inactive systems from the pool to free memory.
   * Useful for managing memory in long-running sessions.
   */
  clearInactive(): void {
    const beforeCount = this.pool.length;
    const toRemove = this.pool.filter((p) => !p.active);
 
    toRemove.forEach((p) => {
      p.points.geometry.dispose();
      Iif (Array.isArray(p.points.material)) {
        p.points.material.forEach((m) => m.dispose());
      } else {
        p.points.material.dispose();
      }
    });
 
    this.pool = this.pool.filter((p) => p.active);
 
    Iif (this.debug) {
      console.log(
        `[ParticlePool] Cleared ${beforeCount - this.pool.length} inactive systems`
      );
    }
  }
}
 
/**
 * Global particle pool instance
 *
 * Shared pool for all particle effects in the application.
 * Use this for most cases unless you need a separate pool.
 *
 * @example
 * ```typescript
 * import { globalParticlePool } from '@/utils/particlePool';
 *
 * const material = new THREE.PointsMaterial({ color: 0xff0000 });
 * const points = globalParticlePool.acquire(100, material);
 * ```
 */
export const globalParticlePool = new ParticlePool({
  maxSize: 50,
  particlesPerSystem: 200,
  debug: process.env.NODE_ENV === "development",
});