dentity 1.9.1
dentity: ^1.9.1 copied to clipboard
A powerful and flexible Entity-Component-System (ECS) framework for Dart application.
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 #
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.
Installation #
Add the following to your pubspec.yaml file:
dependencies:
dentity: ^1.9.1
Note: 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 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,
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;
}
}
Component Access #
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>();
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 overhead. Disable for production builds.
Real-World Example #
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 →
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, or run benchmarks in your browser →
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:
- Views are reused across systems
- Reduced memory allocations
- Consistent view instances throughout the frame
Migration Guides #
Migration from 1.8.x to 1.9.x #
Version 1.9.0 includes optimized component access with list-based indexing 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:
Benefits
After migration:
- Optimized component access with list-based indexing
- Reduced memory allocations (no EntityComposition copies)
- Improved cache locality
Migration from 1.5.x to 1.6.x #
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)- UsecomponentManager.getComponentByTypeinsteadEntityView.getComponentForType(Type, Entity)- Useview.getComponent<T>(entity)instead
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