structecs
A flexible entity-component framework without the System.
Manage your data like ECS, control your logic like OOP.
⚠️ Development Status
This crate is currently under active development. The API is not stable and may change significantly.
Recent Updates:
- ✅ Archetype-based storage implemented
- ✅ Iterator-based queries (zero-allocation)
- ✅ Parallel query support with Rayon
- ✅ Comprehensive test suite (tests covering integration, concurrency, memory safety, edge cases)
- ✅ Thread-safe operations with fine-grained locking
- 🔄 Query composition and filtering (planned)
What is structecs?
structecs is an ECS-inspired data management framework designed for complex applications like game servers where traditional ECS systems can be limiting.
Unlike conventional ECS frameworks (Bevy, specs, hecs), structecs:
- ✅ No rigid System architecture - Write your logic however you want
- ✅ Hierarchical components - Nest components naturally like OOP
- ✅ Dynamic type extraction - Query for any component type on-the-fly
- ✅ Zero-cost abstractions - Uses compile-time offsets for component access
- ✅ Thread-safe - Concurrent operations with fine-grained locking
Quick Start
use *;
// Define your entities with the Extractable derive macro
// Mark nested extractable fields
When to use structecs?
Good for:
- Complex game servers (Minecraft, MMOs) with intricate entity relationships
- Applications where game logic doesn't fit cleanly into Systems
- Projects transitioning from OOP to data-oriented design
- Scenarios requiring flexible, ad-hoc data access patterns
Not ideal for:
- Simple games where traditional ECS works well
- Projects heavily invested in existing ECS ecosystems
- Use cases requiring absolute maximum performance (though structecs is quite fast!)
Core Concepts
1. Entity
An Entity is just an ID - a lightweight handle to your data.
// EntityId implements Display: "Entity(123)"
println!;
Entities don't "own" components. Instead, they reference structured data stored in the World.
2. Component (via Extractable)
In structecs, components are fields within structs. The Extractable trait allows the framework to understand your data structure and extract specific types.
use *;
Key insight: Components are hierarchical. A Player might contain an Entity, which itself is extractable.
3. World
The central data store that manages all entities and their data.
let world = default;
// Add entities
let player_id = world.add_entity;
// Query entities
for in world.
// Batch operations
let ids = vec!;
let count = world.remove_entities; // Efficient batch removal
// Check existence
if world.contains_entity
Core operations:
add_entity<E: Extractable>(entity: E) -> EntityId- Register new entityremove_entity(entity_id: &EntityId) -> bool- Remove single entity from worldremove_entities(entity_ids: &[EntityId]) -> usize- Batch remove entities, returns count of removedcontains_entity(entity_id: &EntityId) -> bool- Check if entity exists in worldclear()- Remove all entities from worldquery<T: 'static>() -> Vec(EntityId, Acquirable<T>)>- Efficiently iterate entities with component Textract_component<T>(entity_id: &EntityId) -> Option<Acquirable<T>>- Get specific component
4. Acquirable
A smart reference to a component that keeps the underlying entity data alive.
Features:
- Implements
Deref<Target = T>for transparent access - Reference-counted to prevent use-after-free
- Can
extract()other component types from the same entity
This enables OOP-like method chaining:
entity.?.?
5. Extractor
The engine that performs type extraction using pre-computed offsets.
Each unique entity structure gets one Extractor (cached in World), which knows:
- Where each component type lives in memory (offset)
- How to safely drop the entity when done
Architecture
For detailed architecture documentation, see Architecture.md.
Memory Layout
Archetype-based Storage:
Entities with the same structure (type) are grouped into archetypes for better cache locality:
World:
Archetype<Player>:
[Entity 0] Player { entity: Entity { name: "A" }, health: 100 }
[Entity 1] Player { entity: Entity { name: "B" }, health: 80 }
[Entity 2] Player { entity: Entity { name: "C" }, health: 90 }
Archetype<Monster>:
[Entity 3] Monster { entity: Entity { name: "X" }, damage: 20 }
[Entity 4] Monster { entity: Entity { name: "Y" }, damage: 30 }
Component Extraction:
Player struct in memory:
┌─────────────────────────────┐
│ Entity { name: String } │ ← offset 0: Entity
│ ├─ name: String │ ← offset 0: String
├─────────────────────────────┤
│ health: u32 │ ← offset X: u32
└─────────────────────────────┘
The Extractor knows:
- TypeId(Entity) -> offset 0
- TypeId(String) -> offset 0 (flattened from Entity)
- TypeId(u32) -> offset X
Data Flow
-
Entity Registration:
User creates struct → Derive macro generates METADATA_LIST → add_entity() creates Extractor → Data stored in World -
Component Query:
query<T>() → Iterate all entities → Check if Extractor has TypeId(T) → Calculate pointer via offset → Wrap in Acquirable -
Component Extraction:
Acquirable<A>.extract<B>() → Reuse same Extractor → Get offset for TypeId(B) → Return new Acquirable<B>
Design Philosophy
"Data is hierarchical, access is flat"
- Store entities as natural Rust structs (hierarchical)
- Query any component type regardless of nesting (flat access)
- No forced System architecture (user controls logic flow)
This gives you:
- Expressiveness of OOP (nested data, clear relationships)
- Performance of data-oriented design (offset-based access, no virtual dispatch)
- Flexibility of procedural code (write systems however you want)
Mutability Design
structecs provides read-only access by default. Users control mutability explicitly using Rust's interior mutability patterns:
use ;
use AtomicU32;
// Pattern 1: Lock-free with Atomics
// Pattern 2: Fine-grained locking with Mutex
// Pattern 3: Read/write separation with RwLock
// Usage
for in world.
Why no query_mut()?
- Prevents lock contention: Locking the entire World would block all operations
- Fine-grained control: Users choose the optimal locking strategy per component
- Multi-threading first: Essential for high-concurrency scenarios like game servers
This design philosophy prioritizes flexibility and performance in concurrent environments.
Performance
structecs is designed for high performance with real-world workloads:
Benchmark Results (Release mode)
Basic Operations:
- Adding 10,000 entities: ~16ms
- Querying 10,000 entities (iterator): ~4ms
- Querying specific type (10,000): ~3.4ms
Key Optimizations:
- Archetype Storage: Entities with the same type are stored contiguously
- Iterator-based Queries: Zero allocation, lazy evaluation
- Extractor Caching: Each type gets one shared extractor
- Compile-time Offsets: Component access via direct pointer arithmetic
Testing
structecs has a comprehensive test suite:
Run tests:
# Run all tests
# Run specific test suite
Comparison with Traditional ECS
| Aspect | Traditional ECS | structecs |
|---|---|---|
| Entity | Opaque ID | Opaque ID ✓ |
| Component | Standalone data types | Fields in structs |
| System | First-class concept with scheduling | User implements freely |
| Data Layout | Archetype/sparse sets | Archetype-based ✓ |
| Query Pattern | Compile-time system parameters | Runtime extraction |
| Nesting | Components are flat | Components can nest ✓ |
| Cache Coherency | Excellent (packed arrays) | Good (archetype storage) |
| Flexibility | Constrained by System API | Maximum flexibility ✓ |
Development Roadmap
Phase 1: Performance ✅ (Completed)
- Archetype-based storage for better cache locality
- Iterator-based queries (eliminate Vec allocation)
- Extractor caching for zero-cost component access
- Type index for fast archetype lookup
- Modular codebase structure
Phase 2: Multi-threading ✅ (Completed)
- Parallel query execution with Rayon
- Thread-safe World operations with DashMap and RwLock
- Fine-grained locking per archetype
- Comprehensive concurrency tests
Phase 3: Quality & Testing ✅ (Completed)
- Entity removal
- Memory safety verification
- Comprehensive test suite (118 tests)
Phase 4: Features (In Progress)
- Query filtering and composition (planned)
- Error handling improvements (planned)
Motivation
This framework was created for building a Minecraft server in Rust, where:
- Entity relationships are complex (Player ⊂ LivingEntity ⊂ Entity)
- Game logic is too varied to fit into rigid Systems
- OOP patterns are familiar but Rust's ownership makes traditional OOP difficult
structecs bridges the gap: data-oriented storage with OOP-like access patterns.
License
Licensed under:
- MIT License
Contributing
This project is in early development. Feedback, ideas, and contributions are welcome!