Dentity - Entity-Component-System Framework
Dentity is a powerful and flexible Entity-Component-System (ECS) framework for Dart applications. This README provides examples and documentation to help you get started with the Dentity package.
Live Demos
Try out Dentity in your browser:
- Asteroids Game - Complete game demonstrating ECS patterns with collision detection, shield system, and scoring
- Performance Benchmarks - Real-time performance visualization with industry-standard metrics
Introduction
This documentation demonstrates how to use Dentity to create ECS-based applications. The examples show how entities with Position and Velocity components are updated by a MovementSystem.
Installation
Add the following to your pubspec.yaml file:
dependencies:
dentity: ^1.9.0
Upgrading from 1.8.x
Version 1.9.0 includes breaking changes for performance improvements. See the Migration Guide below.
Then, run the following command to install the package:
dart pub get
Creating Components
Components are the data containers that represent different aspects of an entity. In this example, we define Position and Velocity components.
class Position extends Component {
double x;
double y;
Position(this.x, this.y);
@override
Position clone() => Position(x, y);
@override
int compareTo(other) {
if (other is Position) {
return x.compareTo(other.x) + y.compareTo(other.y);
}
return -1;
}
}
class Velocity extends Component {
double x;
double y;
Velocity(this.x, this.y);
@override
Velocity clone() => Velocity(x, y);
@override
int compareTo(other) {
if (other is Velocity) {
return x.compareTo(other.x) + y.compareTo(other.y);
}
return -1;
}
}
Defining Component Serializers
To enable serialization of components, you need to define serializers for each component type.
class PositionJsonSerializer extends ComponentSerializer<Position> {
static const type = 'Position';
@override
ComponentRepresentation? serialize(Position component) {
return {
'x': component.x,
'y': component.y,
EntitySerialiserJson.typeField: type,
};
}
@override
Position deserialize(ComponentRepresentation data) {
final positionData = data as Map<String, dynamic>;
return Position(positionData['x'] as double, positionData['y'] as double);
}
}
class VelocityJsonSerializer extends ComponentSerializer<Velocity> {
static const type = 'Velocity';
@override
ComponentRepresentation? serialize(Velocity component) {
return {
'x': component.x,
'y': component.y,
EntitySerialiserJson.typeField: type,
};
}
@override
Velocity deserialize(ComponentRepresentation data) {
final velocityData = data as Map<String, dynamic>;
return Velocity(velocityData['x'] as double, velocityData['y'] as double);
}
}
Creating a System
Systems contain the logic that operates on entities with specific components. The MovementSystem updates the Position of entities based on their Velocity.
class MovementSystem extends EntitySystem {
@override
Set<Type> get filterTypes => const {Position, Velocity};
@override
void processEntity(Entity entity, EntityComposition componentLists, Duration delta) {
final position = componentLists.get<Position>(entity)!;
final velocity = componentLists.get<Velocity>(entity)!;
position.x += velocity.x;
position.y += velocity.y;
}
}
Component Access
The EntityComposition class provides clean, type-safe component access:
// Clean, type-safe access
final position = componentLists.get<Position>(entity);
// Get the sparse list for a component type
final positionList = componentLists.listFor<Position>();
// Backwards compatible Map access
final position = componentLists[Position]?[entity] as Position?;
Setting Up the World
The World class ties everything together. It manages entities, components, and systems.
World createBasicExampleWorld() {
final componentManager = ComponentManager(
archetypeManagerFactory: (types) => ArchetypeManagerBigInt(types),
componentArrayFactories: {
Position: () => ContiguousSparseList<Position>(),
Velocity: () => ContiguousSparseList<Velocity>(),
OtherComponent: () => ContiguousSparseList<OtherComponent>(),
},
);
final entityManager = EntityManager(componentManager);
final movementSystem = MovementSystem();
return World(
componentManager,
entityManager,
[movementSystem],
);
}
Example Usage
Here's how you can use the above setup:
void main() {
final world = createBasicExampleWorld();
// Create an entity with Position and Velocity components
final entity = world.createEntity({
Position(0, 0),
Velocity(1, 1),
});
// Run the system to update positions based on velocity
world.process();
// Check the updated position
final position = world.componentManager.getComponent<Position>(entity);
print('Updated position: (\${position?.x}, \${position?.y})'); // Should output (1, 1)
}
Stats Collection & Profiling
Dentity includes a comprehensive stats collection system for profiling and debugging. Enable stats tracking to monitor entity lifecycle, system performance, and archetype distribution.
Enabling Stats
World createWorldWithStats() {
final componentManager = ComponentManager(
archetypeManagerFactory: (types) => ArchetypeManagerBigInt(types),
componentArrayFactories: {
Position: () => ContiguousSparseList<Position>(),
Velocity: () => ContiguousSparseList<Velocity>(),
},
);
final entityManager = EntityManager(componentManager);
final movementSystem = MovementSystem();
return World(
componentManager,
entityManager,
[movementSystem],
enableStats: true, // Enable stats collection
);
}
Accessing Stats
void main() {
final world = createWorldWithStats();
for (var i = 0; i < 1000; i++) {
world.createEntity({Position(0, 0), Velocity(1, 1)});
}
world.process();
// Access entity stats
print('Entities created: ${world.stats!.entities.totalCreated}');
print('Active entities: ${world.stats!.entities.activeCount}');
print('Peak entities: ${world.stats!.entities.peakCount}');
print('Recycled entities: ${world.stats!.entities.recycledCount}');
// Access system performance stats
for (final systemStats in world.stats!.systems) {
print('${systemStats.name}: ${systemStats.averageTimeMs.toStringAsFixed(3)}ms avg');
}
// Access archetype distribution
final mostUsed = world.stats!.archetypes.getMostUsedArchetypes();
for (final archetype in mostUsed.take(5)) {
print('Archetype ${archetype.archetype}: ${archetype.count} entities');
}
}
Available Metrics
Entity Stats:
totalCreated- Total entities createdtotalDestroyed- Total entities destroyedactiveCount- Currently active entitiesrecycledCount- Number of recycled entitiespeakCount- Maximum concurrent entitiescreationQueueSize- Current creation queue sizedeletionQueueSize- Current deletion queue size
System Stats:
callCount- Number of times the system has runtotalEntitiesProcessed- Total entities processedtotalTime- Cumulative processing timeaverageTimeMicros- Average time in microsecondsaverageTimeMs- Average time in millisecondsminTime- Minimum processing timemaxTime- Maximum processing time
Archetype Stats:
totalArchetypes- Number of unique archetypestotalEntities- Total entities across all archetypesgetMostUsedArchetypes()- Returns archetypes sorted by entity count
Note: Stats collection adds ~24-32% overhead. Disable for production builds.
Benchmarking
Dentity includes industry-standard benchmarks using metrics like ns/op (nanoseconds per operation), ops/s (operations per second), and entities/s (entities per second).
See the benchmark_app for a Flutter app with real-time performance visualization.
Entity Deletion
Entities can be destroyed using world.destroyEntity(entity). Deletions are queued and processed automatically after each system runs during world.process().
void main() {
final world = createBasicExampleWorld();
final entity = world.createEntity({
Position(0, 0),
Velocity(1, 1),
});
// Queue entity for deletion
world.destroyEntity(entity);
// Deletion happens after systems process
world.process();
// Entity is now deleted
final position = world.componentManager.getComponent<Position>(entity);
print(position); // null
}
If you need to manually process deletions outside of world.process(), you can call:
world.entityManager.processDeletionQueue();
Serialization Example
To serialize and deserialize entities:
void main() {
final world = createBasicExampleWorld();
// Create an entity
final entity = world.createEntity({
Position(0, 0),
Velocity(1, 1),
});
// Set up serializers
final entitySerialiser = EntitySerialiserJson(
world.entityManager,
{
Position: PositionJsonSerializer(),
Velocity: VelocityJsonSerializer(),
},
);
// Serialize the entity
final serialized = entitySerialiser.serializeEntityComponents(entity, [
Position(0, 0),
Velocity(1, 1),
]);
print(serialized);
// Deserialize the entity
final deserializedEntity = entitySerialiser.deserializeEntity(serialized);
final deserializedPosition = world.componentManager.getComponent<Position>(deserializedEntity);
print('Deserialized position: (\${deserializedPosition?.x}, \${deserializedPosition?.y})');
}
View Caching
New in v1.6.0: Entity views are now automatically cached for improved performance. When you call viewForTypes() or view() with the same archetype, the same EntityView instance is returned, eliminating redundant object creation.
// These return the same cached instance
final view1 = world.viewForTypes({Position, Velocity});
final view2 = world.viewForTypes({Position, Velocity});
assert(identical(view1, view2)); // true
// Clear the cache if needed (rare)
world.entityManager.clearViewCache();
// Check cache size
print(world.entityManager.viewCacheSize);
Benefits:
- Zero performance overhead - views are reused across systems
- Reduced memory allocations in hot paths
- Consistent view instances throughout the frame
Migration from 1.8.x to 1.9.x
Version 1.9.0 includes major performance improvements (2-3x faster component access) but requires updating custom EntitySystem implementations.
What Changed
EntityComposition class removed - The intermediate EntityComposition abstraction has been removed. Systems now access components directly through ComponentManager for better performance.
EntitySystem.processEntity signature - The second parameter changed from EntityComposition to ComponentManagerReadOnlyInterface.
Migration Steps
1. Update EntitySystem implementations:
Before (v1.8):
class MovementSystem extends EntitySystem {
@override
void processEntity(
Entity entity,
EntityComposition componentLists,
Duration delta,
) {
final position = componentLists.get<Position>(entity)!;
final velocity = componentLists.get<Velocity>(entity)!;
position.x += velocity.x * delta.inMilliseconds / 1000.0;
position.y += velocity.y * delta.inMilliseconds / 1000.0;
}
}
After (v1.9):
class MovementSystem extends EntitySystem {
@override
void processEntity(
Entity entity,
ComponentManagerReadOnlyInterface componentManager,
Duration delta,
) {
final position = componentManager.getComponent<Position>(entity)!;
final velocity = componentManager.getComponent<Velocity>(entity)!;
position.x += velocity.x * delta.inMilliseconds / 1000.0;
position.y += velocity.y * delta.inMilliseconds / 1000.0;
}
}
2. Update EntityView usage in collision/targeting systems:
Before (v1.8):
bool checkCollision(Entity a, Entity b, EntityView view) {
final posA = view.componentLists.get<Position>(a)!;
final posB = view.componentLists.get<Position>(b)!;
// collision logic...
}
After (v1.9):
bool checkCollision(Entity a, Entity b, EntityView view) {
final posA = view.getComponent<Position>(a)!;
final posB = view.getComponent<Position>(b)!;
// collision logic...
}
Quick Find & Replace
For most codebases, these regex replacements will handle the migration:
-
In EntitySystem classes:
- Find:
EntityComposition componentLists - Replace:
ComponentManagerReadOnlyInterface componentManager
- Find:
-
In processEntity methods:
- Find:
componentLists\.get< - Replace:
componentManager.getComponent<
- Find:
-
In EntityView usage:
- Find:
view\.componentLists\.get< - Replace:
view.getComponent<
- Find:
Performance Benefits
After migration, you'll see:
- 2-3x faster component access in hot paths
- Reduced memory allocations (no EntityComposition copies)
- Better cache locality with list-based indexing
Migration Guide (v1.5 → v1.6)
Component Access Updates
The old manual casting pattern has been replaced with the cleaner EntityComposition.get<T>() method:
Old Pattern (v1.5 and earlier):
class MovementSystem extends EntitySystem {
@override
void processEntity(
Entity entity,
Map<Type, SparseList<Component>> componentLists,
Duration delta,
) {
final position = componentLists[Position]?[entity] as Position;
final velocity = componentLists[Velocity]?[entity] as Velocity;
position.x += velocity.x;
position.y += velocity.y;
}
}
New Pattern (v1.6+):
class MovementSystem extends EntitySystem {
@override
void processEntity(
Entity entity,
EntityComposition componentLists,
Duration delta,
) {
final position = componentLists.get<Position>(entity)!;
final velocity = componentLists.get<Velocity>(entity)!;
position.x += velocity.x;
position.y += velocity.y;
}
}
Breaking Changes
- System signature change:
processEntitynow takesEntityCompositioninstead ofMap<Type, SparseList<Component>> - EntityView.componentLists: Now returns
EntityCompositioninstead ofMap
Backwards Compatibility
EntityComposition implements Map<Type, SparseList<Component>>, so old code continues to work:
// Still works (backwards compatible)
final position = componentLists[Position]?[entity] as Position?;
// But the new way is cleaner
final position = componentLists.get<Position>(entity);
Deprecated Methods
The following methods are deprecated and will be removed in v2.0:
EntityView.getComponentArray(Type)- UsecomponentLists[type]orcomponentLists.listFor<T>()EntityView.getComponentForType(Type, Entity)- UsecomponentLists.get<T>(entity)
Contributing
Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Hire us
Please checkout our work on www.wearemobilefirst.com