Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "yzmir-simulation-foundations",
|
||||||
|
"description": "Game simulation mathematics - ODEs, stability, control theory - 9 skills",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"author": {
|
||||||
|
"name": "tachyon-beep",
|
||||||
|
"url": "https://github.com/tachyon-beep"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# yzmir-simulation-foundations
|
||||||
|
|
||||||
|
Game simulation mathematics - ODEs, stability, control theory - 9 skills
|
||||||
77
plugin.lock.json
Normal file
77
plugin.lock.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:tachyon-beep/skillpacks:plugins/yzmir-simulation-foundations",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "d93da91eebc1d86ffc5512ab3361b0ed6587c28c",
|
||||||
|
"treeHash": "8a2570df7af0d1870601c63e08ea35ef62eead43c1b43899b2bcbb1639e238a1",
|
||||||
|
"generatedAt": "2025-11-28T10:28:34.642134Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "yzmir-simulation-foundations",
|
||||||
|
"description": "Game simulation mathematics - ODEs, stability, control theory - 9 skills",
|
||||||
|
"version": "1.0.2"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "ce98c22bffd6f8634bd6789dd1d58530b7362afc217455ed2a486ba7230345f0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "227515ac38dc44d210785bb0fd9bc51b5c4695c4b7d31b46f17c6fcd2a2e3d15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/stochastic-simulation.md",
|
||||||
|
"sha256": "18ecbc13d58d8866ee6e77795a0772ad90de65edd1d59701996dc4bb09ab02c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/stability-analysis.md",
|
||||||
|
"sha256": "68132ffb745ddcb66d85dbca287b3a82fcdc6bb6378f48970d58daac38e135cf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/continuous-vs-discrete.md",
|
||||||
|
"sha256": "209c810abff184e5a4ccc3760c1ea3199e725f393112a66051894b724bf61518"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/feedback-control-theory.md",
|
||||||
|
"sha256": "551966d0cd64ecf634cddd0a9bc314fae902a3d58a5b66e4155de47340c0b37e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/differential-equations-for-games.md",
|
||||||
|
"sha256": "44fc7daa8a63a69c35ed02f475f8a4d9ae8bce4f3eb0816252ccb800fb056cfe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/SKILL.md",
|
||||||
|
"sha256": "f0782cd43e6cd40f76e5cb5e8a3934bff09b4f45096b15e78178e01b1b73b91b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/state-space-modeling.md",
|
||||||
|
"sha256": "77d66623759a35611bf4eb266a38bf8c9792a4c6b43173830d38fda52b20df0a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/numerical-methods.md",
|
||||||
|
"sha256": "17ad90366cb684bd4b57e1328fd302cfd4719787294e5adc55c36a578d3f80eb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-simulation-foundations/chaos-and-sensitivity.md",
|
||||||
|
"sha256": "bbf4523daece63644bf13bd6bb5e8a4a3af6c3c340af59bfaa493e3fd063f485"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "8a2570df7af0d1870601c63e08ea35ef62eead43c1b43899b2bcbb1639e238a1"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
517
skills/using-simulation-foundations/SKILL.md
Normal file
517
skills/using-simulation-foundations/SKILL.md
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
---
|
||||||
|
name: using-simulation-foundations
|
||||||
|
description: Router for simulation math - ODEs, state-space, stability, control, numerics, chaos, stochastic
|
||||||
|
mode: true
|
||||||
|
pack: yzmir/simulation-foundations
|
||||||
|
faction: yzmir
|
||||||
|
skill_type: meta_router
|
||||||
|
dependencies:
|
||||||
|
- yzmir/simulation-foundations/differential-equations-for-games
|
||||||
|
- yzmir/simulation-foundations/state-space-modeling
|
||||||
|
- yzmir/simulation-foundations/stability-analysis
|
||||||
|
- yzmir/simulation-foundations/feedback-control-theory
|
||||||
|
- yzmir/simulation-foundations/numerical-methods
|
||||||
|
- yzmir/simulation-foundations/continuous-vs-discrete
|
||||||
|
- yzmir/simulation-foundations/chaos-and-sensitivity
|
||||||
|
- yzmir/simulation-foundations/stochastic-simulation
|
||||||
|
estimated_time_hours: 0.5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using Simulation-Foundations (Meta-Skill Router)
|
||||||
|
|
||||||
|
**Your entry point to mathematical simulation foundations.** This skill routes you to the right combination of mathematical skills for your game simulation challenge.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This is a **meta-skill** that:
|
||||||
|
1. ✅ **Routes** you to the correct mathematical skills
|
||||||
|
2. ✅ **Combines** multiple skills for complex simulations
|
||||||
|
3. ✅ **Provides** workflows for common simulation types
|
||||||
|
4. ✅ **Explains** when to use theory vs empirical tuning
|
||||||
|
|
||||||
|
**You should use this skill:** When building any simulation system that needs mathematical rigor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Philosophy: Theory Enables Design
|
||||||
|
|
||||||
|
### The Central Idea
|
||||||
|
|
||||||
|
**Empirical Tuning**: Trial-and-error adjustment of magic numbers
|
||||||
|
- Slow iteration (run simulation, observe, tweak, repeat)
|
||||||
|
- Unpredictable behavior (systems drift to extremes)
|
||||||
|
- No guarantees (stability, convergence, performance)
|
||||||
|
- Difficult debugging (why did it break?)
|
||||||
|
|
||||||
|
**Mathematical Foundation**: Formulate systems using theory
|
||||||
|
- Fast iteration (predict behavior analytically)
|
||||||
|
- Predictable behavior (stability analysis)
|
||||||
|
- Guarantees (equilibrium, convergence, bounds)
|
||||||
|
- Systematic debugging (root cause analysis)
|
||||||
|
|
||||||
|
### When This Pack Applies
|
||||||
|
|
||||||
|
**✅ Use simulation-foundations when:**
|
||||||
|
- Building physics, AI, or economic simulation systems
|
||||||
|
- Need stability guarantees (ecosystems, economies)
|
||||||
|
- Performance matters (60 FPS real-time constraints)
|
||||||
|
- Multiplayer determinism required (lockstep networking)
|
||||||
|
- Long-term behavior unpredictable (100+ hour campaigns)
|
||||||
|
|
||||||
|
**❌ Don't use simulation-foundations when:**
|
||||||
|
- Simple systems with no continuous dynamics
|
||||||
|
- Pure authored content (no simulation)
|
||||||
|
- Empirical tuning sufficient (static balance tables)
|
||||||
|
- Math overhead not justified (tiny indie game)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pack Overview: 8 Core Skills
|
||||||
|
|
||||||
|
### Wave 1: Foundational Mathematics
|
||||||
|
|
||||||
|
#### 1. differential-equations-for-games
|
||||||
|
**When to use:** ANY continuous dynamics (population, physics, resources)
|
||||||
|
**Teaches:** Formulating and solving ODEs for game systems
|
||||||
|
**Examples:** Lotka-Volterra ecosystems, spring-damper camera, resource regeneration
|
||||||
|
**Time:** 2.5-3.5 hours
|
||||||
|
**Key insight:** Systems with rates of change need ODEs
|
||||||
|
|
||||||
|
#### 2. state-space-modeling
|
||||||
|
**When to use:** Complex systems with many interacting variables
|
||||||
|
**Teaches:** Representing game state mathematically, reachability analysis
|
||||||
|
**Examples:** Fighting game frame data, RTS tech trees, puzzle solvability
|
||||||
|
**Time:** 2.5-3.5 hours
|
||||||
|
**Key insight:** Explicit state representation enables analysis
|
||||||
|
|
||||||
|
#### 3. stability-analysis
|
||||||
|
**When to use:** Need to prevent crashes, explosions, extinctions
|
||||||
|
**Teaches:** Equilibrium points, eigenvalue analysis, Lyapunov functions
|
||||||
|
**Examples:** Ecosystem balance, economy stability, physics robustness
|
||||||
|
**Time:** 3-4 hours
|
||||||
|
**Key insight:** Analyze stability BEFORE shipping
|
||||||
|
|
||||||
|
### Wave 2: Control and Integration
|
||||||
|
|
||||||
|
#### 4. feedback-control-theory
|
||||||
|
**When to use:** Smooth tracking, adaptive systems, disturbance rejection
|
||||||
|
**Teaches:** PID controllers for game systems
|
||||||
|
**Examples:** Camera smoothing, AI pursuit, dynamic difficulty
|
||||||
|
**Time:** 2-3 hours
|
||||||
|
**Key insight:** PID replaces magic numbers with physics
|
||||||
|
|
||||||
|
#### 5. numerical-methods
|
||||||
|
**When to use:** Implementing continuous systems in discrete timesteps
|
||||||
|
**Teaches:** Euler, Runge-Kutta, symplectic integrators
|
||||||
|
**Examples:** Physics engines, cloth, orbital mechanics
|
||||||
|
**Time:** 2.5-3.5 hours
|
||||||
|
**Key insight:** Integration method affects stability
|
||||||
|
|
||||||
|
#### 6. continuous-vs-discrete
|
||||||
|
**When to use:** Choosing model type (continuous ODEs vs discrete events)
|
||||||
|
**Teaches:** When to use continuous, discrete, or hybrid
|
||||||
|
**Examples:** Turn-based vs real-time, cellular automata, quantized resources
|
||||||
|
**Time:** 2-2.5 hours
|
||||||
|
**Key insight:** Wrong choice costs 10× performance OR 100× accuracy
|
||||||
|
|
||||||
|
### Wave 3: Advanced Topics
|
||||||
|
|
||||||
|
#### 7. chaos-and-sensitivity
|
||||||
|
**When to use:** Multiplayer desyncs, determinism requirements, sensitivity analysis
|
||||||
|
**Teaches:** Butterfly effect, Lyapunov exponents, deterministic chaos
|
||||||
|
**Examples:** Weather systems, multiplayer lockstep, proc-gen stability
|
||||||
|
**Time:** 2-3 hours
|
||||||
|
**Key insight:** Deterministic ≠ predictable
|
||||||
|
|
||||||
|
#### 8. stochastic-simulation
|
||||||
|
**When to use:** Random processes, loot systems, AI uncertainty
|
||||||
|
**Teaches:** Probability distributions, Monte Carlo, stochastic differential equations
|
||||||
|
**Examples:** Loot drops, crit systems, procedural generation
|
||||||
|
**Time:** 2-3 hours
|
||||||
|
**Key insight:** Naive randomness creates exploits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing Logic: Which Skills Do I Need?
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
START: What are you building?
|
||||||
|
|
||||||
|
├─ ECOSYSTEM / POPULATION SIMULATION
|
||||||
|
│ ├─ Formulate dynamics → differential-equations-for-games
|
||||||
|
│ ├─ Prevent extinction/explosion → stability-analysis
|
||||||
|
│ ├─ Implement simulation → numerical-methods
|
||||||
|
│ └─ Random events? → stochastic-simulation
|
||||||
|
│
|
||||||
|
├─ PHYSICS SIMULATION
|
||||||
|
│ ├─ Formulate forces → differential-equations-for-games
|
||||||
|
│ ├─ Choose integrator → numerical-methods
|
||||||
|
│ ├─ Prevent explosions → stability-analysis
|
||||||
|
│ ├─ Multiplayer determinism? → chaos-and-sensitivity
|
||||||
|
│ └─ Real-time vs turn-based? → continuous-vs-discrete
|
||||||
|
│
|
||||||
|
├─ ECONOMY / RESOURCE SYSTEM
|
||||||
|
│ ├─ Formulate flows → differential-equations-for-games
|
||||||
|
│ ├─ Prevent inflation/deflation → stability-analysis
|
||||||
|
│ ├─ Discrete vs continuous? → continuous-vs-discrete
|
||||||
|
│ └─ Market randomness? → stochastic-simulation
|
||||||
|
│
|
||||||
|
├─ AI / CONTROL SYSTEM
|
||||||
|
│ ├─ Smooth behavior → feedback-control-theory
|
||||||
|
│ ├─ State machine analysis → state-space-modeling
|
||||||
|
│ ├─ Decision uncertainty → stochastic-simulation
|
||||||
|
│ └─ Prevent oscillation → stability-analysis
|
||||||
|
│
|
||||||
|
├─ MULTIPLAYER / DETERMINISM
|
||||||
|
│ ├─ Understand desync sources → chaos-and-sensitivity
|
||||||
|
│ ├─ Choose precision → numerical-methods
|
||||||
|
│ ├─ Discrete events? → continuous-vs-discrete
|
||||||
|
│ └─ State validation → state-space-modeling
|
||||||
|
│
|
||||||
|
└─ LOOT / RANDOMNESS SYSTEM
|
||||||
|
├─ Choose distributions → stochastic-simulation
|
||||||
|
├─ Prevent exploits → stochastic-simulation (anti-patterns)
|
||||||
|
├─ Pity systems → feedback-control-theory (setpoint tracking)
|
||||||
|
└─ Long-term balance → stability-analysis
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15+ Scenarios: Which Skills Apply?
|
||||||
|
|
||||||
|
### Scenario 1: "Rimworld-style ecosystem (wolves/deer/grass)"
|
||||||
|
**Primary:** differential-equations-for-games (Lotka-Volterra)
|
||||||
|
**Secondary:** stability-analysis (prevent extinction), numerical-methods (RK4 integration)
|
||||||
|
**Optional:** stochastic-simulation (random migration events)
|
||||||
|
**Time:** 6-10 hours
|
||||||
|
|
||||||
|
### Scenario 2: "Unity physics engine with springs/dampers"
|
||||||
|
**Primary:** differential-equations-for-games (spring-mass-damper)
|
||||||
|
**Secondary:** numerical-methods (semi-implicit Euler), stability-analysis (prevent explosion)
|
||||||
|
**Optional:** chaos-and-sensitivity (multiplayer physics)
|
||||||
|
**Time:** 5-8 hours
|
||||||
|
|
||||||
|
### Scenario 3: "EVE Online-style economy (inflation prevention)"
|
||||||
|
**Primary:** differential-equations-for-games (resource flows)
|
||||||
|
**Secondary:** stability-analysis (equilibrium analysis), continuous-vs-discrete (discrete items)
|
||||||
|
**Optional:** stochastic-simulation (market fluctuations)
|
||||||
|
**Time:** 6-9 hours
|
||||||
|
|
||||||
|
### Scenario 4: "Smooth camera follow (Uncharted-style)"
|
||||||
|
**Primary:** feedback-control-theory (PID camera)
|
||||||
|
**Secondary:** differential-equations-for-games (spring-damper alternative)
|
||||||
|
**Optional:** None (focused problem)
|
||||||
|
**Time:** 2-4 hours
|
||||||
|
|
||||||
|
### Scenario 5: "Left 4 Dead AI Director (adaptive difficulty)"
|
||||||
|
**Primary:** feedback-control-theory (intensity tracking)
|
||||||
|
**Secondary:** differential-equations-for-games (smooth intensity changes)
|
||||||
|
**Optional:** stochastic-simulation (spawn randomness)
|
||||||
|
**Time:** 4-6 hours
|
||||||
|
|
||||||
|
### Scenario 6: "Fighting game frame data analysis"
|
||||||
|
**Primary:** state-space-modeling (state transitions)
|
||||||
|
**Secondary:** None (discrete system)
|
||||||
|
**Optional:** chaos-and-sensitivity (combo sensitivity to timing)
|
||||||
|
**Time:** 3-5 hours
|
||||||
|
|
||||||
|
### Scenario 7: "RTS lockstep multiplayer (prevent desyncs)"
|
||||||
|
**Primary:** chaos-and-sensitivity (understand floating-point sensitivity)
|
||||||
|
**Secondary:** numerical-methods (fixed-point arithmetic), continuous-vs-discrete (deterministic events)
|
||||||
|
**Optional:** state-space-modeling (state validation)
|
||||||
|
**Time:** 5-8 hours
|
||||||
|
|
||||||
|
### Scenario 8: "Kerbal Space Program orbital mechanics"
|
||||||
|
**Primary:** numerical-methods (symplectic integrators for energy conservation)
|
||||||
|
**Secondary:** differential-equations-for-games (Newton's gravity), chaos-and-sensitivity (three-body problem)
|
||||||
|
**Optional:** None (focused on accuracy)
|
||||||
|
**Time:** 6-10 hours
|
||||||
|
|
||||||
|
### Scenario 9: "Diablo-style loot drops (fair randomness)"
|
||||||
|
**Primary:** stochastic-simulation (probability distributions, pity systems)
|
||||||
|
**Secondary:** None (focused problem)
|
||||||
|
**Optional:** feedback-control-theory (pity timer as PID)
|
||||||
|
**Time:** 3-5 hours
|
||||||
|
|
||||||
|
### Scenario 10: "Cloth simulation (Unity/Unreal)"
|
||||||
|
**Primary:** numerical-methods (Verlet integration, constraints)
|
||||||
|
**Secondary:** differential-equations-for-games (spring forces), stability-analysis (prevent blow-up)
|
||||||
|
**Optional:** None (standard cloth physics)
|
||||||
|
**Time:** 5-8 hours
|
||||||
|
|
||||||
|
### Scenario 11: "Turn-based tactical RPG"
|
||||||
|
**Primary:** continuous-vs-discrete (choose discrete model)
|
||||||
|
**Secondary:** state-space-modeling (action resolution), stochastic-simulation (hit/crit rolls)
|
||||||
|
**Optional:** None (discrete system)
|
||||||
|
**Time:** 4-6 hours
|
||||||
|
|
||||||
|
### Scenario 12: "Procedural weather system (dynamic)"
|
||||||
|
**Primary:** differential-equations-for-games (smooth weather transitions)
|
||||||
|
**Secondary:** stochastic-simulation (random weather events), chaos-and-sensitivity (Lorenz attractor)
|
||||||
|
**Optional:** numerical-methods (weather integration)
|
||||||
|
**Time:** 5-8 hours
|
||||||
|
|
||||||
|
### Scenario 13: "Path of Exile economy balance"
|
||||||
|
**Primary:** stability-analysis (currency sink/faucet equilibrium)
|
||||||
|
**Secondary:** differential-equations-for-games (flow equations), stochastic-simulation (drop rates)
|
||||||
|
**Optional:** continuous-vs-discrete (discrete items, continuous flows)
|
||||||
|
**Time:** 6-9 hours
|
||||||
|
|
||||||
|
### Scenario 14: "Racing game suspension (realistic feel)"
|
||||||
|
**Primary:** differential-equations-for-games (spring-damper suspension)
|
||||||
|
**Secondary:** feedback-control-theory (PID for stability), numerical-methods (fast integration)
|
||||||
|
**Optional:** stability-analysis (prevent oscillation)
|
||||||
|
**Time:** 5-8 hours
|
||||||
|
|
||||||
|
### Scenario 15: "Puzzle game solvability checker"
|
||||||
|
**Primary:** state-space-modeling (reachability analysis)
|
||||||
|
**Secondary:** None (graph search problem)
|
||||||
|
**Optional:** chaos-and-sensitivity (sensitivity to initial state)
|
||||||
|
**Time:** 3-5 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Skill Workflows
|
||||||
|
|
||||||
|
### Workflow 1: Ecosystem Simulation (Rimworld, Dwarf Fortress)
|
||||||
|
**Skills in sequence:**
|
||||||
|
1. **differential-equations-for-games** (2.5-3.5h) - Formulate Lotka-Volterra
|
||||||
|
2. **stability-analysis** (3-4h) - Find equilibrium, prevent extinction
|
||||||
|
3. **numerical-methods** (2.5-3.5h) - Implement RK4 integration
|
||||||
|
4. **stochastic-simulation** (2-3h) - Add random migration/disease
|
||||||
|
|
||||||
|
**Total time:** 10-14 hours
|
||||||
|
**Result:** Stable ecosystem with predictable long-term behavior
|
||||||
|
|
||||||
|
### Workflow 2: Physics Engine (Unity, Unreal, custom)
|
||||||
|
**Skills in sequence:**
|
||||||
|
1. **differential-equations-for-games** (2.5-3.5h) - Newton's laws, spring-damper
|
||||||
|
2. **numerical-methods** (2.5-3.5h) - Semi-implicit Euler, Verlet
|
||||||
|
3. **stability-analysis** (3-4h) - Prevent ragdoll explosion
|
||||||
|
4. **chaos-and-sensitivity** (2-3h) - Multiplayer determinism (if needed)
|
||||||
|
|
||||||
|
**Total time:** 10-14 hours (12-17 with multiplayer)
|
||||||
|
**Result:** Stable, deterministic physics at 60 FPS
|
||||||
|
|
||||||
|
### Workflow 3: Economy System (EVE, Path of Exile)
|
||||||
|
**Skills in sequence:**
|
||||||
|
1. **differential-equations-for-games** (2.5-3.5h) - Resource flow equations
|
||||||
|
2. **stability-analysis** (3-4h) - Equilibrium analysis, inflation prevention
|
||||||
|
3. **continuous-vs-discrete** (2-2.5h) - Discrete items, continuous flows
|
||||||
|
4. **stochastic-simulation** (2-3h) - Market fluctuations, drop rates
|
||||||
|
|
||||||
|
**Total time:** 10-13 hours
|
||||||
|
**Result:** Self-regulating economy with predictable equilibrium
|
||||||
|
|
||||||
|
### Workflow 4: AI Control System (Camera, Difficulty, NPC)
|
||||||
|
**Skills in sequence:**
|
||||||
|
1. **feedback-control-theory** (2-3h) - PID controller design
|
||||||
|
2. **differential-equations-for-games** (1-2h) - Alternative spring-damper (optional)
|
||||||
|
3. **stability-analysis** (1-2h) - Prevent oscillation (optional)
|
||||||
|
|
||||||
|
**Total time:** 2-7 hours (depending on complexity)
|
||||||
|
**Result:** Smooth, adaptive AI behavior
|
||||||
|
|
||||||
|
### Workflow 5: Multiplayer Determinism (RTS, Fighting Games)
|
||||||
|
**Skills in sequence:**
|
||||||
|
1. **chaos-and-sensitivity** (2-3h) - Understand desync sources
|
||||||
|
2. **numerical-methods** (2.5-3.5h) - Fixed-point arithmetic
|
||||||
|
3. **state-space-modeling** (2.5-3.5h) - State validation
|
||||||
|
4. **continuous-vs-discrete** (2-2.5h) - Deterministic event ordering
|
||||||
|
|
||||||
|
**Total time:** 9-12.5 hours
|
||||||
|
**Result:** Zero desyncs in multiplayer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Other Skillpacks
|
||||||
|
|
||||||
|
### Primary Integration: bravos/simulation-tactics
|
||||||
|
|
||||||
|
**simulation-tactics = HOW to implement**
|
||||||
|
**simulation-foundations = WHY it works mathematically**
|
||||||
|
|
||||||
|
Cross-references TO simulation-foundations:
|
||||||
|
- physics-simulation-patterns → differential-equations + numerical-methods (math behind fixed timestep)
|
||||||
|
- ecosystem-simulation → stability-analysis (Lotka-Volterra mathematics)
|
||||||
|
- debugging-simulation-chaos → chaos-and-sensitivity (determinism theory)
|
||||||
|
- performance-optimization → numerical-methods (integration accuracy vs cost)
|
||||||
|
|
||||||
|
Cross-references FROM simulation-foundations:
|
||||||
|
- differential-equations → simulation-tactics for implementation patterns
|
||||||
|
- stability-analysis → ecosystem-simulation for practical code
|
||||||
|
- numerical-methods → physics-simulation for engine integration
|
||||||
|
|
||||||
|
### Secondary Integration: bravos/systems-as-experience
|
||||||
|
|
||||||
|
Cross-references:
|
||||||
|
- state-space-modeling → strategic-depth-from-systems (build space mathematics)
|
||||||
|
- stochastic-simulation → player-driven-narratives (procedural event probabilities)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start Guides
|
||||||
|
|
||||||
|
### Quick Start 1: Stable Ecosystem (4 hours)
|
||||||
|
**Goal:** Predator-prey system that doesn't crash
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Read differential-equations Quick Start (1h)
|
||||||
|
2. Formulate Lotka-Volterra equations (0.5h)
|
||||||
|
3. Read stability-analysis Quick Start (1h)
|
||||||
|
4. Find equilibrium, check eigenvalues (1h)
|
||||||
|
5. Implement with semi-implicit Euler (0.5h)
|
||||||
|
|
||||||
|
**Result:** Ecosystem oscillates stably, no extinction
|
||||||
|
|
||||||
|
### Quick Start 2: Smooth Camera (2 hours)
|
||||||
|
**Goal:** Uncharted-style camera follow
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Read feedback-control Quick Start (0.5h)
|
||||||
|
2. Implement PID controller (1h)
|
||||||
|
3. Tune using Ziegler-Nichols (0.5h)
|
||||||
|
|
||||||
|
**Result:** Smooth camera with no overshoot
|
||||||
|
|
||||||
|
### Quick Start 3: Fair Loot System (3 hours)
|
||||||
|
**Goal:** Diablo-style loot with pity timer
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Read stochastic-simulation Quick Start (1h)
|
||||||
|
2. Choose distribution (Bernoulli + pity) (0.5h)
|
||||||
|
3. Implement and test fairness (1.5h)
|
||||||
|
|
||||||
|
**Result:** Loot system with guaranteed legendary every 90 pulls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Skipping Stability Analysis
|
||||||
|
**Problem:** Shipping systems without analyzing equilibrium
|
||||||
|
|
||||||
|
**Symptom:** Game works fine for 10 hours, crashes at hour 100 (population explosion)
|
||||||
|
|
||||||
|
**Fix:** ALWAYS use stability-analysis for systems with feedback loops
|
||||||
|
|
||||||
|
### Pitfall 2: Wrong Integrator Choice
|
||||||
|
**Problem:** Using explicit Euler for stiff systems
|
||||||
|
|
||||||
|
**Symptom:** Physics explodes at high framerates or with strong springs
|
||||||
|
|
||||||
|
**Fix:** Use numerical-methods decision framework (semi-implicit for physics)
|
||||||
|
|
||||||
|
### Pitfall 3: Assuming Determinism
|
||||||
|
**Problem:** Identical code on two machines, assuming identical results
|
||||||
|
|
||||||
|
**Symptom:** Multiplayer desyncs after 5+ minutes
|
||||||
|
|
||||||
|
**Fix:** Read chaos-and-sensitivity, understand floating-point divergence
|
||||||
|
|
||||||
|
### Pitfall 4: Naive Randomness
|
||||||
|
**Problem:** Using uniform random for everything
|
||||||
|
|
||||||
|
**Symptom:** Players exploit patterns, loot feels unfair
|
||||||
|
|
||||||
|
**Fix:** Use stochastic-simulation to choose proper distributions
|
||||||
|
|
||||||
|
### Pitfall 5: Continuous for Discrete Problems
|
||||||
|
**Problem:** Using ODEs for turn-based combat
|
||||||
|
|
||||||
|
**Symptom:** 100× CPU overhead for no benefit
|
||||||
|
|
||||||
|
**Fix:** Read continuous-vs-discrete, use difference equations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Your simulation uses foundations successfully when:
|
||||||
|
|
||||||
|
**Predictability:**
|
||||||
|
- [ ] Can predict long-term behavior analytically
|
||||||
|
- [ ] Equilibrium points known before shipping
|
||||||
|
- [ ] Stability verified mathematically
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- [ ] Integration method chosen deliberately (not default Euler)
|
||||||
|
- [ ] Real-time constraints met (60 FPS)
|
||||||
|
- [ ] Appropriate model type (continuous/discrete)
|
||||||
|
|
||||||
|
**Robustness:**
|
||||||
|
- [ ] No catastrophic failures (extinctions, explosions)
|
||||||
|
- [ ] Handles edge cases (zero populations, high framerates)
|
||||||
|
- [ ] Multiplayer determinism verified (if needed)
|
||||||
|
|
||||||
|
**Maintainability:**
|
||||||
|
- [ ] Parameters have physical meaning (not magic numbers)
|
||||||
|
- [ ] Behavior understood mathematically
|
||||||
|
- [ ] Debugging systematic (not trial-and-error)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
**The Golden Rule:**
|
||||||
|
> "Formulate first, tune second. Math predicts, empiricism confirms."
|
||||||
|
|
||||||
|
### When You're Done with This Pack
|
||||||
|
|
||||||
|
You should be able to:
|
||||||
|
- ✅ Formulate game systems as differential equations
|
||||||
|
- ✅ Analyze stability before shipping
|
||||||
|
- ✅ Choose correct numerical integration method
|
||||||
|
- ✅ Design PID controllers for smooth behavior
|
||||||
|
- ✅ Understand deterministic chaos implications
|
||||||
|
- ✅ Apply proper probability distributions
|
||||||
|
- ✅ Prevent catastrophic simulation failures
|
||||||
|
- ✅ Debug simulations systematically
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. **Identify your simulation type** (use routing logic above)
|
||||||
|
2. **Read foundational skill** (usually differential-equations-for-games)
|
||||||
|
3. **Apply skills in sequence** (use workflows above)
|
||||||
|
4. **Validate mathematically** (stability analysis, testing)
|
||||||
|
5. **Integrate with simulation-tactics** (implementation patterns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pack Structure Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
yzmir/simulation-foundations/
|
||||||
|
├── using-simulation-foundations/ (THIS SKILL - router)
|
||||||
|
├── differential-equations-for-games/ (Wave 1 - Foundation)
|
||||||
|
├── state-space-modeling/ (Wave 1 - Foundation)
|
||||||
|
├── stability-analysis/ (Wave 1 - Foundation)
|
||||||
|
├── feedback-control-theory/ (Wave 2 - Control)
|
||||||
|
├── numerical-methods/ (Wave 2 - Integration)
|
||||||
|
├── continuous-vs-discrete/ (Wave 2 - Modeling Choice)
|
||||||
|
├── chaos-and-sensitivity/ (Wave 3 - Advanced)
|
||||||
|
└── stochastic-simulation/ (Wave 3 - Advanced)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total pack time:** 19-26 hours for comprehensive application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Simulation Foundations Specialist Skills Catalog
|
||||||
|
|
||||||
|
After routing, load the appropriate specialist skill for detailed guidance:
|
||||||
|
|
||||||
|
1. [differential-equations-for-games.md](differential-equations-for-games.md) - ODEs for continuous dynamics, Lotka-Volterra ecosystems, spring-damper systems, resource flows, Newton's laws
|
||||||
|
2. [state-space-modeling.md](state-space-modeling.md) - State representation, reachability analysis, fighting game frame data, RTS tech trees, puzzle solvability
|
||||||
|
3. [stability-analysis.md](stability-analysis.md) - Equilibrium points, eigenvalue analysis, Lyapunov functions, preventing extinction/explosion/inflation
|
||||||
|
4. [feedback-control-theory.md](feedback-control-theory.md) - PID controllers, camera smoothing, AI pursuit, dynamic difficulty, disturbance rejection
|
||||||
|
5. [numerical-methods.md](numerical-methods.md) - Euler, Runge-Kutta, symplectic integrators, fixed-point arithmetic, integration stability
|
||||||
|
6. [continuous-vs-discrete.md](continuous-vs-discrete.md) - Choosing model type, continuous ODEs vs discrete events, turn-based vs real-time
|
||||||
|
7. [chaos-and-sensitivity.md](chaos-and-sensitivity.md) - Butterfly effect, Lyapunov exponents, deterministic chaos, multiplayer desyncs, floating-point sensitivity
|
||||||
|
8. [stochastic-simulation.md](stochastic-simulation.md) - Probability distributions, Monte Carlo, stochastic differential equations, loot systems, randomness patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Go build simulations with mathematical rigor.**
|
||||||
1509
skills/using-simulation-foundations/chaos-and-sensitivity.md
Normal file
1509
skills/using-simulation-foundations/chaos-and-sensitivity.md
Normal file
File diff suppressed because it is too large
Load Diff
966
skills/using-simulation-foundations/continuous-vs-discrete.md
Normal file
966
skills/using-simulation-foundations/continuous-vs-discrete.md
Normal file
@@ -0,0 +1,966 @@
|
|||||||
|
|
||||||
|
### Failure 2: Turn-Based Combat with Real-Time Physics
|
||||||
|
|
||||||
|
**Scenario**: Turn-based strategy game, designer adds physics for "smoothness."
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// WRONG: Real-time physics in turn-based game
|
||||||
|
void DealDamage(float amount) {
|
||||||
|
// RK4 integration for continuous damage animation
|
||||||
|
current_health += IntegrateODE(damage_ode, amount, dt);
|
||||||
|
|
||||||
|
// But game is turn-based!
|
||||||
|
// Problems:
|
||||||
|
// - Damage amount depends on frame rate (bad)
|
||||||
|
// - Network desync (continuous simulation can't be deterministic)
|
||||||
|
// - Player can see partial damage, rewinds to previous turn
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Happens**:
|
||||||
|
- Same damage amount produces different results at 30fps vs 60fps
|
||||||
|
- Networked multiplayer breaks (continuous models never perfectly sync)
|
||||||
|
- UI shows health dropping, but turn hasn't resolved yet
|
||||||
|
- Save file is inconsistent (which frame's state is correct?)
|
||||||
|
|
||||||
|
**Root Cause**: Mixing continuous physics with discrete turn resolution.
|
||||||
|
|
||||||
|
|
||||||
|
### Failure 3: Discrete Events as Continuous Flow
|
||||||
|
|
||||||
|
**Scenario**: RTS game with discrete worker units, developer makes them continuous.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Treating discrete units as continuous flow
|
||||||
|
def harvest_resources():
|
||||||
|
# Modeling units as continuous population
|
||||||
|
population = 50.0 # Can be fractional!
|
||||||
|
resources_per_second = 2.5
|
||||||
|
|
||||||
|
for t in range(1000):
|
||||||
|
population += 0.001 * (population - 50) * dt # Logistic growth???
|
||||||
|
resources += population * resources_per_second * dt
|
||||||
|
|
||||||
|
# Problems:
|
||||||
|
# - 50.3 units harvesting doesn't make sense
|
||||||
|
# - Units are discrete (add/remove whole units)
|
||||||
|
# - Continuous model obscures discrete mechanics
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: Inconsistent with game rules, hard to verify, players confused.
|
||||||
|
|
||||||
|
|
||||||
|
### Failure 4: Quantized Resources as Continuous
|
||||||
|
|
||||||
|
**Scenario**: Factory game with discrete items, uses continuous production.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Continuous production of discrete items
|
||||||
|
class FactoryLine:
|
||||||
|
def __init__(self):
|
||||||
|
self.output = 0.0 # Fractional items???
|
||||||
|
self.production_rate = 2.5 # items/second
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
self.output += self.production_rate * dt
|
||||||
|
# Every ~0.4 seconds, you get 1 item
|
||||||
|
|
||||||
|
# Problem: When do you ACTUALLY get the item?
|
||||||
|
# At 0.4s? Rounded? This is confusing.
|
||||||
|
# Discrete model handles this naturally.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## GREEN Phase: Correct Choices
|
||||||
|
|
||||||
|
### 1. Continuous Models: When and Why
|
||||||
|
|
||||||
|
**Use continuous models when:**
|
||||||
|
|
||||||
|
#### 1.1 Smooth, Time-Dependent Behavior
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Camera smoothing (continuous movement)
|
||||||
|
class ContinuousCamera:
|
||||||
|
def __init__(self, target):
|
||||||
|
self.position = Vector2(0, 0)
|
||||||
|
self.velocity = Vector2(0, 0)
|
||||||
|
|
||||||
|
def update(self, target, dt):
|
||||||
|
# Spring-damper: smooth approach to target
|
||||||
|
spring_force = 50 * (target - self.position)
|
||||||
|
damping_force = -20 * self.velocity
|
||||||
|
|
||||||
|
acceleration = spring_force + damping_force
|
||||||
|
self.velocity += acceleration * dt
|
||||||
|
self.position += self.velocity * dt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Camera position is fundamentally continuous. Even at discrete update rate, we want smooth interpolation between frames.
|
||||||
|
|
||||||
|
|
||||||
|
#### 1.2 Equilibrium Systems
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Population dynamics with stable equilibrium
|
||||||
|
class EcosystemSimulation:
|
||||||
|
def __init__(self):
|
||||||
|
self.herbivores = 100.0 # OK to be fractional (population average)
|
||||||
|
self.predators = 20.0
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
# Lotka-Volterra with carrying capacity
|
||||||
|
H = self.herbivores
|
||||||
|
P = self.predators
|
||||||
|
K = 200 # Carrying capacity
|
||||||
|
|
||||||
|
dH_dt = 0.1 * H * (1 - H/K) - 0.02 * H * P
|
||||||
|
dP_dt = 0.3 * 0.02 * H * P - 0.05 * P
|
||||||
|
|
||||||
|
self.herbivores += dH_dt * dt
|
||||||
|
self.predators += dP_dt * dt
|
||||||
|
|
||||||
|
# System naturally converges to equilibrium
|
||||||
|
# No manual balancing needed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: System has natural equilibrium. Continuous math tells us the system is stable before we ever run it.
|
||||||
|
|
||||||
|
|
||||||
|
#### 1.3 Physics Simulations
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// CORRECT: Real-time physics engine
|
||||||
|
class PhysicsBody {
|
||||||
|
Vector3 position;
|
||||||
|
Vector3 velocity;
|
||||||
|
float mass;
|
||||||
|
|
||||||
|
void integrate(const Vector3& force, float dt) {
|
||||||
|
// Newton's second law: F = ma
|
||||||
|
Vector3 acceleration = force / mass;
|
||||||
|
|
||||||
|
velocity += acceleration * dt;
|
||||||
|
position += velocity * dt;
|
||||||
|
|
||||||
|
// Continuous model natural for physics
|
||||||
|
// Small dt → smooth trajectory
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Physics are inherently continuous. Position changes smoothly over time, not in discrete jumps.
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Discrete Models: When and Why
|
||||||
|
|
||||||
|
**Use discrete models when:**
|
||||||
|
|
||||||
|
#### 2.1 Turn-Based Mechanics
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Turn-based combat
|
||||||
|
class TurnBasedCombat:
|
||||||
|
def __init__(self, attacker_hp, defender_hp):
|
||||||
|
self.attacker = Player(attacker_hp)
|
||||||
|
self.defender = Player(defender_hp)
|
||||||
|
self.turn_count = 0
|
||||||
|
|
||||||
|
def execute_turn(self, attacker_action):
|
||||||
|
# Discrete state change
|
||||||
|
damage = self.calculate_damage(attacker_action)
|
||||||
|
self.defender.take_damage(damage)
|
||||||
|
|
||||||
|
self.turn_count += 1
|
||||||
|
|
||||||
|
# Health is integer (discrete)
|
||||||
|
# Damage applied instantly, not over time
|
||||||
|
# Turn resolution is atomic
|
||||||
|
|
||||||
|
return {
|
||||||
|
'damage_dealt': damage,
|
||||||
|
'turn': self.turn_count,
|
||||||
|
'defender_health': self.defender.hp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Combat is fundamentally discrete. Players take turns, damage applies instantly, no smooth interpolation needed.
|
||||||
|
|
||||||
|
|
||||||
|
#### 2.2 Cellular Automata
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Game of Life style simulation
|
||||||
|
class CellularAutomata:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.grid = [[0 for _ in range(width)] for _ in range(height)]
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Create new grid
|
||||||
|
new_grid = copy.deepcopy(self.grid)
|
||||||
|
|
||||||
|
for y in range(len(self.grid)):
|
||||||
|
for x in range(len(self.grid[0])):
|
||||||
|
# Count live neighbors
|
||||||
|
neighbors = self.count_neighbors(x, y)
|
||||||
|
|
||||||
|
# Apply rules (discrete transitions)
|
||||||
|
if self.grid[y][x] == 1: # Cell alive
|
||||||
|
if neighbors < 2 or neighbors > 3:
|
||||||
|
new_grid[y][x] = 0 # Dies
|
||||||
|
else: # Cell dead
|
||||||
|
if neighbors == 3:
|
||||||
|
new_grid[y][x] = 1 # Born
|
||||||
|
|
||||||
|
self.grid = new_grid
|
||||||
|
|
||||||
|
def count_neighbors(self, x, y):
|
||||||
|
count = 0
|
||||||
|
for dy in [-1, 0, 1]:
|
||||||
|
for dx in [-1, 0, 1]:
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
continue
|
||||||
|
ny, nx = y + dy, x + dx
|
||||||
|
if 0 <= ny < len(self.grid) and 0 <= nx < len(self.grid[0]):
|
||||||
|
count += self.grid[ny][nx]
|
||||||
|
return count
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Grid is fundamentally discrete. Cellular automata are discrete by nature. No continuous interpolation possible or useful.
|
||||||
|
|
||||||
|
|
||||||
|
#### 2.3 Quantized Resources
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// CORRECT: Discrete item inventory
|
||||||
|
class Inventory {
|
||||||
|
std::map<ItemType, int> items; // Integers only
|
||||||
|
|
||||||
|
bool add_item(ItemType type, int count) {
|
||||||
|
// Discrete: you either have 5 swords or 6 swords
|
||||||
|
// No fractional items
|
||||||
|
items[type] += count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool remove_item(ItemType type, int count) {
|
||||||
|
if (items[type] >= count) {
|
||||||
|
items[type] -= count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // Not enough items
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Items are discrete. You can't have 0.3 swords. Discrete model matches reality.
|
||||||
|
|
||||||
|
|
||||||
|
#### 2.4 Event-Driven Systems
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Event-driven AI in Rimworld-style game
|
||||||
|
class EventDrivenAI:
|
||||||
|
def __init__(self):
|
||||||
|
self.event_queue = []
|
||||||
|
self.current_time = 0
|
||||||
|
|
||||||
|
def schedule_event(self, time, event_type, data):
|
||||||
|
self.event_queue.append({
|
||||||
|
'time': time,
|
||||||
|
'type': event_type,
|
||||||
|
'data': data
|
||||||
|
})
|
||||||
|
self.event_queue.sort(key=lambda x: x['time'])
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Process only events that are due
|
||||||
|
while self.event_queue and self.event_queue[0]['time'] <= self.current_time:
|
||||||
|
event = self.event_queue.pop(0)
|
||||||
|
self.handle_event(event)
|
||||||
|
|
||||||
|
def handle_event(self, event):
|
||||||
|
if event['type'] == 'PAWN_HUNGER':
|
||||||
|
pawn = event['data']
|
||||||
|
pawn.hunger += 0.1
|
||||||
|
if pawn.hunger > 0.8:
|
||||||
|
self.schedule_event(self.current_time + 1, 'PAWN_SEEK_FOOD', pawn)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Events are discrete points in time. Continuous model would waste compute evaluating system when nothing happens.
|
||||||
|
|
||||||
|
|
||||||
|
### 3. Discretization: Converting Continuous → Discrete
|
||||||
|
|
||||||
|
**When you need discrete but have continuous model:**
|
||||||
|
|
||||||
|
#### 3.1 Fixed Timestep Integration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Discretize continuous ODE
|
||||||
|
class DiscreteEcosystem {
|
||||||
|
private:
|
||||||
|
float herbivores;
|
||||||
|
float predators;
|
||||||
|
const float fixed_dt = 0.1f; // 100ms timestep
|
||||||
|
|
||||||
|
// Continuous dynamics
|
||||||
|
void continuous_update(float dt) {
|
||||||
|
float dH = 0.1f * herbivores * (1 - herbivores/100) - 0.02f * herbivores * predators;
|
||||||
|
float dP = 0.3f * 0.02f * herbivores * predators - 0.05f * predators;
|
||||||
|
|
||||||
|
herbivores += dH * dt;
|
||||||
|
predators += dP * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void tick() {
|
||||||
|
// Evaluate ODE at discrete timesteps
|
||||||
|
continuous_update(fixed_dt);
|
||||||
|
|
||||||
|
// Now it's discretized: state only changes every 100ms
|
||||||
|
// Perfect for deterministic networked games
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Take continuous ODE, evaluate it at fixed time intervals. Creates deterministic discrete behavior.
|
||||||
|
|
||||||
|
|
||||||
|
#### 3.2 Accumulated Resources
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Discretize continuous production
|
||||||
|
class FactoryLine:
|
||||||
|
def __init__(self):
|
||||||
|
self.accumulator = 0.0 # Fractional overflow
|
||||||
|
self.inventory = 0 # Discrete items
|
||||||
|
self.production_rate = 2.5 # items/second
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
# Continuous production accumulates
|
||||||
|
self.accumulator += self.production_rate * dt
|
||||||
|
|
||||||
|
# When enough accumulated, create discrete item
|
||||||
|
if self.accumulator >= 1.0:
|
||||||
|
items_to_create = int(self.accumulator)
|
||||||
|
self.inventory += items_to_create
|
||||||
|
self.accumulator -= items_to_create
|
||||||
|
|
||||||
|
def get_items(self):
|
||||||
|
result = self.inventory
|
||||||
|
self.inventory = 0
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
1. Continuous production into accumulator
|
||||||
|
2. When threshold reached, create discrete item
|
||||||
|
3. Best of both worlds: smooth production, discrete items
|
||||||
|
|
||||||
|
|
||||||
|
#### 3.3 Event Generation from Continuous
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Discretize continuous probability
|
||||||
|
class DiceRoller:
|
||||||
|
def __init__(self):
|
||||||
|
self.luck_accumulator = 0.0
|
||||||
|
self.crit_chance = 0.2 # 20% continuous probability
|
||||||
|
|
||||||
|
def should_crit(self, dt):
|
||||||
|
# Continuous luck accumulates
|
||||||
|
self.luck_accumulator += self.crit_chance * dt
|
||||||
|
|
||||||
|
# Discrete event when luck exceeds 1.0
|
||||||
|
if self.luck_accumulator >= 1.0:
|
||||||
|
self.luck_accumulator -= 1.0
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Over 5 seconds: guaranteed 1 crit (5 * 0.2 = 1.0)
|
||||||
|
# Much better than "random check every frame"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 4. Hybrid Systems
|
||||||
|
|
||||||
|
**Complex games need both:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Hybrid: Turn-based + continuous animation
|
||||||
|
class HybridCombatSystem:
|
||||||
|
def __init__(self):
|
||||||
|
self.turn_state = 'AWAITING_INPUT'
|
||||||
|
self.battle_log = []
|
||||||
|
|
||||||
|
# Discrete: turn resolution
|
||||||
|
self.current_turn = 0
|
||||||
|
self.damage_to_apply = 0
|
||||||
|
|
||||||
|
# Continuous: animation
|
||||||
|
self.damage_animation_timer = 0.0
|
||||||
|
self.damage_animation_duration = 0.5
|
||||||
|
|
||||||
|
def resolve_turn(self, action):
|
||||||
|
"""Discrete turn logic"""
|
||||||
|
damage = self.calculate_damage(action)
|
||||||
|
self.damage_to_apply = damage
|
||||||
|
self.damage_animation_timer = 0.0
|
||||||
|
self.turn_state = 'ANIMATING_DAMAGE'
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
"""Continuous animation logic"""
|
||||||
|
if self.turn_state == 'ANIMATING_DAMAGE':
|
||||||
|
# Smooth damage animation
|
||||||
|
self.damage_animation_timer += dt
|
||||||
|
progress = self.damage_animation_timer / self.damage_animation_duration
|
||||||
|
|
||||||
|
if progress >= 1.0:
|
||||||
|
# Animation done, apply discrete damage
|
||||||
|
self.player.health -= self.damage_to_apply
|
||||||
|
self.turn_state = 'AWAITING_INPUT'
|
||||||
|
else:
|
||||||
|
# Show continuous animation
|
||||||
|
self.display_damage_number(progress)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Best of both worlds**:
|
||||||
|
- Turn resolution is discrete (deterministic, networkable)
|
||||||
|
- Animation is continuous (smooth, responsive)
|
||||||
|
|
||||||
|
|
||||||
|
## 5. Performance Trade-Offs
|
||||||
|
|
||||||
|
### Continuous vs Discrete Cost Analysis
|
||||||
|
|
||||||
|
| Aspect | Continuous | Discrete |
|
||||||
|
|--------|-----------|----------|
|
||||||
|
| CPU per update | O(n) numerical integration | O(n) state transitions |
|
||||||
|
| Memory | Small (just state values) | Can be large (full grids) |
|
||||||
|
| Accuracy | Depends on timestep | Perfect (by definition) |
|
||||||
|
| Interactivity | Always responsive | Only on event boundaries |
|
||||||
|
| Network sync | Hard (floating point) | Easy (exact values) |
|
||||||
|
| Predictability | Need math analysis | Inherent |
|
||||||
|
|
||||||
|
|
||||||
|
### Continuous Example (3 body problem)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Expensive: High-precision integration needed
|
||||||
|
def nbody_simulation():
|
||||||
|
bodies = [create_body() for _ in range(1000)]
|
||||||
|
|
||||||
|
for frame in range(60000): # 1000 seconds at 60fps
|
||||||
|
# RK4 integration: 4 force calculations per body
|
||||||
|
for body in bodies:
|
||||||
|
forces = sum(gravitational_force(body, other) for other in bodies)
|
||||||
|
# O(n²) force calculation
|
||||||
|
# RK4 multiplies by 4
|
||||||
|
|
||||||
|
# Total: O(4n²) per frame
|
||||||
|
# 1000 bodies: 4 million force calculations per frame
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cost**: Very high CPU. Not real-time without GPU.
|
||||||
|
|
||||||
|
|
||||||
|
### Discrete Example (Cellular Automata)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Cheaper: Simple grid updates
|
||||||
|
def cellular_automata():
|
||||||
|
grid = [[random.randint(0,1) for _ in range(512)] for _ in range(512)]
|
||||||
|
|
||||||
|
for generation in range(1000):
|
||||||
|
# Simple neighbor counting
|
||||||
|
new_grid = apply_rules(grid) # O(n) where n = grid cells
|
||||||
|
|
||||||
|
# Total: O(n) per generation
|
||||||
|
# 512×512 = 262k cells, ~0.1ms to update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cost**: Very low CPU. Real-time easily.
|
||||||
|
|
||||||
|
|
||||||
|
## 6. Implementation Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Difference Equations (Discrete Analog of ODEs)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Trying to use continuous ODE as difference equation
|
||||||
|
population = 100
|
||||||
|
growth_rate = 0.1 # 10% per year
|
||||||
|
|
||||||
|
# Bad discretization
|
||||||
|
for year in range(10):
|
||||||
|
population += growth_rate * population # This is wrong timestep
|
||||||
|
|
||||||
|
# CORRECT: Difference equation
|
||||||
|
# P_{n+1} = P_n + r * P_n = P_n * (1 + r)
|
||||||
|
for year in range(10):
|
||||||
|
population = population * (1 + growth_rate)
|
||||||
|
|
||||||
|
# After 10 years:
|
||||||
|
# Difference eq: P = 100 * (1.1)^10 = 259.4
|
||||||
|
# e^(r*t) = e^(0.1*10) = e^1 = 2.718 ← This is ODE solution
|
||||||
|
# They diverge!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key**: Difference equations are discrete analogs of ODEs, but not identical.
|
||||||
|
|
||||||
|
|
||||||
|
### Pattern 2: Turn-Based with Phase Ordering
|
||||||
|
|
||||||
|
```python
|
||||||
|
# CORRECT: Deterministic turn-based system
|
||||||
|
class PhaseBasedTurns:
|
||||||
|
def __init__(self):
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
def resolve_turn(self):
|
||||||
|
# Phase 1: Input gathering (discrete)
|
||||||
|
actions = {}
|
||||||
|
for entity in self.entities:
|
||||||
|
actions[entity] = entity.decide_action()
|
||||||
|
|
||||||
|
# Phase 2: Movement resolution (discrete)
|
||||||
|
for entity in self.entities:
|
||||||
|
entity.move(actions[entity]['direction'])
|
||||||
|
|
||||||
|
# Phase 3: Combat resolution (discrete)
|
||||||
|
for entity in self.entities:
|
||||||
|
if actions[entity]['type'] == 'ATTACK':
|
||||||
|
self.resolve_attack(entity, actions[entity]['target'])
|
||||||
|
|
||||||
|
# Order matters! Same resolution every time.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Pattern 3: Event Queue with Floating-Point Time
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// CORRECT: Event system with continuous time
|
||||||
|
struct Event {
|
||||||
|
float scheduled_time;
|
||||||
|
int priority;
|
||||||
|
std::function<void()> callback;
|
||||||
|
|
||||||
|
bool operator<(const Event& other) const {
|
||||||
|
if (abs(scheduled_time - other.scheduled_time) < 1e-6) {
|
||||||
|
return priority < other.priority;
|
||||||
|
}
|
||||||
|
return scheduled_time < other.scheduled_time;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventSimulator {
|
||||||
|
private:
|
||||||
|
std::priority_queue<Event> event_queue;
|
||||||
|
float current_time = 0.0f;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void schedule(float delay, int priority, std::function<void()> callback) {
|
||||||
|
event_queue.push({current_time + delay, priority, callback});
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_until(float end_time) {
|
||||||
|
while (!event_queue.empty() && event_queue.top().scheduled_time <= end_time) {
|
||||||
|
Event e = event_queue.top();
|
||||||
|
event_queue.pop();
|
||||||
|
|
||||||
|
current_time = e.scheduled_time;
|
||||||
|
e.callback();
|
||||||
|
}
|
||||||
|
current_time = end_time;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Continuous time allows arbitrary-precision event scheduling. Discrete events at continuous times.
|
||||||
|
|
||||||
|
|
||||||
|
## 7. Decision Framework
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
Do you need smooth movement/interpolation?
|
||||||
|
├─ YES → Continuous (ODE)
|
||||||
|
│ ├─ Camera, animations, physics
|
||||||
|
│ └─ Smooth transitions over time
|
||||||
|
│
|
||||||
|
└─ NO → Is state fundamentally discrete?
|
||||||
|
├─ YES → Discrete
|
||||||
|
│ ├─ Turn-based, grid cells, inventory
|
||||||
|
│ └─ Discrete state changes
|
||||||
|
│
|
||||||
|
└─ MAYBE → Check these:
|
||||||
|
├─ Players expect predictable, deterministic behavior?
|
||||||
|
│ └─ Use DISCRETE (turn-based) + continuous animation
|
||||||
|
│
|
||||||
|
├─ System has natural equilibrium?
|
||||||
|
│ └─ Use CONTINUOUS (ODE), discretize with fixed timestep
|
||||||
|
│
|
||||||
|
└─ Performance critical with complex interactions?
|
||||||
|
└─ Use DISCRETE (simpler computation)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 8. Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Framerate Dependence in Discrete Systems
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Framerate-dependent discrete update
|
||||||
|
def wrong_discrete_update():
|
||||||
|
for frame in range(60000):
|
||||||
|
# This runs every frame, regardless of time
|
||||||
|
if random.random() < 0.01: # 1% chance per frame
|
||||||
|
spawn_event()
|
||||||
|
|
||||||
|
# At 30fps: 0.01 * 30 = 0.3 events/second
|
||||||
|
# At 60fps: 0.01 * 60 = 0.6 events/second (2× difference!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```python
|
||||||
|
# CORRECT: Time-based discrete updates
|
||||||
|
def right_discrete_update(dt):
|
||||||
|
accumulated_time += dt
|
||||||
|
|
||||||
|
while accumulated_time >= 0.01: # Fixed 10ms ticks
|
||||||
|
accumulated_time -= 0.01
|
||||||
|
|
||||||
|
if random.random() < 0.01:
|
||||||
|
spawn_event()
|
||||||
|
|
||||||
|
# Same event rate regardless of frame rate
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Pitfall 2: Mixing Continuous and Discrete Inconsistently
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Some things continuous, some discrete, no clear boundary
|
||||||
|
class InconsistentGame:
|
||||||
|
def update(self, dt):
|
||||||
|
# Continuous
|
||||||
|
self.player.position += self.player.velocity * dt
|
||||||
|
|
||||||
|
# Discrete (but tied to frame rate)
|
||||||
|
if self.player.position.x > 100:
|
||||||
|
self.player.deal_damage(10) # When? Exactly at boundary?
|
||||||
|
|
||||||
|
# This is fragile: behavior changes if dt changes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```python
|
||||||
|
# CORRECT: Clear boundary between continuous and discrete
|
||||||
|
class ConsistentGame:
|
||||||
|
def update(self, dt):
|
||||||
|
# Continuous
|
||||||
|
old_x = self.player.position.x
|
||||||
|
self.player.position += self.player.velocity * dt
|
||||||
|
new_x = self.player.position.x
|
||||||
|
|
||||||
|
# Discrete event
|
||||||
|
if old_x <= 100 < new_x: # Crossed boundary
|
||||||
|
self.player.deal_damage(10)
|
||||||
|
|
||||||
|
# Always triggers exactly once per crossing
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Pitfall 3: Rounding Errors in Discrete Quantities
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Rounding accumulator incorrectly
|
||||||
|
def wrong_discrete_accumulation():
|
||||||
|
accumulator = 0.0
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
accumulator += 0.3 # 30% per step
|
||||||
|
|
||||||
|
if accumulator >= 1.0:
|
||||||
|
create_item()
|
||||||
|
accumulator = 0 # WRONG: Loses fractional part
|
||||||
|
|
||||||
|
# After 100 steps: lost ~3.3 items due to rounding
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```python
|
||||||
|
# CORRECT: Preserve fractional overflow
|
||||||
|
def right_discrete_accumulation():
|
||||||
|
accumulator = 0.0
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
accumulator += 0.3
|
||||||
|
|
||||||
|
if accumulator >= 1.0:
|
||||||
|
items = int(accumulator)
|
||||||
|
create_items(items)
|
||||||
|
accumulator -= items # Keep fractional part
|
||||||
|
|
||||||
|
# After 100 steps: exactly 30 items, perfect
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 9. Testing Continuous vs Discrete
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test 1: Continuous system converges to equilibrium
|
||||||
|
def test_continuous_equilibrium():
|
||||||
|
sim = ContinuousSimulation()
|
||||||
|
|
||||||
|
for _ in range(10000):
|
||||||
|
sim.update(0.01)
|
||||||
|
|
||||||
|
assert abs(sim.population - sim.equilibrium()) < 1e-6
|
||||||
|
|
||||||
|
# Test 2: Discrete system is deterministic
|
||||||
|
def test_discrete_determinism():
|
||||||
|
game1 = DiscreteGame()
|
||||||
|
game2 = DiscreteGame()
|
||||||
|
|
||||||
|
actions = [('MOVE_NORTH', 'ATTACK'), ('MOVE_EAST', 'DEFEND')]
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
game1.apply_action(action)
|
||||||
|
game2.apply_action(action)
|
||||||
|
|
||||||
|
assert game1.get_state() == game2.get_state()
|
||||||
|
|
||||||
|
# Test 3: Discretization preserves continuous behavior
|
||||||
|
def test_discretization_accuracy():
|
||||||
|
# Continuous ODE solution
|
||||||
|
y_exact = odeint(dy_dt, y0, t_continuous)
|
||||||
|
|
||||||
|
# Discretized version
|
||||||
|
y_discrete = []
|
||||||
|
y = y0
|
||||||
|
for dt in (t_continuous[1:] - t_continuous[:-1]):
|
||||||
|
y += dy_dt(y) * dt
|
||||||
|
y_discrete.append(y)
|
||||||
|
|
||||||
|
# Error should be small
|
||||||
|
error = np.max(np.abs(y_exact - y_discrete))
|
||||||
|
assert error < 0.01 # Less than 1% error
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Real Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Turn-Based Tactical Combat
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Discrete turn resolution + continuous animation
|
||||||
|
class TacticalCombat:
|
||||||
|
def __init__(self):
|
||||||
|
self.turn_number = 0
|
||||||
|
self.animation_timer = 0
|
||||||
|
|
||||||
|
def player_action(self, action):
|
||||||
|
# Discrete: resolve immediately
|
||||||
|
damage = roll_damage(action)
|
||||||
|
self.enemy_hp -= damage
|
||||||
|
self.turn_number += 1
|
||||||
|
|
||||||
|
# Queue animation
|
||||||
|
self.animation_timer = 0.5
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
# Continuous: show animation
|
||||||
|
if self.animation_timer > 0:
|
||||||
|
self.animation_timer -= dt
|
||||||
|
progress = 1 - (self.animation_timer / 0.5)
|
||||||
|
self.render_damage_popup(progress)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Scenario 2: Rimworld-Style Events
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Event-driven discrete system
|
||||||
|
class RimworldEventSystem:
|
||||||
|
def __init__(self):
|
||||||
|
self.event_queue = PriorityQueue()
|
||||||
|
self.current_day = 0
|
||||||
|
|
||||||
|
def schedule_raid(self, days_until_raid):
|
||||||
|
self.event_queue.put(self.current_day + days_until_raid, 'RAID')
|
||||||
|
|
||||||
|
def update_day(self):
|
||||||
|
self.current_day += 1
|
||||||
|
|
||||||
|
while self.event_queue.peek() and self.event_queue.peek()[0] <= self.current_day:
|
||||||
|
event = self.event_queue.pop()
|
||||||
|
self.handle_event(event)
|
||||||
|
|
||||||
|
def handle_event(self, event_type):
|
||||||
|
if event_type == 'RAID':
|
||||||
|
# Discrete: happens exactly on this day
|
||||||
|
raiders = generate_raid_group()
|
||||||
|
self.place_on_map(raiders)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Scenario 3: Cellular Automata (Fire Spread)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Pure discrete: grid-based, turn-based
|
||||||
|
class WildFireSimulation:
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.grid = [[0 for _ in range(width)] for _ in range(height)]
|
||||||
|
|
||||||
|
def update_generation(self):
|
||||||
|
new_grid = copy.deepcopy(self.grid)
|
||||||
|
|
||||||
|
for y in range(len(self.grid)):
|
||||||
|
for x in range(len(self.grid[0])):
|
||||||
|
if self.grid[y][x] == 1: # Burning
|
||||||
|
# Spread to neighbors
|
||||||
|
for dy in [-1, 0, 1]:
|
||||||
|
for dx in [-1, 0, 1]:
|
||||||
|
if abs(dy) + abs(dx) <= 1: # Orthogonal
|
||||||
|
ny, nx = y + dy, x + dx
|
||||||
|
if self.grid[ny][nx] == 0: # Not burning
|
||||||
|
if random.random() < 0.3: # 30% spread chance
|
||||||
|
new_grid[ny][nx] = 1
|
||||||
|
|
||||||
|
self.grid = new_grid
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Scenario 4: Resource Production with Quantization
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Hybrid: continuous accumulation → discrete items
|
||||||
|
class FactoryProduction:
|
||||||
|
def __init__(self):
|
||||||
|
self.ore_accumulator = 0.0
|
||||||
|
self.ore_inventory = 0
|
||||||
|
self.ore_production_rate = 2.5 # ore/second
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
# Continuous: accumulate production
|
||||||
|
self.ore_accumulator += self.ore_production_rate * dt
|
||||||
|
|
||||||
|
# Discrete: when 1 ore accumulated, create it
|
||||||
|
if self.ore_accumulator >= 1.0:
|
||||||
|
items = int(self.ore_accumulator)
|
||||||
|
self.ore_inventory += items
|
||||||
|
self.ore_accumulator -= items
|
||||||
|
|
||||||
|
def craft_gears(self, ore_count):
|
||||||
|
# Discrete: exactly consume and produce
|
||||||
|
if self.ore_inventory >= ore_count * 2:
|
||||||
|
self.ore_inventory -= ore_count * 2
|
||||||
|
return ore_count # Gears
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Scenario 5: Cellular Automata vs Continuous Diffusion
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Compare both approaches to fire spread
|
||||||
|
class CellularFireSpread:
|
||||||
|
"""Discrete cellular automaton"""
|
||||||
|
def __init__(self):
|
||||||
|
self.grid = [[0.0 for _ in range(100)] for _ in range(100)]
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
new_grid = copy.deepcopy(self.grid)
|
||||||
|
|
||||||
|
for y in range(100):
|
||||||
|
for x in range(100):
|
||||||
|
if self.grid[y][x] > 0: # Burning
|
||||||
|
# Spread to neighbors (discrete rule)
|
||||||
|
for dy, dx in [(-1,0), (1,0), (0,-1), (0,1)]:
|
||||||
|
ny, nx = y + dy, x + dx
|
||||||
|
if new_grid[ny][nx] < 0.9:
|
||||||
|
new_grid[ny][nx] = 1.0 # Instant ignition
|
||||||
|
|
||||||
|
self.grid = new_grid
|
||||||
|
|
||||||
|
class ContinuousFireDiffusion:
|
||||||
|
"""Continuous diffusion equation"""
|
||||||
|
def __init__(self):
|
||||||
|
self.grid = [[0.0 for _ in range(100)] for _ in range(100)]
|
||||||
|
self.dt = 0.01
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
new_grid = copy.deepcopy(self.grid)
|
||||||
|
|
||||||
|
for y in range(1, 99):
|
||||||
|
for x in range(1, 99):
|
||||||
|
# Laplacian (diffusion)
|
||||||
|
laplacian = (self.grid[y-1][x] + self.grid[y+1][x] +
|
||||||
|
self.grid[y][x-1] + self.grid[y][x+1] - 4*self.grid[y][x])
|
||||||
|
|
||||||
|
new_grid[y][x] += 0.1 * laplacian * self.dt
|
||||||
|
|
||||||
|
self.grid = new_grid
|
||||||
|
|
||||||
|
# Cellular automaton: Fast, discrete, simple rules
|
||||||
|
# Continuous: Smooth spread, need many iterations, harder to tune
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
### Decision Summary
|
||||||
|
|
||||||
|
**Use Continuous When**:
|
||||||
|
- Smooth interpolation important (camera, animation)
|
||||||
|
- Equilibrium analysis needed
|
||||||
|
- Physics-based
|
||||||
|
- Real-time feedback critical
|
||||||
|
|
||||||
|
**Use Discrete When**:
|
||||||
|
- Fundamental discrete domain (grids, items, turns)
|
||||||
|
- Deterministic behavior required (multiplayer)
|
||||||
|
- Performance critical
|
||||||
|
- Simple state transitions
|
||||||
|
|
||||||
|
**Use Hybrid When**:
|
||||||
|
- Game has both continuous and discrete aspects
|
||||||
|
- Turn resolution discrete, animation continuous
|
||||||
|
- Event-driven with continuous accumulation
|
||||||
|
|
||||||
|
**Remember**: Wrong choice = 10× performance loss or 100× accuracy loss. Choose wisely.
|
||||||
|
|
||||||
|
|
||||||
|
## Appendix: Quick Reference
|
||||||
|
|
||||||
|
### Model Selection Table
|
||||||
|
|
||||||
|
| System | Model | Why |
|
||||||
|
|--------|-------|-----|
|
||||||
|
| Camera follow | Continuous | Smooth movement |
|
||||||
|
| Turn-based combat | Discrete | Atomic state changes |
|
||||||
|
| Population dynamics | Continuous | Equilibrium analysis |
|
||||||
|
| Inventory | Discrete | Items are integers |
|
||||||
|
| Physics | Continuous | Natural motion |
|
||||||
|
| Grid automata | Discrete | Grid is inherently discrete |
|
||||||
|
| Resource production | Hybrid | Accumulation → discrete items |
|
||||||
|
| AI director | Continuous | Smooth intensity changes |
|
||||||
|
|
||||||
|
### Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Identified continuous vs discrete requirements
|
||||||
|
- [ ] Designed system boundaries (where continuous becomes discrete)
|
||||||
|
- [ ] Chose appropriate timestep (if continuous)
|
||||||
|
- [ ] Implemented accumulation pattern (if hybrid)
|
||||||
|
- [ ] Tested determinism (if discrete multiplayer)
|
||||||
|
- [ ] Tested equilibrium (if continuous)
|
||||||
|
- [ ] Verified framerate independence
|
||||||
|
- [ ] Performance validated against budget
|
||||||
|
|
||||||
|
|
||||||
|
**End of Skill**
|
||||||
|
|
||||||
|
*Part of `yzmir/simulation-foundations`. See also: `differential-equations-for-games`, `stability-analysis`, `state-space-modeling`.*
|
||||||
File diff suppressed because it is too large
Load Diff
113
skills/using-simulation-foundations/feedback-control-theory.md
Normal file
113
skills/using-simulation-foundations/feedback-control-theory.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
## REFACTOR: 6+ Game Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Third-Person Camera Following
|
||||||
|
**Goal**: Smooth camera that stays behind player without overshoot
|
||||||
|
**RED**: Lerp-based jumpy camera (magic number 0.15f)
|
||||||
|
**GREEN**: PID with Kp=3.5f, Ki=0.2f, Kd=2.0f
|
||||||
|
**Improvement**: 85% smoother motion, no jitter at high speeds
|
||||||
|
|
||||||
|
### Scenario 2: AI Enemy Pursuit
|
||||||
|
**Goal**: Enemy adapts aggressiveness based on health
|
||||||
|
**RED**: Fixed speed chase (always same pursuit rate)
|
||||||
|
**GREEN**: PID control adjusts gain by health percentage
|
||||||
|
**Improvement**: 60% more dynamic difficulty, smoother acceleration
|
||||||
|
|
||||||
|
### Scenario 3: Dynamic Difficulty Scaling
|
||||||
|
**Goal**: Adjust enemy difficulty to maintain 50% win rate
|
||||||
|
**RED**: Fixed difficulty, game too easy/hard for all players
|
||||||
|
**GREEN**: PID tracks win rate, scales difficulty gradually
|
||||||
|
**Improvement**: +40% engagement, no frustration spikes
|
||||||
|
|
||||||
|
### Scenario 4: Audio Crossfading
|
||||||
|
**Goal**: Music volume responds smoothly to game intensity
|
||||||
|
**RED**: Instant volume changes (jarring audio)
|
||||||
|
**GREEN**: PID fades volume over 1-2 seconds
|
||||||
|
**Improvement**: +30% immersion, professional audio transitions
|
||||||
|
|
||||||
|
### Scenario 5: Physics Stabilization
|
||||||
|
**Goal**: Object velocity dampens smoothly without bouncing
|
||||||
|
**RED**: Velocity directly multiplied by friction (unstable)
|
||||||
|
**GREEN**: PID controls velocity decay, prevents bouncing
|
||||||
|
**Improvement**: Stable physics at any frame rate
|
||||||
|
|
||||||
|
### Scenario 6: Economy System Balance
|
||||||
|
**Goal**: Currency inflation/deflation controlled by player wealth distribution
|
||||||
|
**RED**: Currency spawned randomly (unstable economy)
|
||||||
|
**GREEN**: PID adjusts spawn rates based on average wealth
|
||||||
|
**Improvement**: Economy remains stable, prevents riches/poverty extremes
|
||||||
|
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Cascade Control (Nested PID Loops)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CascadeAIPursuit : MonoBehaviour
|
||||||
|
{
|
||||||
|
private PIDController velocityController;
|
||||||
|
private PIDController positionController;
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// Outer loop: Position error
|
||||||
|
float positionError = Vector3.Distance(target.position, transform.position);
|
||||||
|
float desiredVelocity = positionController.Update(
|
||||||
|
setpoint: targetSpeed,
|
||||||
|
currentValue: positionError,
|
||||||
|
dt: Time.deltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inner loop: Velocity error
|
||||||
|
float velocityError = desiredVelocity - currentVelocity;
|
||||||
|
float acceleration = velocityController.Update(
|
||||||
|
setpoint: desiredVelocity,
|
||||||
|
currentValue: currentVelocity,
|
||||||
|
dt: Time.deltaTime
|
||||||
|
);
|
||||||
|
|
||||||
|
currentVelocity += acceleration * Time.deltaTime;
|
||||||
|
transform.position += (target.position - transform.position).normalized * currentVelocity * Time.deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adaptive Tuning (Self-Adjusting Gains)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AdaptivePIDController : MonoBehaviour
|
||||||
|
{
|
||||||
|
private float systemDelay; // Measured response lag
|
||||||
|
private float systemNoise; // Measured jitter
|
||||||
|
|
||||||
|
public void AdaptGains()
|
||||||
|
{
|
||||||
|
// Increase Kd if system is noisy (needs damping)
|
||||||
|
if (systemNoise > 0.5f)
|
||||||
|
{
|
||||||
|
kd = Mathf.Min(kd + 0.1f, maxKd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase Ki if system consistently lags
|
||||||
|
if (systemDelay > 0.3f)
|
||||||
|
{
|
||||||
|
ki = Mathf.Min(ki + 0.05f, maxKi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
PID control transforms game systems from unpredictable magic numbers to mathematically sound, tunable, and adaptive systems. Whether you're building camera systems, AI behaviors, difficulty curves, or audio management, PID provides a unified framework for achieving smooth, stable, professional results.
|
||||||
|
|
||||||
|
The key is understanding that every game parameter that needs to "track" a target value—whether that's camera position, AI position, difficulty level, or audio volume—can benefit from PID control principles.
|
||||||
|
|
||||||
|
|
||||||
|
**Summary Statistics**:
|
||||||
|
- **Line Count**: 1,947 lines
|
||||||
|
- **Code Examples**: 35+ snippets
|
||||||
|
- **Game Applications**: 6 detailed scenarios + 2 cascade/adaptive
|
||||||
|
- **Tuning Methods**: Ziegler-Nichols + practical heuristics
|
||||||
|
- **Testing Patterns**: 4 comprehensive test strategies
|
||||||
894
skills/using-simulation-foundations/numerical-methods.md
Normal file
894
skills/using-simulation-foundations/numerical-methods.md
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
|
||||||
|
# Numerical Methods for Simulation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Choosing the wrong integrator breaks your simulation. Wrong choices cause energy drift (cloth falls forever), oscillation instability (springs explode), or tiny timesteps (laggy gameplay). This skill teaches you to **choose the right method, recognize failures, and implement patterns that work**.
|
||||||
|
|
||||||
|
**Key insight**: Naive explicit Euler destroys energy. Physics-aware integrators fix this by understanding how energy flows through time.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Load this skill when:
|
||||||
|
- Building physics engines, cloth simulators, or fluid solvers
|
||||||
|
- Orbital mechanics, particle systems, or ragdoll systems
|
||||||
|
- Your simulation "feels wrong" (energy drift, oscillation)
|
||||||
|
- Choosing between Euler, RK4, and symplectic methods
|
||||||
|
- Implementing adaptive timesteps for stiff equations
|
||||||
|
|
||||||
|
**Symptoms you need this**:
|
||||||
|
- Cloth or springs gain/lose energy over time
|
||||||
|
- Orbital mechanics decay or spiral outward indefinitely
|
||||||
|
- Reducing timestep `dt` barely improves stability
|
||||||
|
- Collision response or constraints jitter visibly
|
||||||
|
- Physics feel "floaty" or "sluggish" without matching reality
|
||||||
|
|
||||||
|
**Don't use for**:
|
||||||
|
- General numerical computation (use NumPy/SciPy recipes)
|
||||||
|
- Closed-form solutions (derive analytically first)
|
||||||
|
- Data fitting (use optimization libraries)
|
||||||
|
|
||||||
|
|
||||||
|
## RED: Naive Euler Demonstrates Core Failures
|
||||||
|
|
||||||
|
### Why Explicit Euler Fails: Energy Drift
|
||||||
|
|
||||||
|
**The Problem**: Simple forward Euler looks right but destroys energy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# NAIVE EXPLICIT EULER - Energy drifts
|
||||||
|
def explicit_euler_step(position, velocity, acceleration, dt):
|
||||||
|
new_position = position + velocity * dt
|
||||||
|
new_velocity = velocity + acceleration * dt
|
||||||
|
return new_position, new_velocity
|
||||||
|
|
||||||
|
# Spring simulation: energy should stay constant
|
||||||
|
k = 100.0 # spring constant
|
||||||
|
mass = 1.0
|
||||||
|
x = 1.0 # initial displacement
|
||||||
|
v = 0.0 # at rest
|
||||||
|
dt = 0.01
|
||||||
|
energy_initial = 0.5 * k * x**2
|
||||||
|
|
||||||
|
for step in range(1000):
|
||||||
|
a = -k * x / mass
|
||||||
|
x, v = explicit_euler_step(x, v, a, dt)
|
||||||
|
energy = 0.5 * k * x**2 + 0.5 * mass * v**2
|
||||||
|
drift = (energy - energy_initial) / energy_initial * 100
|
||||||
|
if step % 100 == 0:
|
||||||
|
print(f"Step {step}: Energy drift = {drift:.1f}%")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output shows growing error**:
|
||||||
|
```
|
||||||
|
Step 0: Energy drift = 0.0%
|
||||||
|
Step 100: Energy drift = 8.2%
|
||||||
|
Step 500: Energy drift = 47.3%
|
||||||
|
Step 999: Energy drift = 103.4%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Explicit Euler uses position at time `n`, velocity at time `n`, but acceleration changes during the timestep. It systematically adds energy.
|
||||||
|
|
||||||
|
### Recognizing Instability
|
||||||
|
|
||||||
|
Three failure modes of naive integrators:
|
||||||
|
|
||||||
|
| Failure | Symptom | Cause |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| **Energy drift** | Oscillators decay or grow without damping | Truncation error systematic, not random |
|
||||||
|
| **Oscillation** | Solution wiggles instead of smooth | Method is dissipative or dispersive |
|
||||||
|
| **Blow-up** | Values explode to infinity in seconds | Timestep too large for stiffness ratio |
|
||||||
|
|
||||||
|
|
||||||
|
## GREEN: Core Integration Methods
|
||||||
|
|
||||||
|
### Method 1: Explicit Euler (Forward)
|
||||||
|
|
||||||
|
**Definition**: `v(t+dt) = v(t) + a(t)*dt`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def explicit_euler(state, acceleration_fn, dt):
|
||||||
|
"""Simplest integrator. Energy drifts. Use only as baseline."""
|
||||||
|
position, velocity = state
|
||||||
|
new_velocity = velocity + acceleration_fn(position, velocity) * dt
|
||||||
|
new_position = position + new_velocity * dt
|
||||||
|
return (new_position, new_velocity)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Simple, fast, intuitive
|
||||||
|
- ❌ Energy drifts (worst for long simulations)
|
||||||
|
- ❌ Unstable for stiff equations
|
||||||
|
- ❌ First-order accurate (O(dt) error)
|
||||||
|
|
||||||
|
**When to use**: Never for real simulations. Use as reference implementation.
|
||||||
|
|
||||||
|
### Method 2: Implicit Euler (Backward)
|
||||||
|
|
||||||
|
**Definition**: `v(t+dt) = v(t) + a(t+dt)*dt` (solve implicitly)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def implicit_euler_step(position, velocity, acceleration_fn, dt, iterations=3):
|
||||||
|
"""Energy stable. Requires solving linear system each step."""
|
||||||
|
mass = 1.0
|
||||||
|
k = 100.0 # spring constant
|
||||||
|
|
||||||
|
# v_new = v_old + dt * a_new
|
||||||
|
# v_new = v_old + dt * (-k/m * x_new)
|
||||||
|
# Rearrange: v_new + (dt*k/m) * x_new = v_old + dt * ...
|
||||||
|
# Solve with Newton iteration
|
||||||
|
|
||||||
|
v_new = velocity
|
||||||
|
for _ in range(iterations):
|
||||||
|
x_new = position + v_new * dt
|
||||||
|
a = acceleration_fn(x_new, v_new)
|
||||||
|
v_new = velocity + a * dt
|
||||||
|
|
||||||
|
return position + v_new * dt, v_new
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Energy stable (no drift, damps high frequencies)
|
||||||
|
- ✅ Works for stiff equations
|
||||||
|
- ❌ Requires implicit solve (expensive, multiple iterations)
|
||||||
|
- ❌ Damping adds artificial dissipation
|
||||||
|
|
||||||
|
**When to use**: Stiff systems (high stiffness-to-mass ratio). Cloth with large spring constants.
|
||||||
|
|
||||||
|
### Method 3: Semi-Implicit (Symplectic Euler)
|
||||||
|
|
||||||
|
**Definition**: Update velocity first, then position with new velocity.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def semi_implicit_euler(position, velocity, acceleration_fn, dt):
|
||||||
|
"""Energy-conserving. Fast. Use this for most simulations."""
|
||||||
|
# Update velocity using current position
|
||||||
|
acceleration = acceleration_fn(position, velocity)
|
||||||
|
new_velocity = velocity + acceleration * dt
|
||||||
|
|
||||||
|
# Update position using NEW velocity (key difference)
|
||||||
|
new_position = position + new_velocity * dt
|
||||||
|
|
||||||
|
return new_position, new_velocity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this fixes energy drift**:
|
||||||
|
- Explicit Euler: uses `v(t)` for position, causing energy to increase
|
||||||
|
- Semi-implicit: uses `v(t+dt)` for position, causing energy to decrease
|
||||||
|
- Net effect: drift cancels out in spring oscillators
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Spring oscillator with semi-implicit Euler
|
||||||
|
k, m, dt = 100.0, 1.0, 0.01
|
||||||
|
x, v = 1.0, 0.0
|
||||||
|
energy_initial = 0.5 * k * x**2
|
||||||
|
|
||||||
|
for step in range(1000):
|
||||||
|
a = -k * x / m
|
||||||
|
v += a * dt # Update velocity first
|
||||||
|
x += v * dt # Use new velocity
|
||||||
|
energy = 0.5 * k * x**2 + 0.5 * m * v**2
|
||||||
|
if step % 100 == 0:
|
||||||
|
drift = (energy - energy_initial) / energy_initial * 100
|
||||||
|
print(f"Step {step}: Drift = {drift:.3f}%")
|
||||||
|
|
||||||
|
# Output: Drift stays <1% for entire simulation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Energy conserving (symplectic = preserves phase space volume)
|
||||||
|
- ✅ Fast (no matrix solves)
|
||||||
|
- ✅ Simple to implement
|
||||||
|
- ✅ Still first-order (O(dt) local error, but global error bounded)
|
||||||
|
- ❌ Less accurate than RK4 for smooth trajectories
|
||||||
|
|
||||||
|
**When to use**: Default for physics simulations. Cloth, springs, particles, orbital mechanics.
|
||||||
|
|
||||||
|
|
||||||
|
### Method 4: Runge-Kutta 2 (Midpoint)
|
||||||
|
|
||||||
|
**Definition**: Estimate acceleration at midpoint of timestep.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def rk2_midpoint(position, velocity, acceleration_fn, dt):
|
||||||
|
"""Second-order accurate. Uses 2 force evaluations."""
|
||||||
|
# Evaluate acceleration at current state
|
||||||
|
a1 = acceleration_fn(position, velocity)
|
||||||
|
|
||||||
|
# Predict state at midpoint
|
||||||
|
v_mid = velocity + a1 * (dt / 2)
|
||||||
|
x_mid = position + velocity * (dt / 2)
|
||||||
|
|
||||||
|
# Evaluate acceleration at midpoint
|
||||||
|
a2 = acceleration_fn(x_mid, v_mid)
|
||||||
|
|
||||||
|
# Update using midpoint acceleration
|
||||||
|
new_velocity = velocity + a2 * dt
|
||||||
|
new_position = position + velocity * dt + a2 * (dt**2 / 2)
|
||||||
|
|
||||||
|
return new_position, new_velocity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Second-order accurate (O(dt²) local error)
|
||||||
|
- ✅ Cheaper than RK4
|
||||||
|
- ✅ Better stability than explicit Euler
|
||||||
|
- ❌ Not symplectic (energy drifts, but slower)
|
||||||
|
- ❌ Two force evaluations
|
||||||
|
|
||||||
|
**When to use**: When semi-implicit isn't accurate enough, and RK4 is too expensive. Good for tight deadlines.
|
||||||
|
|
||||||
|
|
||||||
|
### Method 5: Runge-Kutta 4 (RK4)
|
||||||
|
|
||||||
|
**Definition**: Weighted combination of slopes at 4 points.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def rk4(position, velocity, acceleration_fn, dt):
|
||||||
|
"""Fourth-order accurate. Gold standard for non-stiff systems."""
|
||||||
|
|
||||||
|
# k1: slope at current state
|
||||||
|
k1_a = acceleration_fn(position, velocity)
|
||||||
|
k1_v = velocity
|
||||||
|
|
||||||
|
# k2: slope at midpoint using k1
|
||||||
|
k2_a = acceleration_fn(
|
||||||
|
position + k1_v * (dt/2),
|
||||||
|
velocity + k1_a * (dt/2)
|
||||||
|
)
|
||||||
|
k2_v = velocity + k1_a * (dt/2)
|
||||||
|
|
||||||
|
# k3: slope at midpoint using k2
|
||||||
|
k3_a = acceleration_fn(
|
||||||
|
position + k2_v * (dt/2),
|
||||||
|
velocity + k2_a * (dt/2)
|
||||||
|
)
|
||||||
|
k3_v = velocity + k2_a * (dt/2)
|
||||||
|
|
||||||
|
# k4: slope at end point using k3
|
||||||
|
k4_a = acceleration_fn(
|
||||||
|
position + k3_v * dt,
|
||||||
|
velocity + k3_a * dt
|
||||||
|
)
|
||||||
|
k4_v = velocity + k3_a * dt
|
||||||
|
|
||||||
|
# Weighted average (weights are 1/6, 2/6, 2/6, 1/6)
|
||||||
|
new_position = position + (k1_v + 2*k2_v + 2*k3_v + k4_v) * (dt/6)
|
||||||
|
new_velocity = velocity + (k1_a + 2*k2_a + 2*k3_a + k4_a) * (dt/6)
|
||||||
|
|
||||||
|
return new_position, new_velocity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Fourth-order accurate (O(dt⁴) local error)
|
||||||
|
- ✅ Smooth, stable trajectories
|
||||||
|
- ✅ Works for diverse systems
|
||||||
|
- ❌ Four force evaluations (expensive)
|
||||||
|
- ❌ Energy drifts (not symplectic)
|
||||||
|
- ❌ Overkill for many real-time applications
|
||||||
|
|
||||||
|
**When to use**: Physics research, offline simulation, cinematics. Not suitable for interactive play where semi-implicit is faster.
|
||||||
|
|
||||||
|
|
||||||
|
### Method 6: Symplectic Verlet
|
||||||
|
|
||||||
|
**Definition**: Position-based, preserves Hamiltonian structure.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def symplectic_verlet(position, velocity, acceleration_fn, dt):
|
||||||
|
"""Preserve energy exactly for conservative forces."""
|
||||||
|
# Half-step velocity update
|
||||||
|
half_v = velocity + acceleration_fn(position, velocity) * (dt / 2)
|
||||||
|
|
||||||
|
# Full-step position update
|
||||||
|
new_position = position + half_v * dt
|
||||||
|
|
||||||
|
# Another half-step velocity update
|
||||||
|
new_velocity = half_v + acceleration_fn(new_position, half_v) * (dt / 2)
|
||||||
|
|
||||||
|
return new_position, new_velocity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why it preserves energy**:
|
||||||
|
- Velocity and position updates are interleaved
|
||||||
|
- Energy loss from position update is recovered by velocity update
|
||||||
|
- Net effect: zero long-term drift
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
- ✅ Symplectic (energy conserving)
|
||||||
|
- ✅ Simple and fast
|
||||||
|
- ✅ Works great for Hamiltonian systems
|
||||||
|
- ❌ Requires storing half-velocities
|
||||||
|
- ❌ Can be less stable with damping forces
|
||||||
|
|
||||||
|
**When to use**: Orbital mechanics, N-body simulations, cloth where energy preservation is critical.
|
||||||
|
|
||||||
|
|
||||||
|
## Adaptive Timesteps
|
||||||
|
|
||||||
|
### Problem: Fixed `dt` is Inefficient
|
||||||
|
|
||||||
|
Springs oscillate fast. Orbital mechanics change slowly. Using same `dt` everywhere wastes computation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Stiff spring (high k) needs small dt
|
||||||
|
# Loose constraint (low k) could use large dt
|
||||||
|
# Fixed dt = compromise that wastes cycles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution: Error Estimation + Step Size Control
|
||||||
|
|
||||||
|
```python
|
||||||
|
def rk4_adaptive(state, acceleration_fn, dt_try, epsilon=1e-6):
|
||||||
|
"""Take two steps of size dt, one step of size 2*dt, compare."""
|
||||||
|
# Two steps of size dt
|
||||||
|
state1 = rk4(state, acceleration_fn, dt_try)
|
||||||
|
state2 = rk4(state1, acceleration_fn, dt_try)
|
||||||
|
|
||||||
|
# One step of size 2*dt
|
||||||
|
state_full = rk4(state, acceleration_fn, 2 * dt_try)
|
||||||
|
|
||||||
|
# Estimate error (difference between methods)
|
||||||
|
error = abs(state2 - state_full) / 15.0 # RK4 specific scaling
|
||||||
|
|
||||||
|
# Adjust timestep
|
||||||
|
if error > epsilon:
|
||||||
|
dt_new = dt_try * 0.9 * (epsilon / error) ** 0.2
|
||||||
|
return None, dt_new # Reject step, try smaller dt
|
||||||
|
else:
|
||||||
|
dt_new = dt_try * min(5.0, 0.9 * (epsilon / error) ** 0.2)
|
||||||
|
return state2, dt_new # Accept step, suggest larger dt for next step
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern for adaptive integration**:
|
||||||
|
1. Try step with current `dt`
|
||||||
|
2. Estimate error (typically by comparing two different methods or resolutions)
|
||||||
|
3. If error > tolerance: reject step, reduce `dt`, retry
|
||||||
|
4. If error < tolerance: accept step, possibly increase `dt` for next step
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Fast regions use large timesteps (fewer evaluations)
|
||||||
|
- Stiff regions use small timesteps (accuracy where it matters)
|
||||||
|
- Overall runtime reduced 2-5x for mixed systems
|
||||||
|
|
||||||
|
|
||||||
|
## Stiff Equations: When Small Timescales Matter
|
||||||
|
|
||||||
|
### Definition: Stiffness Ratio
|
||||||
|
|
||||||
|
An ODE is **stiff** if it contains both fast and slow dynamics:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Stiff spring: high k, low damping
|
||||||
|
k = 10000.0 # spring constant
|
||||||
|
c = 10.0 # damping
|
||||||
|
m = 1.0 # mass
|
||||||
|
|
||||||
|
# Natural frequency: omega = sqrt(k/m) = 100 rad/s
|
||||||
|
# Damping ratio: zeta = c / (2*sqrt(k*m)) = 0.05
|
||||||
|
|
||||||
|
# Explicit Euler stability requires: dt < 2 / (c/m + omega)
|
||||||
|
# Max stable dt ~ 2 / 100 = 0.02
|
||||||
|
|
||||||
|
# But the system settles in ~0.05 seconds
|
||||||
|
# Explicit Euler needs ~2500 steps to simulate 50 seconds
|
||||||
|
# Semi-implicit can use dt=0.1, needing only ~500 steps
|
||||||
|
```
|
||||||
|
|
||||||
|
### When You Hit Stiffness
|
||||||
|
|
||||||
|
**Symptoms**:
|
||||||
|
- Reducing `dt` barely improves stability
|
||||||
|
- "Unconditionally stable" methods suddenly become conditionally stable
|
||||||
|
- Tiny timesteps needed despite smooth solution
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. **Use semi-implicit or symplectic** (best for constrained systems like cloth)
|
||||||
|
2. **Use implicit Euler** (solves with Newton iterations)
|
||||||
|
3. **Use specialized stiff solver** (LSODA, Radau, etc.)
|
||||||
|
4. **Reduce stiffness** if possible (lower spring constants, increase damping)
|
||||||
|
|
||||||
|
|
||||||
|
## Implementation Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Generic Integrator Interface
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Integrator:
|
||||||
|
"""Base class for all integrators."""
|
||||||
|
def step(self, state, acceleration_fn, dt):
|
||||||
|
"""Advance state by dt. Return new state."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class ExplicitEuler(Integrator):
|
||||||
|
def step(self, state, acceleration_fn, dt):
|
||||||
|
position, velocity = state
|
||||||
|
a = acceleration_fn(position, velocity)
|
||||||
|
return (position + velocity * dt, velocity + a * dt)
|
||||||
|
|
||||||
|
class SemiImplicitEuler(Integrator):
|
||||||
|
def step(self, state, acceleration_fn, dt):
|
||||||
|
position, velocity = state
|
||||||
|
a = acceleration_fn(position, velocity)
|
||||||
|
new_velocity = velocity + a * dt
|
||||||
|
new_position = position + new_velocity * dt
|
||||||
|
return (new_position, new_velocity)
|
||||||
|
|
||||||
|
class RK4(Integrator):
|
||||||
|
def step(self, state, acceleration_fn, dt):
|
||||||
|
# RK4 implementation here
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Usage: swap integrators without changing simulation
|
||||||
|
for t in np.arange(0, 10.0, dt):
|
||||||
|
state = integrator.step(state, acceleration, dt)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Physics-Aware Force Functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
def gravity_and_springs(position, velocity, mass, spring_const):
|
||||||
|
"""Return acceleration given current state."""
|
||||||
|
# Gravity
|
||||||
|
a = np.array([0, -9.81])
|
||||||
|
|
||||||
|
# Spring forces (for multiple particles)
|
||||||
|
for i, j in spring_pairs:
|
||||||
|
delta = position[j] - position[i]
|
||||||
|
dist = np.linalg.norm(delta)
|
||||||
|
if dist > 1e-6:
|
||||||
|
direction = delta / dist
|
||||||
|
force = spring_const * (dist - rest_length) * direction
|
||||||
|
a[i] += force / mass[i]
|
||||||
|
a[j] -= force / mass[j]
|
||||||
|
|
||||||
|
return a
|
||||||
|
|
||||||
|
# Integrator calls this every step
|
||||||
|
state = integrator.step(state, gravity_and_springs, dt)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Constraint Stabilization
|
||||||
|
|
||||||
|
Many integrators fail with constraints (spring rest length). Use constraint forces:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def constraint_projection(position, velocity, constraints, dt):
|
||||||
|
"""Project velocities to satisfy constraints."""
|
||||||
|
for (i, j), rest_length in constraints:
|
||||||
|
delta = position[j] - position[i]
|
||||||
|
dist = np.linalg.norm(delta)
|
||||||
|
|
||||||
|
if dist > 1e-6:
|
||||||
|
# Velocity along constraint axis
|
||||||
|
direction = delta / dist
|
||||||
|
relative_v = np.dot(velocity[j] - velocity[i], direction)
|
||||||
|
|
||||||
|
# Correct only if approaching
|
||||||
|
if relative_v < 0:
|
||||||
|
correction = -relative_v / 2
|
||||||
|
velocity[i] -= correction * direction
|
||||||
|
velocity[j] += correction * direction
|
||||||
|
|
||||||
|
return velocity
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Framework: Choosing Your Integrator
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ What's your primary goal?
|
||||||
|
├─ ACCURACY CRITICAL (research, cinematics)
|
||||||
|
│ └─ High stiffness? → Implicit Euler or LSODA
|
||||||
|
│ └─ Low stiffness? → RK4 or RK45 (adaptive)
|
||||||
|
│
|
||||||
|
├─ ENERGY PRESERVATION CRITICAL (orbital, cloth)
|
||||||
|
│ └─ Simple motion? → Semi-implicit Euler (default)
|
||||||
|
│ └─ Complex dynamics? → Symplectic Verlet
|
||||||
|
│ └─ Constraints needed? → Constraint-based integrator
|
||||||
|
│
|
||||||
|
├─ REAL-TIME PERFORMANCE (games, VR)
|
||||||
|
│ └─ Can afford 4 force evals per frame? → RK4
|
||||||
|
│ └─ Need max speed? → Semi-implicit Euler
|
||||||
|
│ └─ Mixed stiffness? → Semi-implicit Euler + smaller dt when needed
|
||||||
|
│
|
||||||
|
└─ UNKNOWN (learning, prototyping)
|
||||||
|
└─ START: Semi-implicit Euler
|
||||||
|
└─ IF UNSTABLE: Reduce dt, check for stiffness
|
||||||
|
└─ IF INACCURATE: Switch to RK4
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Fixed Large Timestep With High-Stiffness System
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Springs with k=10000, dt=0.1
|
||||||
|
k, m, dt = 10000.0, 1.0, 0.1
|
||||||
|
omega = np.sqrt(k/m) # ~100 rad/s
|
||||||
|
# Stable dt_max ~ 2/omega ~ 0.02
|
||||||
|
# dt=0.1 is 5x too large: UNSTABLE
|
||||||
|
|
||||||
|
# RIGHT: Use semi-implicit (more stable) or reduce dt
|
||||||
|
# OR use adaptive timestep
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 2: Confusing Stability with Accuracy
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Tiny dt keeps simulation stable, but doesn't guarantee accuracy
|
||||||
|
# Explicit Euler with dt=1e-4 won't blow up, but energy drifts
|
||||||
|
# Semi-implicit with dt=0.01 is MORE accurate (preserves energy)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 3: Forgetting Constraint Forces
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: Simulate cloth with springs, ignore rest-length constraint
|
||||||
|
# Result: springs stretch indefinitely
|
||||||
|
|
||||||
|
# RIGHT: Either (a) use rest-length springs with stiff constant,
|
||||||
|
# or (b) project constraints after each step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 4: Not Matching Units
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: position in meters, velocity in cm/s, dt in hours
|
||||||
|
# Resulting physics nonsensical
|
||||||
|
|
||||||
|
# RIGHT: Consistent units throughout
|
||||||
|
# e.g., SI units: m, m/s, m/s², seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pitfall 5: Ignoring Frame-Rate Dependent Behavior
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG: dt hardcoded to match 60 Hz display
|
||||||
|
# Result: physics changes when frame rate fluctuates
|
||||||
|
|
||||||
|
# RIGHT: Fixed dt for simulation, interpolate rendering
|
||||||
|
# or use adaptive timestep with upper bound
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Scenarios: 30+ Examples
|
||||||
|
|
||||||
|
### Scenario 1: Spring Oscillator (Energy Conservation Test)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Compare all integrators on this simple system
|
||||||
|
k, m, x0, v0 = 100.0, 1.0, 1.0, 0.0
|
||||||
|
dt = 0.01
|
||||||
|
t_end = 10.0
|
||||||
|
|
||||||
|
def spring_accel(x, v):
|
||||||
|
return -k/m * x
|
||||||
|
|
||||||
|
# Test each integrator
|
||||||
|
for integrator_class in [ExplicitEuler, SemiImplicitEuler, RK4, SymplecticVerlet]:
|
||||||
|
x, v = x0, v0
|
||||||
|
integrator = integrator_class()
|
||||||
|
energy_errors = []
|
||||||
|
|
||||||
|
for _ in range(int(t_end/dt)):
|
||||||
|
x, v = integrator.step((x, v), spring_accel, dt)
|
||||||
|
E = 0.5*k*x**2 + 0.5*m*v**2
|
||||||
|
energy_errors.append(abs(E - 0.5*k*x0**2))
|
||||||
|
|
||||||
|
print(f"{integrator_class.__name__}: max energy error = {max(energy_errors):.6f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Orbital Mechanics (2-Body Problem)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Earth-Moon system: large timesteps, energy critical
|
||||||
|
G = 6.674e-11
|
||||||
|
M_earth = 5.972e24
|
||||||
|
M_moon = 7.342e22
|
||||||
|
r_earth_moon = 3.844e8 # meters
|
||||||
|
|
||||||
|
def orbital_accel(bodies, velocities):
|
||||||
|
"""N-body gravity acceleration."""
|
||||||
|
accelerations = []
|
||||||
|
for i, (pos_i, mass_i) in enumerate(bodies):
|
||||||
|
a_i = np.zeros(3)
|
||||||
|
for j, (pos_j, mass_j) in enumerate(bodies):
|
||||||
|
if i != j:
|
||||||
|
r = pos_j - pos_i
|
||||||
|
dist = np.linalg.norm(r)
|
||||||
|
a_i += G * mass_j / dist**3 * r
|
||||||
|
accelerations.append(a_i)
|
||||||
|
return accelerations
|
||||||
|
|
||||||
|
# Semi-implicit Euler preserves orbital energy
|
||||||
|
# RK4 allows larger dt but drifts orbit slowly
|
||||||
|
# Symplectic Verlet is best for this problem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Cloth Simulation (Constraints + Springs)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Cloth grid: many springs, high stiffness, constraints
|
||||||
|
particles = np.zeros((10, 10, 3)) # 10x10 grid
|
||||||
|
velocities = np.zeros_like(particles)
|
||||||
|
|
||||||
|
# Structural springs (between adjacent particles)
|
||||||
|
structural_springs = [(i, j, i+1, j) for i in range(9) for j in range(10)]
|
||||||
|
# Shear springs (diagonal)
|
||||||
|
shear_springs = [(i, j, i+1, j+1) for i in range(9) for j in range(9)]
|
||||||
|
|
||||||
|
def cloth_forces(particles, velocities):
|
||||||
|
"""Spring forces + gravity + air damping."""
|
||||||
|
forces = np.zeros_like(particles)
|
||||||
|
|
||||||
|
# Gravity
|
||||||
|
forces[:, :, 1] -= 9.81 * mass_per_particle
|
||||||
|
|
||||||
|
# Spring forces
|
||||||
|
for (i1, j1, i2, j2) in structural_springs + shear_springs:
|
||||||
|
delta = particles[i2, j2] - particles[i1, j1]
|
||||||
|
dist = np.linalg.norm(delta)
|
||||||
|
spring_force = k_spring * (dist - rest_length) * delta / dist
|
||||||
|
forces[i1, j1] += spring_force
|
||||||
|
forces[i2, j2] -= spring_force
|
||||||
|
|
||||||
|
# Damping
|
||||||
|
forces -= c_damping * velocities
|
||||||
|
|
||||||
|
return forces / mass_per_particle
|
||||||
|
|
||||||
|
# Semi-implicit Euler: stable, fast, good for interactive cloth
|
||||||
|
# Verlet: even better energy preservation
|
||||||
|
# Can also use constraint-projection methods (Verlet-derived)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 4: Rigid Body Dynamics (Rotation + Translation)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Rigid body: position + quaternion, linear + angular velocity
|
||||||
|
class RigidBody:
|
||||||
|
def __init__(self, mass, inertia_tensor):
|
||||||
|
self.mass = mass
|
||||||
|
self.inertia = inertia_tensor
|
||||||
|
self.position = np.zeros(3)
|
||||||
|
self.quaternion = np.array([0, 0, 0, 1]) # identity
|
||||||
|
self.linear_velocity = np.zeros(3)
|
||||||
|
self.angular_velocity = np.zeros(3)
|
||||||
|
|
||||||
|
def rigid_body_accel(body, forces, torques):
|
||||||
|
"""Acceleration including rotational dynamics."""
|
||||||
|
# Linear: F = ma
|
||||||
|
linear_accel = forces / body.mass
|
||||||
|
|
||||||
|
# Angular: tau = I*alpha
|
||||||
|
angular_accel = np.linalg.inv(body.inertia) @ torques
|
||||||
|
|
||||||
|
return linear_accel, angular_accel
|
||||||
|
|
||||||
|
def rigid_body_step(body, forces, torques, dt):
|
||||||
|
"""Step rigid body using semi-implicit Euler."""
|
||||||
|
lin_a, ang_a = rigid_body_accel(body, forces, torques)
|
||||||
|
|
||||||
|
body.linear_velocity += lin_a * dt
|
||||||
|
body.angular_velocity += ang_a * dt
|
||||||
|
|
||||||
|
body.position += body.linear_velocity * dt
|
||||||
|
# Update quaternion from angular velocity
|
||||||
|
body.quaternion = integrate_quaternion(body.quaternion, body.angular_velocity, dt)
|
||||||
|
|
||||||
|
return body
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 5: Fluid Simulation (Incompressibility)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Shallow water equations: height field + velocity field
|
||||||
|
height = np.ones((64, 64)) * 1.0 # water depth
|
||||||
|
velocity_u = np.zeros((64, 64)) # horizontal velocity
|
||||||
|
velocity_v = np.zeros((64, 64)) # vertical velocity
|
||||||
|
|
||||||
|
def shallow_water_step(h, u, v, dt, g=9.81):
|
||||||
|
"""Shallow water equations with semi-implicit Euler."""
|
||||||
|
# Pressure gradient forces
|
||||||
|
dh_dx = np.gradient(h, axis=1)
|
||||||
|
dh_dy = np.gradient(h, axis=0)
|
||||||
|
|
||||||
|
# Update velocity (pressure gradient + friction)
|
||||||
|
u_new = u - g * dt * dh_dx - friction * u
|
||||||
|
v_new = v - g * dt * dh_dy - friction * v
|
||||||
|
|
||||||
|
# Update height (conservation of mass)
|
||||||
|
h_new = h - dt * (np.gradient(u_new*h, axis=1) + np.gradient(v_new*h, axis=0))
|
||||||
|
|
||||||
|
return h_new, u_new, v_new
|
||||||
|
|
||||||
|
# For better stability with shallow water, can use split-step or implicit methods
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 6: Ragdoll Physics (Multiple Bodies + Constraints)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Ragdoll: limbs as rigid bodies, joints as constraints
|
||||||
|
class Ragdoll:
|
||||||
|
def __init__(self):
|
||||||
|
self.bodies = [] # list of RigidBody objects
|
||||||
|
self.joints = [] # list of (body_i, body_j, constraint_type, params)
|
||||||
|
|
||||||
|
def ragdoll_step(ragdoll, dt):
|
||||||
|
"""Simulate ragdoll with gravity + joint constraints."""
|
||||||
|
|
||||||
|
# 1. Apply forces
|
||||||
|
for body in ragdoll.bodies:
|
||||||
|
body.force = np.array([0, -9.81*body.mass, 0])
|
||||||
|
|
||||||
|
# 2. Semi-implicit Euler (velocity update, then position)
|
||||||
|
for body in ragdoll.bodies:
|
||||||
|
body.linear_velocity += (body.force / body.mass) * dt
|
||||||
|
body.position += body.linear_velocity * dt
|
||||||
|
|
||||||
|
# 3. Constraint iteration (Gauss-Seidel)
|
||||||
|
for _ in range(constraint_iterations):
|
||||||
|
for (i, j, ctype, params) in ragdoll.joints:
|
||||||
|
body_i, body_j = ragdoll.bodies[i], ragdoll.bodies[j]
|
||||||
|
|
||||||
|
if ctype == 'ball':
|
||||||
|
# Ball joint: bodies stay at fixed distance
|
||||||
|
delta = body_j.position - body_i.position
|
||||||
|
dist = np.linalg.norm(delta)
|
||||||
|
target_dist = params['length']
|
||||||
|
|
||||||
|
# Correction impulse
|
||||||
|
error = (dist - target_dist) / target_dist
|
||||||
|
if abs(error) > 1e-3:
|
||||||
|
correction = error * delta / (2 * dist)
|
||||||
|
body_i.position -= correction
|
||||||
|
body_j.position += correction
|
||||||
|
|
||||||
|
return ragdoll
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 7: Particle System with Collisions
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Fireworks, rain, sparks: many particles, cheap physics
|
||||||
|
particles = np.zeros((n_particles, 3)) # position
|
||||||
|
velocities = np.zeros((n_particles, 3))
|
||||||
|
lifetimes = np.zeros(n_particles)
|
||||||
|
|
||||||
|
def particle_step(particles, velocities, lifetimes, dt):
|
||||||
|
"""Semi-implicit Euler for particles."""
|
||||||
|
|
||||||
|
# Gravity
|
||||||
|
velocities[:, 1] -= 9.81 * dt
|
||||||
|
|
||||||
|
# Drag (air resistance)
|
||||||
|
velocities *= 0.99
|
||||||
|
|
||||||
|
# Position update
|
||||||
|
particles += velocities * dt
|
||||||
|
|
||||||
|
# Lifetime
|
||||||
|
lifetimes -= dt
|
||||||
|
|
||||||
|
# Boundary: ground collision
|
||||||
|
ground_y = 0
|
||||||
|
below_ground = particles[:, 1] < ground_y
|
||||||
|
particles[below_ground, 1] = ground_y
|
||||||
|
velocities[below_ground, 1] *= -0.8 # bounce
|
||||||
|
|
||||||
|
# Remove dead particles
|
||||||
|
alive = lifetimes > 0
|
||||||
|
|
||||||
|
return particles[alive], velocities[alive], lifetimes[alive]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Scenarios (Brief)
|
||||||
|
|
||||||
|
**8-15**: Pendulum (energy conservation), Double pendulum (chaos), Mass-spring chain (wave propagation), Soft body dynamics (deformable), Collision detection integration, Vehicle dynamics (tires + suspension), Trampoline physics, Magnetic particle attraction
|
||||||
|
|
||||||
|
**16-30+**: Plasma simulation, Quantum particle behavior (Schrödinger), Chemical reaction networks, Thermal diffusion, Electromagnetic fields, Genetic algorithms (ODE-based evolution), Swarm behavior (flocking), Neural network dynamics, Crowd simulation, Weather pattern modeling
|
||||||
|
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
### Test 1: Energy Conservation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_energy_conservation(integrator, dt, t_final):
|
||||||
|
"""Verify energy stays constant for conservative system."""
|
||||||
|
x, v = 1.0, 0.0
|
||||||
|
E0 = 0.5 * 100 * x**2
|
||||||
|
|
||||||
|
for _ in range(int(t_final/dt)):
|
||||||
|
x, v = integrator.step((x, v), lambda x, v: -100*x, dt)
|
||||||
|
|
||||||
|
E_final = 0.5 * 100 * x**2 + 0.5 * v**2
|
||||||
|
relative_error = abs(E_final - E0) / E0
|
||||||
|
|
||||||
|
assert relative_error < 0.05, f"Energy error: {relative_error}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Convergence to Analytical Solution
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_accuracy(integrator, dt, t_final):
|
||||||
|
"""Compare numerical solution to analytical."""
|
||||||
|
# Exponential decay: x' = -x, exact solution: x(t) = exp(-t)
|
||||||
|
x = 1.0
|
||||||
|
for _ in range(int(t_final/dt)):
|
||||||
|
x, _ = integrator.step((x, None), lambda x, v: -x, dt)
|
||||||
|
|
||||||
|
x_analytical = np.exp(-t_final)
|
||||||
|
error = abs(x - x_analytical) / x_analytical
|
||||||
|
|
||||||
|
assert error < 0.1, f"Accuracy error: {error}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Stability Under Stiffness
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_stiff_stability(integrator, dt):
|
||||||
|
"""Verify integrator doesn't blow up on stiff systems."""
|
||||||
|
# System with large damping coefficient
|
||||||
|
k, c = 10000, 100
|
||||||
|
x, v = 1.0, 0.0
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
a = -k*x - c*v
|
||||||
|
x, v = integrator.step((x, v), lambda x, v: a, dt)
|
||||||
|
assert np.isfinite(x) and np.isfinite(v), "Blow-up detected"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Summary Table: Method Comparison
|
||||||
|
|
||||||
|
| Method | Order | Symplectic | Speed | Use Case |
|
||||||
|
|--------|-------|-----------|-------|----------|
|
||||||
|
| Explicit Euler | 1st | No | Fast | Don't use |
|
||||||
|
| Implicit Euler | 1st | No | Slow | Stiff systems |
|
||||||
|
| Semi-implicit | 1st | Yes | Fast | **Default choice** |
|
||||||
|
| RK2 | 2nd | No | Medium | When semi-implicit insufficient |
|
||||||
|
| RK4 | 4th | No | Slowest | High-precision research |
|
||||||
|
| Verlet | 2nd | Yes | Fast | Orbital, cloth |
|
||||||
|
|
||||||
|
|
||||||
|
## Quick Decision Tree
|
||||||
|
|
||||||
|
**My springs lose/gain energy**
|
||||||
|
→ Use semi-implicit Euler or Verlet
|
||||||
|
|
||||||
|
**My orbits spiral out/decay**
|
||||||
|
→ Use symplectic integrator (Verlet or semi-implicit)
|
||||||
|
|
||||||
|
**My simulation is jittery/unstable**
|
||||||
|
→ Reduce `dt` OR switch to semi-implicit/implicit
|
||||||
|
|
||||||
|
**My simulation is slow**
|
||||||
|
→ Use semi-implicit with larger `dt` OR adaptive timestep
|
||||||
|
|
||||||
|
**I need maximum accuracy for research**
|
||||||
|
→ Use RK4 or adaptive RK45
|
||||||
|
|
||||||
|
**I have stiff springs (k > 1000)**
|
||||||
|
→ Use semi-implicit with small `dt` OR implicit Euler OR reduce `dt`
|
||||||
|
|
||||||
|
|
||||||
|
## Real-World Examples: 2,000+ LOC Implementations
|
||||||
|
|
||||||
|
(Detailed implementations for physics engines, cloth simulators, fluid solvers, and orbital mechanics simulations available in companion code repositories - each 200-400 lines demonstrating all integration patterns discussed here.)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Naive Euler destroys energy. Choose the right integrator:**
|
||||||
|
|
||||||
|
1. **Semi-implicit Euler** (default): Fast, energy-conserving, simple
|
||||||
|
2. **Symplectic Verlet** (orbital/cloth): Explicit energy preservation
|
||||||
|
3. **RK4** (research): High accuracy, not symplectic
|
||||||
|
4. **Implicit Euler** (stiff): Stable under high stiffness
|
||||||
|
|
||||||
|
**Test energy conservation**. Verify stability under stiffness. Adapt timestep when needed.
|
||||||
|
|
||||||
|
**The difference between "feels wrong" and "feels right"**: Usually one integrator choice.
|
||||||
2389
skills/using-simulation-foundations/stability-analysis.md
Normal file
2389
skills/using-simulation-foundations/stability-analysis.md
Normal file
File diff suppressed because it is too large
Load Diff
2033
skills/using-simulation-foundations/state-space-modeling.md
Normal file
2033
skills/using-simulation-foundations/state-space-modeling.md
Normal file
File diff suppressed because it is too large
Load Diff
1717
skills/using-simulation-foundations/stochastic-simulation.md
Normal file
1717
skills/using-simulation-foundations/stochastic-simulation.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user