2816 lines
72 KiB
Markdown
2816 lines
72 KiB
Markdown
|
||
# 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<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**
|
||
```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<NPC>();
|
||
InteractWith(other);
|
||
}
|
||
}
|
||
// Cost: 0.5ms × 100 NPCs = 50ms
|
||
```
|
||
|
||
**Good** (cache and refresh slowly):
|
||
```csharp
|
||
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):
|
||
```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<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:
|
||
```csharp
|
||
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):
|
||
```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<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**:
|
||
```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<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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```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<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
|
||
|
||
```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<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
|
||
|
||
```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.
|