Repository navigation
Destruction System
SparkEngine provides a destructible environment system for FPS gameplay. Objects can take damage, show progressive damage stages, and fracture into physics-simulated debris when destroyed.
Source: SparkEngine/Source/Engine/Destruction/DestructionSystem.h
The destruction system is composed of five core types that work together to manage destructible objects from pattern definition through damage application to debris cleanup:
┌─────────────────────────────────────────────────────────────────┐
│ Game Code │
│ (registers patterns, applies damage, listens for events) │
├─────────────────────────────────────────────────────────────────┤
│ DestructionSystem │
│ ┌──────────────┬─────────────────┬───────────────────────────┐ │
│ │ Pattern │ Damage │ Debris │ │
│ │ Registry │ Processing │ Management │ │
│ │ │ │ │ │
│ │ RegisterPattern() ApplyDamage() Update() (cleanup) │ │
│ │ GetPattern() ForceDestroy() SetMaxDebris() │ │
│ └──────────────┴─────────────────┴───────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ FracturePattern DestructibleComponent DestructionEvent│
│ (piece definitions, (ECS component: health, (callback data: │
│ sound, particles) damage stages, flags) position, force)│
├─────────────────────────────────────────────────────────────────┤
│ Physics System Audio System │
│ (rigid body debris) (destruction sounds) │
└─────────────────────────────────────────────────────────────────┘
| Class | Responsibility |
|---|---|
DestructionSystem |
Singleton manager: registers patterns, applies damage, spawns and cleans debris |
FracturePattern |
Defines how an object breaks apart: debris pieces, sound effect, particle effect |
FracturePiece |
A single debris piece with mesh name, mass, offset, lifetime, and scatter force |
DestructibleComponent |
ECS component attached to entities: health, damage stages, resistance, destroyed flag |
DestructionEvent |
Event payload passed to destruction callbacks with position, impact direction, force |
DebrisInstance |
Internal tracking struct for active debris lifetime management |
All destruction types reside in namespace Spark.
A single debris piece created when an object is destroyed:
struct FracturePiece
{
std::string name; // Piece identifier (e.g. "top", "chunk1")
std::string meshName; // Mesh asset name for this debris
DirectX::XMFLOAT3 localOffset{0, 0, 0}; // Offset from parent center
float mass = 1.0f; // Physics mass (kg)
float lifetime = 10.0f; // Seconds before debris is cleaned up
float scatterForce = 5.0f; // Force applied at fracture time
};| Field | Type | Default | Description |
|---|---|---|---|
name |
std::string |
"" |
Unique identifier within the pattern |
meshName |
std::string |
"" |
Asset name for the debris mesh (loaded via Asset Pipeline) |
localOffset |
XMFLOAT3 |
{0,0,0} |
Position offset from parent object center |
mass |
float |
1.0 |
Physics mass in kg; heavier pieces fly shorter distances |
lifetime |
float |
10.0 |
Seconds the debris stays in the world before auto-removal |
scatterForce |
float |
5.0 |
Impulse magnitude applied in the hit direction at spawn |
Defines how an object breaks apart -- a collection of debris pieces with associated effects:
class FracturePattern
{
public:
void AddPiece(const FracturePiece& piece);
const std::vector<FracturePiece>& GetPieces() const;
void SetDestructionSound(const std::string& sound);
const std::string& GetDestructionSound() const;
void SetParticleEffect(const std::string& effect);
const std::string& GetParticleEffect() const;
private:
std::vector<FracturePiece> m_pieces;
std::string m_destructionSound;
std::string m_particleEffect;
};| Method | Description |
|---|---|
AddPiece(piece) |
Append a debris piece to the pattern |
GetPieces() |
Get the read-only vector of all pieces |
SetDestructionSound(name) |
Set the audio asset played on destruction |
GetDestructionSound() |
Get the destruction sound name |
SetParticleEffect(name) |
Set the particle effect spawned on destruction |
GetParticleEffect() |
Get the particle effect name |
// Simple wooden crate: 2 pieces
FracturePattern crate;
crate.AddPiece(FracturePiece{"top", "meshTop", {0, 0.5f, 0}, 2.0f, 10.0f, 5.0f});
crate.AddPiece(FracturePiece{"side", "meshSide", {0.5f, 0, 0}, 1.5f, 10.0f, 5.0f});
crate.SetDestructionSound("crate_break");
crate.SetParticleEffect("wood_splinters");
destruction.RegisterPattern("wooden_crate", crate);
// Complex brick wall: 5 pieces with varying mass and scatter
FracturePattern brickWall;
brickWall.AddPiece(FracturePiece{"chunk1", "wall_chunk_large", {-0.5f, 0, 0}, 5.0f, 8.0f, 3.0f});
brickWall.AddPiece(FracturePiece{"chunk2", "wall_chunk_large", { 0.5f, 0, 0}, 5.0f, 8.0f, 3.0f});
brickWall.AddPiece(FracturePiece{"brick1", "loose_brick", {0, 0.5f, 0}, 0.5f, 5.0f, 6.0f});
brickWall.AddPiece(FracturePiece{"brick2", "loose_brick", {0, -0.3f, 0}, 0.5f, 5.0f, 6.0f});
brickWall.AddPiece(FracturePiece{"dust", "dust_cloud", {0, 0, 0}, 0.1f, 3.0f, 1.0f});
brickWall.SetDestructionSound("wall_collapse");
brickWall.SetParticleEffect("brick_dust");
destruction.RegisterPattern("brick_wall", brickWall);| Object Type | Recommended Pieces | Mass Range | Scatter Force | Lifetime |
|---|---|---|---|---|
| Wooden crate | 2-4 | 0.5 - 3.0 kg | 4.0 - 8.0 | 8 - 12s |
| Brick wall | 4-8 | 0.5 - 10.0 kg | 2.0 - 6.0 | 5 - 10s |
| Glass window | 3-6 | 0.05 - 0.2 kg | 3.0 - 10.0 | 4 - 8s |
| Metal barrel | 2-3 | 2.0 - 5.0 kg | 5.0 - 12.0 | 10 - 15s |
| Concrete pillar | 5-10 | 5.0 - 20.0 kg | 1.0 - 4.0 | 8 - 15s |
ECS component attached to entities that can be damaged and destroyed:
struct DestructibleComponent
{
float health = 100.0f; // Current health
float maxHealth = 100.0f; // Maximum health
std::string patternName; // Fracture pattern to use
bool isDestroyed = false; // Whether the object has been destroyed
bool destructionProcessed = false; // Whether debris has been spawned
// Damage resistance
float damageThreshold = 0.0f; // Minimum damage to register (ignore scratches)
float damageMultiplier = 1.0f; // Multiplier for incoming damage
// Partial damage states
int damageStage = 0; // Current visual damage stage (0 = pristine)
int maxDamageStages = 3; // Number of stages before destruction
// Method
bool ApplyDamage(float amount); // Returns true if destroyed by this damage
};| Field | Type | Default | Description |
|---|---|---|---|
health |
float |
100.0 |
Current health points |
maxHealth |
float |
100.0 |
Maximum health points |
patternName |
std::string |
"" |
Name of the registered FracturePattern
|
isDestroyed |
bool |
false |
Set to true when health reaches zero |
destructionProcessed |
bool |
false |
Set to true after debris has been spawned |
damageThreshold |
float |
0.0 |
Damage below this value is ignored |
damageMultiplier |
float |
1.0 |
Incoming damage is multiplied by this factor |
damageStage |
int |
0 |
Current visual damage stage (0 = pristine) |
maxDamageStages |
int |
3 |
Total number of visual stages before full destruction |
The ApplyDamage method automatically calculates the damage stage based on remaining health:
healthPercent = clamp(health / maxHealth, 0.0, 1.0)
damageStage = clamp(int((1.0 - healthPercent) * maxDamageStages), 0, maxDamageStages)
For a wall with maxHealth = 200 and maxDamageStages = 3:
| Health Range | healthPercent | damageStage | Visual State |
|---|---|---|---|
| 200 - 134 | 1.00 - 0.67 | 0 | Pristine |
| 133 - 67 | 0.66 - 0.34 | 1 | Cracks visible |
| 66 - 1 | 0.33 - 0.01 | 2 | Chunks missing |
| 0 | 0.00 | 3 | Fully destroyed |
The damageThreshold and damageMultiplier fields allow tuning how objects respond to different damage amounts:
// Armored wall: resists small arms fire, takes double from explosives
auto& wall = world.AddComponent<DestructibleComponent>(entity);
wall.health = 500.0f;
wall.maxHealth = 500.0f;
wall.damageThreshold = 15.0f; // Ignore pistol shots (< 15 damage)
wall.damageMultiplier = 0.5f; // Take half damage from bullets
wall.patternName = "armored_wall";
// Glass window: fragile, shatters easily
auto& glass = world.AddComponent<DestructibleComponent>(entity);
glass.health = 10.0f;
glass.maxHealth = 10.0f;
glass.damageThreshold = 0.0f; // Any damage registers
glass.damageMultiplier = 2.0f; // Double damage from everything
glass.maxDamageStages = 1; // No intermediate stages, just shatters
glass.patternName = "glass_window";Event data passed to destruction callbacks:
struct DestructionEvent
{
uint32_t entityId = 0; // Entity that was destroyed
DirectX::XMFLOAT3 position{0, 0, 0}; // World position of destruction
DirectX::XMFLOAT3 impactDir{0, 0, 0}; // Direction of the destroying hit
float impactForce = 0.0f; // Force of the destroying hit
std::string patternName; // Fracture pattern used
};The main system class that manages patterns, applies damage, and handles debris:
DestructionSystem();
void Initialize();
void Update(float deltaTime); // Clean up expired debrisvoid RegisterPattern(const std::string& name, const FracturePattern& pattern);
const FracturePattern* GetPattern(const std::string& name) const;RegisterPattern stores the pattern in an std::unordered_map<std::string, FracturePattern>. GetPattern returns nullptr if the name is not found.
void ApplyDamage(uint32_t entityId, float damage,
const DirectX::XMFLOAT3& hitPoint,
const DirectX::XMFLOAT3& hitDir);
void ForceDestroy(uint32_t entityId, float force = 10.0f);| Method | Description |
|---|---|
ApplyDamage |
Apply damage respecting threshold and multiplier. If health reaches zero, triggers destruction. |
ForceDestroy |
Immediately destroy regardless of health. The force parameter controls debris scatter intensity. |
size_t GetActiveDebrisCount() const;
void SetMaxDebris(size_t max); // Default: 500
void SetDebrisLifetimeMultiplier(float mult); // Default: 1.0The system tracks all active debris via std::vector<DebrisInstance>:
struct DebrisInstance // (private)
{
uint32_t entityId = 0;
float remainingLifetime = 10.0f;
};void OnDestruction(std::function<void(const DestructionEvent&)> callback);Multiple callbacks can be registered. All are invoked when any destructible object is destroyed.
std::string Console_GetStatus() const;ApplyDamage(entityId, damage, hitPoint, hitDir)
│
├── Lookup DestructibleComponent on entity
│ └── If not found, return silently
│
├── component.ApplyDamage(damage)
│ ├── Apply damageMultiplier
│ ├── Check against damageThreshold
│ ├── Subtract from health
│ ├── Update damageStage
│ └── Return true if health <= 0
│
├── If destroyed:
│ ├── Lookup FracturePattern by patternName
│ ├── For each FracturePiece in pattern:
│ │ ├── Spawn debris entity with mesh
│ │ ├── Apply rigid body with mass
│ │ ├── Apply impulse = hitDir * scatterForce
│ │ └── Track as DebrisInstance
│ │
│ ├── Play destruction sound (if set)
│ ├── Spawn particle effect (if set)
│ ├── Set destructionProcessed = true
│ ├── Increment m_totalDestructions
│ │
│ └── Fire DestructionEvent to all callbacks
│
└── If not destroyed:
└── Update visual damage stage (game code can switch meshes/materials)
Update(deltaTime)
│
├── For each DebrisInstance in m_debris:
│ ├── remainingLifetime -= deltaTime * m_debrisLifetimeMultiplier
│ ├── If remainingLifetime <= 0:
│ │ ├── Remove debris entity from world
│ │ ├── Decrement m_activeDebrisCount
│ │ └── Remove from vector
│ └── If m_activeDebrisCount > m_maxDebris:
│ └── Remove oldest debris to stay under limit
│
└── Return
When m_activeDebrisCount exceeds m_maxDebris, the system removes the oldest debris first (FIFO order). This prevents performance degradation during heavy destruction sequences.
DestructionSystem destruction;
destruction.Initialize();
// Register fracture patterns
FracturePattern crate;
crate.AddPiece(FracturePiece{"top", "meshTop", {0, 0.5f, 0}, 2.0f});
crate.AddPiece(FracturePiece{"side", "meshSide", {0.5f, 0, 0}, 1.5f});
crate.SetDestructionSound("crate_break");
crate.SetParticleEffect("wood_splinters");
destruction.RegisterPattern("wooden_crate", crate);
// Attach to entity via ECS
auto& comp = world.AddComponent<DestructibleComponent>(entity);
comp.health = 50.0f;
comp.maxHealth = 50.0f;
comp.patternName = "wooden_crate";
// Apply damage
destruction.ApplyDamage(entity, 30.0f, hitPoint, hitDirection);
// Force-destroy regardless of health
destruction.ForceDestroy(entity, 15.0f);// Set up a wall with 3 visual damage stages before full destruction
auto& wall = world.AddComponent<DestructibleComponent>(wallEntity);
wall.health = 200.0f;
wall.maxHealth = 200.0f;
wall.patternName = "brick_wall";
wall.damageThreshold = 5.0f; // Ignore damage below 5
wall.damageMultiplier = 1.0f;
wall.maxDamageStages = 3; // Cracks -> chunks -> collapse
// After applying damage, check damage stage to swap visuals
destruction.ApplyDamage(wallEntity, 60.0f, hitPoint, hitDir);
auto& wallComp = world.GetComponent<DestructibleComponent>(wallEntity);
switch (wallComp.damageStage)
{
case 0: SetMesh(wallEntity, "wall_pristine"); break;
case 1: SetMesh(wallEntity, "wall_cracked"); break;
case 2: SetMesh(wallEntity, "wall_broken"); break;
case 3: /* Fully destroyed, debris spawned */ break;
}// Listen for destruction events
destruction.OnDestruction([&](const DestructionEvent& event) {
// Spawn visual effects at the destruction site
particleSystem.Emit(event.position, event.patternName + "_particles");
audio.PlaySound3D("debris_impact", event.position);
// Publish to event bus for other systems
bus.Publish(EntityDestroyedEvent{ event.entityId });
// Update score if player destroyed it
if (IsPlayerDamageSource(event))
{
AddScore(event.entityId, 10);
}
});// When a bullet hits a destructible object
bus.Subscribe<CollisionEvent>([&](const CollisionEvent& e) {
if (world.HasComponent<DestructibleComponent>(e.entityB))
{
XMFLOAT3 hitPoint = GetCollisionPoint(e);
XMFLOAT3 hitDir = GetImpactDirection(e);
destruction.ApplyDamage(e.entityB, e.impactForce, hitPoint, hitDir);
}
});// Radial damage from an explosion
void ApplyExplosionDamage(const XMFLOAT3& center, float radius, float maxDamage)
{
auto entities = QueryEntitiesInRadius<DestructibleComponent>(center, radius);
for (auto entity : entities)
{
float dist = Distance(center, GetPosition(entity));
float falloff = 1.0f - (dist / radius); // Linear falloff
float damage = maxDamage * std::max(0.0f, falloff);
XMFLOAT3 dir = Normalize(GetPosition(entity) - center);
destruction.ApplyDamage(entity, damage, GetPosition(entity), dir);
}
}
// Grenade explosion
ApplyExplosionDamage(grenadePosition, 10.0f, 150.0f);// Performance: limit total active debris
destruction.SetMaxDebris(500); // Cap at 500 pieces (default)
destruction.SetDebrisLifetimeMultiplier(0.5f); // Half lifetime = faster cleanup
// Query current state
size_t active = destruction.GetActiveDebrisCount();The m_debrisLifetimeMultiplier scales the countdown rate for all debris:
| Multiplier | Effect | Use Case |
|---|---|---|
0.25 |
4x faster cleanup | Low-end hardware, mobile |
0.5 |
2x faster cleanup | Standard settings |
1.0 |
Normal lifetime | Default |
2.0 |
2x longer lifetime | High-end hardware, cinematic |
| Scenario | Behavior |
|---|---|
ApplyDamage on non-existent entity |
Silently returns (no crash) |
ApplyDamage on already-destroyed entity |
ApplyDamage returns false, no action |
Unknown patternName on component |
GetPattern returns nullptr; destruction occurs without debris |
ForceDestroy on entity without component |
Silently returns |
Damage below damageThreshold
|
Ignored, no health reduction |
maxHealth is zero |
Division-by-zero guard: healthPercent defaults to 0.0f
|
Debris count exceeds m_maxDebris
|
Oldest debris removed first (FIFO) |
| Parameter | Default | Description |
|---|---|---|
| Max debris | 500 |
m_maxDebris -- hard cap on active debris entities |
| Debris lifetime multiplier | 1.0 |
m_debrisLifetimeMultiplier -- scales all lifetimes |
| Pattern lookup | O(1) |
std::unordered_map keyed by pattern name |
| Debris storage | std::vector |
Linear scan for cleanup; adequate for typical counts |
| Callback storage | std::vector |
All callbacks called sequentially on destruction |
- Limit piece count per pattern. Each piece spawns a physics rigid body. Keep patterns under 8-10 pieces.
- Use short lifetimes for small debris. Dust clouds and tiny fragments should use 2-4 second lifetimes.
-
Reduce debris cap on low-end hardware.
SetMaxDebris(200)for budget GPUs. -
Use
SetDebrisLifetimeMultiplier(0.5f)to clean up debris faster during intense combat. - Batch destruction queries. If many objects might be destroyed in one frame (explosion), the system handles it but consider spreading across frames.
The DestructionSystem is not thread-safe. All methods must be called from the main game thread. The system participates in the ECS execution order: Physics -> Animation -> AI -> Audio -> Lifecycle -> Render. Destruction processing happens during the Lifecycle phase.
Internal state that is not protected by mutexes:
| Member | Type | Access Pattern |
|---|---|---|
m_patterns |
unordered_map |
Write during init, read during gameplay |
m_debris |
vector |
Read/write every frame in Update()
|
m_destructionCallbacks |
vector |
Write during init, invoked during gameplay |
m_activeDebrisCount |
size_t |
Atomic-like but not std::atomic; main thread only |
destruction_status # Show system status: patterns registered, active debris,
# total destructions, debris cap, lifetime multiplier
Example output:
=== Destruction System ===
Patterns registered: 12
Active debris: 147 / 500
Total destructions: 83
Lifetime multiplier: 1.0x
| Symptom | Possible Cause | Solution |
|---|---|---|
| Object takes no damage |
damageThreshold too high |
Lower the threshold or increase weapon damage |
| Object never visually changes |
damageStage not checked in render code |
Switch mesh/material based on damageStage value |
| No debris spawned |
patternName does not match registered pattern |
Verify pattern name matches RegisterPattern() call |
| Debris floats in air | Physics not applied to spawned debris | Ensure Physics system processes debris rigid bodies |
| Too many debris, FPS drops |
m_maxDebris too high for hardware |
Reduce via SetMaxDebris() or increase lifetime multiplier |
| Destruction sound not playing | Sound asset not loaded | Check Audio system; verify sound asset name |
| Callback not firing | Callback registered after destruction | Register callbacks during initialization, before gameplay |
ApplyDamage has no effect |
Entity already destroyed (isDestroyed == true) |
Check isDestroyed before applying; use ForceDestroy if needed |
maxHealth zero causes NaN |
Division by zero in stage calc | Fixed: code guards with (maxHealth > 0.0f) check |
- Physics -- Debris uses rigid body simulation
- Entity Component System -- DestructibleComponent
- Gameplay Systems -- Weapons dealing damage to destructibles
- Audio -- Destruction sound effects
- Rendering and Graphics -- Damage stage visual switching
- Event System -- Publishing destruction events
- Asset Pipeline -- Loading debris mesh assets
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers