dentity 1.9.1 copy "dentity: ^1.9.1" to clipboard
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 created
  • totalDestroyed - Total entities destroyed
  • activeCount - Currently active entities
  • recycledCount - Number of recycled entities
  • peakCount - Maximum concurrent entities
  • creationQueueSize - Current creation queue size
  • deletionQueueSize - Current deletion queue size

System Stats:

  • callCount - Number of times the system has run
  • totalEntitiesProcessed - Total entities processed
  • totalTime - Cumulative processing time
  • averageTimeMicros - Average time in microseconds
  • averageTimeMs - Average time in milliseconds
  • minTime - Minimum processing time
  • maxTime - Maximum processing time

Archetype Stats:

  • totalArchetypes - Number of unique archetypes
  • totalEntities - Total entities across all archetypes
  • getMostUsedArchetypes() - 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:

  1. In EntitySystem classes:

    • Find: EntityComposition componentLists
    • Replace: ComponentManagerReadOnlyInterface componentManager
  2. In processEntity methods:

    • Find: componentLists\.get<
    • Replace: componentManager.getComponent<
  3. In EntityView usage:

    • Find: view\.componentLists\.get<
    • Replace: view.getComponent<

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

  1. System signature change: processEntity now takes EntityComposition instead of Map<Type, SparseList<Component>>
  2. EntityView.componentLists: Now returns EntityComposition instead of Map

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) - Use componentManager.getComponentByType instead
  • EntityView.getComponentForType(Type, Entity) - Use view.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

3
likes
140
points
33
downloads

Publisher

verified publisherwearemobilefirst.com

Weekly Downloads

A powerful and flexible Entity-Component-System (ECS) framework for Dart application.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

mockito

More

Packages that depend on dentity