6rz6
Add Medieval Village AI Emulator
a32dc8b
// 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<Villager>} 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<Object>} 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;