### 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 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 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_queue; float current_time = 0.0f; public: void schedule(float delay, int priority, std::function 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`.*