dentity 1.9.0 copy "dentity: ^1.9.0" to clipboard
dentity: ^1.9.0 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 #

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 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 ~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:

  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<

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 #

  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 componentLists[type] or componentLists.listFor<T>()
  • EntityView.getComponentForType(Type, Entity) - Use componentLists.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

3
likes
140
points
35
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