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.
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
Dentity is an Entity-Component-System (ECS) framework for Dart and Flutter applications. It provides:
- List-Based Component Indexing - Direct array access for component lookups
- Type-Safe APIs - Generic methods for compile-time type checking
- Flexible Archetypes - Efficient entity filtering and querying
- Production Ready - Powers real games and applications (see the Asteroids demo)
This documentation demonstrates how to use Dentity to create ECS-based applications, with examples showing how entities with Position and Velocity components are updated by systems.
Add the following to your pubspec.yaml file:
dependencies:
dentity: ^1.9.1Note: If upgrading from 1.8.x or earlier, see the Migration Guides at the bottom of this document.
Then, run the following command to install the package:
dart pub getComponents 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;
}
}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);
}
}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,
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;
}
}The ComponentManager provides clean, type-safe component access:
// Type-safe component access
final position = componentManager.getComponent<Position>(entity);
// Check if entity has a component
if (componentManager.hasComponent<Position>(entity)) {
// ...
}
// Get all components of a type
final allPositions = componentManager.getComponentsOfType<Position>();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],
);
}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)
}Dentity includes a comprehensive stats collection system for profiling and debugging. Enable stats tracking to monitor entity lifecycle, system performance, and archetype distribution.
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
);
}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');
}
}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 overhead. Disable for production builds.
For a complete, production-ready example of Dentity in action, check out the Asteroids Game included in this repository. The game demonstrates:
- Collision Detection - Efficient collision checking between asteroids, bullets, and the player ship
- Shield System - Temporary invulnerability with visual feedback
- Scoring & Lives - Game state management with entity lifecycle
- Sound & Rendering - Integration with Flutter for rendering and audio
- Smooth Gameplay - Real-time entity management and physics
The complete source code is available in asteroids_app/lib/asteroids_systems.dart and shows real-world patterns for:
- Component design for game entities (Position, Velocity, Health, Collidable)
- System implementation for movement, collision, rendering, and game logic
- Entity creation and destruction during gameplay
- Performance-optimized component access patterns
Play it live in your browser →
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, or run benchmarks in your browser →
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();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})');
}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:
- Views are reused across systems
- Reduced memory allocations
- Consistent view instances throughout the frame
Version 1.9.0 includes optimized component access with list-based indexing but requires updating custom EntitySystem implementations.
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.
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...
}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:
After migration:
- Optimized component access with list-based indexing
- Reduced memory allocations (no EntityComposition copies)
- Improved cache locality
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;
}
}- System signature change:
processEntitynow takesEntityCompositioninstead ofMap<Type, SparseList<Component>> - EntityView.componentLists: Now returns
EntityCompositioninstead ofMap
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);The following methods are deprecated and will be removed in v2.0:
EntityView.getComponentArray(Type)- UsecomponentManager.getComponentByTypeinsteadEntityView.getComponentForType(Type, Entity)- Useview.getComponent<T>(entity)instead
Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.
This project is licensed under the MIT License. See the LICENSE file for details.
Please checkout our work on www.wearemobilefirst.com