Claude Code Plugins

Community-maintained marketplace

Feedback

simulation-vs-faking

@tachyon-beep/skillpacks
1
0

Decide what to simulate vs fake - balance performance vs immersion

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name simulation-vs-faking
description Decide what to simulate vs fake - balance performance vs immersion

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:

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:

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<NPC> 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

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

// 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)

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)

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)

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)

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)

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.

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):

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):

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:

// 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):

void Update()
{
    Collider[] nearby = Physics.OverlapSphere(position, socialRadius); // 0.5ms
    foreach (var col in nearby)
    {
        NPC other = col.GetComponent<NPC>();
        InteractWith(other);
    }
}
// Cost: 0.5ms × 100 NPCs = 50ms

Good (cache and refresh slowly):

List<NPC> cachedNearby = new List<NPC>();
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):

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):

class CityPopulation
{
    int totalPopulation = 1000;

    // Distribution of activities
    Dictionary<Activity, float> activityDistribution = new Dictionary<Activity, float>()
    {
        { 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:

class PopulationVisualizer
{
    List<VisibleNPC> visibleNPCs = new List<VisibleNPC>();
    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):

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):

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:

// Simulate entire battle off-screen
foreach (var unit in offScreenUnits)
{
    unit.FindTarget();
    unit.Attack();
    unit.TakeDamage();
}
// Cost: High

Theater of Mind:

// 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:

void Update()
{
    if (state == Idle)
    {
        // Just stand still
        animator.Play("Idle");
    }
}
// Looks robotic

With Randomization:

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.

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:

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:

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

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

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):

class Enemy
{
    float health = 100f;

    void TakeDamage(float damage)
    {
        health -= damage; // Precise calculation
        if (health <= 0)
            Die();
    }
}

Cosmetic-Observable (Background Birds):

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):

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.

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):

void Update()
{
    if (needsNewPath)
    {
        path = Pathfinding.FindPath(position, target); // 3.2ms
    }
}

After (0.1ms for pathfinding):

// Time-slice: Only path 10 units per frame
static Queue<Unit> pathfindingQueue = new Queue<Unit>();
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:

// FAKE: Hard-coded schedule
void Update()
{
    if (GameTime.Hour == 8)
        transform.position = workplacePosition; // Teleport!
}

Alpha Stage:

// BASIC SIMULATION: Simple pathfinding
void Update()
{
    if (GameTime.Hour == 8 && !atWorkplace)
        agent.SetDestination(workplacePosition);
}

Beta Stage:

// OPTIMIZED SIMULATION: Pre-computed paths
void Update()
{
    if (GameTime.Hour == 8 && !atWorkplace)
        FollowPrecomputedPath(pathToWork);
}

Gold Stage:

// 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

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

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

public class TimeSlicedUpdater<T> where T : class
{
    private List<T> entities = new List<T>();
    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<T> 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

public class NPCManager : MonoBehaviour
{
    private TimeSlicedUpdater<NPC> npcUpdater;

    void Start()
    {
        // Update 10 NPCs per frame (100 NPCs = 10 frames for full update)
        npcUpdater = new TimeSlicedUpdater<NPC>(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

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

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

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

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

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<Home>();
        var workplaces = FindObjectsOfType<Workplace>();
        var taverns = FindObjectsOfType<Tavern>();

        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

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

public class GameClock : MonoBehaviour
{
    public static GameClock Instance;

    public event Action<int> OnHourChanged;
    public event Action<float> 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

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

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:

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:

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:

// 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:

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:

// 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:

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:

// 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:

// 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:

// 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:

// 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.