# Simulation vs Faking: The Foundational Trade-off ## Purpose **This is the MOST CRITICAL skill in the simulation-tactics skillpack.** It teaches the fundamental decision framework that prevents two catastrophic failure modes: 1. **Over-simulation**: Wasting performance simulating things players never notice 2. **Under-simulation**: Breaking immersion by failing to simulate what players DO notice Every other simulation skill builds on this foundation. Master this first. ## When to Use This Skill Use this skill when: - Designing ANY game system with simulation elements - Facing performance budgets with complex simulations - Deciding what to simulate vs what to fake - Players can observe systems at varying levels of scrutiny - Building NPCs, crowds, ecosystems, economies, physics, or AI - Choosing between realistic simulation and performance - System has background elements and foreground elements **ALWAYS use this skill BEFORE implementing simulation systems.** Retrofitting after over-engineering is painful. ## Core Philosophy: The "Good Enough" Threshold ### The Fundamental Truth > **Players don't experience your simulation—they experience their PERCEPTION of your simulation.** The goal is not perfect simulation. The goal is creating the ILLUSION of a living, breathing world within your performance budget. ### The Good Enough Threshold For every system, there exists a "good enough" threshold where: - **Below**: Players notice something is off (immersion breaks) - **Above**: Players don't notice improvements (wasted performance) Your job is to find this threshold and stay JUST above it. ### Example: NPC Hunger Consider an NPC hunger system: **Over-Simulated** (wasted performance): ``` Hunger = 100.0 Every frame: Hunger -= 0.0001 * Time.deltaTime Tracks: Last meal, calorie intake, metabolism rate, digestion state Result: Frame-perfect accuracy nobody notices Cost: 0.1ms per NPC × 100 NPCs = 10ms ``` **Good Enough** (optimized): ``` Hunger = 100.0 Every 60 seconds: Hunger -= 5.0 Tracks: Just hunger value Result: Player sees NPC eat when hungry Cost: 0.001ms per NPC × 100 NPCs = 0.1ms (100× faster) ``` **Under-Simulated** (breaks immersion): ``` Hunger = always 50 NPCs never eat Result: Player notices NPCs don't eat for days Cost: 0ms but ruins experience ``` The middle option is "good enough"—NPCs eat, players believe the simulation, performance is fine. ## CORE CONCEPT #1: Player Scrutiny Levels The SINGLE MOST IMPORTANT factor in simulation-vs-faking decisions is: **How closely will the player observe this?** ### Scrutiny Hierarchy ``` █████████████████████████ EXTREME SCRUTINY █████████████████████████ │ Center screen, zoomed in, player controlling │ Examples: Player character, boss enemy, inspected NPC │ Strategy: FULL SIMULATION, no corners cut │ Budget: High (0.5-2ms per entity) │ ████████████████████████ HIGH SCRUTINY ████████████████████████ │ On screen, player watching, can interact │ Examples: Enemy in combat, nearby NPC, active vehicle │ Strategy: DETAILED SIMULATION with minor optimizations │ Budget: Medium (0.1-0.5ms per entity) │ ███████████████████ MEDIUM SCRUTINY ███████████████████ │ On screen, visible, background │ Examples: Crowd member, distant traffic, ambient wildlife │ Strategy: HYBRID (key features real, details faked) │ Budget: Low (0.01-0.05ms per entity) │ ██████████████ LOW SCRUTINY ██████████████ │ Barely visible, distant, or peripheral │ Examples: Distant NPCs, far traffic, background crowd │ Strategy: MOSTLY FAKE with occasional reality │ Budget: Minimal (0.001-0.01ms per entity) │ █████ MINIMAL SCRUTINY █████ │ Off-screen, occluded, or player never observes │ Examples: NPCs in buildings, crowd outside view, distant city │ Strategy: FULLY FAKE or statistical │ Budget: Negligible (0.0001ms per entity or bulk) │ ``` ### Scrutiny-Based Decision Matrix | Scrutiny | Simulation Level | Examples | Techniques | |----------|-----------------|----------|------------| | **Extreme** | 100% real | Player character, inspected NPC, boss | Full physics, full AI, full needs, high-res animation | | **High** | 90% real | Combat enemies, dialogue NPCs | Real AI, simplified needs, standard animation | | **Medium** | 50% real / 50% fake | Visible background NPCs | State machines, no needs, scripted paths, LOD animation | | **Low** | 90% fake | Distant crowd, far traffic | Fake AI, no needs, waypoint movement, simple animation | | **Minimal** | 100% fake | Off-screen entities | Statistical simulation, no individual updates | ### Practical Application When designing ANY system, ask: 1. **How close can the player get?** (distance-based scrutiny) 2. **How long will they observe?** (time-based scrutiny) 3. **Can they interact?** (interaction-based scrutiny) 4. **Does it affect gameplay?** (relevance-based scrutiny) Then allocate simulation budget based on MAXIMUM scrutiny level. ### Example: City Builder NPCs **Scenario**: City with 100 NPCs, player can zoom in/out and click NPCs. **Scrutiny Analysis**: - **10 Important NPCs**: High scrutiny (player knows them by name, clicks often) - **30 Nearby NPCs**: Medium scrutiny (visible on screen, occasionally clicked) - **60 Distant NPCs**: Low scrutiny (tiny on screen, rarely clicked) **Simulation Strategy**: ```csharp void UpdateNPC(NPC npc) { float scrutiny = CalculateScrutiny(npc); if (scrutiny > 0.8f) // High scrutiny { UpdateFullSimulation(npc); // 0.5ms per NPC } else if (scrutiny > 0.4f) // Medium scrutiny { UpdateHybridSimulation(npc); // 0.05ms per NPC } else if (scrutiny > 0.1f) // Low scrutiny { UpdateFakeSimulation(npc); // 0.005ms per NPC } else // Minimal scrutiny { // Don't update, or bulk statistical update } } float CalculateScrutiny(NPC npc) { float distance = Vector3.Distance(camera.position, npc.position); float visibility = IsVisible(npc) ? 1.0f : 0.1f; float interaction = npc.isImportant ? 1.5f : 1.0f; // Closer = higher scrutiny float distanceScore = 1.0f / (1.0f + distance / 50.0f); return distanceScore * visibility * interaction; } ``` **Result**: - 10 important NPCs: 10 × 0.5ms = 5ms - 30 nearby NPCs: 30 × 0.05ms = 1.5ms - 60 distant NPCs: 60 × 0.005ms = 0.3ms - **Total**: 6.8ms (fits in budget) Compare to naïve approach: 100 × 0.5ms = 50ms (3× frame budget) ## CORE CONCEPT #2: Gameplay Relevance The second most important factor: **Does this affect player decisions or outcomes?** ### Relevance Hierarchy ``` CRITICAL TO GAMEPLAY │ Directly affects win/lose, progression, or core decisions │ Examples: Enemy health, ammo count, quest state │ Strategy: ALWAYS SIMULATE (never fake) │ SIGNIFICANT TO GAMEPLAY │ Affects player choices or secondary goals │ Examples: NPC happiness (affects quests), traffic (blocks player) │ Strategy: SIMULATE when relevant, fake when not │ COSMETIC (OBSERVABLE) │ Visible to player but doesn't affect gameplay │ Examples: Crowd animations, ambient wildlife, background traffic │ Strategy: FAKE heavily, simulate minimally │ COSMETIC (UNOBSERVABLE) │ Exists for "realism" but player rarely sees │ Examples: NPC sleep schedules, off-screen animals, distant city lights │ Strategy: FULLY FAKE or remove entirely │ ``` ### Relevance Assessment Questions For every simulation system, ask: 1. **Does it affect win/lose?** - YES → Simulate accurately - NO → Continue to Q2 2. **Does it affect player decisions?** - YES → Simulate when decision is active - NO → Continue to Q3 3. **Can the player observe it?** - YES → Fake convincingly - NO → Continue to Q4 4. **Does it affect observable systems?** - YES → Fake with minimal updates - NO → Remove or use statistics ### Example: NPC Needs System **System**: NPCs have hunger, energy, social, hygiene needs. **Relevance Analysis**: | Need | Affects Gameplay? | Observable? | Relevance | Strategy | |------|------------------|-------------|-----------|----------| | **Hunger** | YES (unhappy NPCs leave city) | YES (eating animation) | SIGNIFICANT | Simulate (tick-based, not frame-based) | | **Energy** | NO (doesn't affect anything) | YES (sleeping NPCs) | COSMETIC-OBS | Fake (schedule-based, no simulation) | | **Social** | NO | YES (chatting NPCs) | COSMETIC-OBS | Fake (pre-assign friends, no dynamics) | | **Hygiene** | NO | NO (never shown) | COSMETIC-UNOBS | **Remove entirely** | **Implementation**: ```csharp class NPC { // SIMULATED (affects gameplay) float hunger; // Decreases every 10 minutes (tick-based) // FAKED (cosmetic but observable) bool isSleeping => GameTime.Hour >= 22 || GameTime.Hour < 6; // Schedule-based // FAKED (cosmetic but observable) List friends; // Pre-assigned at spawn, never changes // REMOVED (cosmetic and unobservable) // float hygiene; // DON'T IMPLEMENT } void Update() { // Only update hunger (gameplay-relevant) if (Time.frameCount % 600 == 0) // Every 10 seconds at 60fps { hunger -= 5.0f; if (hunger < 20.0f) StartEatingBehavior(); } // Energy is faked via schedule (no updates needed) if (isSleeping) PlaySleepAnimation(); // Social is faked (no updates needed) if (Time.frameCount % 300 == 0) // Every 5 seconds { if (friends.Any(f => f.IsNearby())) PlayChatAnimation(); } } ``` **Result**: - Hunger simulation: Believable and affects gameplay - Energy/Social: Faked but look real - Hygiene: Removed (didn't add value) - Performance: 0.01ms per NPC (vs 0.5ms if all simulated) ## CORE CONCEPT #3: Performance Budgets You can't manage what you don't measure. Start with budgets, design within constraints. ### Frame Budget Breakdown Typical 60 FPS game (16.67ms per frame): ``` RENDERING: 8.0ms (48%) ████████████ SIMULATION: 5.0ms (30%) ███████ ├─ Physics: 2.0ms ████ ├─ AI/NPCs: 2.0ms ████ └─ Game Logic: 1.0ms ██ UI: 1.5ms (9%) ██ AUDIO: 1.0ms (6%) ██ OTHER: 1.17ms (7%) ██ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ TOTAL: 16.67ms (100%) ``` ### NPC Simulation Budget Example **Budget**: 2.0ms for 100 NPCs **Allocation**: ``` IMPORTANT NPCs (10): 1.0ms (50%) 0.100ms each NEARBY NPCs (30): 0.6ms (30%) 0.020ms each DISTANT NPCs (60): 0.3ms (15%) 0.005ms each MANAGER OVERHEAD: 0.1ms (5%) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ TOTAL: 2.0ms ``` ### Budgeting Process **Step 1: Measure baseline** ```csharp using (new ProfilerScope("NPC_Update")) { foreach (var npc in allNPCs) npc.Update(); } ``` **Step 2: Identify hotspots** ``` NPC_Update: 10.5ms ⚠️ (5× over budget) ├─ Pathfinding: 5.2ms (49%) ├─ Social Queries: 3.1ms (30%) ├─ Needs Update: 1.8ms (17%) └─ Animation: 0.4ms (4%) ``` **Step 3: Optimize based on scrutiny** ``` TARGET: 2.0ms total Strategy: • Pathfinding (5.2ms → 0.5ms): - Pre-compute paths for background NPCs (90% savings) - Use waypoints instead of NavMesh for distant NPCs • Social Queries (3.1ms → 0.3ms): - Remove for background NPCs (they don't need dynamic friends) - Check every 5 seconds, not every frame • Needs Update (1.8ms → 0.8ms): - Tick-based (every 10s) instead of frame-based - Remove cosmetic needs (hygiene) • Animation (0.4ms → 0.4ms): - Already efficient, keep as-is NEW TOTAL: 0.5 + 0.3 + 0.8 + 0.4 = 2.0ms ✅ ``` **Step 4: Validate** ```csharp // Add budget assertions float startTime = Time.realtimeSinceStartup; NPCManager.UpdateAll(); float elapsed = (Time.realtimeSinceStartup - startTime) * 1000f; if (elapsed > 2.0f) Debug.LogWarning($"NPC update exceeded budget: {elapsed:F2}ms"); ``` ### Budget Allocation Strategy **Rule**: Budget should match scrutiny: | Scrutiny Level | Budget per Entity | Max Entities at 60 FPS | |----------------|------------------|----------------------| | **Extreme** | 1.0-5.0ms | 3-16 | | **High** | 0.1-1.0ms | 16-160 | | **Medium** | 0.01-0.1ms | 160-1600 | | **Low** | 0.001-0.01ms | 1600-16000 | | **Minimal** | <0.001ms | Unlimited (bulk ops) | **Example**: 100 NPCs with 2ms budget - 10 important: 0.1ms each = 1.0ms total (high scrutiny) - 90 background: 0.01ms each = 0.9ms total (medium scrutiny) - Overhead: 0.1ms - Total: 2.0ms ✅ ## CORE CONCEPT #4: Hybrid Approaches (LOD for Simulation) The most powerful technique: Don't choose "simulate OR fake"—use BOTH with LOD. ### Simulation LOD Pyramid ``` ╱▔▔▔▔▔▔▔╲ ╱ ULTRA ╲ Player character ╱ (100% real) ╲ Boss enemy ╱________________╲ ╱ ╲ ╱ HIGH (90% real) ╲ Important NPCs ╱______________________╲ Combat enemies ╱ ╲ ╱ MEDIUM (50% hybrid) ╲ Nearby NPCs ╱____________________________╲ Visible crowd ╱ ╲ ╱ LOW (90% fake) ╲ Distant NPCs ╱__________________________________╲ Far traffic ╱ ╲ ╱ MINIMAL (100% fake/statistical) ╲ Off-screen ╱________________________________________╲ Bulk population ``` ### Example: NPC Simulation LOD **Level 0: ULTRA** (Player inspecting NPC) ```csharp class UltraDetailNPC { // Full simulation void Update() { UpdateNeedsEveryFrame(); // 0.1ms UpdateRelationshipsRealTime(); // 0.1ms UpdateGoalsEveryFrame(); // 0.1ms UpdatePathfindingRealTime(); // 0.2ms UpdateAnimationFullRig(); // 0.1ms // Total: 0.6ms } } ``` **Level 1: HIGH** (Important NPCs) ```csharp class HighDetailNPC { // Detailed simulation with optimizations void Update() { if (Time.frameCount % 10 == 0) // 6 fps for needs UpdateNeedsTick(); // 0.01ms if (Time.frameCount % 30 == 0) // 2 fps for relationships UpdateRelationshipsTick(); // 0.01ms UpdateGoalsEveryFrame(); // 0.05ms (simplified) UpdatePathfindingCached(); // 0.02ms (use cached paths) UpdateAnimationStandard(); // 0.05ms // Total: 0.14ms (per frame amortized) } } ``` **Level 2: MEDIUM** (Nearby NPCs) ```csharp class MediumDetailNPC { // Hybrid: State machine + minimal updates void Update() { if (Time.frameCount % 60 == 0) // 1 fps for needs UpdateNeedsStateMachine(); // 0.005ms (just state, not values) // No relationships (pre-assigned at spawn) UpdateStateMachine(); // 0.01ms (simple FSM) FollowWaypoints(); // 0.005ms (no pathfinding) UpdateAnimationLOD(); // 0.01ms // Total: 0.03ms (per frame amortized) } } ``` **Level 3: LOW** (Distant NPCs) ```csharp class LowDetailNPC { // Mostly fake: Scripted behavior void Update() { if (Time.frameCount % 300 == 0) // 0.2 fps { AdvanceScriptedPath(); // 0.001ms (just move along spline) } // No needs, no relationships, no AI UpdateAnimationMinimal(); // 0.005ms (simple walk cycle) // Total: 0.005ms (per frame amortized) } } ``` **Level 4: MINIMAL** (Off-screen / Far away) ```csharp class MinimalDetailNPC { // Fully fake: Statistical or frozen void Update() { // NO INDIVIDUAL UPDATES // Managed by PopulationManager in bulk } } class PopulationManager { void Update() { // Update entire population statistically if (Time.frameCount % 600 == 0) // 0.1 fps { UpdatePopulationStatistics(); // 0.1ms for ALL minimal NPCs } } } ``` ### LOD Transition Strategy **Smooth Transitions**: Avoid jarring switches between LOD levels. ```csharp class AdaptiveNPC { SimulationLevel currentLevel; float lodDistance = 50f; void Update() { float distance = Vector3.Distance(Camera.main.transform.position, transform.position); // Determine target LOD SimulationLevel targetLevel; if (isBeingInspected) targetLevel = SimulationLevel.Ultra; else if (isImportant && distance < lodDistance) targetLevel = SimulationLevel.High; else if (distance < lodDistance * 2) targetLevel = SimulationLevel.Medium; else if (distance < lodDistance * 4) targetLevel = SimulationLevel.Low; else targetLevel = SimulationLevel.Minimal; // Smooth transition if (targetLevel != currentLevel) { TransitionToLOD(targetLevel); currentLevel = targetLevel; } // Update at current level UpdateAtLevel(currentLevel); } void TransitionToLOD(SimulationLevel newLevel) { if (newLevel > currentLevel) // Upgrading { // Generate missing state from current state if (newLevel >= SimulationLevel.High && currentLevel < SimulationLevel.High) { // Add needs simulation needs.hunger = PredictHungerFromTimeAndActivity(); needs.energy = PredictEnergyFromTimeAndActivity(); } } else // Downgrading { // Cache important state, discard details if (newLevel < SimulationLevel.High) { // Freeze needs at current values cachedHunger = needs.hunger; needs = null; // GC will collect } } } } ``` **Result**: NPCs smoothly transition between detail levels as player moves camera, with no jarring pops or sudden behavior changes. ## CORE CONCEPT #5: Pre-Computation and Caching If something is PREDICTABLE, compute it ONCE and reuse. ### Pre-Computation Opportunities **Pattern**: Behavior repeats or follows patterns → pre-compute. **Example 1: Daily NPC Paths** NPCs follow same routes daily: - Home → Workplace (8am) - Workplace → Tavern (5pm) - Tavern → Home (10pm) **Bad** (recompute every day): ```csharp void Update() { if (GameTime.Hour == 8 && !hasComputedWorkPath) { path = Pathfinding.FindPath(home, workplace); // 2ms hasComputedWorkPath = true; } } // Cost: 2ms × 100 NPCs = 200ms spike at 8am ``` **Good** (pre-compute at spawn): ```csharp void Start() { // Compute all paths once pathToWork = Pathfinding.FindPath(home, workplace); pathToTavern = Pathfinding.FindPath(workplace, tavern); pathToHome = Pathfinding.FindPath(tavern, home); } void Update() { if (GameTime.Hour == 8) FollowPath(pathToWork); // 0.001ms (just interpolate) } // Cost: 2ms × 100 NPCs at spawn (spread over time), 0.1ms per frame ``` **Savings**: 200ms → 0.1ms (2000× faster) **Example 2: Crowd Animation** Background NPCs walk in circles. Instead of unique animations: ```csharp // Pre-compute animation offsets at spawn void Start() { animationOffset = Random.Range(0f, 1f); // Randomize start frame } void Update() { // All NPCs use same animation, different offsets float animTime = (Time.time + animationOffset) % animationClip.length; animator.Play("Walk", 0, animTime); } ``` Result: 100 NPCs use 1 animation clip, near-zero cost. ### Caching Strategy **Pattern**: Expensive computation with infrequent changes → cache result. **Example: Social Proximity Queries** Finding nearby NPCs for social interactions: **Bad** (compute every frame): ```csharp void Update() { Collider[] nearby = Physics.OverlapSphere(position, socialRadius); // 0.5ms foreach (var col in nearby) { NPC other = col.GetComponent(); InteractWith(other); } } // Cost: 0.5ms × 100 NPCs = 50ms ``` **Good** (cache and refresh slowly): ```csharp List cachedNearby = new List(); float cacheRefreshInterval = 5f; // Refresh every 5 seconds void Update() { if (Time.time > lastCacheRefresh + cacheRefreshInterval) { cachedNearby = FindNearbyNPCs(); // 0.5ms lastCacheRefresh = Time.time; } // Use cached list (free) foreach (var other in cachedNearby) InteractWith(other); } // Cost: 0.5ms × 100 NPCs / (5 seconds × 60 fps) = 0.16ms per frame ``` **Savings**: 50ms → 0.16ms (300× faster) ### Pre-Computation Checklist Ask for EVERY system: 1. ☐ Does this repeat? → Pre-compute once, replay 2. ☐ Can this be computed offline? → Bake into assets 3. ☐ Does this change slowly? → Cache and refresh infrequently 4. ☐ Is this deterministic? → Compute on-demand, cache result 5. ☐ Can this use lookup tables? → Replace computation with table lookup ## CORE CONCEPT #6: Statistical and Aggregate Simulation When you have MANY similar entities, simulate them as a GROUP, not individuals. ### Statistical Simulation Pattern **Concept**: Instead of tracking 1000 individual NPCs, track the POPULATION distribution. **Example: City Population** **Naïve Approach** (1000 individual NPCs): ```csharp class NPC { Vector3 position; Activity currentActivity; float hunger, energy, happiness; void Update() { UpdateNeeds(); UpdateActivity(); UpdatePosition(); } } // 1000 NPCs × 0.1ms each = 100ms ``` **Statistical Approach** (aggregate population): ```csharp class CityPopulation { int totalPopulation = 1000; // Distribution of activities Dictionary activityDistribution = new Dictionary() { { Activity.Working, 0.0f }, { Activity.Eating, 0.0f }, { Activity.Sleeping, 0.0f }, { Activity.Socializing, 0.0f }, }; // Average needs float averageHunger = 50f; float averageHappiness = 70f; void Update() { // Update distribution based on time of day float hour = GameTime.Hour; if (hour >= 8 && hour < 17) // Work hours { activityDistribution[Activity.Working] = 0.7f; activityDistribution[Activity.Eating] = 0.1f; activityDistribution[Activity.Socializing] = 0.2f; } else if (hour >= 22 || hour < 6) // Night { activityDistribution[Activity.Sleeping] = 0.9f; activityDistribution[Activity.Working] = 0.0f; } // Update average needs (simple model) averageHunger -= 1f * Time.deltaTime; if (activityDistribution[Activity.Eating] > 0.1f) averageHunger += 5f * Time.deltaTime; averageHappiness = Mathf.Lerp(averageHappiness, 70f, Time.deltaTime * 0.1f); } } // Cost: 0.01ms total (10,000× faster than 1000 individual NPCs) ``` **Visualization**: Spawn visible NPCs to match distribution: ```csharp class PopulationVisualizer { List visibleNPCs = new List(); int maxVisibleNPCs = 50; void Update() { // Spawn/despawn NPCs to match statistical distribution int targetWorking = (int)(maxVisibleNPCs * population.activityDistribution[Activity.Working]); int currentWorking = visibleNPCs.Count(n => n.activity == Activity.Working); if (currentWorking < targetWorking) SpawnWorkingNPC(); else if (currentWorking > targetWorking) DespawnWorkingNPC(); } } ``` **Result**: City FEELS like 1000 people, but only simulates 50 visible NPCs + aggregate stats. ### Aggregate Physics Example **Scenario**: 500 leaves falling from tree. **Individual Physics** (500 rigidbodies): ```csharp foreach (var leaf in leaves) { leaf.rigidbody.velocity += Physics.gravity * Time.deltaTime; leaf.rigidbody.AddForce(wind); } // Cost: 10ms+ (physics engine overhead) ``` **Aggregate Approach** (particle system + fake physics): ```csharp class LeafParticleSystem { ParticleSystem particles; void Start() { particles.maxParticles = 500; particles.gravityModifier = 1.0f; // Use particle system's built-in physics (GPU-accelerated) var velocityOverLifetime = particles.velocityOverLifetime; velocityOverLifetime.enabled = true; velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-1f, 1f); // Wind variation } } // Cost: 0.1ms (50× faster, GPU-accelerated) ``` **Result**: Leaves look real, but use particles instead of individual physics. ### When to Use Statistical Simulation Use statistical simulation when: - ✅ Entities are numerous (100+) - ✅ Entities are similar (same type/behavior) - ✅ Individual state doesn't affect gameplay - ✅ Player observes aggregate, not individuals - ✅ Performance is constrained Don't use when: - ❌ Player can inspect individuals - ❌ Individual state affects gameplay - ❌ Entities have unique behaviors - ❌ Small number of entities (<10) ## CORE CONCEPT #7: Cognitive Tricks and Illusions The human brain is TERRIBLE at noticing details. Exploit this. ### Perceptual Limits **Fact 1**: Humans can track ~4-7 objects simultaneously. - **Exploit**: Only simulate 5-10 NPCs in detail, rest can be simple **Fact 2**: Humans notice motion more than detail. - **Exploit**: Animate everything, even if behavior is fake **Fact 3**: Humans fill in gaps (pareidolia). - **Exploit**: Suggest behavior, let player imagine the rest **Fact 4**: Humans notice sudden changes, not gradual ones. - **Exploit**: Fade transitions, avoid instant pops **Fact 5**: Humans notice center-screen more than periphery. - **Exploit**: Focus simulation on camera center ### Technique #1: Theater of the Mind **Concept**: Show hints of a system, let player imagine it's fully simulated. **Example: Off-screen Combat** **Full Simulation**: ```csharp // Simulate entire battle off-screen foreach (var unit in offScreenUnits) { unit.FindTarget(); unit.Attack(); unit.TakeDamage(); } // Cost: High ``` **Theater of Mind**: ```csharp // Just play sound effects and show particles if (battleIsHappening) { if (Random.value < 0.1f) // 10% chance per frame PlayRandomCombatSound(); SpawnParticlesBeyondHill(); } // Cost: Negligible ``` **Result**: Player hears fighting, sees particles, assumes battle is happening. No actual simulation needed. ### Technique #2: Randomization Hides Patterns **Concept**: Random variation makes simple systems feel complex. **Example: NPC Idle Behavior** **Simple FSM**: ```csharp void Update() { if (state == Idle) { // Just stand still animator.Play("Idle"); } } // Looks robotic ``` **With Randomization**: ```csharp void Update() { if (state == Idle) { // Randomly look around, shift weight, scratch head if (Random.value < 0.01f) // 1% chance per frame { int randomAction = Random.Range(0, 3); switch (randomAction) { case 0: animator.Play("LookAround"); break; case 1: animator.Play("ShiftWeight"); break; case 2: animator.Play("ScratchHead"); break; } } } } // Looks alive ``` **Result**: Same simple FSM, but feels much more realistic. ### Technique #3: Persistence of Vision **Concept**: Objects that briefly disappear aren't scrutinized when they return. **Example: NPC Teleportation** **Problem**: NPC needs to move 500m, pathfinding is expensive. **Solution**: Hide, teleport, reveal. ```csharp void TravelToLocation(Vector3 destination) { // Walk behind building WalkTo(nearestOccluder); // When occluded, teleport if (IsOccluded()) { transform.position = destination; } // Walk out from destination } ``` **Result**: Player sees NPC walk behind building, then emerge at destination. Brain fills in the gap. ### Technique #4: Focal Point Misdirection **Concept**: Players look where you direct them, not at background. **Example: Crowd During Cutscene** During cutscene, player watches characters talking: ```csharp void PlayCutscene() { // Focus camera on speakers Camera.main.FocusOn(speaker); // Background crowd? Freeze them. foreach (var npc in backgroundNPCs) { npc.Freeze(); // No simulation } } ``` **Result**: Player never notices background is frozen because they're watching speakers. ### Technique #5: Temporal Aliasing **Concept**: If something changes slower than perception threshold, fake it. **Example: Distant Vehicle Traffic** Far-away cars (200m+) change slowly from player POV: ```csharp void UpdateDistantTraffic() { // Only update every 2 seconds if (Time.frameCount % 120 == 0) { foreach (var car in distantCars) { // Teleport along path (2-second jumps) car.position += car.velocity * 2.0f; } } } ``` **Result**: At 200m distance, 2-second jumps are imperceptible. Saves 119/120 frames of updates. ## DECISION FRAMEWORK #1: Scrutiny-Based LOD **Use this framework for EVERY simulation system.** ### Step 1: Identify Scrutiny Levels For each entity type, determine scrutiny levels: ``` EXAMPLE: City Builder NPCs Scrutiny Levels: • EXTREME: Player clicking "Inspect" button (shows detailed stats) • HIGH: Player-selected NPCs (mayor, quest givers) • MEDIUM: On-screen NPCs within 50m • LOW: On-screen NPCs beyond 50m • MINIMAL: Off-screen NPCs ``` ### Step 2: Assign Simulation Tiers For each scrutiny level, define simulation tier: ``` EXAMPLE TIERS: EXTREME Scrutiny: ✓ Full needs simulation (hunger, energy, social, updated every frame) ✓ Full pathfinding (A* with dynamic obstacles) ✓ Full social system (track relationships, update in real-time) ✓ Full animation (all body parts, IK, facial expressions) HIGH Scrutiny: ✓ Tick-based needs (updated every 10 seconds) ✓ Cached pathfinding (pre-computed paths, no A*) ✓ Simplified social (static friend list) ✓ Standard animation (body only, no IK/face) MEDIUM Scrutiny: ✓ State-machine behavior (no needs simulation) ✓ Waypoint following (no pathfinding) ✓ No social system ✓ LOD animation (lower frame rate) LOW Scrutiny: ✓ Scripted movement (spline-based) ✓ No AI ✓ Minimal animation (simple walk cycle) MINIMAL Scrutiny: ✓ Statistical (bulk population model) ✓ No individual entities ``` ### Step 3: Implement LOD Thresholds ```csharp SimulationTier DetermineSimulationTier(NPC npc) { // EXTREME: Player inspecting if (npc == PlayerSelection.inspectedNPC) return SimulationTier.Extreme; float distance = Vector3.Distance(npc.position, Camera.main.transform.position); bool isVisible = IsVisibleToCamera(npc); // HIGH: Important and visible if (npc.isImportant && isVisible && distance < 50f) return SimulationTier.High; // MEDIUM: Visible and nearby if (isVisible && distance < 50f) return SimulationTier.Medium; // LOW: Visible but distant if (isVisible && distance < 200f) return SimulationTier.Low; // MINIMAL: Off-screen or very distant return SimulationTier.Minimal; } ``` ### Step 4: Update Based on Tier ```csharp void Update() { SimulationTier tier = DetermineSimulationTier(this); switch (tier) { case SimulationTier.Extreme: UpdateFullSimulation(); break; case SimulationTier.High: UpdateHighDetailSimulation(); break; case SimulationTier.Medium: UpdateMediumDetailSimulation(); break; case SimulationTier.Low: UpdateLowDetailSimulation(); break; case SimulationTier.Minimal: // No update (handled by PopulationManager) break; } } ``` ## DECISION FRAMEWORK #2: Gameplay Relevance ### Step 1: Classify Systems For every simulation system, classify: ``` SYSTEM: NPC Hunger Questions: Q1: Does it affect win/lose conditions? → NO Q2: Does it affect player progression? → YES (unhappy NPCs leave city → lose citizens → fail) Q3: Can player directly interact with it? → YES (player can build food sources) CLASSIFICATION: GAMEPLAY-CRITICAL STRATEGY: Simulate accurately ``` ``` SYSTEM: NPC Sleep Schedules Questions: Q1: Does it affect win/lose conditions? → NO Q2: Does it affect player progression? → NO Q3: Can player directly interact with it? → NO Q4: Is it observable? → YES (NPCs visibly sleep at night) CLASSIFICATION: COSMETIC-OBSERVABLE STRATEGY: Fake (schedule-based, no simulation) ``` ### Step 2: Apply Strategy Matrix | Classification | Strategy | Implementation | |---------------|----------|----------------| | **Gameplay-Critical** | ALWAYS SIMULATE | Full accuracy, no shortcuts | | **Gameplay-Significant** | SIMULATE WHEN RELEVANT | Full sim when player cares, fake otherwise | | **Cosmetic-Observable** | FAKE CONVINCINGLY | No sim, just appearance | | **Cosmetic-Unobservable** | REMOVE OR FAKE | Cut it or use statistics | ### Step 3: Implementation Examples **Gameplay-Critical** (Enemy Health): ```csharp class Enemy { float health = 100f; void TakeDamage(float damage) { health -= damage; // Precise calculation if (health <= 0) Die(); } } ``` **Cosmetic-Observable** (Background Birds): ```csharp class BirdFlock { void Update() { // Fake: Use boids algorithm (cheap), not real physics foreach (var bird in birds) { bird.position += bird.velocity * Time.deltaTime; bird.velocity += CalculateBoidsForce(bird); // Simple math } } } ``` **Cosmetic-Unobservable** (Distant City Lights): ```csharp class CityLights { void Update() { // Remove: Don't simulate, just use emissive texture // Lights turn on/off based on time of day (shader-driven) } } ``` ## DECISION FRAMEWORK #3: Performance-First Design ### Step 1: Start with Budget **ALWAYS start with performance budget, then design within constraints.** ``` EXAMPLE: RTS with 500 units Frame Budget: 16.67ms (60 FPS) Unit Simulation Budget: 4ms Budget per Unit: 4ms / 500 = 0.008ms = 8 microseconds This is VERY tight. Can't afford complex AI. Design Constraints: • No pathfinding per frame (too expensive) • No complex collision checks • Simple state machines only • Bulk operations where possible ``` ### Step 2: Profile Early Don't wait until the end to optimize. Profile DURING design. ```csharp void PrototypeSystem() { // Create minimal version for (int i = 0; i < 500; i++) { units.Add(new Unit()); } // Profile immediately using (new ProfilerScope("Unit_Update")) { foreach (var unit in units) { unit.Update(); } } // Check results // If > 4ms, simplify BEFORE adding more features } ``` ### Step 3: Identify Bottlenecks ``` Profile Results: Unit_Update: 6.5ms ⚠️ (1.6× over budget) ├─ Pathfinding: 3.2ms (49%) ← BOTTLENECK ├─ Combat: 1.8ms (28%) ├─ Animation: 0.9ms (14%) └─ Other: 0.6ms (9%) Action: Fix pathfinding first (biggest impact) ``` ### Step 4: Optimize Bottlenecks **Before** (3.2ms for pathfinding): ```csharp void Update() { if (needsNewPath) { path = Pathfinding.FindPath(position, target); // 3.2ms } } ``` **After** (0.1ms for pathfinding): ```csharp // Time-slice: Only path 10 units per frame static Queue pathfindingQueue = new Queue(); static int maxPathsPerFrame = 10; void Update() { if (needsNewPath && !pathfindingQueue.Contains(this)) { pathfindingQueue.Enqueue(this); } } static void UpdatePathfinding() { for (int i = 0; i < maxPathsPerFrame && pathfindingQueue.Count > 0; i++) { Unit unit = pathfindingQueue.Dequeue(); unit.path = Pathfinding.FindPath(unit.position, unit.target); } } // 500 units need paths → 50 frames to complete all (acceptable) // Cost per frame: 10 paths × 0.01ms = 0.1ms ``` **Result**: 3.2ms → 0.1ms (32× faster) ### Step 5: Iterate ``` NEW Profile Results: Unit_Update: 2.4ms ✅ (within budget!) ├─ Combat: 1.8ms (75%) ├─ Animation: 0.5ms (21%) ├─ Pathfinding: 0.1ms (4%) Budget Remaining: 4ms - 2.4ms = 1.6ms Can now add more features within remaining budget. ``` ## DECISION FRAMEWORK #4: Pragmatic Trade-offs Balance perfection vs time-to-ship. ### The Trade-off Triangle ``` QUALITY / \\ / \\ / \\ / \\ / PICK \\ / TWO \\ /_____________\\ SPEED SCOPE ``` **Reality**: You can't have all three. Choose wisely. ### Example Scenarios **Scenario 1: Prototype (Speed + Scope)** ``` Goal: Prove concept in 2 weeks Strategy: Sacrifice quality • Fake everything possible • Hard-code values • Skip edge cases • No optimization • Placeholder art ``` **Scenario 2: Production (Quality + Scope)** ``` Goal: Ship polished game in 1 year Strategy: Take time needed • Implement properly • Optimize carefully • Handle edge cases • Polish visuals • Iterate on feedback ``` **Scenario 3: Jam/Demo (Speed + Quality)** ``` Goal: Impressive demo in 48 hours Strategy: Reduce scope aggressively • Single level • One mechanic • Fake everything not shown • Polish what player sees • Cut everything else ``` ### Decision Matrix | Context | Time Budget | Quality Target | Strategy | |---------|-------------|---------------|----------| | **Prototype** | 1-2 weeks | Working, ugly | Fake everything, prove concept | | **Vertical Slice** | 1-2 months | Polished sample | Full quality for slice, fake rest | | **Alpha** | 3-6 months | Feature-complete | Broad features, low polish | | **Beta** | 6-12 months | Optimized | Optimize critical paths, fake background | | **Gold** | 12+ months | Shippable | Polish everything visible | ### Pragmatic Simulation Choices **Prototype Stage**: ```csharp // FAKE: Hard-coded schedule void Update() { if (GameTime.Hour == 8) transform.position = workplacePosition; // Teleport! } ``` **Alpha Stage**: ```csharp // BASIC SIMULATION: Simple pathfinding void Update() { if (GameTime.Hour == 8 && !atWorkplace) agent.SetDestination(workplacePosition); } ``` **Beta Stage**: ```csharp // OPTIMIZED SIMULATION: Pre-computed paths void Update() { if (GameTime.Hour == 8 && !atWorkplace) FollowPrecomputedPath(pathToWork); } ``` **Gold Stage**: ```csharp // POLISHED: LOD system, smooth transitions void Update() { SimulationTier tier = DetermineSimulationTier(); if (GameTime.Hour == 8 && !atWorkplace) { if (tier >= SimulationTier.High) FollowPrecomputedPath(pathToWork); else TeleportToWork(); // Still fake for low-detail NPCs! } } ``` **Key Insight**: Even at Gold, background NPCs still fake (teleport). Polish doesn't mean simulate everything—it means simulate what matters. ## IMPLEMENTATION PATTERN #1: Tick-Based Updates **Problem**: Systems update every frame but change slowly. **Solution**: Update on a schedule, not every frame. ### Basic Tick System ```csharp public class TickManager : MonoBehaviour { public static TickManager Instance; public event Action OnSlowTick; // 1 Hz (every 1 second) public event Action OnMediumTick; // 10 Hz (every 0.1 seconds) public event Action OnFastTick; // 30 Hz (every 0.033 seconds) private float slowTickInterval = 1.0f; private float mediumTickInterval = 0.1f; private float fastTickInterval = 0.033f; private float lastSlowTick, lastMediumTick, lastFastTick; void Update() { float time = Time.time; if (time - lastSlowTick >= slowTickInterval) { OnSlowTick?.Invoke(); lastSlowTick = time; } if (time - lastMediumTick >= mediumTickInterval) { OnMediumTick?.Invoke(); lastMediumTick = time; } if (time - lastFastTick >= fastTickInterval) { OnFastTick?.Invoke(); lastFastTick = time; } } } ``` ### Usage Example ```csharp class NPC : MonoBehaviour { void Start() { // Subscribe to appropriate tick rate TickManager.Instance.OnSlowTick += UpdateNeeds; TickManager.Instance.OnMediumTick += UpdateBehavior; } void UpdateNeeds() { // Slow-changing systems (1 Hz) hunger -= 5.0f; energy -= 3.0f; } void UpdateBehavior() { // Medium-speed systems (10 Hz) UpdateStateMachine(); CheckGoals(); } void Update() { // Fast systems (every frame) UpdateAnimation(); UpdateVisuals(); } } ``` **Performance Gain**: 3 systems × 60 FPS = 180 updates/sec → (1 + 10 + 60) = 71 updates/sec (2.5× faster) ## IMPLEMENTATION PATTERN #2: Time-Slicing **Problem**: 100 entities need expensive updates, but not every frame. **Solution**: Stagger updates across multiple frames. ### Time-Slicing System ```csharp public class TimeSlicedUpdater where T : class { private List entities = new List(); private int entitiesPerFrame; private int currentIndex = 0; public TimeSlicedUpdater(int entitiesPerFrame) { this.entitiesPerFrame = entitiesPerFrame; } public void Register(T entity) { entities.Add(entity); } public void Unregister(T entity) { entities.Remove(entity); } public void Update(Action updateFunc) { int count = Mathf.Min(entitiesPerFrame, entities.Count); for (int i = 0; i < count; i++) { if (currentIndex >= entities.Count) currentIndex = 0; updateFunc(entities[currentIndex]); currentIndex++; } } } ``` ### Usage Example ```csharp public class NPCManager : MonoBehaviour { private TimeSlicedUpdater npcUpdater; void Start() { // Update 10 NPCs per frame (100 NPCs = 10 frames for full update) npcUpdater = new TimeSlicedUpdater(10); foreach (var npc in allNPCs) npcUpdater.Register(npc); } void Update() { npcUpdater.Update(npc => { npc.UpdateExpensiveLogic(); }); } } ``` **Performance Gain**: 100 NPCs × 2ms = 200ms → 10 NPCs × 2ms = 20ms (10× faster) **Trade-off**: Each NPC updates every 10 frames instead of every frame. For slow-changing systems, this is imperceptible. ## IMPLEMENTATION PATTERN #3: Lazy State Generation **Problem**: Storing full state for all entities wastes memory. **Solution**: Generate state on-demand when needed. ### Lazy State System ```csharp class BackgroundNPC { // Minimal stored state public int id; public Vector3 position; public Activity currentActivity; // Expensive state (generated on-demand) private NPCDetailedState _cachedDetails = null; public NPCDetailedState GetDetails() { if (_cachedDetails == null) { _cachedDetails = GenerateDetails(); } return _cachedDetails; } private NPCDetailedState GenerateDetails() { // Procedurally generate detailed state return new NPCDetailedState { name = NameGenerator.Generate(id), backstory = StoryGenerator.Generate(id), friends = FriendGenerator.GenerateFriends(id, position), hunger = PredictHunger(currentActivity, GameTime.Hour), energy = PredictEnergy(currentActivity, GameTime.Hour), personality = PersonalityGenerator.Generate(id), }; } public void InvalidateCache() { _cachedDetails = null; // Clear cache when state changes significantly } } ``` ### Predictive State Generation ```csharp float PredictHunger(Activity activity, float hour) { // Use time-of-day to predict plausible hunger value float hoursSinceLastMeal = hour - 12.0f; // Assume lunch at 12pm if (hoursSinceLastMeal < 0) hoursSinceLastMeal += 24; float hunger = 100 - (hoursSinceLastMeal * 5.0f); return Mathf.Clamp(hunger, 0, 100); } ``` **Result**: NPC appears to have persistent state, but it's generated on-demand using seed (id) and current time. ## IMPLEMENTATION PATTERN #4: State Prediction **Problem**: Background NPCs need plausible state when inspected. **Solution**: Predict what state SHOULD be based on context. ### Prediction Functions ```csharp class NPCStatePredictor { public static float PredictHunger(NPC npc, float currentHour) { // Hunger decreases linearly, resets at meal times float hungerDecayRate = 5.0f; // per hour // Determine time since last meal float[] mealTimes = { 7.0f, 12.0f, 19.0f }; // Breakfast, lunch, dinner float timeSinceLastMeal = CalculateTimeSinceLastEvent(currentHour, mealTimes); float hunger = 100 - (timeSinceLastMeal * hungerDecayRate); return Mathf.Clamp(hunger, 0, 100); } public static float PredictEnergy(NPC npc, float currentHour) { // Energy low during day, high after sleep if (currentHour >= 6 && currentHour < 22) // Awake { float hoursAwake = currentHour - 6; return Mathf.Clamp(100 - (hoursAwake * 6.25f), 20, 100); // 20% min } else // Sleeping { return 100f; } } public static Activity PredictActivity(NPC npc, float currentHour) { // Simple schedule-based prediction if (currentHour >= 22 || currentHour < 6) return Activity.Sleeping; else if (currentHour >= 8 && currentHour < 17) return Activity.Working; else if (currentHour >= 18 && currentHour < 22) return Activity.Socializing; else return Activity.Eating; } private static float CalculateTimeSinceLastEvent(float currentTime, float[] eventTimes) { float minDelta = float.MaxValue; foreach (float eventTime in eventTimes) { float delta = currentTime - eventTime; if (delta < 0) delta += 24; // Wrap around if (delta < minDelta) minDelta = delta; } return minDelta; } } ``` ### Usage ```csharp void OnPlayerInspectNPC(NPC npc) { if (npc.simulationLevel == SimLevel.Fake) { // Generate plausible state npc.hunger = NPCStatePredictor.PredictHunger(npc, GameTime.Hour); npc.energy = NPCStatePredictor.PredictEnergy(npc, GameTime.Hour); npc.currentActivity = NPCStatePredictor.PredictActivity(npc, GameTime.Hour); // Upgrade to real simulation npc.simulationLevel = SimLevel.Real; } // Show UI with generated state UI.ShowNPCDetails(npc); } ``` **Result**: Player inspects background NPC, sees plausible stats that match time-of-day. NPC appears to have been simulated all along. ## IMPLEMENTATION PATTERN #5: Pre-Computed Paths **Problem**: NPCs follow predictable routes, but pathfinding is expensive. **Solution**: Compute paths once, store as waypoints, replay. ### Pre-Computation System ```csharp class NPCPathDatabase : MonoBehaviour { private Dictionary<(Vector3, Vector3), Path> pathCache = new Dictionary<(Vector3, Vector3), Path>(); public void PrecomputeCommonPaths() { // Pre-compute paths between common locations var homes = FindObjectsOfType(); var workplaces = FindObjectsOfType(); var taverns = FindObjectsOfType(); foreach (var home in homes) { foreach (var workplace in workplaces) { Path path = Pathfinding.FindPath(home.position, workplace.position); pathCache[(home.position, workplace.position)] = path; } foreach (var tavern in taverns) { Path path = Pathfinding.FindPath(home.position, tavern.position); pathCache[(home.position, tavern.position)] = path; } } Debug.Log($"Pre-computed {pathCache.Count} paths"); } public Path GetPath(Vector3 from, Vector3 to) { // Round to nearest grid cell (for cache hits) Vector3 fromKey = RoundToGrid(from); Vector3 toKey = RoundToGrid(to); if (pathCache.TryGetValue((fromKey, toKey), out Path path)) { return path; } else { // Fallback: compute on-demand (rare) return Pathfinding.FindPath(from, to); } } private Vector3 RoundToGrid(Vector3 pos) { float gridSize = 5f; return new Vector3( Mathf.Round(pos.x / gridSize) * gridSize, pos.y, Mathf.Round(pos.z / gridSize) * gridSize ); } } ``` ### Path Following ```csharp class NPC : MonoBehaviour { private Path currentPath; private int waypointIndex = 0; public void StartPath(Vector3 destination) { currentPath = NPCPathDatabase.Instance.GetPath(transform.position, destination); waypointIndex = 0; } void Update() { if (currentPath != null && waypointIndex < currentPath.waypoints.Count) { Vector3 target = currentPath.waypoints[waypointIndex]; transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime); if (Vector3.Distance(transform.position, target) < 0.5f) { waypointIndex++; } } } } ``` **Performance Gain**: Pathfinding cost moves from runtime (2ms per path) to startup (pre-computed once). ## IMPLEMENTATION PATTERN #6: Event-Driven State Changes **Problem**: Polling for state changes wastes CPU. **Solution**: Use events to trigger updates only when needed. ### Event System ```csharp public class GameClock : MonoBehaviour { public static GameClock Instance; public event Action OnHourChanged; public event Action OnDayChanged; private float currentHour = 6f; private int lastHourTriggered = 6; void Update() { currentHour += Time.deltaTime / 60f; // 1 game hour = 1 real minute if (currentHour >= 24f) { currentHour -= 24f; OnDayChanged?.Invoke(currentHour); } int hourInt = Mathf.FloorToInt(currentHour); if (hourInt != lastHourTriggered) { OnHourChanged?.Invoke(hourInt); lastHourTriggered = hourInt; } } } ``` ### Event-Driven NPC ```csharp class NPC : MonoBehaviour { void Start() { // Subscribe to time events GameClock.Instance.OnHourChanged += OnHourChanged; } void OnHourChanged(int hour) { // React to specific hours switch (hour) { case 6: WakeUp(); break; case 8: GoToWork(); break; case 17: LeaveWork(); break; case 22: GoToSleep(); break; } } // No Update() needed for schedule! } ``` **Performance Gain**: 100 NPCs × 60 FPS × time-check = 6,000 checks/sec → 100 NPCs × 24 events/day = 2,400 events/day (negligible) ## IMPLEMENTATION PATTERN #7: Hybrid Real-Fake Transition **Problem**: NPC transitions from background (fake) to foreground (real) are jarring. **Solution**: Smooth transition with state interpolation. ### Hybrid NPC System ```csharp class HybridNPC : MonoBehaviour { public enum SimulationMode { Fake, Transitioning, Real } private SimulationMode currentMode = SimulationMode.Fake; private float transitionProgress = 0f; // Fake state (minimal) private Activity scheduledActivity; // Real state (detailed) private NPCNeeds needs; private AIBehavior ai; void Update() { // Determine target mode based on scrutiny SimulationMode targetMode = DetermineTargetMode(); // Handle transitions if (targetMode != currentMode) { if (targetMode == SimulationMode.Real && currentMode == SimulationMode.Fake) { StartTransitionToReal(); } else if (targetMode == SimulationMode.Fake && currentMode == SimulationMode.Real) { StartTransitionToFake(); } } // Update based on current mode switch (currentMode) { case SimulationMode.Fake: UpdateFake(); break; case SimulationMode.Transitioning: UpdateTransition(); break; case SimulationMode.Real: UpdateReal(); break; } } SimulationMode DetermineTargetMode() { float distance = Vector3.Distance(transform.position, Camera.main.transform.position); if (distance < 30f || isImportant) return SimulationMode.Real; else return SimulationMode.Fake; } void StartTransitionToReal() { currentMode = SimulationMode.Transitioning; transitionProgress = 0f; // Initialize real state from fake state needs = new NPCNeeds(); needs.hunger = NPCStatePredictor.PredictHunger(this, GameTime.Hour); needs.energy = NPCStatePredictor.PredictEnergy(this, GameTime.Hour); ai = new AIBehavior(); ai.currentActivity = scheduledActivity; } void UpdateTransition() { transitionProgress += Time.deltaTime * 2f; // 0.5 second transition if (transitionProgress >= 1f) { currentMode = SimulationMode.Real; } // Blend between fake and real UpdateFake(); UpdateReal(); } void StartTransitionToFake() { currentMode = SimulationMode.Transitioning; transitionProgress = 0f; // Cache important state scheduledActivity = ai.currentActivity; } void UpdateFake() { // Simple schedule-based behavior scheduledActivity = NPCStatePredictor.PredictActivity(this, GameTime.Hour); // Follow scripted path FollowScheduledPath(); } void UpdateReal() { // Full simulation needs.Update(); ai.Update(needs); // Pathfinding, social, etc. } } ``` **Result**: Smooth fade between fake and real simulation, no jarring pops. ## COMMON PITFALL #1: Over-Simulating Background Elements ### The Mistake Simulating systems at full detail even when player can't observe them. **Example**: ```csharp void Update() { foreach (var npc in allNPCs) // ALL 1000 NPCs { npc.UpdateNeeds(); // Full simulation npc.UpdateAI(); npc.UpdatePhysics(); } } // Cost: 1000 NPCs × 0.1ms = 100ms (6× frame budget) ``` ### Why It Happens - **Perfectionism**: "It should be realistic!" - **Lack of profiling**: Didn't measure cost - **No scrutiny awareness**: Treated all NPCs equally ### The Fix **LOD System**: ```csharp void Update() { // Only simulate visible/important NPCs foreach (var npc in visibleNPCs) // Only 50 visible { if (npc.isImportant) npc.UpdateFullSimulation(); else npc.UpdateSimplifiedSimulation(); } // Off-screen NPCs: bulk update PopulationManager.UpdateOffScreenNPCs(); } // Cost: 10 important × 0.1ms + 40 visible × 0.01ms + 0.5ms bulk = 2.4ms ✅ ``` ### Prevention ✅ **Always** classify entities by scrutiny level ✅ **Always** profile early ✅ **Always** use LOD for simulation, not just rendering ## COMMON PITFALL #2: Under-Simulating Critical Systems ### The Mistake Faking systems that player CAN notice or that affect gameplay. **Example**: ```csharp // Enemy health is rounded to nearest 10% void TakeDamage(float damage) { health = Mathf.Round((health - damage) / 10f) * 10f; } // Player deals 35 damage, sees enemy lose 40 health (off by 5!) // Breaks game feel ``` ### Why It Happens - **Over-optimization**: "Let's round for performance!" - **Misunderstanding scrutiny**: Assumed player wouldn't notice - **No playtesting**: Didn't verify impact ### The Fix **Don't fake gameplay-critical systems**: ```csharp void TakeDamage(float damage) { health -= damage; // Precise, no rounding } // Cost: Negligible, but preserves game feel ``` ### Prevention ✅ **Never** fake systems that affect win/lose ✅ **Never** fake systems player directly interacts with ✅ **Always** playtest optimizations ## COMMON PITFALL #3: Jarring Transitions ### The Mistake Instant transitions between fake and real states. **Example**: ```csharp // Background NPC: frozen state npc.hunger = 50f; // Static // Player clicks to inspect void OnInspect() { npc.StartSimulation(); // Suddenly hunger changes! // Player sees: 50 → 47 → 44 → 41... (obvious!) } ``` ### Why It Happens - **No transition plan**: Didn't consider upgrade path - **Binary thinking**: Fake OR real, no in-between ### The Fix **Generate plausible state on transition**: ```csharp void OnInspect() { // Generate state that matches current time/activity npc.hunger = PredictHungerFromTimeOfDay(); npc.energy = PredictEnergyFromTimeOfDay(); // Start simulation from predicted state npc.StartSimulation(); } // Player sees consistent state ``` ### Prevention ✅ **Always** plan transition from fake → real ✅ **Always** generate plausible state on upgrade ✅ **Test** by rapidly switching between states ## COMMON PITFALL #4: No Performance Budgeting ### The Mistake Implementing systems without measuring cost or setting limits. **Example**: ```csharp // Implemented social system without profiling void Update() { foreach (var npc in allNPCs) { Collider[] nearby = Physics.OverlapSphere(npc.position, 10f); // ... process nearby NPCs } } // LATER: Discovers this takes 50ms (3× frame budget) ``` ### Why It Happens - **Premature implementation**: Coded before designing - **No profiling**: "It should be fine..." - **No budget**: Didn't allocate time budget upfront ### The Fix **Budget first, implement within constraints**: ``` Budget: 2ms for social system Reality check: 100 NPCs × Physics.OverlapSphere (0.5ms each) = 50ms ❌ New design: • Time-slice: 10 queries per frame = 5ms • Cache results: Refresh every 5 seconds = 1ms amortized • Spatial grid: Pre-partition space = 0.1ms ✅ Implement cached spatial grid approach. ``` ### Prevention ✅ **Always** set performance budget before implementing ✅ **Always** profile prototype before building full system ✅ **Always** measure, don't guess ## COMMON PITFALL #5: Synchronous Behavior ### The Mistake All entities do the same thing at the same time. **Example**: ```csharp // All NPCs go to work at exactly 8:00am if (GameTime.Hour == 8) { GoToWork(); } // Result: 100 NPCs path simultaneously → 200ms spike // Visual: Everyone leaves home at exact same time (robotic) ``` ### Why It Happens - **Simple logic**: Exact schedules are easy to code - **No randomization**: Forgot to add variance ### The Fix **Add variance and staggering**: ```csharp // Each NPC has slightly different schedule void Start() { workStartTime = 8f + Random.Range(-0.5f, 0.5f); // 7:30-8:30am } void Update() { if (GameTime.Hour >= workStartTime && !hasGoneToWork) { GoToWork(); hasGoneToWork = true; } } // Performance: Spread 100 path requests over 1 hour = smooth // Visual: NPCs leave gradually = realistic ``` ### Prevention ✅ **Always** add variance to schedules ✅ **Always** stagger expensive operations ✅ **Test** with many entities to spot patterns ## COMMON PITFALL #6: Binary All-or-Nothing Thinking ### The Mistake Assuming simulation is binary: full detail OR nothing. **Example**: ```csharp // Thought process: // "We need NPC hunger system, so we'll simulate all 100 NPCs" // OR // "We can't afford hunger system, so we'll remove it entirely" // Missing: Hybrid options! ``` ### Why It Happens - **Lack of framework**: Doesn't know hybrid approaches exist - **Inexperience**: Haven't seen LOD systems ### The Fix **Use hybrid spectrum**: ``` Option 1 (FULL): Simulate all 100 NPCs, all needs, every frame Cost: 100ms ❌ Option 2 (HYBRID-A): Simulate 10 important, fake 90 background Cost: 5ms ✅ Option 3 (HYBRID-B): Tick-based updates, all NPCs Cost: 2ms ✅ Option 4 (MINIMAL): Remove hunger, use happiness only Cost: 0.5ms ✅ Choose based on gameplay need and budget. ``` ### Prevention ✅ **Always** consider spectrum of options ✅ **Always** use LOD, not binary ✅ **Study** existing games (they use hybrids) ## COMMON PITFALL #7: Ignoring Development Time ### The Mistake Proposing complex solutions without considering implementation time. **Example**: ``` "Implement full ecosystem with: • Predator-prey relationships • Food web dynamics • Seasonal migration • Population genetics • Disease spread" Reality: This would take 6 months. Deadline is 2 weeks. ``` ### Why It Happens - **Enthusiasm**: Excited about cool systems - **No project management**: Doesn't consider timeline ### The Fix **Pragmatic scoping**: ``` Week 1: Simple predator-prey (just rabbits and foxes) Week 2: Polish and balance Post-launch (if time): • Add more species • Add migration • Add genetics ``` ### Prevention ✅ **Always** consider development time in proposals ✅ **Always** start with MVP (minimum viable product) ✅ **Always** scope to timeline, not dreams ## REAL-WORLD EXAMPLE #1: Hitman Crowds **Game**: Hitman (2016-2021) **Challenge**: Hundreds of NPCs in crowded locations (Paris fashion show, Mumbai streets). **Solution**: Multi-tier LOD system ### Implementation **Tier 1: Hero NPCs** (~20) - Full AI (behavior trees) - Detailed animation - Can be disguised as - React to player actions - Cost: High **Tier 2: Featured NPCs** (~50) - Simplified AI (state machines) - Standard animation - Can be interacted with - React to nearby events - Cost: Medium **Tier 3: Background Crowd** (~200) - No AI (scripted paths) - LOD animation - Can't interact - Don't react - Cost: Low **Tier 4: Distant Crowd** (~500+) - Particle system or imposters - No individual entities - Cost: Negligible ### Key Techniques 1. **Disguise targets are Hero NPCs**: Player can inspect → full simulation 2. **Nearby NPCs upgrade on approach**: Tier 3 → Tier 2 when player gets close 3. **Crowd Flow**: Background NPCs follow spline paths, no pathfinding 4. **Reactions**: Only nearby NPCs react to player actions (gunshots, bodies) ### Result - Feels like 1000+ people - Actually simulates ~70 in detail - 60 FPS on console **Lesson**: Player never knows background is faked because focus is on hero NPCs. ## REAL-WORLD EXAMPLE #2: GTA V Traffic **Game**: Grand Theft Auto V **Challenge**: Massive city with constant traffic, 60+ vehicles visible. **Solution**: Hybrid real-fake traffic system ### Implementation **Near Player** (0-50m): Real vehicles - Full physics (collisions, suspension) - Detailed AI (lane changes, turns, reactions) - High-poly models **Medium Distance** (50-150m): Simplified vehicles - Simplified physics (kinematic) - Scripted behavior (follow spline) - Medium-poly models **Far Distance** (150-300m): Fake vehicles - No physics (transform only) - No AI (just move along road) - Low-poly models or imposters **Off-Screen**: No vehicles - Vehicles despawn when out of view - New vehicles spawn ahead of player ### Key Techniques 1. **Spawning**: Vehicles spawn just beyond player's view, despawn when far behind 2. **Transition**: Vehicle smoothly upgrades from fake → simplified → real as player approaches 3. **Parked Cars**: Static props (not vehicles) until player gets very close 4. **Highway Traffic**: Uses particle system at far distances (just moving dots) ### Result - City feels alive with traffic - Actually simulates ~30 vehicles in detail - Scales from 0 (empty road) to 60+ (highway) dynamically **Lesson**: Traffic LOD based on distance. Player never notices because transitions are smooth. ## REAL-WORLD EXAMPLE #3: Red Dead Redemption 2 Ecosystem **Game**: Red Dead Redemption 2 **Challenge**: Living ecosystem with animals, hunting, predator-prey. **Solution**: Hybrid simulation-statistical system ### Implementation **Near Player** (0-100m): Full simulation - Individual animals with AI - Predator-prey behaviors - Hunting mechanics - Can be killed/skinned **Medium Distance** (100-300m): Simplified simulation - Reduced update rate - Simplified behaviors - Can still be shot (for sniping) **Far Distance** (300m+): Statistical - No individual animals - Population density map - Spawn animals when player enters region **Off-Screen**: Statistical model - Track population levels - Simulate hunting pressure - Repopulate over time ### Key Techniques 1. **Population Density**: Each region has animal density (high/medium/low) 2. **Overhunting**: If player kills too many deer, density decreases 3. **Recovery**: Population recovers over in-game days 4. **Spawning**: Animals spawn just outside player's view, matching density 5. **Migration**: Statistical model moves populations between regions ### Result - Ecosystem feels dynamic and responsive - Overhunting has consequences - Performance is manageable **Lesson**: Combine local simulation (what player sees) with global statistics (what player doesn't see). ## REAL-WORLD EXAMPLE #4: The Sims 4 Needs System **Game**: The Sims 4 **Challenge**: Needs system (hunger, bladder, energy, social, fun, hygiene) for all Sims. **Solution**: LOD based on player control and visibility ### Implementation **Active Household** (1-8 Sims): Full simulation - All needs simulated every tick - Full AI for autonomy - Detailed animations **Same Lot** (up to 20 Sims): Simplified simulation - Needs updated less frequently - Simplified AI - Standard animations **Off-Lot** (neighborhood Sims): Minimal simulation - Needs update very slowly - No AI (time-advances their schedule) - No rendering **World Population**: Statistical - "Story progression" (birth, death, aging) - No individual needs simulation - State advances on schedule ### Key Techniques 1. **Lot-Based LOD**: Simulation detail tied to physical location 2. **Schedule Advancement**: Off-lot Sims teleport through their schedule 3. **Needs Freezing**: Off-lot Sims' needs decay very slowly 4. **Pre-computed States**: When Sim loads onto lot, needs are predicted from time ### Result - Active household feels fully simulated - Neighborhood feels alive - Performance scales from 1 to 1000+ Sims **Lesson**: Use physical space (lots, zones) to define simulation boundaries. ## REAL-WORLD EXAMPLE #5: Cities: Skylines Traffic **Game**: Cities: Skylines **Challenge**: Simulate traffic for city of 100,000+ population. **Solution**: Individual agents with aggressive culling and simplification ### Implementation **On-Screen Vehicles**: Full simulation - Pathfinding (A*) - Lane changes - Traffic rules - Collisions **Off-Screen Vehicles**: Simplified - Pathfinding only (no rendering) - No lane changes - No collisions **Long Trips**: Teleportation - Vehicles on long trips (>5 minutes) teleport partway - Only simulated at start/end of trip **Citizen Agents**: Fake - Citizens (people) choose destinations - Cars are spawned to represent them - Cars despawn when destination reached ### Key Techniques 1. **Agent Pool**: Reuse vehicle entities (object pooling) 2. **Pathfinding Budget**: Only N paths computed per frame 3. **Simulation Speed**: Can be slowed/paused to reduce load 4. **Highway Optimization**: Highway traffic uses faster pathfinding ### Result - Can simulate 50,000+ vehicles - Traffic jams are realistic - Performance degrades gracefully (slowdown, not crash) **Lesson**: Even in simulation-heavy games, aggressive culling is essential. ## CROSS-REFERENCE: Related Skills ### Within simulation-tactics Skillpack 1. **crowd-simulation**: Focuses on crowds specifically (this skill is broader) 2. **ai-and-agent-simulation**: AI behavior details (this skill covers when to use AI vs fake) 3. **physics-simulation-patterns**: Physics-specific (this skill covers all simulation types) 4. **economic-simulation-patterns**: Economics (this skill teaches decision framework) 5. **ecosystem-simulation**: Ecosystem-specific (this skill teaches LOD approach) 6. **traffic-and-pathfinding**: Traffic-specific (this skill teaches when to path vs fake) 7. **weather-and-time**: Environmental (this skill teaches perf budgeting) **Use simulation-vs-faking FIRST**, then dive into specific skill for implementation details. ### From Other Skillpacks - **performance-optimization**: General optimization (this skill focuses on simulation) - **lod-systems**: Visual LOD (this skill is LOD for simulation) - **procedural-generation**: Content generation (complements lazy state generation here) ## TESTING CHECKLIST Before shipping any simulation system, verify: ### Performance Validation - ☐ **Profiled** actual frame time for simulation - ☐ **Budget met**: Stays within allocated time budget - ☐ **Worst case tested**: Maximum entity count, worst scenario - ☐ **Fallback tested**: System degrades gracefully under load - ☐ **Platform tested**: Tested on minimum spec hardware ### Scrutiny Validation - ☐ **LOD working**: Entities use correct detail level based on distance/visibility - ☐ **Transitions smooth**: No jarring pops when upgrading/downgrading - ☐ **Background indistinguishable**: Player can't tell background is faked - ☐ **Foreground detailed**: Player-inspected entities have appropriate detail ### Gameplay Validation - ☐ **Gameplay systems simulated**: Critical systems are NOT faked - ☐ **Cosmetic systems optimized**: Non-gameplay systems are faked appropriately - ☐ **Player actions work**: Interactions with entities work as expected - ☐ **Consistency maintained**: Fake entities match real entities when inspected ### Immersion Validation - ☐ **Playtested**: Real players couldn't spot fakes - ☐ **No patterns**: No obvious synchronization or repetition - ☐ **Feels alive**: World feels dynamic and believable - ☐ **No glitches**: Transitions don't cause visual bugs ### Development Validation - ☐ **Timeline met**: Implementation finished on schedule - ☐ **Maintainable**: Code is clean and documented - ☐ **Extensible**: Easy to add more entities/features - ☐ **Debuggable**: Tools exist to visualize simulation state ## SUMMARY: The Decision Framework ### Step-by-Step Process **1. Classify by Scrutiny** - How closely will player observe this? - Extreme / High / Medium / Low / Minimal **2. Classify by Gameplay Relevance** - Does this affect player decisions or outcomes? - Critical / Significant / Cosmetic-Observable / Cosmetic-Unobservable **3. Assign Simulation Strategy** ``` IF scrutiny >= High AND relevance >= Significant: → SIMULATE (full or hybrid) ELSE IF scrutiny >= Medium AND relevance >= Cosmetic-Observable: → HYBRID (key features real, details faked) ELSE IF scrutiny >= Low: → FAKE (convincing appearance, no simulation) ELSE: → REMOVE or STATISTICAL (bulk operations) ``` **4. Allocate Performance Budget** - Measure baseline cost - Set budget based on frame time - Design within constraints **5. Implement with LOD** - Multiple detail levels - Smooth transitions - Distance/visibility-based **6. Validate** - Profile - Playtest - Iterate ### The Golden Rule > **Simulate what the player OBSERVES and what affects GAMEPLAY. Fake everything else.** This is the foundational skill. Master this, and all other simulation decisions become clear. ## END OF SKILL This skill should be used at the START of any simulation design. It prevents the two catastrophic failure modes: 1. Over-simulation (wasted performance) 2. Under-simulation (broken immersion) Apply the frameworks rigorously, and your simulations will be performant, believable, and maintainable.