NPC Survival Simulation — Complete Technical Documentation
by melnykk-dev
An actual simulation you can find here: https://melnykk-dev.itch.io/island
Table of Contents
Formatted in a bit different way. Listed in here just to explain what you can find in this article.
Project Overview
Architecture & Design
Core Systems
Game State Management
Behavior Tree System
3D Environment Creation
NPC AI Logic
Resource Management
Visual Elements
Animation & Movement
User Interface
Project Overview
This is a complete AI-driven NPC survival simulator built using Three.js and vanilla JavaScript. NPCs autonomously manage their survival needs (hunger, thirst, energy, social) while exploring a 3D island environment and making intelligent decisions about resource gathering, shelter seeking, and social interactions.
Key Features
Autonomous NPC Behavior: NPCs use behavior trees to make decisions
Dynamic Environment: Weather system, day/night cycle, resource respawning
Realistic Survival Mechanics: Hunger, thirst, energy, and social needs
Interactive Elements: Animals, shelters, resources, crafting system
Visual Polish: Textured 3D models, shadows, lighting effects
Performance Optimized: Efficient update loops and memory management
Technical Stack
Three.js: 3D graphics rendering
JavaScript ES6+: Core logic and AI systems
HTML5 Canvas: Texture generation
CSS3: UI styling and animations
No external dependencies: Pure vanilla implementation
Architecture & Design
System Overview
┌─────────────────────────────────────────────────────────────┐
│ Main Game Loop │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Environment │ │ NPCs │ │ Animals │ │
│ │ Update │ │ Update │ │ Update │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Camera │ │ UI │ │ Render │ │
│ │ Update │ │ Update │ │ Scene │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘Core Design Principles
Modular Architecture: Each system is self-contained and reusable
Data-Driven Behavior: NPCs make decisions based on current state and environment
Emergent Gameplay: Complex behaviors emerge from simple rules
Performance First: Efficient algorithms and optimized rendering
Core Systems
1. Game State Management
The gameState object is the central data store that manages all game information:
const gameState = {
environment: {
timeOfDay: 12.0, // 24-hour format
weather: 'sunny', // Current weather condition
temperature: 25, // Celsius
isRaining: false, // Boolean weather flags
isStormy: false,
isNight: false,
weatherTimer: 0 // Controls weather changes
},
npcs: [], // Array of all NPCs
animals: [], // Array of all animals
resources: [ // Resource nodes on the island
{ pos: {x, y, z}, type: 'berries', amount: 10 },
{ pos: {x, y, z}, type: 'water', amount: 999 },
// ... more resources
],
shelters: [ // Available shelters
{ pos: {x, y, z}, comfort: 0.8, type: 'cave', capacity: 3 },
// ... more shelters
],
memories: [], // Shared memory system
crafted: [], // Items crafted by NPCs
built: [], // Structures built by NPCs
islandRadius: 32, // Island boundary
timeSpeed: 1, // Time acceleration multiplier
npcCount: 1 // Current NPC count
};Why this structure works:
Centralized State: All game data is accessible from one location
Easy Serialization: Can save/load game state easily
Efficient Updates: Systems only update what they need
Scalable: Easy to add new systems and data
2. NPC Data Structure
Each NPC is a complex object with multiple properties:
const createNPC = (id) => ({
// Identity
id: id,
name: generateRandomName(),
color: getRandomColor(),
// Position & Movement
position: { x: 0, y: 0, z: 0 },
lastPosition: { x: 0, y: 0, z: 0 },
moveDirection: { x: 0, y: 0, z: 0 },
lastDirectionChange: 0,
// Survival Needs (0–1 scale, higher = more need)
hunger: 0.2, // 0 = full, 1 = starving
thirst: 0.3, // 0 = hydrated, 1 = dehydrated
energy: 0.8, // 0 = exhausted, 1 = energetic
socialNeed: 0.4, // 0 = social, 1 = lonely
// Behavior State
state: 'exploring', // Current action
currentGoal: 'Looking around', // What they’re trying to do
emotion: 'neutral', // Emotional state
// Inventory & Skills
inventory: [], // Items carried
craftingSkill: 0, // Skill levels
buildingSkill: 0,
intelligence: 0.5,
// Personality
personality: 'explorer', // Affects behavior priorities
// Interaction State
nearbyNPCs: [], // NPCs within interaction range
communicating: false, // Currently talking
lastCommunication: 0, // Last interaction time
// Anti-Stuck Mechanisms
stuckCounter: 0, // Movement detection
seekingStartTime: 0, // Timeout prevention
isOnMission: false // Mission state
});Behavior Tree System
Overview
The behavior tree is the “brain” of each NPC. It evaluates conditions and executes actions based on priority.
Decision Flow
1. Check Critical Needs (Hunger > 98% or Thirst > 98%)
├─ If critical → Seek food/water immediately
└─ If not critical → Continue to next check
2. Check Environmental Threats
├─ If stormy weather → Seek shelter
└─ If safe → Continue to next check
3. Check Basic Needs (Hunger/Thirst > 60%)
├─ If hungry → Seek food
├─ If thirsty → Seek water
└─ If satisfied → Continue to next check
4. Check Social/Comfort Needs
├─ If lonely → Socialize
├─ If tired → Rest
└─ If comfortable → Continue to next check
5. Check Productive Activities
├─ If materials available → Craft/Build
├─ If inventory low → Gather resources
└─ Default → ExploreKey Behavior Functions
// shouldSeekFood(context)
shouldSeekFood(context) {
return context.npc.hunger > 0.6;
}
// shouldSeekWater(context)
shouldSeekWater(context) {
return context.npc.thirst > 0.8; // Higher threshold = less water obsession
}
// shouldSeekShelter(context)
shouldSeekShelter(context) {
return context.isRaining || context.isStormy ||
(context.isNight && Math.random() < 0.7) ||
(context.weather === 'hurricane' && Math.random() < 0.95);
}shouldSeekFood(context)
Returns
truewhen NPC needs foodTriggers food-seeking behavior
Priority: High (survival need)
shouldSeekWater(context)
Returns
truewhen NPC needs waterReduced from 0.6 to 0.8 to prevent water obsession
Priority: High (survival need)
shouldSeekShelter(context)
Weather-dependent shelter seeking
Randomness prevents all NPCs moving at once
Priority: Medium (comfort need)
Behavior Tree Implementation
class BehaviorTree {
update(context) {
const npc = context.npc;
let result = {
newState: npc.state,
emotion: npc.emotion,
goal: npc.currentGoal
};
// CRITICAL NEEDS - Life threatening (98%+ threshold)
if (npc.hunger > 0.98 || npc.thirst > 0.98) {
const seekingFood = npc.hunger > npc.thirst;
result.newState = seekingFood ? 'seeking_food' : 'seeking_water';
result.emotion = 'desperate';
result.goal = seekingFood ? 'STARVING! Need food NOW!' : 'DEHYDRATED! Need water NOW!';
return result;
}
// WEATHER & ENVIRONMENT
if (this.shouldSeekShelter(context)) {
result.newState = 'seeking_shelter';
result.emotion = context.isNight ? 'concerned' : 'cautious';
result.goal = `Seeking shelter from ${context.isNight ? 'the night' : context.weather}`;
return result;
}
// BASIC NEEDS
if (this.shouldSeekFood(context)) {
result.newState = 'seeking_food';
result.emotion = 'hungry';
result.goal = 'Looking for food';
return result;
}
if (this.shouldSeekWater(context)) {
result.newState = 'seeking_water';
result.emotion = 'thirsty';
result.goal = 'Looking for water';
return result;
}
// SOCIAL & COMFORT NEEDS
if (this.shouldSocialize(context)) {
result.newState = 'socializing';
result.emotion = 'friendly';
result.goal = 'Socializing with others';
return result;
}
// PRODUCTIVE ACTIVITIES
if (this.shouldCraft(context)) {
result.newState = 'crafting';
result.emotion = 'focused';
result.goal = 'Crafting items';
return result;
}
if (this.shouldBuild(context)) {
result.newState = 'building';
result.emotion = 'determined';
result.goal = 'Building structures';
return result;
}
if (this.shouldGather(context)) {
result.newState = 'gathering';
result.emotion = 'productive';
result.goal = 'Gathering resources';
return result;
}
// DEFAULT - EXPLORATION
result.newState = 'exploring';
result.emotion = 'curious';
result.goal = 'Exploring the island';
return result;
}
}3D Environment Creation
Island Generation
The island is created using multiple geometric layers (grass, beach, water), with custom textures generated via HTML5 Canvas.
createIsland() {
// 1. MAIN ISLAND (Grass-covered circular land)
const islandGeometry = new THREE.CircleGeometry(35, 32);
const grassTexture = createTexture('#90EE90', 'grass');
grassTexture.wrapS = THREE.RepeatWrapping;
grassTexture.wrapT = THREE.RepeatWrapping;
grassTexture.repeat.set(6, 6);
const islandMaterial = new THREE.MeshLambertMaterial({ map: grassTexture });
const island = new THREE.Mesh(islandGeometry, islandMaterial);
island.rotation.x = -Math.PI / 2; // Rotate to lie flat
island.position.y = -0.5; // Slightly below ground
island.receiveShadow = true;
this.scene.add(island);
// 2. BEACH (Sandy ring around island)
const beachGeometry = new THREE.RingGeometry(32, 38, 32);
const sandTexture = createTexture('#F4A460', 'sand');
sandTexture.wrapS = THREE.RepeatWrapping;
sandTexture.wrapT = THREE.RepeatWrapping;
sandTexture.repeat.set(3, 3);
const beachMaterial = new THREE.MeshLambertMaterial({ map: sandTexture });
const beach = new THREE.Mesh(beachGeometry, beachMaterial);
beach.rotation.x = -Math.PI / 2;
beach.position.y = -0.49; // Slightly above island
beach.receiveShadow = true;
this.scene.add(beach);
// 3. WATER (Large ocean around everything)
const waterGeometry = new THREE.CircleGeometry(70, 32);
const waterTexture = createTexture('#4682B4', 'water');
waterTexture.wrapS = THREE.RepeatWrapping;
waterTexture.wrapT = THREE.RepeatWrapping;
waterTexture.repeat.set(12, 12);
const waterMaterial = new THREE.MeshLambertMaterial({
map: waterTexture,
transparent: true,
opacity: 0.8
});
const water = new THREE.Mesh(waterGeometry, waterMaterial);
water.rotation.x = -Math.PI / 2;
water.position.y = -0.6; // Below beach level
this.scene.add(water);
// 4. ANIMATED WATER TEXTURE
setInterval(() => {
waterTexture.offset.x += 0.001;
waterTexture.offset.y += 0.0005;
}, 50);
}Texture Generation System
Custom textures are created using HTML5 Canvas:
const createTexture = (color, pattern = 'solid') => {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
if (pattern === 'grass') {
// Base green color
ctx.fillStyle = color;
ctx.fillRect(0, 0, 256, 256);
// Add grass texture details
ctx.fillStyle = '#228B22';
for (let i = 0; i < 1000; i++) {
ctx.fillRect(
Math.random() * 256,
Math.random() * 256,
2, 2
);
}
// Add darker grass patches
ctx.fillStyle = '#006400';
for (let i = 0; i < 500; i++) {
ctx.fillRect(
Math.random() * 256,
Math.random() * 256,
1, 1
);
}
} else if (pattern === 'sand') {
// Base sand color
ctx.fillStyle = color;
ctx.fillRect(0, 0, 256, 256);
// Add sand grain texture
ctx.fillStyle = '#DEB887';
for (let i = 0; i < 100; i++) {
ctx.beginPath();
ctx.arc(
Math.random() * 256,
Math.random() * 256,
5, 0, Math.PI * 2
);
ctx.fill();
}
} else if (pattern === 'water') {
// Base water color
ctx.fillStyle = color;
ctx.fillRect(0, 0, 256, 256);
// Add wave patterns
ctx.fillStyle = '#5F9EA0';
for (let i = 0; i < 50; i++) {
ctx.beginPath();
ctx.arc(
Math.random() * 256,
Math.random() * 256,
10, 0, Math.PI * 2
);
ctx.fill();
}
}
// ... more patterns
return new THREE.CanvasTexture(canvas);
};Tree Generation
Trees are created as grouped objects with procedural variation:
// Tree positions spread across the island
const treePositions = [
[8, 0, 5], [12, 0, -3], [-8, 0, 7], [-5, 0, -9],
[6, 0, 12], [-12, 0, 2], [3, 0, -8], [-7, 0, -12],
[2, 0, 15], [-15, 0, -2], [15, 0, 6], [-3, 0, -15],
[18, 0, 8], [-18, 0, 4], [25, 0, -8], [-25, 0, 12],
[22, 0, 15], [-22, 0, -15], [28, 0, 2], [-28, 0, -8],
[30, 0, 10], [-30, 0, 6], [20, 0, -18], [-20, 0, 18]
];
const woodTexture = createTexture('#8B4513', 'wood');
const leafTexture = createTexture('#228B22', 'grass');
treePositions.forEach(pos => {
const treeGroup = new THREE.Group();
// Tree trunk
const trunkGeometry = new THREE.CylinderGeometry(0.3, 0.4, 4);
const trunkMaterial = new THREE.MeshLambertMaterial({ map: woodTexture });
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
trunk.position.set(0, 2, 0);
trunk.castShadow = true;
treeGroup.add(trunk);
// Tree foliage
const foliageGeometry = new THREE.SphereGeometry(2.5);
const foliageMaterial = new THREE.MeshLambertMaterial({ map: leafTexture });
const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial);
foliage.position.set(0, 5, 0);
foliage.castShadow = true;
treeGroup.add(foliage);
treeGroup.position.set(pos[0], pos[1], pos[2]);
this.scene.add(treeGroup);
});Resource Management
Resource Types and Properties
const gameState = {
resources: [
// Food sources
{ pos: { x: 5, y: 0, z: 10 }, type: 'berries', amount: 8 },
{ pos: { x: -8, y: 0, z: 5 }, type: 'berries', amount: 8 },
// Water sources
{ pos: { x: -15, y: 0, z: 2 }, type: 'water', amount: 999 },
{ pos: { x: 14, y: 0, z: -3 }, type: 'water', amount: 999 },
// Crafting materials
{ pos: { x: 12, y: 0, z: 6 }, type: 'sticks', amount: 15 },
{ pos: { x: -5, y: 0, z: -8 }, type: 'sticks', amount: 15 },
{ pos: { x: 8, y: 0, z: -12 }, type: 'stones', amount: 20 },
{ pos: { x: -12, y: 0, z: 8 }, type: 'stones', amount: 20 },
{ pos: { x: 3, y: 0, z: 15 }, type: 'stones', amount: 20 },
{ pos: { x: -8, y: 0, z: -12 }, type: 'sticks', amount: 10 }
]
};Resource Gathering, Consumption, and Respawning
// In the NPC update loop
gameState.resources.forEach(resource => {
const distance = calculateDistance(npc.position, resource.pos);
if (distance < 2 && resource.amount > 0) {
if (npc.inventory.length < 5) {
npc.inventory.push(resource.type);
resource.amount -= 1;
console.log(`NPC ${npc.id} collected ${resource.type}, inventory: ${npc.inventory.length} items`);
}
}
});
------------
// Food consumption
if ((npc.hunger > 0.2 || npc.hunger === 0) && npc.inventory.length > 0) {
if (npc.inventory.includes('fish')) {
const fishIndex = npc.inventory.indexOf('fish');
npc.inventory.splice(fishIndex, 1);
npc.hunger = Math.max(0, npc.hunger - 0.6); // Reduce hunger by 60%
console.log(`NPC ${npc.id} ate fish, hunger now: ${Math.round((1-npc.hunger)*100)}%`);
} else if (npc.inventory.includes('berries')) {
const berryIndex = npc.inventory.indexOf('berries');
npc.inventory.splice(berryIndex, 1);
npc.hunger = Math.max(0, npc.hunger - 0.4); // Reduce hunger by 40%
console.log(`NPC ${npc.id} ate berries, hunger now: ${Math.round((1-npc.hunger)*100)}%`);
}
}
// Water consumption
if (npc.thirst > 0.3 && npc.inventory.includes('water')) {
const waterIndex = npc.inventory.indexOf('water');
npc.inventory.splice(waterIndex, 1);
npc.thirst = Math.max(0, npc.thirst - 0.4); // Reduce thirst by 40%
console.log(`NPC ${npc.id} drank water, thirst now: ${Math.round((1-npc.thirst)*100)}%`);
}
------------
// Resource regeneration system
setInterval(() => {
gameState.resources.forEach(resource => {
if (resource.type === 'berries' && resource.amount < 8) {
resource.amount = Math.min(8, resource.amount + 1);
} else if (resource.type === 'sticks' && resource.amount < 15) {
resource.amount = Math.min(15, resource.amount + 2);
} else if (resource.type === 'stones' && resource.amount < 20) {
resource.amount = Math.min(20, resource.amount + 1);
}
});
}, 30000); // Every 30 secondsAnimal System
Animal Creation & AI
Animals are procedurally generated and move autonomously within the island bounds. NPCs can interact with animals to boost social needs.
addAnimals() {
const animalTypes = [
{ name: 'rabbit', color: 0xFFFFFF, size: 0.5, speed: 0.02 },
{ name: 'bird', color: 0xFF6347, size: 0.3, speed: 0.03 },
{ name: 'squirrel', color: 0x8B4513, size: 0.4, speed: 0.025 },
{ name: 'deer', color: 0xDEB887, size: 1.0, speed: 0.015 }
];
gameState.animals = [];
// Create 8 animals spread across the island
for (let i = 0; i < 8; i++) {
const animalType = animalTypes[Math.floor(Math.random() * animalTypes.length)];
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 25 + 5;
const animal = {
id: i,
type: animalType.name,
position: {
x: Math.cos(angle) * distance,
y: 0,
z: Math.sin(angle) * distance
},
moveDirection: { x: 0, y: 0, z: 0 },
speed: animalType.speed,
lastDirectionChange: 0,
socialValue: 0.3 // How much social need they reduce
};
gameState.animals.push(animal);
// Create 3D model
const animalGeometry = new THREE.SphereGeometry(animalType.size, 8, 6);
const animalMaterial = new THREE.MeshLambertMaterial({
color: animalType.color,
emissive: animalType.color,
emissiveIntensity: 0.1
});
const animalMesh = new THREE.Mesh(animalGeometry, animalMaterial);
animalMesh.position.set(animal.position.x, animalType.size, animal.position.z);
animalMesh.castShadow = true;
this.scene.add(animalMesh);
// Store reference to mesh
animal.mesh = animalMesh;
}
}
-------------
updateAnimals(deltaTime) {
if (!gameState.animals) return;
const speedMultiplier = gameState.timeSpeed;
const acceleratedDelta = deltaTime * speedMultiplier;
const time = Date.now() / 1000;
gameState.animals.forEach(animal => {
// Change direction occasionally
if (time - animal.lastDirectionChange > 8 + Math.random() * 6) {
animal.moveDirection = getRandomDirection();
animal.lastDirectionChange = time;
}
// Move animal
animal.position.x += animal.moveDirection.x * animal.speed * acceleratedDelta;
animal.position.z += animal.moveDirection.z * animal.speed * acceleratedDelta;
// Keep within island bounds
const distanceFromCenter = Math.sqrt(animal.position.x * animal.position.x + animal.position.z * animal.position.z);
if (distanceFromCenter > gameState.islandRadius - 5) {
const angle = Math.atan2(animal.position.z, animal.position.x);
animal.position.x = Math.cos(angle) * (gameState.islandRadius - 5);
animal.position.z = Math.sin(angle) * (gameState.islandRadius - 5);
animal.moveDirection = {
x: -animal.position.x / gameState.islandRadius,
y: 0,
z: -animal.position.z / gameState.islandRadius
};
}
// Update mesh position
if (animal.mesh) {
animal.mesh.position.set(animal.position.x, animal.mesh.geometry.parameters.radius, animal.position.z);
}
});
}
-------------
// ANIMAL INTERACTION - Boost social needs
if (gameState.animals) {
gameState.animals.forEach(animal => {
const distance = calculateDistance(npc.position, animal.position);
if (distance < 2.5 && npc.socialNeed > 0.3) {
npc.socialNeed = Math.max(0, npc.socialNeed - acceleratedDelta * 0.008);
console.log(`NPC ${npc.id} is interacting with ${animal.type}! Social: ${Math.round((1-npc.socialNeed)*100)}%`);
}
});
}Shelter and Energy Recovery System
NPCs recover energy faster when resting in shelters such as tents or caves.
addTents(woodTexture, stoneTexture) {
const tentTexture = this.createTentTexture('#8B4513');
const tentPositions = [
[5, 0, -12], [-8, 0, 15], [20, 0, 8], [-18, 0, -5]
];
tentPositions.forEach(pos => {
const tentGroup = new THREE.Group();
// Tent body
const tentGeometry = new THREE.ConeGeometry(2, 3, 4);
const tentMaterial = new THREE.MeshLambertMaterial({
color: 0x8B4513,
map: tentTexture
});
const tent = new THREE.Mesh(tentGeometry, tentMaterial);
tent.position.set(0, 1.5, 0);
tent.castShadow = true;
tentGroup.add(tent);
// Tent entrance
const entranceGeometry = new THREE.PlaneGeometry(1.5, 1.5);
const entranceMaterial = new THREE.MeshLambertMaterial({
color: 0x2F4F4F,
transparent: true,
opacity: 0.7
});
const entrance = new THREE.Mesh(entranceGeometry, entranceMaterial);
entrance.position.set(0, 0.75, 1.8);
tentGroup.add(entrance);
tentGroup.position.set(pos[0], pos[1], pos[2]);
this.scene.add(tentGroup);
// Add tent to shelters list
gameState.shelters.push({
pos: { x: pos[0], y: pos[1], z: pos[2] },
comfort: 0.8,
type: 'tent',
capacity: 1
});
});
}
-----------------
// ENHANCED ENERGY RECOVERY - Sleep in tents/shelters
if (npc.energy < 0.4) {
const nearestShelter = gameState.shelters.find(shelter =>
calculateDistance(npc.position, shelter.pos) < 3
);
if (nearestShelter) {
npc.energy = Math.min(1, npc.energy + acceleratedDelta * 0.015);
if (npc.energy < 0.2) {
npc.state = 'sleeping';
npc.currentGoal = `Sleeping in ${nearestShelter.type}`;
console.log(`NPC ${npc.id} is sleeping in ${nearestShelter.type}! Energy: ${Math.round(npc.energy*100)}%`);
}
}
}NPC Visual Representation
NPCs are rendered in 3D with body, head, eyes, state indicators, and name tags.
createNPCVisual(npc) {
const npcGroup = new THREE.Group();
// Body
const bodyGeometry = new THREE.CapsuleGeometry(0.5, 1);
const bodyMaterial = new THREE.MeshLambertMaterial({ color: npc.color });
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.set(0, 1, 0);
body.castShadow = true;
npcGroup.add(body);
// Head
const headGeometry = new THREE.SphereGeometry(0.3);
const headMaterial = new THREE.MeshLambertMaterial({ color: 0xFFE4B5 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.set(0, 2, 0);
head.castShadow = true;
npcGroup.add(head);
// Eyes
const eyeGeometry = new THREE.SphereGeometry(0.05);
const eyeMaterial = new THREE.MeshLambertMaterial({ color: 0x000000 });
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
leftEye.position.set(-0.1, 2.1, 0.25);
npcGroup.add(leftEye);
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
rightEye.position.set(0.1, 2.1, 0.25);
npcGroup.add(rightEye);
// State indicator
const indicatorGeometry = new THREE.SphereGeometry(0.1);
const indicatorMaterial = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });
const indicator = new THREE.Mesh(indicatorGeometry, indicatorMaterial);
indicator.position.set(0, 3, 0);
npcGroup.add(indicator);
// Name tag
const nameCanvas = document.createElement('canvas');
nameCanvas.width = 256;
nameCanvas.height = 64;
const nameCtx = nameCanvas.getContext('2d');
nameCtx.fillStyle = '#000000';
nameCtx.font = '24px Arial';
nameCtx.textAlign = 'center';
nameCtx.fillText(npc.name, 128, 40);
const nameTexture = new THREE.CanvasTexture(nameCanvas);
const nameMaterial = new THREE.MeshLambertMaterial({
map: nameTexture,
transparent: true
});
const nameGeometry = new THREE.PlaneGeometry(2, 0.5);
const nameTag = new THREE.Mesh(nameGeometry, nameMaterial);
nameTag.position.set(0, 3.5, 0);
npcGroup.add(nameTag);
npcGroup.position.set(npc.position.x, npc.position.y, npc.position.z);
this.scene.add(npcGroup);
// Store reference
if (!this.npcGroups) this.npcGroups = [];
this.npcGroups.push(npcGroup);
}
-----------------
// Update NPC visual state
updateNPCVisuals() {
if (!this.npcGroups) return;
gameState.npcs.forEach((npc, index) => {
const npcGroup = this.npcGroups[index];
if (!npcGroup) return;
// Update position
npcGroup.position.set(npc.position.x, npc.position.y, npc.position.z);
// Update state indicator color
const indicator = npcGroup.children.find(child =>
child.geometry instanceof THREE.SphereGeometry &&
child.position.y === 3
);
if (indicator) {
switch(npc.state) {
case 'seeking_food':
indicator.material.color.setHex(0xFF0000); // Red
break;
case 'seeking_water':
indicator.material.color.setHex(0x0000FF); // Blue
break;
case 'seeking_shelter':
indicator.material.color.setHex(0x8B4513); // Brown
break;
case 'sleeping':
indicator.material.color.setHex(0x800080); // Purple
break;
case 'socializing':
indicator.material.color.setHex(0xFFFF00); // Yellow
break;
case 'crafting':
indicator.material.color.setHex(0xFFA500); // Orange
break;
case 'building':
indicator.material.color.setHex(0x228B22); // Green
break;
default:
indicator.material.color.setHex(0xFFFFFF); // White
}
}
});
}Performance Optimization
Efficient update loops, memory management, and selective shadow rendering keep the simulation performant even with many entities.
animate(currentTime = 0) {
requestAnimationFrame((time) => this.animate(time));
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
// Only update if deltaTime is reasonable (prevents large jumps)
if (deltaTime < 0.1) {
this.updateEnvironment(deltaTime);
this.updateNPC(deltaTime);
this.updateAnimals(deltaTime);
this.updateCamera(deltaTime);
this.updateUI();
}
this.renderer.render(this.scene, this.camera);
}
-----------------
// Clean up removed NPCs
removeNPC() {
if (gameState.npcs.length <= 1) return;
// Remove from game state
gameState.npcs.pop();
// Remove visual representation
if (this.npcGroups && this.npcGroups.length > 0) {
const npcGroup = this.npcGroups.pop();
if (npcGroup) {
// Clean up geometry and materials
npcGroup.traverse((child) => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (child.material.map) child.material.map.dispose();
child.material.dispose();
}
});
this.scene.remove(npcGroup);
}
}
// Remove behavior tree
if (this.behaviorTrees && this.behaviorTrees.length > 0) {
this.behaviorTrees.pop();
}
}
-----------------
// Only enable shadows on important objects
setupShadows() {
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Light shadow settings
this.lightRef.castShadow = true;
this.lightRef.shadow.mapSize.width = 1024;
this.lightRef.shadow.mapSize.height = 1024;
this.lightRef.shadow.camera.near = 0.1;
this.lightRef.shadow.camera.far = 50;
this.lightRef.shadow.camera.left = -20;
this.lightRef.shadow.camera.right = 20;
this.lightRef.shadow.camera.top = 20;
this.lightRef.shadow.camera.bottom = -20;
}User Interface System
The UI displays real-time stats on NPC needs, game state, and provides controls for simulation speed and NPC management.
<div id="ui">
<h2>NPC Survival Simulation</h2>
<div class="stats">
<div class="stat-row">
<span>Time:</span>
<span id="time">12.0h (Day)</span>
</div>
<div class="stat-row">
<span>Weather:</span>
<span id="weather">Sunny ☀️</span>
</div>
<div class="stat-row">
<span>NPC State:</span>
<span id="npcState">Exploring</span>
</div>
<div class="stat-row">
<span>Emotion:</span>
<span id="emotion">Neutral</span>
</div>
<div class="stat-row">
<span>Goal:</span>
<span id="goal">Wandering around</span>
</div>
<!-- Survival needs -->
<div class="stat-row">
<span>Hunger:</span>
<span id="hungerPercent">80%</span>
</div>
<div class="stat-row">
<span>Thirst:</span>
<span id="thirstPercent">70%</span>
</div>
<div class="stat-row">
<span>Energy:</span>
<span id="energyPercent">60%</span>
</div>
<div class="stat-row">
<span>Social:</span>
<span id="socialPercent">90%</span>
</div>
</div>
<div class="controls">
<button id="speed1" class="speed-btn active">1x</button>
<button id="speed10" class="speed-btn">10x</button>
<button id="speed100" class="speed-btn">100x</button>
<button id="addNPC" class="add-btn">Add NPC</button>
<button id="removeNPC" class="remove-btn">Remove NPC</button>
</div>
</div>
-------------------
updateUI() {
const env = gameState.environment;
const primaryNPC = gameState.npcs[0];
if (!primaryNPC) return;
// Time display
const timeString = env.timeOfDay.toFixed(1) + 'h';
const timeOfDayString = env.isNight ? 'Night' : 'Day';
document.getElementById('time').textContent = `${timeString} (${timeOfDayString})`;
// Weather display
const weatherEmojis = {
sunny: '☀️',
partly_cloudy: '⛅',
cloudy: '☁️',
rainy: '🌧️',
stormy: '⛈️',
foggy: '🌫️',
hurricane: '🌀',
blizzard: '❄️'
};
document.getElementById('weather').textContent =
`${env.weather.charAt(0).toUpperCase() + env.weather.slice(1)} ${weatherEmojis[env.weather] || ''}`;
// NPC state
document.getElementById('npcState').textContent = primaryNPC.state;
document.getElementById('emotion').textContent = primaryNPC.emotion;
document.getElementById('goal').textContent = primaryNPC.currentGoal;
// Survival needs (convert to percentages)
const hungerPercent = Math.max(0, Math.round((1-primaryNPC.hunger)*100));
const thirstPercent = Math.max(0, Math.round((1-primaryNPC.thirst)*100));
const energyPercent = Math.max(0, Math.round(primaryNPC.energy*100));
const socialPercent = Math.max(0, Math.round((1-primaryNPC.socialNeed)*100));
document.getElementById('hungerPercent').textContent = `${hungerPercent}%`;
document.getElementById('thirstPercent').textContent = `${thirstPercent}%`;
document.getElementById('energyPercent').textContent = `${energyPercent}%`;
document.getElementById('socialPercent').textContent = `${socialPercent}%`;
// Color coding for needs
const hungerEl = document.getElementById('hungerPercent');
const thirstEl = document.getElementById('thirstPercent');
const energyEl = document.getElementById('energyPercent');
const socialEl = document.getElementById('socialPercent');
[hungerEl, thirstEl, energyEl, socialEl].forEach((el, index) => {
const percent = [hungerPercent, thirstPercent, energyPercent, socialPercent][index];
if (percent < 30) el.style.color = '#ff4444'; // Red for low
else if (percent < 60) el.style.color = '#ffaa44'; // Orange for medium
else el.style.color = '#44ff44'; // Green for high
});
// Other stats
document.getElementById('memories').textContent = gameState.memories.length;
document.getElementById('speed').textContent = `${gameState.timeSpeed}x`;
document.getElementById('npcCount').textContent = gameState.npcs.length;
document.getElementById('temperature').textContent = `${env.temperature}°C`;
document.getElementById('resources').textContent = `${gameState.resources.filter(r => r.amount > 0).length} types`;
document.getElementById('crafted').textContent = gameState.crafted.length;
}Lighting and Weather System
Dynamic lighting, fog, and weather effects (rain, storm, blizzard, etc.) enhance immersion and influence NPC behavior.
createEnvironment() {
// Ambient light (overall scene brightness)
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(this.ambientLight);
// Directional light (sun)
this.lightRef = new THREE.DirectionalLight(0xFFE4B5, 1.0);
this.lightRef.position.set(10, 10, 5);
this.lightRef.castShadow = true;
this.lightRef.shadow.mapSize.width = 1024;
this.lightRef.shadow.mapSize.height = 1024;
this.lightRef.shadow.camera.near = 0.1;
this.lightRef.shadow.camera.far = 50;
this.lightRef.shadow.camera.left = -20;
this.lightRef.shadow.camera.right = 20;
this.lightRef.shadow.camera.top = 20;
this.lightRef.shadow.camera.bottom = -20;
this.scene.add(this.lightRef);
// Weather effects container
this.rainGroup = new THREE.Group();
this.scene.add(this.rainGroup);
// Fog for atmosphere
this.scene.fog = new THREE.Fog(0x87CEEB, 30, 100);
}
--------------
updateEnvironment(deltaTime) {
const env = gameState.environment;
const speedMultiplier = gameState.timeSpeed;
// Update time of day
env.timeOfDay += (deltaTime * speedMultiplier) / 30;
if (env.timeOfDay >= 24) {
env.timeOfDay -= 24;
}
// Determine if it's night
env.isNight = env.timeOfDay < 6 || env.timeOfDay > 18;
// Weather changes
env.weatherTimer += deltaTime * speedMultiplier;
if (env.weatherTimer > 60 + Math.random() * 120) {
const weathers = ['sunny', 'sunny', 'partly_cloudy', 'cloudy', 'rainy', 'stormy', 'foggy', 'hurricane', 'blizzard'];
const weights = [0.3, 0.25, 0.15, 0.1, 0.08, 0.05, 0.04, 0.02, 0.01];
let random = Math.random();
let selectedWeather = 'sunny';
for (let i = 0; i < weathers.length; i++) {
if (random < weights[i]) {
selectedWeather = weathers[i];
break;
}
random -= weights[i];
}
env.weather = selectedWeather;
env.isRaining = ['rainy', 'stormy', 'hurricane', 'blizzard'].includes(env.weather);
env.isStormy = ['stormy', 'hurricane', 'blizzard'].includes(env.weather);
env.isFoggy = env.weather === 'foggy';
env.weatherTimer = 0;
}
// Update temperature
let baseTemp = 25;
if (env.isNight) baseTemp -= 10;
if (env.weather === 'stormy') baseTemp -= 5;
if (env.weather === 'hurricane') baseTemp -= 8;
if (env.weather === 'blizzard') baseTemp -= 15;
if (env.weather === 'sunny') baseTemp += 5;
env.temperature = Math.round(baseTemp);
// Update lighting based on time and weather
const sunIntensity = env.isNight ? 0.3 :
env.weather === 'stormy' ? 0.5 :
env.weather === 'cloudy' ? 0.7 : 1.0;
this.lightRef.intensity = sunIntensity;
this.ambientLight.intensity = env.isNight ? 0.2 : 0.5;
// Update fog
if (this.scene.fog) {
this.scene.fog.near = env.isFoggy ? 5 : 30;
this.scene.fog.far = env.isFoggy ? 20 : 100;
}
this.updateWeatherEffects(env);
}
--------------
updateWeatherEffects(env) {
// Clear existing effects
if (!env.isRaining && this.rainGroup.children.length > 0) {
while (this.rainGroup.children.length > 0) {
this.rainGroup.remove(this.rainGroup.children[0]);
}
}
// Create weather effects
if (env.isRaining && this.rainGroup.children.length === 0) {
let particleCount = 100;
let particleColor = 0x4682B4;
let particleSize = 0.5;
let fallSpeed = 0.5;
if (env.weather === 'stormy') {
particleCount = 200;
fallSpeed = 0.8;
} else if (env.weather === 'hurricane') {
particleCount = 300;
fallSpeed = 1.2;
particleColor = 0x2F4F4F;
} else if (env.weather === 'blizzard') {
particleCount = 250;
particleColor = 0xFFFFFF;
particleSize = 0.2;
fallSpeed = 0.3;
}
for (let i = 0; i < particleCount; i++) {
const geometry = env.weather === 'blizzard' ?
new THREE.SphereGeometry(0.02) :
new THREE.CylinderGeometry(0.01, 0.01, particleSize);
const material = new THREE.MeshLambertMaterial({
color: particleColor,
transparent: true,
opacity: env.weather === 'blizzard' ? 0.8 : 0.6
});
const particle = new THREE.Mesh(geometry, material);
particle.position.set(
(Math.random() - 0.5) * 50,
Math.random() * 20 + 10,
(Math.random() - 0.5) * 50
);
particle.userData = { fallSpeed, weather: env.weather };
this.rainGroup.add(particle);
}
}
// Animate particles
if (env.isRaining) {
this.rainGroup.children.forEach(particle => {
const speed = particle.userData.fallSpeed;
const weather = particle.userData.weather;
if (weather === 'blizzard') {
particle.position.y -= speed;
particle.position.x += Math.sin(Date.now() * 0.001) * 0.1;
particle.rotation.x += 0.01;
particle.rotation.y += 0.01;
} else if (weather === 'hurricane') {
particle.position.y -= speed;
particle.position.x += Math.sin(Date.now() * 0.003) * 0.2;
particle.position.z += Math.cos(Date.now() * 0.003) * 0.2;
} else {
particle.position.y -= speed;
}
if (particle.position.y < -5) {
particle.position.y = 25;
}
});
}
}Conclusion
This NPC survival simulator demonstrates how complex, emergent behavior can arise from simple, modular systems. The architecture is extensible and performance-optimized, making it a strong foundation for further development—whether you want to add seasonal changes, trading, territory management, or even multiplayer support.
Further Reading
This documentation provides everything needed to understand and recreate the NPC survival simulator from scratch. The system demonstrates how complex, realistic behavior can emerge from simple rules when properly implemented.
Thank you for reading. It this article helped you, please follow me on LinkedIn.

