// No direct THREE.js import needed /** * Crowd Simulation with Collision Avoidance for Villagers * * This implementation uses a combination of steering behaviors and spatial partitioning * to efficiently manage crowd simulation with collision avoidance for multiple villagers. */ class CrowdManager { constructor() { this.villagers = []; this.spatialGrid = new SpatialGrid(100, 100, 5); // 100x100 grid with 5 unit cells } /** * Add a villager to the crowd simulation * @param {Villager} villager - The villager to add */ addVillager(villager) { this.villagers.push(villager); this.spatialGrid.insert(villager); } /** * Update all villagers in the crowd simulation * @param {number} deltaTime - Time since last frame */ update(deltaTime) { // Update spatial grid this.spatialGrid.clear(); for (const villager of this.villagers) { this.spatialGrid.insert(villager); } // Update each villager for (const villager of this.villagers) { // Get nearby villagers for collision avoidance const nearby = this.spatialGrid.query(villager.position, 10); // Calculate steering forces const avoidanceForce = this.calculateAvoidanceForce(villager, nearby); const steeringForce = this.calculateSteeringForce(villager); // Apply forces to velocity (using arrays) this.addScaledVector(villager.velocity, avoidanceForce, deltaTime); this.addScaledVector(villager.velocity, steeringForce, deltaTime); // Limit velocity const speed = this.calculateVectorLength(villager.velocity); if (speed > villager.maxSpeed) { this.normalizeVector(villager.velocity); this.scaleVectorInPlace(villager.velocity, villager.maxSpeed); } // Update position const moveVector = this.scaleVector(villager.velocity, deltaTime); this.addVectors(villager.position, moveVector); } } /** * Calculate avoidance force to prevent collisions * @param {Villager} villager - The villager to calculate for * @param {Array} nearby - Nearby villagers * @returns {Array} Avoidance force as [x, y, z] */ calculateAvoidanceForce(villager, nearby) { const force = [0, 0, 0]; for (const neighbor of nearby) { if (neighbor === villager) continue; const distance = this.calculateDistance(villager.position, neighbor.position); if (distance < villager.avoidanceRadius && distance > 0) { // Calculate avoidance direction (away from neighbor) const direction = this.subtractVectors(villager.position, neighbor.position); this.normalizeVector(direction); // Scale force by inverse of distance (stronger when closer) const strength = (villager.avoidanceRadius - distance) / villager.avoidanceRadius; this.addScaledVector(force, direction, strength); } } return this.scaleVector(force, villager.avoidanceWeight); } /** * Calculate distance between two positions */ calculateDistance(pos1, pos2) { const dx = pos1[0] - pos2[0]; const dy = pos1[1] - pos2[1]; const dz = pos1[2] - pos2[2]; return Math.sqrt(dx * dx + dy * dy + dz * dz); } /** * Subtract two vectors */ subtractVectors(vec1, vec2) { return [ vec1[0] - vec2[0], vec1[1] - vec2[1], vec1[2] - vec2[2] ]; } /** * Normalize a vector */ normalizeVector(vec) { const length = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]); if (length > 0) { vec[0] /= length; vec[1] /= length; vec[2] /= length; } } /** * Add scaled vector to another vector */ addScaledVector(target, source, scale) { target[0] += source[0] * scale; target[1] += source[1] * scale; target[2] += source[2] * scale; } /** * Scale a vector */ scaleVector(vec, scale) { return [ vec[0] * scale, vec[1] * scale, vec[2] * scale ]; } /** * Calculate vector length */ calculateVectorLength(vec) { return Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]); } /** * Scale vector in place */ scaleVectorInPlace(vec, scale) { vec[0] *= scale; vec[1] *= scale; vec[2] *= scale; } /** * Add two vectors */ addVectors(target, source) { target[0] += source[0]; target[1] += source[1]; target[2] += source[2]; } /** * Calculate steering force to follow path * @param {Villager} villager - The villager to calculate for * @returns {Array} Steering force as [x, y, z] */ calculateSteeringForce(villager) { if (villager.path.length === 0) return [0, 0, 0]; const target = villager.path[0]; const direction = this.subtractVectors(target, villager.position); this.normalizeVector(direction); const desired = this.scaleVector(direction, villager.maxSpeed); const steer = this.subtractVectors(desired, villager.velocity); return this.scaleVector(steer, villager.steeringWeight); } } /** * Spatial Grid for efficient neighbor queries */ class SpatialGrid { constructor(width, height, cellSize) { this.width = width; this.height = height; this.cellSize = cellSize; this.grid = []; // Initialize grid const cols = Math.ceil(width / cellSize); const rows = Math.ceil(height / cellSize); for (let i = 0; i < cols * rows; i++) { this.grid.push([]); } } /** * Get cell index for a position * @param {Array} position - Position to get cell for [x, y, z] * @returns {number} Cell index */ getCellIndex(position) { const col = Math.floor(position[0] / this.cellSize); const row = Math.floor(position[2] / this.cellSize); return row * Math.ceil(this.width / this.cellSize) + col; } /** * Insert an object into the grid * @param {Object} obj - Object to insert */ insert(obj) { const index = this.getCellIndex(obj.position); if (index >= 0 && index < this.grid.length) { this.grid[index].push(obj); } } /** * Clear the grid */ clear() { for (let i = 0; i < this.grid.length; i++) { this.grid[i] = []; } } /** * Query objects within a radius * @param {Array} position - Center position [x, y, z] * @param {number} radius - Query radius * @returns {Array} Objects within radius */ query(position, radius) { const results = []; const col = Math.floor(position[0] / this.cellSize); const row = Math.floor(position[2] / this.cellSize); const radiusCells = Math.ceil(radius / this.cellSize); const cols = Math.ceil(this.width / this.cellSize); const rows = Math.ceil(this.height / this.cellSize); // Check surrounding cells for (let r = Math.max(0, row - radiusCells); r <= Math.min(rows - 1, row + radiusCells); r++) { for (let c = Math.max(0, col - radiusCells); c <= Math.min(cols - 1, col + radiusCells); c++) { const index = r * cols + c; if (index >= 0 && index < this.grid.length) { results.push(...this.grid[index]); } } } return results; } } export default CrowdManager;