├── .pubignore ├── watch.cmd ├── .gitignore ├── test ├── all_tests.dart └── dartemis │ └── core │ ├── systems │ ├── test_systems.dart │ └── interval_entity_system_test.dart │ ├── utils │ ├── all_tests.dart │ ├── bit_set_test.dart │ ├── bag_test.dart │ └── entity_bag_test.dart │ ├── managers │ ├── test_managers.dart │ ├── group_manager_test.dart │ └── tag_manager_test.dart │ ├── entity_manager_test.dart │ ├── all_tests.dart │ ├── mapper_test.dart │ ├── component_test.dart │ ├── components_setup.dart │ ├── aspect_test.dart │ ├── component_manager_test.dart │ ├── world_test.mocks.dart │ └── world_test.dart ├── lib ├── src │ ├── core │ │ ├── component_type.dart │ │ ├── entity.dart │ │ ├── entity_observer.dart │ │ ├── systems │ │ │ ├── entity_processing_system.dart │ │ │ ├── void_entity_system.dart │ │ │ ├── interval_entity_processing_system.dart │ │ │ └── interval_entity_system.dart │ │ ├── manager.dart │ │ ├── component.dart │ │ ├── utils │ │ │ ├── entity_bag.dart │ │ │ ├── object_pool.dart │ │ │ ├── bag.dart │ │ │ └── bit_set.dart │ │ ├── managers │ │ │ ├── team_manager.dart │ │ │ ├── player_manager.dart │ │ │ ├── tag_manager.dart │ │ │ └── group_manager.dart │ │ ├── mapper.dart │ │ ├── entity_manager.dart │ │ ├── aspect.dart │ │ ├── entity_system.dart │ │ ├── component_manager.dart │ │ └── world.dart │ └── metadata │ │ └── generate.dart └── dartemis.dart ├── example ├── styles.css ├── index.html ├── darteroids │ ├── components.dart │ ├── input_systems.dart │ ├── render_systems.dart │ └── gamelogic_systems.dart └── main.dart ├── .github ├── dependabot.yml └── workflows │ └── dart.yml ├── pubspec.yaml ├── LICENSE ├── README.md ├── analysis_options.yaml └── CHANGELOG.md /.pubignore: -------------------------------------------------------------------------------- 1 | watch.cmd -------------------------------------------------------------------------------- /watch.cmd: -------------------------------------------------------------------------------- 1 | dart run build_runner watch 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | pubspec.lock 3 | build/ 4 | .idea/ 5 | .packages 6 | /.dart_tool/ 7 | -------------------------------------------------------------------------------- /test/all_tests.dart: -------------------------------------------------------------------------------- 1 | import 'dartemis/core/all_tests.dart' as core; 2 | 3 | void main() { 4 | core.main(); 5 | } 6 | -------------------------------------------------------------------------------- /test/dartemis/core/systems/test_systems.dart: -------------------------------------------------------------------------------- 1 | import 'interval_entity_system_test.dart' as interval_entity_system; 2 | 3 | void main() { 4 | interval_entity_system.main(); 5 | } 6 | -------------------------------------------------------------------------------- /test/dartemis/core/utils/all_tests.dart: -------------------------------------------------------------------------------- 1 | import 'bag_test.dart' as bag; 2 | import 'entity_bag_test.dart' as entity_bag; 3 | 4 | void main() { 5 | bag.main(); 6 | entity_bag.main(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/core/component_type.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// The [ComponentType] handles the internal id and bitmask of a [Component]. 4 | extension type ComponentType(int bitIndex) {} 5 | -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | border: solid 1px black; 3 | user-select: none; 4 | } 5 | 6 | div#canvascontainer { 7 | text-align: center; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /test/dartemis/core/managers/test_managers.dart: -------------------------------------------------------------------------------- 1 | import 'group_manager_test.dart' as group_manager; 2 | import 'tag_manager_test.dart' as tag_manager; 3 | 4 | void main() { 5 | group_manager.main(); 6 | tag_manager.main(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/core/entity.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// The Entity type. Cannot be instantiated outside of dartemis. You must 4 | /// create new entities using [World.createEntity]. 5 | extension type Entity(int _id) { 6 | Entity._(this._id); 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: monthly 11 | labels: 12 | - autosubmit 13 | groups: 14 | github-actions: 15 | patterns: 16 | - "*" -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dartemis 2 | version: 0.10.0 3 | description: An Entity System Framework for game development. Based on Artemis. 4 | repository: https://github.com/denniskaselow/dartemis 5 | environment: 6 | sdk: ^3.3.0 7 | 8 | dependencies: 9 | meta: ^1.16.0 10 | 11 | dev_dependencies: 12 | build_runner: ^2.0.0 13 | build_web_compilers: ^4.0.0 14 | mockito: ^5.5.0 15 | test: ^1.23.0 16 | web: ^1.1.0 17 | -------------------------------------------------------------------------------- /lib/src/core/entity_observer.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// Interface for [EntitySystem]s and [Manager]s to get informed about changes 4 | /// to the state of an [int]. 5 | abstract class EntityObserver { 6 | /// Called when an [entity] is added to the world. 7 | @visibleForOverriding 8 | void added(Entity entity); 9 | 10 | /// Called when an [entity] is being deleted from the world. 11 | @visibleForOverriding 12 | void deleted(Entity entity); 13 | } 14 | -------------------------------------------------------------------------------- /test/dartemis/core/entity_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('integration tests for EntityManager', () { 6 | late World world; 7 | setUp(() { 8 | world = World(); 9 | }); 10 | test('entities have uniqure IDs', () { 11 | final a = world.createEntity(); 12 | final b = world.createEntity(); 13 | 14 | expect(a, isNot(equals(b))); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Darteroids 8 | 9 | 10 | 11 | 12 |

Darteroids

13 | 14 |
15 |
Move the red circle with WASD-keys and shoot with the left mouse button
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /test/dartemis/core/all_tests.dart: -------------------------------------------------------------------------------- 1 | import 'aspect_test.dart' as aspect; 2 | import 'component_manager_test.dart' as component_manager; 3 | import 'component_test.dart' as component; 4 | import 'entity_manager_test.dart' as entity_manager; 5 | import 'managers/test_managers.dart' as managers; 6 | import 'mapper_test.dart' as mapper; 7 | import 'systems/test_systems.dart' as systems; 8 | import 'utils/all_tests.dart' as utils; 9 | import 'world_test.dart' as world; 10 | 11 | void main() { 12 | aspect.main(); 13 | component.main(); 14 | component_manager.main(); 15 | entity_manager.main(); 16 | mapper.main(); 17 | 18 | managers.main(); 19 | systems.main(); 20 | utils.main(); 21 | 22 | world.main(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/core/systems/entity_processing_system.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// A typical entity system. Use this when you need to process entities 4 | /// possessing the provided component types. 5 | abstract class EntityProcessingSystem extends EntitySystem { 6 | /// Create a new [EntityProcessingSystem]. It requires at least one component. 7 | EntityProcessingSystem(super.aspect, {super.group, super.passive}); 8 | 9 | /// Process an [entity] this system is interested in. 10 | @visibleForOverriding 11 | void processEntity(Entity entity); 12 | 13 | @override 14 | @visibleForOverriding 15 | void processEntities(Iterable entities) => 16 | entities.forEach(processEntity); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/core/systems/void_entity_system.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// This system has an empty aspect so it processes no entities, but it still 4 | /// gets invoked. 5 | /// You can use this system if you need to execute some game logic and not have 6 | /// to concern yourself about aspects or entities. 7 | abstract class VoidEntitySystem extends EntitySystem { 8 | /// Create the [VoidEntitySystem]. 9 | VoidEntitySystem({super.group, super.passive}) : super(Aspect()); 10 | 11 | @override 12 | @visibleForOverriding 13 | void processEntities(Iterable entities) => processSystem(); 14 | 15 | /// Execute the logic for this system. 16 | @visibleForOverriding 17 | void processSystem(); 18 | } 19 | -------------------------------------------------------------------------------- /test/dartemis/core/utils/bit_set_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/src/core/utils/bit_set.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('BitSet', () { 6 | test('toIntValues() works in general', () { 7 | final sut = BitSet(32)..setAll(); 8 | 9 | expect(sut.toIntValues(), equals(List.generate(32, (index) => index))); 10 | }); 11 | test('toIntValues() works for edge cases', () { 12 | final sut = BitSet(128); 13 | sut[0] = true; 14 | sut[31] = true; 15 | sut[32] = true; 16 | sut[63] = true; 17 | sut[64] = true; 18 | sut[127] = true; 19 | 20 | expect(sut.toIntValues(), equals([0, 31, 32, 63, 64, 127])); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/core/manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// Manager. 4 | abstract class Manager implements EntityObserver { 5 | late final World _world; 6 | 7 | /// The [World] where this manager resides. 8 | World get world => _world; 9 | 10 | /// Override to implement code that gets executed when managers are 11 | /// initialized. 12 | @mustCallSuper 13 | @visibleForOverriding 14 | // ignore: use_setters_to_change_properties 15 | void initialize(World world) { 16 | _world = world; 17 | } 18 | 19 | @override 20 | @visibleForOverriding 21 | void added(Entity entity) {} 22 | 23 | @override 24 | @visibleForOverriding 25 | void deleted(Entity entity) {} 26 | 27 | /// Called when the world gets destroyed. Override if you need to clean up 28 | /// your manager. 29 | @visibleForOverriding 30 | void destroy() {} 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/core/systems/interval_entity_processing_system.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// If you need to process entities at a certain interval then use this. 4 | /// A typical usage would be to regenerate ammo or health at certain intervals, 5 | /// no need to do that every game loop, but perhaps every 100 ms. or every 6 | /// second. 7 | abstract class IntervalEntityProcessingSystem extends IntervalEntitySystem { 8 | /// Create a new [IntervalEntityProcessingSystem]. It requires at least one 9 | /// component. 10 | IntervalEntityProcessingSystem( 11 | super.interval, 12 | super.aspect, { 13 | super.group, 14 | super.passive, 15 | }); 16 | 17 | /// Process an [entity] this system is interested in. 18 | @visibleForOverriding 19 | void processEntity(Entity entity); 20 | 21 | @override 22 | @visibleForOverriding 23 | void processEntities(Iterable entities) => 24 | entities.forEach(processEntity); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/core/component.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// All components extend from this class. 4 | /// 5 | /// If you want to use a pooled component that will be added to a FreeList when 6 | /// it is being removed use [PooledComponent] instead. 7 | abstract class Component { 8 | /// Does nothing in [Component], only relevant for [PooledComponent]. 9 | void _removed() {} 10 | } 11 | 12 | /// All components that should be managed in a [ObjectPool] must extend this 13 | /// class and have a factory constructor that calls `Pooled.of(...)` to create 14 | /// a component. By doing so, dartemis can handle the construction of 15 | /// [PooledComponent]s and reuse them when they are no longer needed. 16 | class PooledComponent> extends Component with Pooled { 17 | @override 18 | void _removed() { 19 | moveToPool(); 20 | } 21 | 22 | /// If you need to do some cleanup when removing this component override this 23 | /// method. 24 | @override 25 | @visibleForOverriding 26 | void cleanUp() {} 27 | } 28 | -------------------------------------------------------------------------------- /test/dartemis/core/utils/bag_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Bag tests', () { 6 | late Bag sut; 7 | setUp(() { 8 | sut = Bag(capacity: 1) 9 | ..add('A') 10 | ..add('B'); 11 | }); 12 | test('removing an element', () { 13 | sut.remove('A'); 14 | expect(sut.contains('A'), equals(false)); 15 | expect(sut.contains('B'), equals(true)); 16 | expect(sut.size, equals(1)); 17 | }); 18 | test('removing at position', () { 19 | sut.removeAt(0); 20 | expect(sut.contains('A'), equals(false)); 21 | expect(sut.contains('B'), equals(true)); 22 | expect(sut.size, equals(1)); 23 | }); 24 | test('clear', () { 25 | sut.clear(); 26 | expect(sut.size, equals(0)); 27 | }); 28 | test('setting a value by index should not shrink the bag', () { 29 | sut[9] = 'A'; 30 | sut[5] = 'B'; 31 | expect(sut.size, equals(10)); 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /lib/dartemis.dart: -------------------------------------------------------------------------------- 1 | library dartemis; 2 | 3 | import 'dart:collection'; 4 | import 'dart:core'; 5 | 6 | import 'package:meta/meta.dart'; 7 | 8 | import 'src/core/utils/bit_set.dart'; 9 | 10 | part 'src/core/aspect.dart'; 11 | part 'src/core/component.dart'; 12 | part 'src/core/component_manager.dart'; 13 | part 'src/core/component_type.dart'; 14 | part 'src/core/entity.dart'; 15 | part 'src/core/entity_manager.dart'; 16 | part 'src/core/entity_observer.dart'; 17 | part 'src/core/entity_system.dart'; 18 | part 'src/core/manager.dart'; 19 | part 'src/core/managers/group_manager.dart'; 20 | part 'src/core/managers/player_manager.dart'; 21 | part 'src/core/managers/tag_manager.dart'; 22 | part 'src/core/managers/team_manager.dart'; 23 | part 'src/core/mapper.dart'; 24 | part 'src/core/systems/entity_processing_system.dart'; 25 | part 'src/core/systems/interval_entity_processing_system.dart'; 26 | part 'src/core/systems/interval_entity_system.dart'; 27 | part 'src/core/systems/void_entity_system.dart'; 28 | part 'src/core/utils/bag.dart'; 29 | part 'src/core/utils/entity_bag.dart'; 30 | part 'src/core/utils/object_pool.dart'; 31 | part 'src/core/world.dart'; 32 | part 'src/metadata/generate.dart'; 33 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 6 1,15 * *' 7 | push: 8 | branches: [ develop, master ] 9 | pull_request: 10 | branches: [ develop, master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | - uses: dart-lang/setup-dart@v1.4 19 | with: 20 | sdk: dev 21 | 22 | - name: Install dependencies 23 | run: dart pub get 24 | 25 | - name: Verify formatting 26 | run: dart format --output=none --set-exit-if-changed . 27 | 28 | - name: Analyze 29 | run: dart analyze --fatal-infos 30 | 31 | - name: Run Tests 32 | run: dart test 33 | 34 | - name: Activate test coverage 35 | if: github.event_name != 'pull_request' 36 | run: dart pub global activate -sgit https://github.com/denniskaselow/dart-coveralls.git 37 | 38 | - name: Run test coverage 39 | if: github.event_name != 'pull_request' 40 | run: dart pub global run dart_coveralls report --token ${{ secrets.COVERALLS_TOKEN }} --exclude-test-files test/all_tests.dart --throw-on-error 41 | -------------------------------------------------------------------------------- /test/dartemis/core/utils/entity_bag_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('EntityBag tests', () { 6 | late EntityBag bag; 7 | final world = World(); 8 | final e1 = world.createEntity(); 9 | final e2 = world.createEntity(); 10 | setUp(() { 11 | bag = EntityBag() 12 | ..add(e1) 13 | ..add(e2); 14 | }); 15 | test('removing an element', () { 16 | bag.remove(e1); 17 | expect(bag.contains(e1), equals(false)); 18 | expect(bag.contains(e2), equals(true)); 19 | expect(bag.length, equals(1)); 20 | }); 21 | test('iterating', () { 22 | var iter = bag.iterator; 23 | expect(iter.moveNext(), equals(true)); 24 | expect(iter.current, equals(e1)); 25 | expect(iter.moveNext(), equals(true)); 26 | expect(iter.current, equals(e2)); 27 | 28 | bag.remove(e1); 29 | iter = bag.iterator; 30 | expect(iter.moveNext(), equals(true)); 31 | expect(iter.current, equals(e2)); 32 | expect(iter.moveNext(), equals(false)); 33 | }); 34 | test('clear', () { 35 | bag.clear(); 36 | expect(bag.length, equals(0)); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/core/utils/entity_bag.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// A [Bag] that uses a [BitSet] to manage entities. Results in faster 4 | /// removal of entities. 5 | class EntityBag with Iterable { 6 | BitSet _entities; 7 | 8 | /// Creates an [EntityBag]. 9 | EntityBag() : _entities = BitSet(32); 10 | 11 | /// Add a new [entity]. If the entity already exists, nothing changes. 12 | void add(Entity entity) { 13 | if (entity._id >= _entities.length) { 14 | _entities = BitSet.fromBitSet(_entities, length: entity._id + 1); 15 | } 16 | _entities[entity._id] = true; 17 | } 18 | 19 | /// Removes [entity]. Returns `true` if there was an element and `false` 20 | /// otherwise. 21 | bool remove(Entity entity) { 22 | final result = _entities[entity._id]; 23 | _entities[entity._id] = false; 24 | return result; 25 | } 26 | 27 | @override 28 | bool contains(covariant Entity element) => _entities[element._id]; 29 | 30 | @override 31 | int get length => _entities.cardinality; 32 | 33 | /// Removes all entites. 34 | void clear() => _entities.clearAll(); 35 | 36 | @override 37 | Iterator get iterator => 38 | _entities.toIntValues().map(Entity._).iterator; 39 | } 40 | -------------------------------------------------------------------------------- /test/dartemis/core/mapper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'components_setup.dart'; 5 | 6 | void main() { 7 | group('Mapper', () { 8 | late World world; 9 | setUp(() { 10 | world = World(); 11 | }); 12 | test('gets component for entity', () { 13 | final componentA = Component0(); 14 | final componentB = Component1(); 15 | final entity = world.createEntity([componentA, componentB]); 16 | world 17 | ..initialize() 18 | ..process(); 19 | 20 | final mapper = Mapper(world); 21 | 22 | expect(mapper[entity], equals(componentA)); 23 | }); 24 | }); 25 | group('OptionalMapper', () { 26 | late World world; 27 | setUp(() { 28 | world = World(); 29 | }); 30 | test('gets component for entity', () { 31 | final componentA = Component0(); 32 | final entity = world.createEntity([componentA]); 33 | world 34 | ..initialize() 35 | ..process(); 36 | 37 | final mapperA = OptionalMapper(world); 38 | final mapperB = OptionalMapper(world); 39 | 40 | expect(mapperA[entity], equals(componentA)); 41 | expect(mapperB[entity], equals(null)); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/core/systems/interval_entity_system.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// A system that processes entities at a interval in milliseconds. 4 | /// A typical usage would be a collision system or physics system. 5 | abstract class IntervalEntitySystem extends EntitySystem { 6 | double _acc = 0; 7 | double _intervalDelta = 0; 8 | 9 | /// The interval in which the system will be processed. 10 | final double interval; 11 | 12 | /// Create an [IntervalEntitySystem] with the specified [interval] and 13 | /// [aspect]. 14 | IntervalEntitySystem( 15 | this.interval, 16 | super.aspect, { 17 | super.group, 18 | super.passive, 19 | }); 20 | 21 | /// Returns the accumulated delta since the system was last invoked. 22 | @override 23 | double get delta => _intervalDelta; 24 | 25 | @override 26 | @visibleForOverriding 27 | bool checkProcessing() { 28 | _acc += world.delta; 29 | _intervalDelta += world.delta; 30 | if (_acc >= interval) { 31 | _acc -= interval; 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | /// Resets the accumulated delta to 0. 38 | /// 39 | /// Call `super.end()` if you overwrite this function. 40 | @override 41 | @visibleForOverriding 42 | void end() { 43 | _intervalDelta = 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/darteroids/components.dart: -------------------------------------------------------------------------------- 1 | part of '../main.dart'; 2 | 3 | class CircularBody extends Component { 4 | num radius; 5 | String color; 6 | 7 | CircularBody(this.radius, this.color); 8 | } 9 | 10 | class Position extends Component { 11 | num _x; 12 | num _y; 13 | 14 | Position(this._x, this._y); 15 | 16 | set x(num x) => _x = x % maxWidth; 17 | 18 | num get x => _x; 19 | 20 | set y(num y) => _y = y % maxHeight; 21 | 22 | num get y => _y; 23 | } 24 | 25 | class Velocity extends Component { 26 | num x; 27 | num y; 28 | 29 | Velocity([this.x = 0, this.y = 0]); 30 | } 31 | 32 | class PlayerDestroyer extends Component {} 33 | 34 | class AsteroidDestroyer extends Component {} 35 | 36 | class Cannon extends Component { 37 | bool shoot = false; 38 | num targetX = 0; 39 | num targetY = 0; 40 | num cooldown = 0; 41 | 42 | void target(num targetX, num targetY) { 43 | this.targetX = targetX; 44 | this.targetY = targetY; 45 | } 46 | 47 | bool get canShoot => shoot && cooldown <= 0; 48 | } 49 | 50 | class Decay extends Component { 51 | num timer; 52 | 53 | Decay(this.timer); 54 | } 55 | 56 | class Status extends Component { 57 | int lifes; 58 | num invisiblityTimer; 59 | 60 | Status({this.lifes = 1, this.invisiblityTimer = 0}); 61 | 62 | bool get invisible => invisiblityTimer > 0; 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/core/managers/team_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// Use this class together with PlayerManager. 4 | /// 5 | /// You may sometimes want to create teams in your game, so that 6 | /// some players are team mates. 7 | /// 8 | /// A player can only belong to a single team. 9 | class TeamManager extends Manager { 10 | final Map> _playersByTeam; 11 | final Map _teamByPlayer; 12 | 13 | /// Create a TeamManager. 14 | TeamManager() 15 | : _playersByTeam = >{}, 16 | _teamByPlayer = {}; 17 | 18 | /// Returns the team of [player]. 19 | String? getTeam(String player) => _teamByPlayer[player]; 20 | 21 | /// Set the [team] of [player]. 22 | void setTeam(String player, String team) { 23 | removeFromTeam(player); 24 | 25 | _teamByPlayer[player] = team; 26 | _playersByTeam.putIfAbsent(team, () => []).add(player); 27 | } 28 | 29 | /// Returns all players of [team]. 30 | Iterable getPlayers(String team) => 31 | _playersByTeam[team] ?? []; 32 | 33 | /// Removes [player] from their team. 34 | void removeFromTeam(String player) { 35 | final team = _teamByPlayer.remove(player); 36 | if (team != null) { 37 | _playersByTeam[team]?.remove(player); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright 2011 GAMADU.COM 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /lib/src/core/managers/player_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// You may sometimes want to specify to which player an entity belongs to. 4 | /// 5 | /// An entity can only belong to a single player at a time. 6 | class PlayerManager extends Manager { 7 | final Map _playerByEntity; 8 | final Map _entitiesByPlayer; 9 | 10 | /// Creates the [PlayerManager]. 11 | PlayerManager() 12 | : _playerByEntity = {}, 13 | _entitiesByPlayer = {}; 14 | 15 | /// Make [entity] belong to [player]. 16 | void setPlayer(Entity entity, String player) { 17 | _playerByEntity[entity] = player; 18 | _entitiesByPlayer.putIfAbsent(player, EntityBag.new).add(entity); 19 | } 20 | 21 | /// Returns all entities that belong to [player]. 22 | Iterable getEntitiesOfPlayer(String player) => 23 | _entitiesByPlayer[player] ??= EntityBag(); 24 | 25 | /// Removes [entity] from the player it is associated with. 26 | void removeFromPlayer(Entity entity) { 27 | final player = _playerByEntity[entity]; 28 | if (player != null) { 29 | _entitiesByPlayer[player]?.remove(entity); 30 | } 31 | } 32 | 33 | /// Returns the player associated with [entity]. 34 | String? getPlayer(Entity entity) => _playerByEntity[entity]; 35 | 36 | @override 37 | void deleted(Entity entity) => removeFromPlayer(entity); 38 | } 39 | -------------------------------------------------------------------------------- /test/dartemis/core/systems/interval_entity_system_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('IntervalEntitySystem tests', () { 6 | test('delta returns accumulated time since last processing', () { 7 | final world = World(); 8 | final sut = TestIntervalEntitySystem(40); 9 | world 10 | ..addSystem(sut) 11 | ..initialize() 12 | ..delta = 16; 13 | 14 | // ignore: invalid_use_of_visible_for_overriding_member 15 | expect(sut.checkProcessing(), equals(false)); 16 | // ignore: invalid_use_of_visible_for_overriding_member 17 | expect(sut.checkProcessing(), equals(false)); 18 | // ignore: invalid_use_of_visible_for_overriding_member 19 | expect(sut.checkProcessing(), equals(true)); 20 | expect(sut.delta, equals(48)); 21 | // ignore: invalid_use_of_visible_for_overriding_member 22 | sut.end(); 23 | // ignore: invalid_use_of_visible_for_overriding_member 24 | expect(sut.checkProcessing(), equals(false)); 25 | // ignore: invalid_use_of_visible_for_overriding_member 26 | expect(sut.checkProcessing(), equals(true)); 27 | expect(sut.delta, equals(32)); 28 | }); 29 | }); 30 | } 31 | 32 | class TestIntervalEntitySystem extends IntervalEntitySystem { 33 | TestIntervalEntitySystem(double interval) : super(interval, Aspect()); 34 | @override 35 | void processEntities(Iterable entities) {} 36 | } 37 | -------------------------------------------------------------------------------- /test/dartemis/core/component_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'components_setup.dart'; 5 | 6 | void main() { 7 | group('Component tests', () { 8 | late World world; 9 | setUp(() { 10 | world = World(); 11 | }); 12 | test('creating a new Component creates a new instance', () { 13 | final entity = world.createEntity(); 14 | final c = Component0(); 15 | world 16 | ..addComponent(entity, c) 17 | ..removeComponent(entity); 18 | 19 | expect(Component0(), isNot(same(c))); 20 | }); 21 | test('creating a new FreeListComponent reuses a removed instance', () { 22 | final entity = world.createEntity(); 23 | final c = PooledComponent2(); 24 | world.addComponent(entity, c); 25 | 26 | expect(PooledComponent2(), isNot(same(c))); 27 | world.removeComponent(entity); 28 | expect(PooledComponent2(), same(c)); 29 | }); 30 | 31 | test('moving components should not crash', () { 32 | final entity0 = world.createEntity(); 33 | world.addComponent(entity0, PooledComponent2()); 34 | 35 | var previousEntity = entity0; 36 | for (var i = 0; i < 128; i++) { 37 | final entity = world.createEntity(); 38 | world.moveComponent(previousEntity, entity); 39 | previousEntity = entity; 40 | } 41 | 42 | expect(world.getComponents(previousEntity), isNotEmpty); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/core/managers/tag_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// If you need to tag any entity, use this. A typical usage would be to tag 4 | /// entities such as "PLAYER", "BOSS" or something that is very unique. 5 | /// An entity can only belong to one tag (0,1) at a time. 6 | class TagManager extends Manager { 7 | final Map _entitiesByTag; 8 | final Map _tagsByEntity; 9 | 10 | /// Create the [TagManager]. 11 | TagManager() 12 | : _entitiesByTag = {}, 13 | _tagsByEntity = {}; 14 | 15 | /// Register a [tag] to an [entity]. 16 | void register(Entity entity, String tag) { 17 | unregister(tag); 18 | _entitiesByTag[tag] = entity; 19 | _tagsByEntity[entity] = tag; 20 | } 21 | 22 | /// Unregister entity tagged with [tag]. 23 | void unregister(String tag) { 24 | _tagsByEntity.remove(_entitiesByTag.remove(tag)); 25 | } 26 | 27 | /// Returns [:true:] if there is an entity with [tag]. 28 | bool isRegistered(String tag) => _entitiesByTag.containsKey(tag); 29 | 30 | /// Returns the entity with [tag]. 31 | Entity? getEntity(String tag) => _entitiesByTag[tag]; 32 | 33 | /// Returns the tag of the [entity]. 34 | String? getTag(Entity entity) => _tagsByEntity[entity]; 35 | 36 | /// Returns all known tags. 37 | Iterable getRegisteredTags() => 38 | _tagsByEntity.values as Iterable; 39 | 40 | @override 41 | void deleted(Entity entity) { 42 | final removedTag = _tagsByEntity.remove(entity); 43 | if (removedTag != null) { 44 | _entitiesByTag.remove(removedTag); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/dartemis/core/managers/group_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('GroupManager tests', () { 6 | late World world; 7 | late GroupManager sut; 8 | late Entity entityA; 9 | late Entity entityAB; 10 | late Entity entity0; 11 | setUp(() { 12 | world = World(); 13 | sut = GroupManager(); 14 | world.addManager(sut); 15 | 16 | entityA = world.createEntity(); 17 | sut.add(entityA, 'A'); 18 | entityAB = world.createEntity(); 19 | sut 20 | ..add(entityAB, 'A') 21 | ..add(entityAB, 'B'); 22 | entity0 = world.createEntity(); 23 | }); 24 | test('isInAnyGroup', () { 25 | expect(sut.isInAnyGroup(entityA), equals(true)); 26 | expect(sut.isInAnyGroup(entityAB), equals(true)); 27 | expect(sut.isInAnyGroup(entity0), equals(false)); 28 | }); 29 | test('isInGroup', () { 30 | expect(sut.isInGroup(entityA, 'A'), equals(true)); 31 | expect(sut.isInGroup(entityAB, 'A'), equals(true)); 32 | expect(sut.isInGroup(entity0, 'A'), equals(false)); 33 | expect(sut.isInGroup(entityA, 'B'), equals(false)); 34 | expect(sut.isInGroup(entityAB, 'B'), equals(true)); 35 | expect(sut.isInGroup(entity0, 'B'), equals(false)); 36 | }); 37 | test('isInGroup after add and remove', () { 38 | final entity00 = world.createEntity(); 39 | expect(sut.isInGroup(entity00, 'A'), equals(false)); 40 | sut.add(entity00, 'A'); 41 | expect(sut.isInGroup(entity00, 'A'), equals(true)); 42 | sut.remove(entity00, 'A'); 43 | expect(sut.isInGroup(entity00, 'A'), equals(false)); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/metadata/generate.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// Metadata to annotate [Manager]s and [EntitySystem]s to generate code 4 | /// required for [Mapper]s, other [Manager]s and other [EntitySystem]s using 5 | /// dartemis_builder. 6 | class Generate { 7 | /// The [EntitySystem] or [Manager] that is the base class. 8 | final Type base; 9 | 10 | /// Additional mappers to declare and initialize. 11 | final List mapper; 12 | 13 | /// Other [EntitySystem]s to declare and initialize. 14 | final List systems; 15 | 16 | /// Other [Manager]s to declare and initialize. 17 | final List manager; 18 | 19 | /// All [Aspect]s that an [int] needs to be processed by the 20 | /// [EntitySystem]. 21 | /// The required [Mapper]s will also be created. 22 | /// 23 | /// Has no effect if used in a [Manager]. 24 | final List allOf; 25 | 26 | /// One of the [Aspect]s that an [int] needs to be processed by the 27 | /// [EntitySystem]. Required [Mapper]s will also be created. 28 | /// 29 | /// Has no effect if used in a [Manager]. 30 | final List oneOf; 31 | 32 | /// Excludes [int]s that have these [Aspect]s from being processed by the 33 | /// [EntitySystem]. 34 | /// 35 | /// Has no effect if used in a [Manager]. 36 | final List exclude; 37 | 38 | /// Generate a class that extends [base] with an [Aspect] based on [allOf], 39 | /// [oneOf] and [exclude] as well as the additional [Mapper]s defined by 40 | /// [mapper] and the [EntitySystem]s and [Manager]s defined by [systems] and 41 | /// [manager]. 42 | const Generate( 43 | this.base, { 44 | this.allOf = const [], 45 | this.oneOf = const [], 46 | this.exclude = const [], 47 | this.mapper = const [], 48 | this.systems = const [], 49 | this.manager = const [], 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/dartemis/core/components_setup.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | 3 | class Component0 extends Component {} 4 | 5 | class Component1 extends Component {} 6 | 7 | class PooledComponent2 extends PooledComponent { 8 | factory PooledComponent2() => Pooled.of(PooledComponent2._); 9 | PooledComponent2._(); 10 | } 11 | 12 | class Component3 extends Component {} 13 | 14 | class Component4 extends Component {} 15 | 16 | class Component5 extends Component {} 17 | 18 | class Component6 extends Component {} 19 | 20 | class Component7 extends Component {} 21 | 22 | class Component8 extends Component {} 23 | 24 | class Component9 extends Component {} 25 | 26 | class Component10 extends Component {} 27 | 28 | class Component11 extends Component {} 29 | 30 | class Component12 extends Component {} 31 | 32 | class Component13 extends Component {} 33 | 34 | class Component14 extends Component {} 35 | 36 | class Component15 extends Component {} 37 | 38 | class Component16 extends Component {} 39 | 40 | class Component17 extends Component {} 41 | 42 | class Component18 extends Component {} 43 | 44 | class Component19 extends Component {} 45 | 46 | class Component20 extends Component {} 47 | 48 | class Component21 extends Component {} 49 | 50 | class Component22 extends Component {} 51 | 52 | class Component23 extends Component {} 53 | 54 | class Component24 extends Component {} 55 | 56 | class Component25 extends Component {} 57 | 58 | class Component26 extends Component {} 59 | 60 | class Component27 extends Component {} 61 | 62 | class Component28 extends Component {} 63 | 64 | class Component29 extends Component {} 65 | 66 | class Component30 extends Component {} 67 | 68 | class Component31 extends Component {} 69 | 70 | class Component32 extends Component {} 71 | 72 | class UnusedComponent extends Component {} 73 | -------------------------------------------------------------------------------- /lib/src/core/mapper.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// High performance component retrieval from entities. Use this wherever you 4 | /// need to retrieve components from entities often and fast. 5 | class Mapper { 6 | final List _components; 7 | 8 | /// Create a Mapper for [T] in [world]. 9 | Mapper(World world) 10 | : _components = world.componentManager._getComponentsByType(); 11 | 12 | /// Fast but unsafe retrieval of a component for this entity. 13 | /// No bounding checks, so this could throw a [RangeError], 14 | /// however in most scenarios you already know the entity possesses this 15 | /// component. 16 | T operator [](Entity entity) => _components[entity._id]!; 17 | 18 | /// Fast and safe retrieval of a component for this entity. 19 | /// If the entity does not have this component then null is returned. 20 | T? getSafe(Entity entity) { 21 | if (_components.length > entity._id) { 22 | return _components[entity._id]; 23 | } 24 | return null; 25 | } 26 | 27 | /// Checks if the entity has this type of component. 28 | bool has(Entity entity) => getSafe(entity) != null; 29 | } 30 | 31 | /// Same as [Mapper], except the [[]] operator returns [T?] instead of [T] and 32 | /// no getSafe method. 33 | /// For use in combination with [Aspect.oneOf]. 34 | class OptionalMapper { 35 | final List _components; 36 | 37 | /// Create a Mapper for [T] in [world]. 38 | OptionalMapper(World world) 39 | : _components = world.componentManager._getComponentsByType(); 40 | 41 | /// Fast and safe retrieval of a component for this entity. 42 | /// If the entity does not have this component then null is returned. 43 | T? operator [](Entity entity) { 44 | if (_components.length > entity._id) { 45 | return _components[entity._id]; 46 | } 47 | return null; 48 | } 49 | 50 | /// Checks if the entity has this type of component. 51 | bool has(Entity entity) => this[entity] != null; 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/core/managers/group_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// If you need to group your entities together, e.g. tanks going into "units" 4 | /// group or explosions into "effects", then use this manager. You must retrieve 5 | /// it using world instance. 6 | /// 7 | /// An [int] can only belong to several groups (0,n) at a time. 8 | class GroupManager extends Manager { 9 | final Map _entitiesByGroup; 10 | final Map> _groupsByEntity; 11 | 12 | /// Creates the [GroupManager]. 13 | GroupManager() 14 | : _entitiesByGroup = {}, 15 | _groupsByEntity = >{}; 16 | 17 | /// Set the group of the entity. 18 | void add(Entity entity, String group) { 19 | _entitiesByGroup.putIfAbsent(group, EntityBag.new).add(entity); 20 | _groupsByEntity.putIfAbsent(entity, Bag.new).add(group); 21 | } 22 | 23 | /// Remove the entity from the specified group. 24 | void remove(Entity entity, String group) { 25 | _entitiesByGroup[group]?.remove(entity); 26 | _groupsByEntity[entity]?.remove(group); 27 | } 28 | 29 | /// Remove [entity] from all existing groups. 30 | void removeFromAllGroups(Entity entity) { 31 | final groups = _groupsByEntity[entity]; 32 | if (groups != null) { 33 | groups 34 | ..forEach((group) { 35 | _entitiesByGroup[group]?.remove(entity); 36 | }) 37 | ..clear(); 38 | } 39 | } 40 | 41 | /// Get all entities that belong to the provided group. 42 | Iterable getEntities(String group) => 43 | _entitiesByGroup.putIfAbsent(group, EntityBag.new); 44 | 45 | /// Returns the groups the entity belongs to, null if none. 46 | Iterable? getGroups(Entity entity) => _groupsByEntity[entity]; 47 | 48 | /// Checks if the entity belongs to any group. 49 | bool isInAnyGroup(Entity entity) => getGroups(entity) != null; 50 | 51 | /// Check if the entity is in the supplied group. 52 | bool isInGroup(Entity entity, String group) { 53 | final groups = _groupsByEntity[entity]; 54 | return (groups != null) && groups.contains(group); 55 | } 56 | 57 | @override 58 | void deleted(Entity entity) => removeFromAllGroups(entity); 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/core/entity_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// Manages creation and deletion of every [int] and gives access to some 4 | /// basic statistcs. 5 | class EntityManager extends Manager { 6 | BitSet _entities; 7 | final Bag _deletedEntities; 8 | 9 | int _active = 0; 10 | int _added = 0; 11 | int _created = 0; 12 | int _deleted = 0; 13 | 14 | final _EntityPool _identifierPool; 15 | 16 | EntityManager._internal() 17 | : _entities = BitSet(32), 18 | _deletedEntities = Bag(), 19 | _identifierPool = _EntityPool(); 20 | 21 | Entity _createEntityInstance() { 22 | final entity = _deletedEntities.removeLast() ?? _identifierPool.checkOut(); 23 | _created++; 24 | return entity; 25 | } 26 | 27 | void _add(Entity entity) { 28 | _active++; 29 | _added++; 30 | if (entity._id >= _entities.length) { 31 | _entities = BitSet.fromBitSet(_entities, length: entity._id + 1); 32 | } 33 | _entities[entity._id] = true; 34 | } 35 | 36 | void _delete(Entity entity) { 37 | if (_entities[entity._id]) { 38 | _entities[entity._id] = false; 39 | 40 | _deletedEntities.add(entity); 41 | 42 | _active--; 43 | _deleted++; 44 | } 45 | } 46 | 47 | /// Check if this entity is active. 48 | /// Active means the entity is being actively processed. 49 | bool isActive(Entity entity) => _entities[entity._id]; 50 | 51 | /// Get how many entities are active in this world. 52 | int get activeEntityCount => _active; 53 | 54 | /// Get how many entities have been created in the world since start. 55 | /// Note: A created entity may not have been added to the world, thus 56 | /// created count is always equal or larger than added count. 57 | int get totalCreated => _created; 58 | 59 | /// Get how many entities have been added to the world since start. 60 | int get totalAdded => _added; 61 | 62 | /// Get how many entities have been deleted from the world since start. 63 | int get totalDeleted => _deleted; 64 | } 65 | 66 | /// Used only internally to generate distinct ids for entities and reuse them. 67 | class _EntityPool { 68 | final List _entities = []; 69 | int _nextAvailableId = 0; 70 | 71 | _EntityPool(); 72 | 73 | Entity checkOut() { 74 | if (_entities.isNotEmpty) { 75 | return _entities.removeLast(); 76 | } 77 | return Entity._(_nextAvailableId++); 78 | } 79 | 80 | void checkIn(Entity entity) => _entities.add(entity); 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/core/utils/object_pool.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// Inspired by 4 | /// this class stores objects that are no longer used in the game for later 5 | /// reuse. 6 | class ObjectPool { 7 | static final Map> _objectPools = >{}; 8 | 9 | /// Returns a pooled object of type [T]. If there is no object in the pool 10 | /// it will create a new one using [createPooled]. 11 | static T get>(CreatePooled createPooled) { 12 | final pool = _getPool(); 13 | var obj = pool.removeLast(); 14 | return obj ??= createPooled(); 15 | } 16 | 17 | static Bag _getPool>() { 18 | var pooledObjects = _objectPools[T] as Bag?; 19 | if (null == pooledObjects) { 20 | pooledObjects = Bag(); 21 | _objectPools[T] = pooledObjects; 22 | } 23 | return pooledObjects; 24 | } 25 | 26 | /// Adds a [Pooled] object to the [ObjectPool]. 27 | static void add>(T pooled) { 28 | _getPool().add(pooled); 29 | } 30 | 31 | /// Add a specific [amount] of [Pooled]s for later reuse. 32 | static void addMany>( 33 | CreatePooled createPooled, 34 | int amount, 35 | ) { 36 | final pool = _getPool(); 37 | for (var i = 0; i < amount; i++) { 38 | pool.add(createPooled()); 39 | } 40 | } 41 | } 42 | 43 | /// Create a [Pooled] object. 44 | typedef CreatePooled> = T Function(); 45 | 46 | /// Objects of this class can be pooled in the [ObjectPool] for later reuse. 47 | /// 48 | /// Should be added as a mixin. 49 | mixin Pooled> { 50 | /// Creates a new [Pooled] of type [T]. 51 | /// 52 | /// The instance created with [createPooled] should be created with 53 | /// a zero-argument contructor because it will only be called once. All fields 54 | /// of the created object should be set in the calling factory constructor. 55 | static T of>(CreatePooled createPooled) => 56 | ObjectPool.get(createPooled); 57 | 58 | /// If you need to do some cleanup before this object moves into the Pool of 59 | /// reusable objects. 60 | void cleanUp(); 61 | 62 | /// Calls the cleanup function and moves this object to the [ObjectPool]. 63 | void moveToPool() { 64 | cleanUp(); 65 | ObjectPool.add(this as T); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/dartemis/core/managers/tag_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('TagManager tests', () { 6 | const tag = 'some tag'; 7 | 8 | late World world; 9 | late TagManager sut; 10 | late Entity entityWithTag; 11 | late Entity entityWithoutTag; 12 | setUp(() { 13 | world = World(); 14 | sut = TagManager(); 15 | world.addManager(sut); 16 | 17 | entityWithTag = world.createEntity(); 18 | entityWithoutTag = world.createEntity(); 19 | 20 | sut.register(entityWithTag, tag); 21 | }); 22 | test('getEntity returns registered entity', () { 23 | final actualEntity = sut.getEntity(tag); 24 | 25 | expect(actualEntity, equals(entityWithTag)); 26 | }); 27 | test('getEntity returns null if tag has been unregistered', () { 28 | sut.unregister(tag); 29 | 30 | final actualEntity = sut.getEntity(tag); 31 | 32 | expect(actualEntity, isNull); 33 | }); 34 | test('getEntity returns null if tag does not exist', () { 35 | final actualEntity = sut.getEntity('nonexistent tag'); 36 | 37 | expect(actualEntity, isNull); 38 | }); 39 | test('getTag returns registered tag', () { 40 | final actualTag = sut.getTag(entityWithTag); 41 | 42 | expect(actualTag, equals(tag)); 43 | }); 44 | test('getTag returns null if entity has no tag', () { 45 | final actualTag = sut.getTag(entityWithoutTag); 46 | 47 | expect(actualTag, isNull); 48 | }); 49 | test('getTag returns null if tag has been unregistered', () { 50 | sut.unregister(tag); 51 | 52 | final actualTag = sut.getTag(entityWithTag); 53 | 54 | expect(actualTag, isNull); 55 | }); 56 | test( 57 | '''register overwrites existing entity if a another entity is registered using the same tag''', 58 | () { 59 | final anotherEntity = world.createEntity(); 60 | sut.register(anotherEntity, 'tag'); 61 | 62 | final actualEntity = sut.getEntity('tag'); 63 | expect(actualEntity, equals(anotherEntity)); 64 | }); 65 | test( 66 | '''deleting a previously registered entity does not mess up accessing a newly registered entity''', 67 | () { 68 | final anotherEntity = world.createEntity(); 69 | sut.register(anotherEntity, 'tag'); 70 | world.deleteEntity(entityWithTag); 71 | 72 | final actualEntity = sut.getEntity('tag'); 73 | expect(actualEntity, equals(anotherEntity)); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /test/dartemis/core/aspect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'components_setup.dart'; 5 | 6 | void main() { 7 | group('Aspect Tests', () { 8 | test('getAspectForAll with one component', () { 9 | final aspect = Aspect(allOf: [PooledComponent2]); 10 | expect(aspect.all, contains(PooledComponent2)); 11 | expect(aspect.all, hasLength(1)); 12 | expect(aspect.excluded, isEmpty); 13 | expect(aspect.one, isEmpty); 14 | }); 15 | test('getAspectForAll with all components', () { 16 | final aspect = Aspect(allOf: [Component0, Component1, PooledComponent2]); 17 | expect( 18 | aspect.all, 19 | containsAll([Component0, Component1, PooledComponent2]), 20 | ); 21 | expect(aspect.excluded, isEmpty); 22 | expect(aspect.one, isEmpty); 23 | }); 24 | test('getAspectForAll with one component, excluding another one', () { 25 | final aspect = Aspect(allOf: [PooledComponent2])..exclude([Component0]); 26 | expect(aspect.all, containsAll([PooledComponent2])); 27 | expect(aspect.excluded, containsAll([Component0])); 28 | expect(aspect.one, isEmpty); 29 | }); 30 | test('getAspectForAll with one component, excluding another two', () { 31 | final aspect = Aspect(allOf: [PooledComponent2]) 32 | ..exclude([Component0, Component1]); 33 | expect(aspect.all, containsAll([PooledComponent2])); 34 | expect(aspect.excluded, containsAll([Component0, Component1])); 35 | expect(aspect.one, isEmpty); 36 | }); 37 | test('getAspectForAll with one component, and one of two', () { 38 | final aspect = Aspect(allOf: [PooledComponent2]) 39 | ..oneOf([Component0, Component1]); 40 | expect(aspect.all, containsAll([PooledComponent2])); 41 | expect(aspect.excluded, isEmpty); 42 | expect(aspect.one, containsAll([Component0, Component1])); 43 | }); 44 | test('getAspectForOne with all components', () { 45 | final aspect = Aspect(oneOf: [Component0, Component1, PooledComponent2]); 46 | expect(aspect.all, isEmpty); 47 | expect(aspect.excluded, isEmpty); 48 | expect( 49 | aspect.one, 50 | containsAll([Component0, Component1, PooledComponent2]), 51 | ); 52 | }); 53 | test('getAspectForOne with chaining each component', () { 54 | final aspect = Aspect(oneOf: [Component0]) 55 | ..oneOf([Component1]) 56 | ..oneOf([PooledComponent2]); 57 | expect(aspect.all, isEmpty); 58 | expect(aspect.excluded, isEmpty); 59 | expect( 60 | aspect.one, 61 | containsAll([Component0, Component1, PooledComponent2]), 62 | ); 63 | }); 64 | test('getEmpty()', () { 65 | final aspect = Aspect(); 66 | expect(aspect.all, isEmpty); 67 | expect(aspect.excluded, isEmpty); 68 | expect(aspect.one, isEmpty); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /example/darteroids/input_systems.dart: -------------------------------------------------------------------------------- 1 | part of '../main.dart'; 2 | 3 | class PlayerControlSystem extends IntervalEntitySystem { 4 | static const int up = KeyCode.W; 5 | static const int down = KeyCode.S; 6 | static const int left = KeyCode.A; 7 | static const int right = KeyCode.D; 8 | 9 | bool moveUp = false; 10 | bool moveDown = false; 11 | bool moveLeft = false; 12 | bool moveRight = false; 13 | bool shoot = false; 14 | 15 | num targetX = 0; 16 | num targetY = 0; 17 | 18 | late final Mapper velocityMapper; 19 | late final Mapper cannonMapper; 20 | late final TagManager tagManager; 21 | 22 | final HTMLCanvasElement canvas; 23 | 24 | PlayerControlSystem(this.canvas) 25 | : super(20, Aspect(allOf: [Velocity, Cannon])); 26 | 27 | @override 28 | void initialize(World world) { 29 | super.initialize(world); 30 | tagManager = world.getManager(); 31 | velocityMapper = Mapper(world); 32 | cannonMapper = Mapper(world); 33 | 34 | window.onKeyDown.listen(handleKeyDown); 35 | EventStreamProviders.keyUpEvent.forTarget(window).listen(handleKeyUp); 36 | canvas.onMouseDown.listen(handleMouseDown); 37 | canvas.onMouseUp.listen(handleMouseUp); 38 | } 39 | 40 | @override 41 | void processEntities(Iterable entities) { 42 | final player = tagManager.getEntity(tagPlayer)!; 43 | final velocity = velocityMapper[player]; 44 | final cannon = cannonMapper[player]; 45 | 46 | if (moveUp) { 47 | velocity.y -= 0.1; 48 | } else if (moveDown) { 49 | velocity.y += 0.1; 50 | } 51 | if (moveLeft) { 52 | velocity.x -= 0.1; 53 | } else if (moveRight) { 54 | velocity.x += 0.1; 55 | } 56 | cannon.shoot = shoot; 57 | if (shoot) { 58 | cannon.target(targetX, targetY); 59 | } 60 | } 61 | 62 | void handleKeyDown(KeyboardEvent e) { 63 | final keyCode = e.keyCode; 64 | if (keyCode == up) { 65 | moveUp = true; 66 | moveDown = false; 67 | } else if (keyCode == down) { 68 | moveUp = false; 69 | moveDown = true; 70 | } else if (keyCode == left) { 71 | moveLeft = true; 72 | moveRight = false; 73 | } else if (keyCode == right) { 74 | moveLeft = false; 75 | moveRight = true; 76 | } 77 | } 78 | 79 | void handleKeyUp(KeyboardEvent e) { 80 | final keyCode = e.keyCode; 81 | if (keyCode == up) { 82 | moveUp = false; 83 | } else if (keyCode == down) { 84 | moveDown = false; 85 | } else if (keyCode == left) { 86 | moveLeft = false; 87 | } else if (keyCode == right) { 88 | moveRight = false; 89 | } 90 | } 91 | 92 | void handleMouseDown(MouseEvent e) { 93 | targetX = e.offsetX; 94 | targetY = e.offsetY; 95 | shoot = true; 96 | } 97 | 98 | void handleMouseUp(MouseEvent e) { 99 | shoot = false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/core/aspect.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// An Aspect is used by systems as a matcher against entities, to check if a 4 | /// system is interested in an entity. Aspects define what sort of component 5 | /// types an entity must possess, or not possess. 6 | /// 7 | /// This creates an aspect where an entity must possess A and B and C: 8 | /// Aspect(allOf: [A, B, C]) 9 | /// 10 | /// This creates an aspect where an entity must possess A and B and C, but must 11 | /// not possess U or V. 12 | /// Aspect(allOf: [A, B, C])..exclude([U, V]) 13 | /// 14 | /// This creates an aspect where an entity must possess A and B and C, but must 15 | /// not possess U or V, but must possess one of X or Y or Z. 16 | /// Aspect(allOf: [A, B, C])..exclude([U, V])..oneOf([X, Y, Z]) 17 | /// 18 | /// You can create and compose aspects in many ways: 19 | /// Aspect.empty()..oneOf([X, Y, Z])..allOf([A, B, C])..exclude([U, V]) 20 | /// is the same as: 21 | /// Aspect(allOf: [A, B, C])..exclude([U, V])..oneOf([X, Y, Z]) 22 | class Aspect { 23 | /// All components an [Entity] needs to be processed by an [EntitySystem]. 24 | final Set all = {}; 25 | 26 | /// An [Entity] needs one of these components to be processed by the 27 | /// [EntitySystem]. 28 | final Set one = {}; 29 | 30 | /// An [Entity] will not be processed by the [EntitySystem] if it has one of 31 | /// these [Component] types. 32 | final Set excluded = {}; 33 | 34 | /// Creates and returns an aspect. 35 | /// 36 | /// A system only processes an [Entity] that posses all [Component]s 37 | /// given by [allOf]. 38 | /// 39 | /// With [oneOf] an [Entity] must posses at least one of the specified 40 | /// [Component]s to be processed by a system. 41 | /// 42 | /// [exclude] can be used to prevent an [Entity] to be processed when it has 43 | /// one of the specified [Component]s. 44 | /// 45 | /// If no arguments are passed it will be an empty aspect. 46 | /// This can be used if you want a system that processes no entities, 47 | /// but still gets invoked. Typical usages is when 48 | /// you need to create special purpose systems for debug rendering, like 49 | /// rendering FPS, how many entities are active in the world, etc. 50 | Aspect({ 51 | Iterable allOf = const {}, 52 | Iterable oneOf = const {}, 53 | Iterable exclude = const {}, 54 | }) { 55 | all.addAll(allOf); 56 | one.addAll(oneOf); 57 | excluded.addAll(exclude); 58 | } 59 | 60 | /// Modifies the aspect in a way that an entity must possess all of the 61 | /// specified components. 62 | void allOf(Iterable componentTypes) { 63 | all.addAll(componentTypes); 64 | } 65 | 66 | /// Excludes all of the specified components from the aspect. A system will 67 | /// not be interested in an entity that possesses one of the specified 68 | /// excluded components. 69 | void exclude(Iterable componentTypes) { 70 | excluded.addAll(componentTypes); 71 | } 72 | 73 | /// Modifies the aspect in a way that an entity must possess one of the 74 | /// specified components. 75 | void oneOf(Iterable componentTypes) { 76 | one.addAll(componentTypes); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | library darteroids; 2 | 3 | import 'dart:async'; 4 | import 'dart:js_interop'; 5 | import 'dart:math'; 6 | 7 | import 'package:dartemis/dartemis.dart'; 8 | import 'package:web/web.dart'; 9 | 10 | part 'darteroids/components.dart'; 11 | part 'darteroids/gamelogic_systems.dart'; 12 | part 'darteroids/input_systems.dart'; 13 | part 'darteroids/render_systems.dart'; 14 | 15 | const String tagPlayer = 'player'; 16 | const String groupAsteroids = 'ASTEROIDS'; 17 | const String playerColor = '#ff0000'; 18 | const String asteroidColor = '#BBB'; 19 | const int maxWidth = 600; 20 | const int maxHeight = 600; 21 | const int hudHeight = 100; 22 | 23 | final Random random = Random(); 24 | 25 | void main() async { 26 | final canvas = document.querySelector('#gamecontainer')! as HTMLCanvasElement 27 | ..width = maxWidth 28 | ..height = maxHeight + hudHeight; 29 | 30 | await Darteroids(canvas).start(); 31 | } 32 | 33 | class Darteroids { 34 | final HTMLCanvasElement canvas; 35 | final CanvasRenderingContext2D context2d; 36 | final World world; 37 | num lastTime = 0; 38 | final Stopwatch physicsLoopTimer = Stopwatch()..start(); 39 | 40 | Darteroids(this.canvas) 41 | : context2d = canvas.context2D, 42 | world = World(); 43 | 44 | Future start() async { 45 | final player = world.createEntity([ 46 | Position(maxWidth ~/ 2, maxHeight ~/ 2), 47 | Velocity(), 48 | CircularBody(20, playerColor), 49 | Cannon(), 50 | Status(lifes: 3, invisiblityTimer: 5000), 51 | ]); 52 | 53 | final tagManager = TagManager()..register(player, tagPlayer); 54 | final groupManager = GroupManager(); 55 | world 56 | ..addManager(tagManager) 57 | ..addManager(groupManager); 58 | 59 | addAsteroids(groupManager); 60 | 61 | world 62 | ..addSystem(PlayerControlSystem(canvas)) 63 | ..addSystem(BulletSpawningSystem()) 64 | ..addSystem(DecaySystem()) 65 | ..addSystem(MovementSystem()) 66 | ..addSystem(AsteroidDestructionSystem()) 67 | ..addSystem(PlayerCollisionDetectionSystem()) 68 | ..addSystem(BackgroundRenderSystem(context2d, group: 1)) 69 | ..addSystem(CircleRenderingSystem(context2d, group: 1)) 70 | ..addSystem(HudRenderSystem(context2d, group: 1)) 71 | ..initialize(); 72 | 73 | physicsLoop(); 74 | renderLoop(16.66); 75 | } 76 | 77 | void addAsteroids(GroupManager groupManager) { 78 | for (var i = 0; i < 33; i++) { 79 | final vx = generateRandomVelocity(); 80 | final vy = generateRandomVelocity(); 81 | final asteroid = world.createEntity([ 82 | Position( 83 | maxWidth * random.nextDouble(), 84 | maxHeight * random.nextDouble(), 85 | ), 86 | Velocity(vx, vy), 87 | CircularBody(5 + 10 * random.nextDouble(), asteroidColor), 88 | PlayerDestroyer(), 89 | ]); 90 | groupManager.add(asteroid, groupAsteroids); 91 | } 92 | } 93 | 94 | void physicsLoop() { 95 | world 96 | ..delta = physicsLoopTimer.elapsedMicroseconds / 1000.0 97 | ..process(); 98 | physicsLoopTimer.reset(); 99 | 100 | Future.delayed(const Duration(milliseconds: 5), physicsLoop); 101 | } 102 | 103 | void renderLoop(num time) { 104 | world.delta = (time - lastTime).toDouble(); 105 | lastTime = time; 106 | world.process(1); 107 | 108 | window.requestAnimationFrame(renderLoop.toJS); 109 | } 110 | } 111 | 112 | num generateRandomVelocity() => 113 | 0.5 + 1.5 * random.nextDouble() * (random.nextBool() ? 1 : -1); 114 | 115 | bool doCirclesCollide( 116 | num x1, 117 | num y1, 118 | num radius1, 119 | num x2, 120 | num y2, 121 | num radius2, 122 | ) { 123 | final dx = x2 - x1; 124 | final dy = y2 - y1; 125 | final d = radius1 + radius2; 126 | return (dx * dx + dy * dy) < (d * d); 127 | } 128 | -------------------------------------------------------------------------------- /example/darteroids/render_systems.dart: -------------------------------------------------------------------------------- 1 | part of '../main.dart'; 2 | 3 | class CircleRenderingSystem extends EntityProcessingSystem { 4 | final CanvasRenderingContext2D context; 5 | 6 | late final Mapper positionMapper; 7 | late final Mapper bodyMapper; 8 | late final Mapper statusMapper; 9 | 10 | CircleRenderingSystem(this.context, {super.group}) 11 | : super(Aspect(allOf: [Position, CircularBody])); 12 | 13 | @override 14 | void initialize(World world) { 15 | super.initialize(world); 16 | positionMapper = Mapper(world); 17 | statusMapper = Mapper(world); 18 | bodyMapper = Mapper(world); 19 | } 20 | 21 | @override 22 | void processEntity(Entity entity) { 23 | final pos = positionMapper[entity]; 24 | final body = bodyMapper[entity]; 25 | final status = statusMapper.getSafe(entity); 26 | 27 | context.save(); 28 | 29 | try { 30 | context 31 | ..lineWidth = 0.5 32 | ..fillStyle = body.color.toJS 33 | ..strokeStyle = body.color.toJS; 34 | if (null != status && status.invisible) { 35 | if (status.invisiblityTimer % 600 < 300) { 36 | context.globalAlpha = 0.4; 37 | } 38 | } 39 | 40 | drawCirle(pos, body); 41 | 42 | if (pos.x + body.radius > maxWidth) { 43 | drawCirle(pos, body, offsetX: -maxWidth); 44 | } else if (pos.x - body.radius < 0) { 45 | drawCirle(pos, body, offsetX: maxWidth); 46 | } 47 | if (pos.y + body.radius > maxHeight) { 48 | drawCirle(pos, body, offsetY: -maxHeight); 49 | } else if (pos.y - body.radius < 0) { 50 | drawCirle(pos, body, offsetY: maxHeight); 51 | } 52 | 53 | context.stroke(); 54 | } finally { 55 | context.restore(); 56 | } 57 | } 58 | 59 | void drawCirle( 60 | Position pos, 61 | CircularBody body, { 62 | int offsetX = 0, 63 | int offsetY = 0, 64 | }) { 65 | context 66 | ..beginPath() 67 | ..arc(pos.x + offsetX, pos.y + offsetY, body.radius, 0, pi * 2) 68 | ..closePath() 69 | ..fill(); 70 | } 71 | } 72 | 73 | class BackgroundRenderSystem extends VoidEntitySystem { 74 | final CanvasRenderingContext2D context; 75 | 76 | BackgroundRenderSystem(this.context, {super.group}); 77 | 78 | @override 79 | void processSystem() { 80 | context.save(); 81 | try { 82 | context 83 | ..fillStyle = 'black'.toJS 84 | ..beginPath() 85 | ..rect(0, 0, maxWidth, maxHeight + hudHeight) 86 | ..closePath() 87 | ..fill(); 88 | } finally { 89 | context.restore(); 90 | } 91 | } 92 | } 93 | 94 | class HudRenderSystem extends VoidEntitySystem { 95 | final CanvasRenderingContext2D context; 96 | late final TagManager tagManager; 97 | late final Mapper statusMapper; 98 | 99 | HudRenderSystem(this.context, {super.group}); 100 | 101 | @override 102 | void initialize(World world) { 103 | super.initialize(world); 104 | tagManager = world.getManager(); 105 | statusMapper = Mapper(world); 106 | } 107 | 108 | @override 109 | void processSystem() { 110 | context.save(); 111 | try { 112 | context 113 | ..fillStyle = '#555'.toJS 114 | ..beginPath() 115 | ..rect(0, maxHeight, maxWidth, maxHeight + hudHeight) 116 | ..closePath() 117 | ..fill(); 118 | 119 | final player = tagManager.getEntity(tagPlayer)!; 120 | final status = statusMapper[player]; 121 | 122 | context.fillStyle = playerColor.toJS; 123 | for (var i = 0; i < status.lifes; i++) { 124 | context 125 | ..beginPath() 126 | ..arc(50 + i * 50, maxHeight + hudHeight ~/ 2, 15, 0, pi * 2) 127 | ..closePath() 128 | ..fill(); 129 | } 130 | } finally { 131 | context.restore(); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/core/utils/bag.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dartemis.dart'; 2 | 3 | /// Collection type a bit like List but does not preserve the order of its 4 | /// entities, speedwise it is very good, especially suited for games. 5 | class Bag with Iterable { 6 | List _data; 7 | int _size = 0; 8 | 9 | /// Create a [Bag] with an initial capacity of [capacity]. 10 | Bag({int capacity = 32}) : _data = List.filled(capacity, null); 11 | 12 | /// Creates a new [Bag] with the elements of [iterable]. 13 | Bag.from(Iterable iterable) 14 | : _data = iterable.toList(growable: false), 15 | _size = iterable.length; 16 | 17 | /// Returns the element at the specified [index] in the bag. 18 | E? operator [](int index) => _data[index]; 19 | 20 | /// Returns the number of elements in this bag. 21 | int get size => _size; 22 | 23 | /// Returns [:true:] if this bag contains no elements. 24 | @override 25 | bool get isEmpty => size == 0; 26 | 27 | /// Removes the element at the specified [index] in this bag. Does this by 28 | /// overwriting with the last element and then removing the last element. 29 | E? removeAt(int index) { 30 | // make copy of element to remove so it can be returned 31 | final o = _data[index]; 32 | // overwrite item to remove with last element 33 | _data[index] = _data[--_size]; 34 | // null last element, so gc can do its work 35 | _data[size] = null; 36 | 37 | return o; 38 | } 39 | 40 | /// Remove and return the last object in the bag. 41 | E? removeLast() { 42 | if (_size > 0) { 43 | final current = _data[--_size]; 44 | _data[size] = null; 45 | return current; 46 | } 47 | return null; 48 | } 49 | 50 | /// Removes the first occurrence of the specified element from this bag, if 51 | /// it is present. If the Bag does not contain the element, it is unchanged. 52 | /// Does this by overwriting with the last element and then removing the last 53 | /// element. 54 | /// Returns [:true:] if this list contained the specified [element]. 55 | bool remove(E element) { 56 | for (var i = 0; i < size; i++) { 57 | final current = _data[i]; 58 | 59 | if (element == current) { 60 | // overwrite item to remove with last element 61 | _data[i] = _data[--_size]; 62 | // null last element, so gc can do its work 63 | _data[size] = null; 64 | return true; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /// Returns the number of elements the bag can hold without growing. 72 | int get capacity => _data.length; 73 | 74 | /// Adds the specified [element] to the end of this bag. If needed also 75 | /// increases the capacity of the bag. 76 | void add(E element) { 77 | // is size greater than capacity increase capacity 78 | if (_size == _data.length) { 79 | _grow(); 80 | } 81 | _data[_size++] = element; 82 | } 83 | 84 | /// Sets [element] at specified [index] in the bag. 85 | void operator []=(int index, E element) { 86 | if (index >= _data.length) { 87 | _growTo(index * 2); 88 | } 89 | if (_size <= index) { 90 | _size = index + 1; 91 | } 92 | _data[index] = element; 93 | } 94 | 95 | void _grow() => _growTo(_calculateNewCapacity(_data.length)); 96 | 97 | int _calculateNewCapacity(int requiredLength) => 98 | (requiredLength * 3) ~/ 2 + 1; 99 | 100 | void _growTo(int newCapacity) { 101 | final oldData = _data; 102 | _data = List.filled(newCapacity, null) 103 | ..setRange(0, oldData.length, oldData); 104 | } 105 | 106 | void _ensureCapacity(int index) { 107 | if (index >= _data.length) { 108 | _growTo(index * 2); 109 | } 110 | } 111 | 112 | /// Removes all of the elements from this bag. The bag will be empty after 113 | /// this call returns. 114 | void clear() { 115 | // null all elements so gc can clean up 116 | for (var i = 0; i < _size; i++) { 117 | _data[i] = null; 118 | } 119 | _size = 0; 120 | } 121 | 122 | /// Add all [items] into this bag. 123 | void addAll(Bag items) => items.forEach(add); 124 | 125 | /// Returns [:true:] iff the [index] is within the capacity of the underlying 126 | /// list. 127 | bool isIndexWithinBounds(int index) => index < capacity; 128 | 129 | @override 130 | Iterator get iterator => _data.sublist(0, size).cast().iterator; 131 | 132 | @override 133 | int get length => size; 134 | 135 | @override 136 | Bag cast() => Bag(capacity: _data.length) 137 | .._size = _size 138 | .._data = _data.cast(); 139 | } 140 | -------------------------------------------------------------------------------- /test/dartemis/core/component_manager_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'components_setup.dart'; 5 | 6 | void main() { 7 | group('integration tests for ComponentManager', () { 8 | late World world; 9 | setUp(() { 10 | world = World(); 11 | }); 12 | test('returns correct bit', () { 13 | final componentManager = world.getManager(); 14 | expect(componentManager.getBitIndex(Component0), 0); 15 | expect(componentManager.getBitIndex(Component1), 1); 16 | expect(componentManager.getBitIndex(PooledComponent2), 2); 17 | }); 18 | test('ComponentManager correctly associates entity and components', () { 19 | final entity = world.createEntity(); 20 | final componentA = Component0(); 21 | final componentC = PooledComponent2(); 22 | world.addComponents(entity, [componentA, componentC]); 23 | 24 | final components = world.getComponents(entity); 25 | 26 | expect(components, containsAll([componentA, componentC])); 27 | expect(components.length, equals(2)); 28 | }); 29 | test('ComponentManager correctly associates multiple entity and components', 30 | () { 31 | final entity1 = world.createEntity(); 32 | final component1A = Component0(); 33 | final component1C = PooledComponent2(); 34 | world 35 | ..addComponent(entity1, component1A) 36 | ..addComponent(entity1, component1C); 37 | 38 | final entity2 = world.createEntity(); 39 | final component2A = Component0(); 40 | final component2B = Component1(); 41 | final component2C = PooledComponent2(); 42 | world.addComponents(entity2, [component2A, component2B, component2C]); 43 | 44 | final components1 = world.getComponents(entity1); 45 | final components2 = world.getComponents(entity2); 46 | 47 | expect(components1, containsAll([component1A, component1C])); 48 | expect(components1.length, equals(2)); 49 | 50 | expect(components2, containsAll([component2A, component2B, component2C])); 51 | expect(components2.length, equals(3)); 52 | }); 53 | test('ComponentManager removes Components of deleted entity', () { 54 | final entity = world.createEntity(); 55 | final componentA = Component0(); 56 | final componentC = PooledComponent2(); 57 | world 58 | ..addComponents(entity, [componentA, componentC]) 59 | ..addEntity(entity) 60 | ..initialize() 61 | ..process() 62 | ..deleteEntity(entity) 63 | ..process(); 64 | 65 | final fillBag = world.getComponents(entity); 66 | expect(fillBag.length, equals(0)); 67 | }); 68 | test('ComponentManager can be created for unused Component', () { 69 | final componentsByType = 70 | world.componentManager.getComponentsByType(); 71 | expect(componentsByType.length, equals(0)); 72 | }); 73 | test('ComponentManager returns specific component for specific entity', () { 74 | final componentA = Component0(); 75 | final entity = world.createEntity([componentA]); 76 | 77 | expect( 78 | world.componentManager.getComponent(entity), 79 | equals(componentA), 80 | ); 81 | }); 82 | test( 83 | 'ComponentManager returns null if component for specific entity ' 84 | 'has not been registered', () { 85 | final entity = world.createEntity([Component0()]); 86 | 87 | expect( 88 | world.componentManager.getComponent(entity), 89 | isNull, 90 | ); 91 | }); 92 | test( 93 | 'ComponentManager returns null if component for specific entity does ' 94 | 'not exist', () { 95 | final entity = world.createEntity([Component0()]); 96 | // create an entity with the component we want to access so it gets 97 | // registered with the ComponentManager and a _ComponentInfo to access 98 | // is created 99 | world.createEntity([Component1()]); 100 | 101 | expect( 102 | world.componentManager.getComponent(entity), 103 | isNull, 104 | ); 105 | }); 106 | test( 107 | 'ComponentManager returns null if no component for high index entity ' 108 | 'exist', () { 109 | final componentA = Component0(); 110 | world.createEntity([componentA]); 111 | for (var i = 0; i < 1000; i++) { 112 | world.createEntity([]); 113 | } 114 | final highIdEntity = world.createEntity([]); 115 | 116 | expect( 117 | world.componentManager.getComponent(highIdEntity), 118 | isNull, 119 | ); 120 | }); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/core/entity_system.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// The most raw entity system. It should not typically be used, but you can 4 | /// create your own entity system handling by extending this. It is recommended 5 | /// that you use the other provided entity system implementations. 6 | /// 7 | /// There is no need to ever call any other method than process on objects of 8 | /// this class. 9 | abstract class EntitySystem { 10 | late final World _world; 11 | 12 | List _actives = []; 13 | 14 | final List _interestingComponentsIndices = []; 15 | final List _componentIndicesAll = []; 16 | final List _componentIndicesOne = []; 17 | final List _componentIndicesExcluded = []; 18 | 19 | final Aspect _aspect; 20 | final BitSet _all = BitSet(64); 21 | final BitSet _excluded = BitSet(64); 22 | final BitSet _one = BitSet(64); 23 | 24 | double _time = 0; 25 | double _delta = 0; 26 | int _frame = 0; 27 | 28 | /// If [passive] is set to true the [EntitySystem] will not be processed by 29 | /// the world. 30 | bool passive; 31 | 32 | /// This [EntitySystem] will only be processed when calling [World.process()] 33 | /// with the same [group]. 34 | final int group; 35 | 36 | /// Creates an [EntitySystem] with [aspect]. 37 | /// 38 | /// If [passive] is set to [`true`] the system will not be processed as long 39 | /// as it stays passive. 40 | /// 41 | /// If [group] is set, [World.process] needs to be called with this group 42 | /// to be processed. For example the group can be used to handle systems 43 | /// for physics and rendering separately and with different deltas. 44 | EntitySystem(Aspect aspect, {this.passive = false, this.group = 0}) 45 | : _aspect = aspect; 46 | 47 | /// Returns the [World] this [EntitySystem] belongs to. 48 | World get world => _world; 49 | 50 | /// Returns how often the systems in this [group] have been processed. 51 | int get frame => _frame; 52 | 53 | /// Returns the time that has elapsed for the systems in this [group] since 54 | /// the game has started (sum of all deltas). 55 | double get time => _time; 56 | 57 | /// Returns the delta that has elapsed since the last update of the world. 58 | double get delta => _delta; 59 | 60 | /// Called before processing of entities begins. 61 | @visibleForOverriding 62 | void begin() {} 63 | 64 | /// This is the only method that is supposed to be called from outside the 65 | /// library, 66 | @visibleForOverriding 67 | void process() { 68 | _frame = world._frame[group]!; 69 | _time = world._time[group]!; 70 | _delta = world.delta; 71 | if (checkProcessing()) { 72 | begin(); 73 | processEntities(_actives); 74 | end(); 75 | } 76 | } 77 | 78 | /// Called after the processing of entities ends. 79 | @visibleForOverriding 80 | void end() {} 81 | 82 | /// Any implementing entity system must implement this method and the logic 83 | /// to process the given [entities] of the system. 84 | @visibleForOverriding 85 | void processEntities(Iterable entities); 86 | 87 | /// Returns true if the system should be processed, false if not. 88 | @visibleForOverriding 89 | bool checkProcessing() => true; 90 | 91 | /// Override to implement code that gets executed when systems are 92 | /// initialized. 93 | @mustCallSuper 94 | @visibleForOverriding 95 | void initialize(World world) { 96 | _world = world; 97 | 98 | _updateBitMask(_all, _aspect.all); 99 | _updateBitMask(_one, _aspect.one); 100 | _updateBitMask(_excluded, _aspect.excluded); 101 | 102 | _componentIndicesAll.addAll(_all.toIntValues()); 103 | _componentIndicesOne.addAll(_one.toIntValues()); 104 | _componentIndicesExcluded.addAll(_excluded.toIntValues()); 105 | _interestingComponentsIndices.addAll( 106 | _componentIndicesAll 107 | .followedBy(_componentIndicesOne) 108 | .followedBy(_componentIndicesExcluded) 109 | .toList(), 110 | ); 111 | } 112 | 113 | void _updateBitMask(BitSet mask, Iterable componentTypes) { 114 | final componentManager = world.getManager(); 115 | for (final componentType in componentTypes) { 116 | mask[componentManager.getBitIndex(componentType)] = true; 117 | } 118 | } 119 | 120 | /// Gets called if the world gets destroyed. Override if there is cleanup to 121 | /// do. 122 | @visibleForOverriding 123 | void destroy() {} 124 | 125 | /// Add a [component] to an [entity]. 126 | void addComponent(Entity entity, T component) => 127 | world.addComponent(entity, component); 128 | 129 | /// Remove the component with type [T] from an [entity]. 130 | void removeComponent(Entity entity) => 131 | world.removeComponent(entity); 132 | 133 | /// Delete [entity] from the world. 134 | void deleteFromWorld(Entity entity) => world.deleteEntity(entity); 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dartemis 2 | ======== 3 | [![Build Status](https://github.com/denniskaselow/dartemis/actions/workflows/dart.yml/badge.svg)](https://github.com/denniskaselow/dartemis/actions/workflows/dart.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/denniskaselow/dartemis/badge.svg?branch=master)](https://coveralls.io/github/denniskaselow/dartemis?branch=master) 5 | [![Pub](https://img.shields.io/pub/v/dartemis.svg)](https://pub.dartlang.org/packages/dartemis) 6 | 7 | Content 8 | ======= 9 | * [About](#about) 10 | * [Getting Started](#getting-started) 11 | * [Documentation](#documentation) 12 | * [Example Games](#example-games-using-dartemis) 13 | 14 | About 15 | ===== 16 | **dartemis** is a Dart port of the Entity System Framework **Artemis**. 17 | 18 | The original has been written in Java by Arni Arent and Tiago Costa and can be found here: 19 | [https://gamadu.com/artemis/ (archived)](https://archive.is/1xRWW) with the source available here: 20 | https://code.google.com/p/artemis-framework/ 21 | 22 | Ports for other languages are also available: 23 | 24 | * C#: https://github.com/thelinuxlich/artemis_CSharp 25 | * Python: https://github.com/kernhanda/PyArtemis 26 | 27 | Some useful links about what an Entity System/Entity Component System is: 28 | 29 | * [https://piemaster.net/2011/07/entity-component-artemis/ (archived)](https://archive.ph/yGyxW) 30 | * http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/ 31 | * https://www.richardlord.net/blog/what-is-an-entity-framework 32 | 33 | Getting started 34 | =============== 35 | 1. Add dartemis to your project by adding it to your **pubspec.yaml**: 36 | 37 | ```yaml 38 | dependencies: 39 | dartemis: any 40 | ``` 41 | 42 | 2. Import it in your project: 43 | 44 | ```dart 45 | import 'package:dartemis/dartemis.dart'; 46 | ``` 47 | 3. Create a world: 48 | 49 | ```dart 50 | final world = World(); 51 | ``` 52 | 4. Create an entity from a list of components. Entities with different components will be processed by different systems: 53 | 54 | ```dart 55 | world.createEntity([ 56 | Position(0, 0), 57 | Velocity(1, 1), 58 | ]); 59 | ``` 60 | A `Component` is a pretty simple structure and should not contain any logic: 61 | 62 | ```dart 63 | class Position extends Component { 64 | num x, y; 65 | Position(this.x, this.y); 66 | } 67 | ``` 68 | Or if you want to use a `PooledComponent`: 69 | 70 | ```dart 71 | class Position extends PooledComponent { 72 | late num x, y; 73 | 74 | Position._(); 75 | factory Position(num x, num y) { 76 | final position = Pooled.of(() => Position._()) 77 | ..x = x 78 | ..y = y; 79 | return position; 80 | } 81 | } 82 | ``` 83 | By using a factory constructor and calling the static function `Pooled.of`, dartemis is able to reuse destroyed components and they will not be garbage collected. 84 | 85 | 5. Define a systems that should process your entities. The `Aspect` defines which components an entity needs to have in order to be processed by the system: 86 | 87 | ```dart 88 | class MovementSystem extends EntityProcessingSystem { 89 | late Mapper positionMapper; 90 | late Mapper velocityMapper; 91 | 92 | MovementSystem() : super(Aspect.forAllOf([Position, Velocity])); 93 | 94 | void initialize() { 95 | // initialize your system 96 | // Mappers, Systems and Managers have to be assigned here 97 | // see dartemis_builder if you don't want to write this code 98 | positionMapper = Mapper(world); 99 | velocityMapper = Mapper(world); 100 | } 101 | 102 | void processEntity(Entity entity) { 103 | Position position = positionMapper[entity]; 104 | Velocity vel = velocityMapper[entity]; 105 | position 106 | ..x += vel.x * world.delta 107 | ..y += vel.y * world.delta; 108 | } 109 | } 110 | ``` 111 | Or using [dartemis_builder](https://pub.dev/packages/dartemis_builder) 112 | 113 | ```dart 114 | part 'filename.g.part'; 115 | 116 | @Generate( 117 | EntityProcessingSystem, 118 | allOf: [ 119 | Position, 120 | Velocity, 121 | ], 122 | ) 123 | class SimpleMovementSystem extends _$SimpleMovementSystem { 124 | @override 125 | void processEntity(Entity entity, Position position, Velocity velocity) { 126 | position 127 | ..x += velocity.x * world.delta 128 | ..y += velocity.y * world.delta; 129 | } 130 | } 131 | ``` 132 | 6. Add your system to the world: 133 | 134 | ```dart 135 | world.addSystem(MovementSystem()); 136 | ``` 137 | 7. Initialize the world: 138 | 139 | ```dart 140 | world.initialize(); 141 | ``` 142 | 8. Usually your logic requires a delta, so you need to set it in your game loop: 143 | 144 | ```dart 145 | world.delta = delta; 146 | ``` 147 | 9. In your game loop you then process your systems: 148 | 149 | ```dart 150 | world.process(); 151 | ``` 152 | 153 | Documentation 154 | ============= 155 | API 156 | --- 157 | [Reference Manual](https://pub.dartlang.org/documentation/dartemis/latest/index.html) 158 | 159 | Example Games using dartemis 160 | ============================ 161 | * [darteroids](https://denniskaselow.github.io/dartemis/example/darteroids/web/darteroids.html) - Very simple example included in the example folder of dartemis, ([Source](https://github.com/denniskaselow/dartemis/tree/master/example/web)) 162 | * [Shapeocalypse](https://isowosi.itch.io/shapeocalypse/) - A fast paced reaction game using 163 | Angular, WebAudio and WebGL 164 | * [damacreat](https://isowosi.itch.io/damacreat) - An iogame similar to agar.io about creatures 165 | made of dark matter (circles) consuming dark energy (circles) and other dark matter creatures (circles), which can shoot black holes (circles) 166 | -------------------------------------------------------------------------------- /example/darteroids/gamelogic_systems.dart: -------------------------------------------------------------------------------- 1 | part of '../main.dart'; 2 | 3 | class MovementSystem extends EntityProcessingSystem { 4 | late final Mapper positionMapper; 5 | late final Mapper velocityMapper; 6 | 7 | MovementSystem() : super(Aspect(allOf: [Position, Velocity])); 8 | 9 | @override 10 | void initialize(World world) { 11 | super.initialize(world); 12 | positionMapper = Mapper(world); 13 | velocityMapper = Mapper(world); 14 | } 15 | 16 | @override 17 | void processEntity(Entity entity) { 18 | final pos = positionMapper[entity]; 19 | final vel = velocityMapper[entity]; 20 | 21 | pos 22 | ..x += vel.x * world.delta / 10.0 23 | ..y += vel.y * world.delta / 10.0; 24 | } 25 | } 26 | 27 | class BulletSpawningSystem extends EntityProcessingSystem { 28 | static const num bulletSpeed = 2.5; 29 | 30 | late final Mapper positionMapper; 31 | late final Mapper cannonMapper; 32 | late final Mapper velocityMapper; 33 | 34 | BulletSpawningSystem() : super(Aspect(allOf: [Cannon, Position, Velocity])); 35 | 36 | @override 37 | void initialize(World world) { 38 | super.initialize(world); 39 | positionMapper = Mapper(world); 40 | cannonMapper = Mapper(world); 41 | velocityMapper = Mapper(world); 42 | } 43 | 44 | @override 45 | void processEntity(Entity entity) { 46 | final cannon = cannonMapper[entity]; 47 | 48 | if (cannon.canShoot) { 49 | final pos = positionMapper[entity]; 50 | final vel = velocityMapper[entity]; 51 | fireBullet(pos, vel, cannon); 52 | } else if (cannon.cooldown > 0) { 53 | cannon.cooldown -= world.delta; 54 | } 55 | } 56 | 57 | void fireBullet(Position shooterPos, Velocity shooterVel, Cannon cannon) { 58 | cannon.cooldown = 1000; 59 | final dirX = cannon.targetX - shooterPos.x; 60 | final dirY = cannon.targetY - shooterPos.y; 61 | final distance = sqrt(pow(dirX, 2) + pow(dirY, 2)); 62 | final velX = shooterVel.x + bulletSpeed * (dirX / distance); 63 | final velY = shooterVel.y + bulletSpeed * (dirY / distance); 64 | 65 | world.createEntity([ 66 | Position(shooterPos.x, shooterPos.y), 67 | Velocity(velX, velY), 68 | CircularBody(2, 'red'), 69 | Decay(5000), 70 | AsteroidDestroyer(), 71 | ]); 72 | } 73 | } 74 | 75 | class DecaySystem extends EntityProcessingSystem { 76 | late final Mapper decayMapper; 77 | 78 | DecaySystem() : super(Aspect(allOf: [Decay])); 79 | 80 | @override 81 | void initialize(World world) { 82 | super.initialize(world); 83 | decayMapper = Mapper(world); 84 | } 85 | 86 | @override 87 | void processEntity(Entity entity) { 88 | final decay = decayMapper[entity]; 89 | 90 | if (decay.timer < 0) { 91 | world.deleteEntity(entity); 92 | } else { 93 | decay.timer -= world.delta; 94 | } 95 | } 96 | } 97 | 98 | class AsteroidDestructionSystem extends EntityProcessingSystem { 99 | static final num sqrtOf2 = sqrt(2); 100 | late final GroupManager groupManager; 101 | late final Mapper positionMapper; 102 | late final Mapper bodyMapper; 103 | 104 | AsteroidDestructionSystem() 105 | : super(Aspect(allOf: [AsteroidDestroyer, Position])); 106 | 107 | @override 108 | void initialize(World world) { 109 | super.initialize(world); 110 | positionMapper = Mapper(world); 111 | bodyMapper = Mapper(world); 112 | groupManager = world.getManager(); 113 | } 114 | 115 | @override 116 | void processEntity(Entity entity) { 117 | final destroyerPos = positionMapper[entity]; 118 | 119 | for (final asteroid in groupManager.getEntities(groupAsteroids)) { 120 | final asteroidPos = positionMapper[asteroid]; 121 | final asteroidBody = bodyMapper[asteroid]; 122 | 123 | if (doCirclesCollide( 124 | destroyerPos.x, 125 | destroyerPos.y, 126 | 0, 127 | asteroidPos.x, 128 | asteroidPos.y, 129 | asteroidBody.radius, 130 | )) { 131 | deleteFromWorld(asteroid); 132 | deleteFromWorld(entity); 133 | if (asteroidBody.radius > 10) { 134 | createNewAsteroids(asteroidPos, asteroidBody); 135 | createNewAsteroids(asteroidPos, asteroidBody); 136 | } 137 | } 138 | } 139 | } 140 | 141 | void createNewAsteroids(Position asteroidPos, CircularBody asteroidBody) { 142 | final vx = generateRandomVelocity(); 143 | final vy = generateRandomVelocity(); 144 | final radius = asteroidBody.radius / sqrtOf2; 145 | 146 | final asteroid = world.createEntity([ 147 | Position(asteroidPos.x, asteroidPos.y), 148 | Velocity(vx, vy), 149 | CircularBody(radius, asteroidColor), 150 | PlayerDestroyer(), 151 | ]); 152 | 153 | groupManager.add(asteroid, groupAsteroids); 154 | } 155 | } 156 | 157 | class PlayerCollisionDetectionSystem extends EntitySystem { 158 | late final TagManager tagManager; 159 | late final Mapper statusMapper; 160 | late final Mapper positionMapper; 161 | late final Mapper bodyMapper; 162 | 163 | PlayerCollisionDetectionSystem() 164 | : super(Aspect(allOf: [PlayerDestroyer, Position, CircularBody])); 165 | 166 | @override 167 | void initialize(World world) { 168 | super.initialize(world); 169 | positionMapper = Mapper(world); 170 | statusMapper = Mapper(world); 171 | bodyMapper = Mapper(world); 172 | tagManager = world.getManager(); 173 | } 174 | 175 | @override 176 | void processEntities(Iterable entities) { 177 | final player = tagManager.getEntity(tagPlayer)!; 178 | final playerPos = positionMapper[player]; 179 | final playerStatus = statusMapper[player]; 180 | final playerBody = bodyMapper[player]; 181 | 182 | if (!playerStatus.invisible) { 183 | for (final entity in entities) { 184 | final pos = positionMapper[entity]; 185 | final body = bodyMapper[entity]; 186 | 187 | if (doCirclesCollide( 188 | pos.x, 189 | pos.y, 190 | body.radius, 191 | playerPos.x, 192 | playerPos.y, 193 | playerBody.radius, 194 | )) { 195 | playerStatus.lifes--; 196 | playerStatus.invisiblityTimer = 5000; 197 | playerPos 198 | ..x = maxWidth ~/ 2 199 | ..y = maxHeight ~/ 2; 200 | return; 201 | } 202 | } 203 | } else { 204 | playerStatus.invisiblityTimer -= world.delta; 205 | } 206 | } 207 | 208 | @override 209 | bool checkProcessing() => true; 210 | } 211 | -------------------------------------------------------------------------------- /lib/src/core/utils/bit_set.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | /// [BitSet] to store bits. 4 | class BitSet { 5 | Uint32List _data; 6 | int _length; 7 | 8 | /// Creates a [BitSet] with maximum [length] items. 9 | /// 10 | /// [length] will be rounded up to match the 32-bit boundary. 11 | factory BitSet(int length) => BitSet._(Uint32List(_bufferLength32(length))); 12 | 13 | /// Creates a [BitSet] using an existing [BitSet]. 14 | factory BitSet.fromBitSet(BitSet set, {int? length}) { 15 | length ??= set.length; 16 | final data = Uint32List(_bufferLength32(length)) 17 | ..setRange(0, set._data.length, set._data); 18 | return BitSet._(data); 19 | } 20 | 21 | BitSet._(this._data) : _length = _data.length << 5; 22 | 23 | /// The value of the bit with the specified [index]. 24 | bool operator [](int index) => 25 | (_data[index >> 5] & _bitMask[index & 0x1f]) != 0; 26 | 27 | /// Sets the bit specified by the [index] to the [value]. 28 | void operator []=(int index, bool value) { 29 | if (value) { 30 | _data[index >> 5] |= _bitMask[index & 0x1f]; 31 | } else { 32 | _data[index >> 5] &= _clearMask[index & 0x1f]; 33 | } 34 | } 35 | 36 | /// The number of bit in this [BitSet]. 37 | /// 38 | /// [length] will be rounded up to match the 32-bit boundary. 39 | /// 40 | /// The valid index values for the array are `0` through `length - 1`. 41 | int get length => _length; 42 | 43 | /// The number of bits set to true. 44 | int get cardinality => _data.buffer 45 | .asUint8List() 46 | .fold(0, (sum, value) => sum + _cardinalityBitCounts[value]); 47 | 48 | /// Whether the [BitSet] is empty == has only zero values. 49 | bool get isEmpty => _data.every((i) => i == 0); 50 | 51 | /// Whether the [BitSet] is not empty == has set values. 52 | bool get isNotEmpty => _data.any((i) => i != 0); 53 | 54 | /// Sets all of the bits in the current [BitSet] to true. 55 | void setAll() { 56 | for (var i = 0; i < _data.length; i++) { 57 | _data[i] = 0xffffffff; 58 | } 59 | } 60 | 61 | /// Sets all of the bits in the current [BitSet] to false. 62 | void clearAll() { 63 | for (var i = 0; i < _data.length; i++) { 64 | _data[i] = 0; 65 | } 66 | } 67 | 68 | /// Update the current [BitSet] using a logical AND operation with the 69 | /// corresponding elements in the specified [other]. 70 | void and(BitSet other) { 71 | var i = 0; 72 | for (; i < _data.length && i < other._data.length; i++) { 73 | _data[i] &= other._data[i]; 74 | } 75 | for (; i < _data.length; i++) { 76 | _data[i] = 0; 77 | } 78 | } 79 | 80 | /// Update the current [BitSet] using a logical OR operation with the 81 | /// corresponding elements in the specified [other]. 82 | void or(BitSet other) { 83 | if (other._data.length > _data.length) { 84 | _data = Uint32List(other.length)..setRange(0, _data.length, _data); 85 | _length = other.length; 86 | } 87 | var i = 0; 88 | for (; i < _data.length && i < other._data.length; i++) { 89 | _data[i] |= other._data[i]; 90 | } 91 | for (; i < other._data.length; i++) { 92 | _data[i] = other._data[i]; 93 | } 94 | } 95 | 96 | /// Update the current [BitSet] using a logical AND NOT operation with the 97 | /// corresponding elements in the specified [other]. 98 | void andNot(BitSet other) { 99 | var i = 0; 100 | for (; i < _data.length && i < other._data.length; i++) { 101 | // ignore: unnecessary_parenthesis 102 | _data[i] &= ~(other._data[i]); 103 | } 104 | } 105 | 106 | /// Creates a copy of the current [BitSet]. 107 | BitSet _clone() => 108 | BitSet._(Uint32List(_data.length)..setRange(0, _data.length, _data)); 109 | 110 | /// Creates a [BitSet] using a logical AND operation with the 111 | /// corresponding elements in the specified [other]. 112 | /// Length of [other] has to be the same. 113 | BitSet operator &(BitSet other) => _clone()..and(other); 114 | 115 | /// Not implemented 116 | BitSet operator %(BitSet set) => 117 | throw UnimplementedError('andNot not implemented'); 118 | 119 | /// Creates a [BitSet] using a logical OR operation with the 120 | /// corresponding elements in the specified [other]. 121 | /// Length of [other] has to be the same. 122 | BitSet operator |(BitSet other) => _clone()..or(other); 123 | 124 | /// Not implemented 125 | BitSet operator ^(BitSet other) => 126 | throw UnimplementedError('xor not implemented'); 127 | 128 | @override 129 | String toString() { 130 | final sb = StringBuffer(); 131 | for (var i = 0; i < length; i++) { 132 | sb.write(this[i] ? '1' : '0'); 133 | } 134 | return sb.toString(); 135 | } 136 | 137 | @override 138 | // ignore: type_annotate_public_apis 139 | bool operator ==(other) { 140 | if (identical(this, other)) { 141 | return true; 142 | } 143 | if (other is BitSet && runtimeType == other.runtimeType) { 144 | return equals(other); 145 | } 146 | return false; 147 | } 148 | 149 | /// Compares two bitsets. 150 | bool equals(BitSet other) { 151 | if (length == other.length) { 152 | for (var i = 0; i < _data.length; i++) { 153 | if (_data[i] != other._data[i]) { 154 | return false; 155 | } 156 | } 157 | return true; 158 | } 159 | return false; 160 | } 161 | 162 | @override 163 | int get hashCode => _data.hashCode ^ _length.hashCode; 164 | 165 | static int _bufferLength32(int length) => 1 + (length - 1) ~/ 32; 166 | 167 | /// Returns the set indices. 168 | List toIntValues() { 169 | final result = []; 170 | var index = 0; 171 | for (var value in _data) { 172 | for (var i = 0; i < 4; i++) { 173 | result.addAll( 174 | _indices[value & 0xff].map((internalValue) => internalValue + index), 175 | ); 176 | index += 8; 177 | value = value >> 8; 178 | } 179 | } 180 | return result; 181 | } 182 | } 183 | 184 | final _bitMask = List.generate(32, (i) => 1 << i); 185 | final _clearMask = List.generate(32, (i) => ~(1 << i)); 186 | final _cardinalityBitCounts = List.generate(256, _cardinalityOfByte); 187 | int _cardinalityOfByte(int index) { 188 | var result = 0; 189 | var value = index; 190 | while (value > 0) { 191 | if (value & 0x01 != 0) { 192 | result++; 193 | } 194 | value = value >> 1; 195 | } 196 | return result; 197 | } 198 | 199 | final _indices = List>.generate(256, _indicesOfByte); 200 | List _indicesOfByte(int index) { 201 | final result = []; 202 | var value = index; 203 | var count = 0; 204 | while (value > 0) { 205 | if (value & 0x01 != 0) { 206 | result.add(count); 207 | } 208 | count++; 209 | value = value >> 1; 210 | } 211 | return result; 212 | } 213 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | errors: 3 | unused_element: error 4 | unused_import: error 5 | unused_local_variable: error 6 | dead_code: error 7 | language: 8 | strict-casts: true 9 | strict-inference: true 10 | strict-raw-types: true 11 | linter: 12 | rules: 13 | # http://dart-lang.github.io/linter/lints/options/options.html 14 | - always_declare_return_types 15 | - always_put_control_body_on_new_line 16 | - always_put_required_named_parameters_first 17 | # - always_specify_types 18 | # - always_use_package_imports 19 | - annotate_overrides 20 | - avoid_annotating_with_dynamic 21 | - avoid_bool_literals_in_conditional_expressions 22 | - avoid_catches_without_on_clauses 23 | - avoid_catching_errors 24 | # - avoid_classes_with_only_static_members 25 | - avoid_double_and_int_checks 26 | - avoid_dynamic_calls 27 | - avoid_empty_else 28 | # - avoid_equals_and_hash_code_on_mutable_classes 29 | - avoid_escaping_inner_quotes 30 | - avoid_field_initializers_in_const_classes 31 | # - avoid_final_parameters 32 | - avoid_function_literals_in_foreach_calls 33 | - avoid_implementing_value_types 34 | - avoid_init_to_null 35 | - avoid_js_rounded_ints 36 | - avoid_multiple_declarations_per_line 37 | - avoid_null_checks_in_equality_operators 38 | - avoid_positional_boolean_parameters 39 | - avoid_print 40 | - avoid_private_typedef_functions 41 | - avoid_redundant_argument_values 42 | - avoid_relative_lib_imports 43 | - avoid_renaming_method_parameters 44 | - avoid_return_types_on_setters 45 | - avoid_returning_null_for_void 46 | - avoid_returning_this 47 | - avoid_setters_without_getters 48 | - avoid_shadowing_type_parameters 49 | - avoid_single_cascade_in_expression_statements 50 | - avoid_slow_async_io 51 | - avoid_type_to_string 52 | - avoid_types_as_parameter_names 53 | - avoid_types_on_closure_parameters 54 | - avoid_unnecessary_containers 55 | - avoid_unused_constructor_parameters 56 | - avoid_void_async 57 | - avoid_web_libraries_in_flutter 58 | - await_only_futures 59 | - camel_case_extensions 60 | - camel_case_types 61 | - cancel_subscriptions 62 | - cascade_invocations 63 | - cast_nullable_to_non_nullable 64 | - close_sinks 65 | - collection_methods_unrelated_type 66 | - combinators_ordering 67 | - comment_references 68 | - conditional_uri_does_not_exist 69 | - constant_identifier_names 70 | - control_flow_in_finally 71 | - curly_braces_in_flow_control_structures 72 | - dangling_library_doc_comments 73 | - depend_on_referenced_packages 74 | - deprecated_consistency 75 | - deprecated_member_use_from_same_package 76 | - diagnostic_describe_all_properties 77 | - directives_ordering 78 | - discarded_futures 79 | - do_not_use_environment 80 | - empty_catches 81 | - empty_constructor_bodies 82 | - empty_statements 83 | - eol_at_end_of_file 84 | - exhaustive_cases 85 | - file_names 86 | - flutter_style_todos 87 | - hash_and_equals 88 | - implementation_imports 89 | - implicit_call_tearoffs 90 | - implicit_reopen 91 | - invalid_case_patterns 92 | - join_return_with_assignment 93 | - leading_newlines_in_multiline_strings 94 | - library_annotations 95 | - library_names 96 | - library_prefixes 97 | - library_private_types_in_public_api 98 | - lines_longer_than_80_chars 99 | - literal_only_boolean_expressions 100 | - matching_super_parameters 101 | - missing_whitespace_between_adjacent_strings 102 | - no_adjacent_strings_in_list 103 | - no_default_cases 104 | - no_duplicate_case_values 105 | - no_leading_underscores_for_library_prefixes 106 | - no_leading_underscores_for_local_identifiers 107 | - no_literal_bool_comparisons 108 | - no_logic_in_create_state 109 | - no_runtimeType_toString 110 | - no_self_assignments 111 | - non_constant_identifier_names 112 | - noop_primitive_operations 113 | - null_check_on_nullable_type_parameter 114 | - null_closures 115 | - omit_local_variable_types 116 | - one_member_abstracts 117 | - only_throw_errors 118 | - overridden_fields 119 | - package_names 120 | - package_prefixed_library_names 121 | - parameter_assignments 122 | - prefer_adjacent_string_concatenation 123 | - prefer_asserts_in_initializer_lists 124 | - prefer_asserts_with_message 125 | - prefer_collection_literals 126 | - prefer_conditional_assignment 127 | - prefer_const_constructors 128 | - prefer_const_constructors_in_immutables 129 | - prefer_const_declarations 130 | - prefer_const_literals_to_create_immutables 131 | - prefer_constructors_over_static_methods 132 | - prefer_contains 133 | # - prefer_double_quotes 134 | - prefer_expression_function_bodies 135 | - prefer_final_fields 136 | - prefer_final_in_for_each 137 | - prefer_final_locals 138 | # - prefer_final_parameters 139 | - prefer_for_elements_to_map_fromIterable 140 | - prefer_foreach 141 | - prefer_function_declarations_over_variables 142 | - prefer_generic_function_type_aliases 143 | - prefer_if_elements_to_conditional_expressions 144 | - prefer_if_null_operators 145 | - prefer_initializing_formals 146 | - prefer_inlined_adds 147 | - prefer_int_literals 148 | - prefer_interpolation_to_compose_strings 149 | - prefer_is_empty 150 | - prefer_is_not_empty 151 | - prefer_is_not_operator 152 | - prefer_iterable_whereType 153 | - prefer_mixin 154 | - prefer_null_aware_method_calls 155 | - prefer_null_aware_operators 156 | - prefer_relative_imports 157 | - prefer_single_quotes 158 | - prefer_spread_collections 159 | - prefer_typing_uninitialized_variables 160 | - prefer_void_to_null 161 | - provide_deprecation_message 162 | - public_member_api_docs 163 | - recursive_getters 164 | - require_trailing_commas 165 | - secure_pubspec_urls 166 | - sized_box_for_whitespace 167 | - sized_box_shrink_expand 168 | - slash_for_doc_comments 169 | - sort_child_properties_last 170 | # - sort_constructors_first 171 | - sort_pub_dependencies 172 | - sort_unnamed_constructors_first 173 | - test_types_in_equals 174 | - throw_in_finally 175 | - tighten_type_of_initializing_formals 176 | - type_annotate_public_apis 177 | - type_init_formals 178 | - type_literal_in_constant_pattern 179 | - unawaited_futures 180 | - unnecessary_await_in_return 181 | - unnecessary_brace_in_string_interps 182 | - unnecessary_breaks 183 | - unnecessary_const 184 | - unnecessary_constructor_name 185 | # - unnecessary_final 186 | - unnecessary_getters_setters 187 | - unnecessary_lambdas 188 | - unnecessary_late 189 | - unnecessary_library_directive 190 | - unnecessary_new 191 | - unnecessary_null_aware_assignments 192 | - unnecessary_null_aware_operator_on_extension_on_nullable 193 | - unnecessary_null_checks 194 | - unnecessary_null_in_if_null_operators 195 | - unnecessary_nullable_for_final_variable_declarations 196 | - unnecessary_overrides 197 | - unnecessary_parenthesis 198 | - unnecessary_raw_strings 199 | - unnecessary_statements 200 | - unnecessary_string_escapes 201 | - unnecessary_string_interpolations 202 | - unnecessary_this 203 | - unnecessary_to_list_in_spreads 204 | - unreachable_from_main 205 | - unrelated_type_equality_checks 206 | - use_build_context_synchronously 207 | - use_colored_box 208 | - use_decorated_box 209 | - use_enums 210 | - use_full_hex_values_for_flutter_colors 211 | - use_function_type_syntax_for_parameters 212 | - use_if_null_to_convert_nulls_to_bools 213 | - use_is_even_rather_than_modulo 214 | - use_key_in_widget_constructors 215 | - use_late_for_private_fields_and_variables 216 | - use_named_constants 217 | - use_raw_strings 218 | - use_rethrow_when_possible 219 | - use_setters_to_change_properties 220 | - use_string_buffers 221 | - use_string_in_part_of_directives 222 | - use_super_parameters 223 | - use_test_throws_matchers 224 | - use_to_and_as_if_applicable 225 | - valid_regexps 226 | - void_checks 227 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 0.10.0 3 | 4 | ### Breaking API Changes 5 | * entities are no longer simple `int`s and have been turned into an `extension type Entity(int)`. 6 | Methods that previously expected `int entity` or `Iterable entities>` 7 | now expect `Entity entity` or `Iterable entities` 8 | * removed named parameters `group` and `passive` from `World.addSystem`, 9 | they are now named parameters of the constructor of `EntitySystem` 10 | * the `initialize`-method of `Manager`s and `EntitySystem`s now has a 11 | parameter for the `World` and must be called when overriding `initialize` 12 | * it's no longer possible to add systems or managers after the world has been initialized 13 | * `ComponentType` has been turned into an extension type, static methods of this class are now instance methods on 14 | `ComponentManager` 15 | * combined the different `Aspect`-constructors into a single one with optional named parameters for `allOf`, `oneOf` 16 | and `exclude` 17 | 18 | ### Enhancements 19 | * it's now possible to have multiple worlds (e.g. multiple games in the same webpage/app) 20 | * sharing instances of components/entities/managers/systems between worlds is NOT possible and things will break 21 | * added `@visibleForOverriding`-annotations to several methods that are only supposed to be called by dartemis 22 | 23 | ## 0.9.9 24 | ### Enhancements 25 | * new method `getTag` in `TagManager` to get the tag of an entity 26 | 27 | ## 0.9.8+1 28 | ### Bugfix 29 | * fix crash when moving components 30 | 31 | ## 0.9.8 32 | ### Enhancements 33 | * new method in `World` to move a component from one entity to another 34 | 35 | ## 0.9.7 36 | ### Enhancements 37 | * `EntitySystem.checkProcessing()` is no longer abstract, returns `true` 38 | * delta can be directly accessed in systems (**BREAKING CHANGE** if variable delta already exists in extending system) 39 | 40 | ### Internal 41 | * use SDK 2.17 (super parameters, constructor tear-offs) 42 | 43 | ## 0.9.6 44 | ### Enhancements 45 | * performance improvement when adding/removing components 46 | 47 | ## 0.9.5+3 48 | ### Bugfix 49 | * `componentManager.getComponent` couldn't handle accessing components that don't exist for the 50 | specific entity 51 | 52 | ## 0.9.5+2 53 | ### Documentation 54 | * updated links and minor changes to the example code in README.md 55 | 56 | ## 0.9.5+1 57 | ### Bugfix 58 | * `componentManager.getComponent` couldn't handle accessing components if no high index entities 59 | with those components have been created 60 | 61 | ## 0.9.5 62 | ### Enhancements 63 | * allow direct access to a specific component without use of mappers via `world.componentManager.getComponent(int entity, ComponentType componentType)` 64 | 65 | ## 0.9.4 66 | ### Bugfix 67 | * don't update the active entities in a systems if nothing changed 68 | * don't allow multiple instances of the same system or manager 69 | 70 | ## 0.9.3 71 | ### Bugfix 72 | * process deleted entities after a system finishes in case the system interacts multiple times with the deleted entity 73 | 74 | ## 0.9.2 75 | ### Bugfix 76 | * handle more than 32 entities with the same components 77 | 78 | ## 0.9.1 79 | ### Bugfix 80 | * handle more than 32 components when adding systems for components that haven't been used yet 81 | 82 | ## 0.9.0 83 | ### Internal 84 | * updated dependencies to stable versions 85 | 86 | ## 0.9.0-nullsafety.0 87 | ### Breaking API Changes 88 | * removed `ComponentTypeManager` and moved methods to `ComponentType` 89 | * `getComponents*` methods in `ComponentManager` now return a `List` instead of a `Bag` 90 | 91 | ### Enhancements 92 | * switched to NNBD mode 93 | * added `OptionalMapper` with a nullable return type for the `[]` operator 94 | 95 | ## 0.8.0 (Dart 2.0+ required) 96 | ### Breaking API Changes 97 | * removed deprecated code 98 | * replaced `Entity` with `int`, methods previously on `Entity` need to be called on `World`, with the `int` value of the entity as the first parameter 99 | * removed `world.processEntityChanges`, it's now done by default after every system 100 | * `Aspect` no longer uses static methods, uses named constructors instead 101 | (migration: replace `Aspect.getAspectF` with `Aspect.f`) 102 | * methods in `Aspect` no longer return the aspect, use cascading operator to chain calls 103 | * improved type safety for `world.getManager` and `world.getSystem`, no longer takes a `Type` as parameter and uses 104 | generic methods instead (e.g. `world.getManager()` instead of `world.getManager(TagManager)`) 105 | * removed `Type` parameter in the constructor of `Mapper`, change code from `Mapper(Position, world)` to `Mapper(world)` 106 | 107 | ### Enhancements 108 | * `world.destroy()` for cleaning up `EntitySystem`s and `Manager`s 109 | 110 | ### Internal 111 | * existing entities are processed first, addition of new entities is processed last, makes more sense now that the 112 | processing is done after every system 113 | 114 | ## 0.7.3 115 | ### Bugfixes 116 | * adding an entity to a dirty EntityBag could lead to an inconsistency between the bitset and the list of entities 117 | 118 | ## 0.7.2 119 | ### Bugfixes 120 | * removing an entity multiple times caused it to be added to the entity pool multiple times 121 | 122 | ## 0.7.1 123 | ### Internal 124 | * upgraded dependencies 125 | 126 | ## 0.7.0 127 | ### Breaking API Changes 128 | * renamed `Poolable` to `Pooled` 129 | * renamed `ComponentPoolable` to `PooledComponent` 130 | * removed `FastMath` and `Utils`, unrelated to ECS 131 | * removed `removeAll` from `Bag` 132 | * `time` and `frame` getters have been moved from `World` to `EntitySystem`, `World` has methods instead 133 | ### API Changes 134 | * deprecated `ComponentMapper` use `Mapper` instead 135 | * deprecated `ComponentMapper#get(Entity)`, use `Mapper[Entity]` instead 136 | * properties have been added to the `World`, can be accessed using the `[]` operator 137 | * `System`s can be assigned to a group when adding them to the `World`, `Word.process()` can be called for a specific group 138 | ### Enhancements 139 | * performance improvement when removing entities 140 | ### Bugfixes 141 | * DelayedEntityProcessingSystem keeps running until all current entities have expired 142 | ### Internal 143 | * upgraded dependencies 144 | 145 | ## 0.6.0 146 | ### API Changes 147 | * `Bag` is `Iterable` 148 | * removed `ReadOnlyBag`, when upgrading to 0.6.0 replace every occurence of `ReadOnlyBag` with `Iterable` 149 | 150 | ## 0.5.2 151 | ### Enhancements 152 | * injection works for `Manager`s 153 | * `initialize()` in the `Manager` is no longer abstract (same as in `EntitySystem`) 154 | * `World.createEntity` got an optional paramter to create an `Entity` with components 155 | * new function `World.createAndAddEntity` which adds the `Entity` to the world after creation 156 | 157 | ### Bugfixes 158 | * added getter for the `World` in `Manager` 159 | * the uniqueId of an `Entity` was always 0, not very unique 160 | 161 | ## 0.5.1 162 | ### Internal 163 | * added version constraint for release of Dart 164 | 165 | ## 0.5.0 166 | ### Enhancements 167 | * more injection, less boilerplate (when using dartemis_mirrors) 168 | * Instances of `ComponentMapper` no longer need to be created in the `initialize`-method of a system, they will be injected 169 | * `Manager`s and `EntitySystem`s no longer need to be requested from the `World` in the `initialize`-method of a system, they will be injected 170 | 171 | ## 0.4.2 172 | ### Bugfixes 173 | * `EntityManager.isEnabled()` no longer fails if the bag of disabled entities is smaller than the id of the checked entity 174 | 175 | ### Enhancements 176 | * added getters for current `time` and `frame` to `World` 177 | 178 | ## 0.4.1 179 | ### Bugfixes 180 | * `World.deleteAllEntites()` did not work if there was already a deleted entity 181 | * writing to the `Bag` by index doesn't make it smaller anymore 182 | 183 | ## 0.4.0 184 | ### API Changes 185 | * swapped parameters of `Tagmanager.register` 186 | * replaced `ImmutableBag` with `ReadOnlyBag`, added getter for `ReadOnlyBag` to `Bag` 187 | * changed `FreeComponents` to `ObjectPool` 188 | * old `Component` has changed, there are two different kinds of components now: 189 | * instances of classes extending `ComponentPoolable` will be added to the `ObjectPool` when they are removed from an `Entity` (preventing garbage collection and allowing reuse) 190 | * instances of classes extending `Component` will not be added to the `ObjectPool` when they are removed from an `Entity` (allowing garbage collection) 191 | 192 | ### Enhancements 193 | * added function `deleteAllEntities` to `World` 194 | * `IntervalEntitySystem` has a getter for the `delta` since the systm was processed last 195 | * updated to work with Dart M4 196 | 197 | ### Bugfixes 198 | * `GroupManager.isInGroup` works if entity is in no group 199 | -------------------------------------------------------------------------------- /lib/src/core/component_manager.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// Manages als components of all entities. 4 | class ComponentManager extends Manager { 5 | final Bag<_ComponentInfo> _componentInfoByType; 6 | final _componentTypes = {}; 7 | final _systemIndices = {}; 8 | int _systemCount = 0; 9 | int _componentTypeCount = 0; 10 | 11 | ComponentManager._internal() : _componentInfoByType = Bag<_ComponentInfo>(); 12 | 13 | /// Register a system to know if it needs to be updated when an entity 14 | /// changed. 15 | @visibleForTesting 16 | void registerSystem(EntitySystem system) { 17 | final systemBitIndex = _systemIndices.putIfAbsent( 18 | system.runtimeType, 19 | () => _systemCount++, 20 | ); 21 | for (final index in system._interestingComponentsIndices) { 22 | _componentInfoByType._ensureCapacity(index); 23 | var componentInfo = _componentInfoByType[index]; 24 | if (componentInfo == null) { 25 | componentInfo = _ComponentInfo(); 26 | _componentInfoByType[index] = componentInfo; 27 | } 28 | 29 | componentInfo.addInterestedSystem(systemBitIndex); 30 | } 31 | } 32 | 33 | void _unregisterSystem(EntitySystem system) { 34 | final systemBitIndex = _systemIndices.remove(system.runtimeType)!; 35 | for (final index in system._interestingComponentsIndices) { 36 | _componentInfoByType[index]!.removeInterestedSystem(systemBitIndex); 37 | } 38 | } 39 | 40 | /// Removes all components from the [entity]. 41 | @visibleForTesting 42 | void removeComponentsOfEntity(Entity entity) { 43 | _forComponentsOfEntity(entity, (components) { 44 | components.remove(entity); 45 | }); 46 | } 47 | 48 | void _addComponent( 49 | Entity entity, 50 | T component, 51 | ) { 52 | // needs the runtimeType instead of T because this method gets called 53 | // in a loop over a list of Components, so T would be Component 54 | final type = getTypeFor(component.runtimeType); 55 | final index = type.bitIndex; 56 | _componentInfoByType._ensureCapacity(index); 57 | var componentInfo = _componentInfoByType[index]; 58 | if (componentInfo == null) { 59 | componentInfo = _ComponentInfo(); 60 | _componentInfoByType[index] = componentInfo; 61 | } 62 | componentInfo[entity] = component; 63 | } 64 | 65 | void _removeComponent(Entity entity) { 66 | final type = getTypeFor(T); 67 | final typeId = type.bitIndex; 68 | _componentInfoByType[typeId]!.remove(entity); 69 | } 70 | 71 | void _moveComponent(Entity entitySrc, Entity entityDst) { 72 | final type = getTypeFor(T); 73 | final typeId = type.bitIndex; 74 | _componentInfoByType[typeId]?.move(entitySrc, entityDst); 75 | } 76 | 77 | /// Returns all components of [ComponentType type] accessible by their entity 78 | /// id. 79 | List _getComponentsByType() { 80 | final type = getTypeFor(T); 81 | final index = type.bitIndex; 82 | _componentInfoByType._ensureCapacity(index); 83 | 84 | var components = _componentInfoByType[index]; 85 | if (components == null) { 86 | components = _ComponentInfo(); 87 | _componentInfoByType[index] = components; 88 | } else if (components.components is! List) { 89 | // when components get added to an entity as part of a list containing 90 | // multiple different components, the type is infered as Component 91 | // instead of the actual type of the component. So if _addComponent was 92 | // called first a Bag would have been created and this fixes 93 | // the type 94 | _componentInfoByType[index]!.components = 95 | components.components.cast(); 96 | components = _componentInfoByType[index]; 97 | } 98 | 99 | return components!.components.cast(); 100 | } 101 | 102 | /// Returns all components of [ComponentType type]. 103 | List getComponentsByType() => 104 | _getComponentsByType().whereType().toList(); 105 | 106 | /// Returns all components of [entity]. 107 | List getComponentsFor(Entity entity) { 108 | final result = []; 109 | _forComponentsOfEntity( 110 | entity, 111 | (components) => result.add(components[entity]), 112 | ); 113 | 114 | return result; 115 | } 116 | 117 | void _forComponentsOfEntity( 118 | Entity entity, 119 | void Function(_ComponentInfo components) f, 120 | ) { 121 | for (var index = 0; index < _componentTypeCount; index++) { 122 | final componentInfo = _componentInfoByType[index]; 123 | if (componentInfo != null && 124 | componentInfo.entities.length > entity._id && 125 | componentInfo.entities[entity._id]) { 126 | f(componentInfo); 127 | } 128 | } 129 | } 130 | 131 | /// Returns true if the list of entities of [system] need to be updated. 132 | bool isUpdateNeededForSystem(EntitySystem system) { 133 | final systemBitIndex = _systemIndices[system.runtimeType]!; 134 | for (final interestingComponent in system._interestingComponentsIndices) { 135 | if (_componentInfoByType[interestingComponent]! 136 | .systemRequiresUpdate(systemBitIndex)) { 137 | return true; 138 | } 139 | } 140 | return false; 141 | } 142 | 143 | /// Marks the [system] as updated for the necessary component types. 144 | void _systemUpdated(EntitySystem system) { 145 | final systemBitIndex = _systemIndices[system.runtimeType]!; 146 | for (final interestingComponent in system._interestingComponentsIndices) { 147 | _componentInfoByType[interestingComponent]!.systemUpdated(systemBitIndex); 148 | } 149 | } 150 | 151 | /// Returns every entity that is of interest for [system]. 152 | List _getEntitiesForSystem( 153 | EntitySystem system, 154 | int entitiesBitSetLength, 155 | ) { 156 | final baseAll = BitSet(entitiesBitSetLength)..setAll(); 157 | for (final interestingComponent in system._componentIndicesAll) { 158 | baseAll.and(_componentInfoByType[interestingComponent]!.entities); 159 | } 160 | final baseOne = BitSet(entitiesBitSetLength); 161 | if (system._componentIndicesOne.isEmpty) { 162 | baseOne.setAll(); 163 | } else { 164 | for (final interestingComponent in system._componentIndicesOne) { 165 | baseOne.or(_componentInfoByType[interestingComponent]!.entities); 166 | } 167 | } 168 | final baseExclude = BitSet(entitiesBitSetLength); 169 | for (final interestingComponent in system._componentIndicesExcluded) { 170 | baseExclude.or(_componentInfoByType[interestingComponent]!.entities); 171 | } 172 | baseAll 173 | ..and(baseOne) 174 | ..andNot(baseExclude); 175 | return baseAll.toIntValues().map(Entity._).toList(growable: false); 176 | } 177 | 178 | /// Returns the component of type [T] for the given [entity]. 179 | T? getComponent( 180 | Entity entity, 181 | ) { 182 | final componentType = getTypeFor(T); 183 | final index = componentType.bitIndex; 184 | final components = _componentInfoByType[index]; 185 | if (components != null && entity._id < components.components.length) { 186 | return components.components[entity._id] as T?; 187 | } 188 | return null; 189 | } 190 | 191 | /// Returns the [ComponentType] for the runtimeType of a [Component]. 192 | ComponentType getTypeFor(Type typeOfComponent) => _componentTypes.putIfAbsent( 193 | typeOfComponent, 194 | () => ComponentType(_componentTypeCount++), 195 | ); 196 | 197 | /// Returns the index of the bit of the [componentType]. 198 | int getBitIndex(Type componentType) => getTypeFor(componentType).bitIndex; 199 | } 200 | 201 | class _ComponentInfo { 202 | BitSet entities = BitSet(32); 203 | List components = List.filled(32, null, growable: true); 204 | BitSet interestedSystems = BitSet(32); 205 | BitSet requiresUpdate = BitSet(32); 206 | bool dirty = false; 207 | 208 | _ComponentInfo(); 209 | 210 | void operator []=(Entity entity, T component) { 211 | if (entity._id >= entities.length) { 212 | entities = BitSet.fromBitSet(entities, length: entity._id + 1); 213 | final newCapacity = (entities.length * 3) ~/ 2 + 1; 214 | final filler = List.filled(newCapacity - components.length, null); 215 | components.addAll(filler); 216 | } 217 | entities[entity._id] = true; 218 | components[entity._id] = component; 219 | dirty = true; 220 | } 221 | 222 | T operator [](Entity entity) => components[entity._id]!; 223 | 224 | void remove(Entity entity) { 225 | if (entities.length > entity._id && entities[entity._id]) { 226 | entities[entity._id] = false; 227 | components[entity._id]!._removed(); 228 | components[entity._id] = null; 229 | dirty = true; 230 | } 231 | } 232 | 233 | void move(Entity srcEntity, Entity dstEntity) { 234 | if (entities.length > srcEntity._id && entities[srcEntity._id]) { 235 | remove(dstEntity); 236 | this[dstEntity] = components[srcEntity._id]!; 237 | entities[srcEntity._id] = false; 238 | components[srcEntity._id] = null; 239 | dirty = true; 240 | } 241 | } 242 | 243 | void addInterestedSystem(int systemBitIndex) { 244 | if (systemBitIndex >= interestedSystems.length) { 245 | interestedSystems = 246 | BitSet.fromBitSet(interestedSystems, length: systemBitIndex + 1); 247 | requiresUpdate = 248 | BitSet.fromBitSet(requiresUpdate, length: systemBitIndex + 1); 249 | } 250 | interestedSystems[systemBitIndex] = true; 251 | requiresUpdate[systemBitIndex] = true; 252 | } 253 | 254 | void removeInterestedSystem(int systemBitIndex) { 255 | interestedSystems[systemBitIndex] = false; 256 | requiresUpdate[systemBitIndex] = false; 257 | } 258 | 259 | bool systemRequiresUpdate(int systemBitIndex) { 260 | if (dirty) { 261 | requiresUpdate.or(interestedSystems); 262 | dirty = false; 263 | } 264 | return requiresUpdate[systemBitIndex]; 265 | } 266 | 267 | void systemUpdated(int systemBitIndex) => 268 | requiresUpdate[systemBitIndex] = false; 269 | 270 | _ComponentInfo cast() => this as _ComponentInfo; 271 | } 272 | -------------------------------------------------------------------------------- /lib/src/core/world.dart: -------------------------------------------------------------------------------- 1 | part of '../../dartemis.dart'; 2 | 3 | /// The primary instance for the framework. It contains all the managers. 4 | /// 5 | /// You must use this to create, delete and retrieve entities. 6 | /// 7 | /// It is also important to set the delta each game loop iteration, and 8 | /// initialize before game loop. 9 | class World { 10 | final EntityManager _entityManager; 11 | final ComponentManager _componentManager; 12 | 13 | final Map _systems = {}; 14 | final List _systemsList = []; 15 | 16 | final Map _managers = {}; 17 | final Bag _managersBag = Bag(); 18 | 19 | // -1 for triggering deleteEntities when calling process() without processing 20 | // any systems, for testing purposes 21 | final Map _frame = {0: 0, -1: 0}; 22 | final Map _time = {0: 0.0, -1: 0.0}; 23 | bool _initialized = false; 24 | 25 | final Set _entitiesMarkedForDeletion = {}; 26 | 27 | /// The time that passed since the last time [process] was called. 28 | double delta = 0; 29 | 30 | /// World-related properties that can be written and read by the user. 31 | final Map properties = {}; 32 | 33 | /// Create the [World] with the default [EntityManager] and 34 | /// [ComponentManager]. 35 | World({EntityManager? entityManager, ComponentManager? componentManager}) 36 | : _entityManager = entityManager ?? EntityManager._internal(), 37 | _componentManager = componentManager ?? ComponentManager._internal() { 38 | addManager(_entityManager); 39 | addManager(_componentManager); 40 | } 41 | 42 | /// Returns the current frame/how often the systems in [group] have been processed. 43 | int frame([int group = 0]) => _frame[group]!; 44 | 45 | /// Returns the time that has elapsed for the systems in the [group] since 46 | /// the game has started (sum of all deltas). 47 | double time([int group = 0]) => _time[group]!; 48 | 49 | /// Makes sure all managers systems are initialized in the order they were 50 | /// added. 51 | void initialize() { 52 | _managersBag.forEach(_initializeManager); 53 | _systemsList 54 | ..forEach(_initializeSystem) 55 | ..forEach(componentManager.registerSystem); 56 | _initialized = true; 57 | } 58 | 59 | void _initializeManager(Manager manager) => manager.initialize(this); 60 | 61 | void _initializeSystem(EntitySystem system) => system.initialize(this); 62 | 63 | /// Returns a manager that takes care of all the entities in the world. 64 | /// entities of this world. 65 | EntityManager get entityManager => _entityManager; 66 | 67 | /// Returns a manager that takes care of all the components in the world. 68 | ComponentManager get componentManager => _componentManager; 69 | 70 | /// Add a manager into this world. It can be retrieved later. World will 71 | /// notify this manager of changes to entity. 72 | void addManager(Manager manager) { 73 | if (_managers.containsKey(manager.runtimeType)) { 74 | throw ArgumentError.value( 75 | manager, 76 | 'manager', 77 | 'A manager of type "${manager.runtimeType}" has already been added ' 78 | 'to the world.'); 79 | } 80 | if (_initialized) { 81 | throw StateError( 82 | 'The world has already been initialized. The manager needs to be ' 83 | 'added before calling initialize.'); 84 | } 85 | _managers[manager.runtimeType] = manager; 86 | _managersBag.add(manager); 87 | } 88 | 89 | /// Returns a [Manager] of the specified type [T]. 90 | T getManager() { 91 | final result = _managers[T]; 92 | assert( 93 | result != null, 94 | 'No manager of type "$T" has been added to the world.', 95 | ); 96 | return result! as T; 97 | } 98 | 99 | /// Deletes the manager from this world. 100 | void deleteManager(Manager manager) { 101 | _managers.remove(manager.runtimeType); 102 | _managersBag.remove(manager); 103 | } 104 | 105 | /// Create and return a new or reused [int] instance, optionally with 106 | /// [components]. 107 | Entity createEntity([List components = const []]) { 108 | final e = _entityManager._createEntityInstance(); 109 | for (final component in components) { 110 | addComponent(e, component); 111 | } 112 | addEntity(e); 113 | return e; 114 | } 115 | 116 | /// Adds a [component] to the [entity]. 117 | void addComponent(Entity entity, T component) => 118 | componentManager._addComponent( 119 | entity, 120 | component, 121 | ); 122 | 123 | /// Adds [components] to the [entity]. 124 | void addComponents(Entity entity, List components) { 125 | for (final component in components) { 126 | addComponent(entity, component); 127 | } 128 | } 129 | 130 | /// Removes a [Component] of type [T] from the [entity]. 131 | void removeComponent(Entity entity) => 132 | componentManager._removeComponent(entity); 133 | 134 | /// Moves a [Component] of type [T] from the [srcEntity] to the [dstEntity]. 135 | /// if the [srcEntity] does not have the [Component] of type [T] nothing will 136 | /// happen. 137 | void moveComponent(Entity srcEntity, Entity dstEntity) => 138 | componentManager._moveComponent( 139 | srcEntity, 140 | dstEntity, 141 | ); 142 | 143 | /// Gives you all the systems in this world for possible iteration. 144 | Iterable get systems => _systemsList; 145 | 146 | /// Adds a [system] to this world that will be processed by [process()]. 147 | void addSystem(EntitySystem system) { 148 | if (_systems.containsKey(system.runtimeType)) { 149 | throw ArgumentError.value( 150 | system, 151 | 'system', 152 | 'A system of type "${system.runtimeType}" has already been added to ' 153 | 'the world.'); 154 | } 155 | if (_initialized) { 156 | throw StateError( 157 | 'The world has already been initialized. The system needs to be ' 158 | 'added before calling initialize.'); 159 | } 160 | 161 | _systems[system.runtimeType] = system; 162 | _systemsList.add(system); 163 | _time.putIfAbsent(system.group, () => 0.0); 164 | _frame.putIfAbsent(system.group, () => 0); 165 | } 166 | 167 | /// Removed the specified system from the world. 168 | void deleteSystem(EntitySystem system) { 169 | _systems.remove(system.runtimeType); 170 | _systemsList.remove(system); 171 | componentManager._unregisterSystem(system); 172 | } 173 | 174 | /// Retrieve a system for specified system type. 175 | T getSystem() { 176 | final result = _systems[T]; 177 | assert( 178 | result != null, 179 | 'No system of type "$T" has been added to the world.', 180 | ); 181 | return result! as T; 182 | } 183 | 184 | /// Processes all changes to entities and executes all non-passive systems. 185 | void process([int group = 0]) { 186 | assert(_frame.containsKey(group), 'No group $group exists'); 187 | // delete entites that have been deleted outside of a system 188 | _deleteEntities(); 189 | _frame[group] = _frame[group]! + 1; 190 | _time[group] = _time[group]! + delta; 191 | 192 | for (final system in _systemsList 193 | .where((system) => !system.passive && system.group == group)) { 194 | _updateSystem(system); 195 | system.process(); 196 | 197 | _deleteEntities(); 198 | } 199 | } 200 | 201 | /// Actually delete the entities in the world that have been marked for 202 | /// deletion. 203 | void _deleteEntities() { 204 | _entitiesMarkedForDeletion 205 | ..forEach(_deleteEntity) 206 | ..clear(); 207 | } 208 | 209 | /// Delete an entity. 210 | void _deleteEntity(Entity entity) { 211 | for (final manager in _managers.values) { 212 | manager.deleted(entity); 213 | } 214 | componentManager.removeComponentsOfEntity(entity); 215 | entityManager._delete(entity); 216 | } 217 | 218 | void _updateSystem(EntitySystem system) { 219 | if (componentManager.isUpdateNeededForSystem(system)) { 220 | system._actives = componentManager._getEntitiesForSystem( 221 | system, 222 | entityManager._entities.length, 223 | ); 224 | componentManager._systemUpdated(system); 225 | } 226 | } 227 | 228 | /// Removes all entities from the world. 229 | /// 230 | /// Every entity and component has to be created anew. Make sure not to reuse 231 | /// [Component]s that were added to an [int] and referenced in you code 232 | /// because they will be added to a free list and might be overwritten once a 233 | /// new [Component] of that type is created. 234 | void deleteAllEntities() { 235 | entityManager._entities 236 | .toIntValues() 237 | .forEach((id) => _entitiesMarkedForDeletion.add(Entity._(id))); 238 | _deleteEntities(); 239 | } 240 | 241 | /// Adds a [Entity entity] to this world. 242 | void addEntity(Entity entity) { 243 | entityManager._add(entity); 244 | for (final manager in _managers.values) { 245 | manager.added(entity); 246 | } 247 | } 248 | 249 | /// Mark an [entity] for deletion from the world. Will be deleted after the 250 | /// current system finished running. 251 | void deleteEntity(Entity entity) { 252 | _entitiesMarkedForDeletion.add(entity); 253 | } 254 | 255 | /// Returns the value for [key] from [properties]. 256 | Object? operator [](String key) => properties[key]; 257 | 258 | /// Set the [value] of [key] in [properties]. 259 | void operator []=(String key, Object value) { 260 | properties[key] = value; 261 | } 262 | 263 | /// Destroy the [World] by destroying all [EntitySystem]s and [Manager]s. 264 | void destroy() { 265 | for (final system in _systemsList) { 266 | system.destroy(); 267 | } 268 | for (final manager in _managersBag) { 269 | manager.destroy(); 270 | } 271 | } 272 | 273 | /// Get all components belonging to this entity. 274 | List getComponents(Entity entity) => 275 | _componentManager.getComponentsFor(entity); 276 | } 277 | 278 | /// A [World] which measures performance by measureing elapsed time between 279 | /// calls. 280 | @experimental 281 | class PerformanceMeasureWorld extends World { 282 | final int _framesToMeasure; 283 | final Map> _systemTimes = >{}; 284 | final Map> _processEntityChangesTimes = 285 | >{}; 286 | 287 | /// Create the world and define how many frames should be included when 288 | /// calculating the [PerformanceStats]. 289 | PerformanceMeasureWorld(this._framesToMeasure) { 290 | _systemTimes[runtimeType] = ListQueue(_framesToMeasure); 291 | } 292 | 293 | @override 294 | void process([int group = 0]) { 295 | _frame[group] = _frame[group]! + 1; 296 | _time[group] = _time[group]! + delta; 297 | final stopwatch = Stopwatch()..start(); 298 | var lastStop = stopwatch.elapsedMicroseconds; 299 | for (final system in _systemsList 300 | .where((system) => !system.passive && system.group == group)) { 301 | _updateSystem(system); 302 | final afterProcessEntityChanges = stopwatch.elapsedMicroseconds; 303 | system.process(); 304 | final afterSystem = stopwatch.elapsedMicroseconds; 305 | _storeTime(_systemTimes, system, afterSystem, afterProcessEntityChanges); 306 | _storeTime( 307 | _processEntityChangesTimes, 308 | system, 309 | afterProcessEntityChanges, 310 | lastStop, 311 | ); 312 | lastStop = stopwatch.elapsedMicroseconds; 313 | } 314 | final now = stopwatch.elapsedMicroseconds; 315 | final times = _systemTimes[runtimeType]!; 316 | if (times.length >= _framesToMeasure) { 317 | times.removeFirst(); 318 | } 319 | times.add(now); 320 | } 321 | 322 | void _storeTime( 323 | Map> measuredTimes, 324 | EntitySystem system, 325 | int afterSystem, 326 | int lastStop, 327 | ) { 328 | final times = measuredTimes[system.runtimeType]!; 329 | if (times.length >= _framesToMeasure) { 330 | times.removeFirst(); 331 | } 332 | times.add(afterSystem - lastStop); 333 | } 334 | 335 | @override 336 | void addSystem(EntitySystem system) { 337 | super.addSystem(system); 338 | _systemTimes[system.runtimeType] = ListQueue(_framesToMeasure); 339 | _processEntityChangesTimes[system.runtimeType] = 340 | ListQueue(_framesToMeasure); 341 | } 342 | 343 | /// Returns the [PerformanceStats] for every system and and the 344 | /// [PerformanceStats] for changes to [int]s that require updates to other 345 | /// [EntitySystem]s and [Manager]s. 346 | List getPerformanceStats() { 347 | final result = []; 348 | _createPerformanceStats(_systemTimes, result); 349 | _createPerformanceStats(_processEntityChangesTimes, result); 350 | return result; 351 | } 352 | 353 | void _createPerformanceStats( 354 | Map> measuredTimes, 355 | List result, 356 | ) { 357 | for (final entry in measuredTimes.entries) { 358 | final measurements = entry.value.length; 359 | final sorted = List.from(entry.value)..sort(); 360 | final meanTime = sorted[measurements ~/ 2]; 361 | final averageTime = 362 | sorted.fold(0, (sum, item) => sum + item) / measurements; 363 | final minTime = sorted.first; 364 | final maxTime = sorted.last; 365 | result.add( 366 | PerformanceStats._internal( 367 | entry.key, 368 | measurements, 369 | minTime, 370 | maxTime, 371 | averageTime, 372 | meanTime, 373 | ), 374 | ); 375 | } 376 | } 377 | } 378 | 379 | /// Performance statistics for all systems. 380 | @experimental 381 | class PerformanceStats { 382 | /// The [Type] of the system. 383 | Type system; 384 | 385 | /// The number of measurements. 386 | int measurements; 387 | 388 | /// The fastest ([minTime]) time in microseconds. 389 | int minTime; 390 | 391 | /// The slowest ([maxTime]) time in microseconds. 392 | int maxTime; 393 | 394 | /// The mean time in microseconds. 395 | int meanTime; 396 | 397 | /// The avaerage time in microseconds. 398 | double averageTime; 399 | 400 | PerformanceStats._internal( 401 | this.system, 402 | this.measurements, 403 | this.minTime, 404 | this.maxTime, 405 | this.averageTime, 406 | this.meanTime, 407 | ); 408 | 409 | @override 410 | String toString() => ''' 411 | PerformanceStats{system: $system, measurements: $measurements, minTime: $minTime, maxTime: $maxTime, meanTime: $meanTime, averageTime: $averageTime}'''; 412 | } 413 | -------------------------------------------------------------------------------- /test/dartemis/core/world_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.5-wip from annotations 2 | // in dartemis/test/dartemis/core/world_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'package:dartemis/dartemis.dart' as _i2; 7 | import 'package:mockito/mockito.dart' as _i1; 8 | 9 | // ignore_for_file: type=lint 10 | // ignore_for_file: avoid_redundant_argument_values 11 | // ignore_for_file: avoid_setters_without_getters 12 | // ignore_for_file: comment_references 13 | // ignore_for_file: deprecated_member_use 14 | // ignore_for_file: deprecated_member_use_from_same_package 15 | // ignore_for_file: implementation_imports 16 | // ignore_for_file: invalid_use_of_visible_for_testing_member 17 | // ignore_for_file: must_be_immutable 18 | // ignore_for_file: prefer_const_constructors 19 | // ignore_for_file: unnecessary_parenthesis 20 | // ignore_for_file: camel_case_types 21 | // ignore_for_file: subtype_of_sealed_class 22 | 23 | class _FakeWorld_0 extends _i1.SmartFake implements _i2.World { 24 | _FakeWorld_0( 25 | Object parent, 26 | Invocation parentInvocation, 27 | ) : super( 28 | parent, 29 | parentInvocation, 30 | ); 31 | } 32 | 33 | /// A class which mocks [EntitySystem]. 34 | /// 35 | /// See the documentation for Mockito's code generation for more information. 36 | class MockEntitySystem2 extends _i1.Mock implements _i2.EntitySystem { 37 | @override 38 | bool get passive => (super.noSuchMethod( 39 | Invocation.getter(#passive), 40 | returnValue: false, 41 | returnValueForMissingStub: false, 42 | ) as bool); 43 | 44 | @override 45 | set passive(bool? _passive) => super.noSuchMethod( 46 | Invocation.setter( 47 | #passive, 48 | _passive, 49 | ), 50 | returnValueForMissingStub: null, 51 | ); 52 | 53 | @override 54 | int get group => (super.noSuchMethod( 55 | Invocation.getter(#group), 56 | returnValue: 0, 57 | returnValueForMissingStub: 0, 58 | ) as int); 59 | 60 | @override 61 | _i2.World get world => (super.noSuchMethod( 62 | Invocation.getter(#world), 63 | returnValue: _FakeWorld_0( 64 | this, 65 | Invocation.getter(#world), 66 | ), 67 | returnValueForMissingStub: _FakeWorld_0( 68 | this, 69 | Invocation.getter(#world), 70 | ), 71 | ) as _i2.World); 72 | 73 | @override 74 | int get frame => (super.noSuchMethod( 75 | Invocation.getter(#frame), 76 | returnValue: 0, 77 | returnValueForMissingStub: 0, 78 | ) as int); 79 | 80 | @override 81 | double get time => (super.noSuchMethod( 82 | Invocation.getter(#time), 83 | returnValue: 0.0, 84 | returnValueForMissingStub: 0.0, 85 | ) as double); 86 | 87 | @override 88 | double get delta => (super.noSuchMethod( 89 | Invocation.getter(#delta), 90 | returnValue: 0.0, 91 | returnValueForMissingStub: 0.0, 92 | ) as double); 93 | 94 | @override 95 | void begin() => super.noSuchMethod( 96 | Invocation.method( 97 | #begin, 98 | [], 99 | ), 100 | returnValueForMissingStub: null, 101 | ); 102 | 103 | @override 104 | void process() => super.noSuchMethod( 105 | Invocation.method( 106 | #process, 107 | [], 108 | ), 109 | returnValueForMissingStub: null, 110 | ); 111 | 112 | @override 113 | void end() => super.noSuchMethod( 114 | Invocation.method( 115 | #end, 116 | [], 117 | ), 118 | returnValueForMissingStub: null, 119 | ); 120 | 121 | @override 122 | void processEntities(Iterable<_i2.Entity>? entities) => super.noSuchMethod( 123 | Invocation.method( 124 | #processEntities, 125 | [entities], 126 | ), 127 | returnValueForMissingStub: null, 128 | ); 129 | 130 | @override 131 | bool checkProcessing() => (super.noSuchMethod( 132 | Invocation.method( 133 | #checkProcessing, 134 | [], 135 | ), 136 | returnValue: false, 137 | returnValueForMissingStub: false, 138 | ) as bool); 139 | 140 | @override 141 | void initialize(_i2.World? world) => super.noSuchMethod( 142 | Invocation.method( 143 | #initialize, 144 | [world], 145 | ), 146 | returnValueForMissingStub: null, 147 | ); 148 | 149 | @override 150 | void destroy() => super.noSuchMethod( 151 | Invocation.method( 152 | #destroy, 153 | [], 154 | ), 155 | returnValueForMissingStub: null, 156 | ); 157 | 158 | @override 159 | void addComponent( 160 | _i2.Entity? entity, 161 | T? component, 162 | ) => 163 | super.noSuchMethod( 164 | Invocation.method( 165 | #addComponent, 166 | [ 167 | entity, 168 | component, 169 | ], 170 | ), 171 | returnValueForMissingStub: null, 172 | ); 173 | 174 | @override 175 | void removeComponent(_i2.Entity? entity) => 176 | super.noSuchMethod( 177 | Invocation.method( 178 | #removeComponent, 179 | [entity], 180 | ), 181 | returnValueForMissingStub: null, 182 | ); 183 | 184 | @override 185 | void deleteFromWorld(_i2.Entity? entity) => super.noSuchMethod( 186 | Invocation.method( 187 | #deleteFromWorld, 188 | [entity], 189 | ), 190 | returnValueForMissingStub: null, 191 | ); 192 | } 193 | 194 | /// A class which mocks [EntitySystem]. 195 | /// 196 | /// See the documentation for Mockito's code generation for more information. 197 | class MockEntitySystem extends _i1.Mock implements _i2.EntitySystem { 198 | @override 199 | bool get passive => (super.noSuchMethod( 200 | Invocation.getter(#passive), 201 | returnValue: false, 202 | returnValueForMissingStub: false, 203 | ) as bool); 204 | 205 | @override 206 | set passive(bool? _passive) => super.noSuchMethod( 207 | Invocation.setter( 208 | #passive, 209 | _passive, 210 | ), 211 | returnValueForMissingStub: null, 212 | ); 213 | 214 | @override 215 | int get group => (super.noSuchMethod( 216 | Invocation.getter(#group), 217 | returnValue: 0, 218 | returnValueForMissingStub: 0, 219 | ) as int); 220 | 221 | @override 222 | _i2.World get world => (super.noSuchMethod( 223 | Invocation.getter(#world), 224 | returnValue: _FakeWorld_0( 225 | this, 226 | Invocation.getter(#world), 227 | ), 228 | returnValueForMissingStub: _FakeWorld_0( 229 | this, 230 | Invocation.getter(#world), 231 | ), 232 | ) as _i2.World); 233 | 234 | @override 235 | int get frame => (super.noSuchMethod( 236 | Invocation.getter(#frame), 237 | returnValue: 0, 238 | returnValueForMissingStub: 0, 239 | ) as int); 240 | 241 | @override 242 | double get time => (super.noSuchMethod( 243 | Invocation.getter(#time), 244 | returnValue: 0.0, 245 | returnValueForMissingStub: 0.0, 246 | ) as double); 247 | 248 | @override 249 | double get delta => (super.noSuchMethod( 250 | Invocation.getter(#delta), 251 | returnValue: 0.0, 252 | returnValueForMissingStub: 0.0, 253 | ) as double); 254 | 255 | @override 256 | void begin() => super.noSuchMethod( 257 | Invocation.method( 258 | #begin, 259 | [], 260 | ), 261 | returnValueForMissingStub: null, 262 | ); 263 | 264 | @override 265 | void process() => super.noSuchMethod( 266 | Invocation.method( 267 | #process, 268 | [], 269 | ), 270 | returnValueForMissingStub: null, 271 | ); 272 | 273 | @override 274 | void end() => super.noSuchMethod( 275 | Invocation.method( 276 | #end, 277 | [], 278 | ), 279 | returnValueForMissingStub: null, 280 | ); 281 | 282 | @override 283 | void processEntities(Iterable<_i2.Entity>? entities) => super.noSuchMethod( 284 | Invocation.method( 285 | #processEntities, 286 | [entities], 287 | ), 288 | returnValueForMissingStub: null, 289 | ); 290 | 291 | @override 292 | bool checkProcessing() => (super.noSuchMethod( 293 | Invocation.method( 294 | #checkProcessing, 295 | [], 296 | ), 297 | returnValue: false, 298 | returnValueForMissingStub: false, 299 | ) as bool); 300 | 301 | @override 302 | void initialize(_i2.World? world) => super.noSuchMethod( 303 | Invocation.method( 304 | #initialize, 305 | [world], 306 | ), 307 | returnValueForMissingStub: null, 308 | ); 309 | 310 | @override 311 | void destroy() => super.noSuchMethod( 312 | Invocation.method( 313 | #destroy, 314 | [], 315 | ), 316 | returnValueForMissingStub: null, 317 | ); 318 | 319 | @override 320 | void addComponent( 321 | _i2.Entity? entity, 322 | T? component, 323 | ) => 324 | super.noSuchMethod( 325 | Invocation.method( 326 | #addComponent, 327 | [ 328 | entity, 329 | component, 330 | ], 331 | ), 332 | returnValueForMissingStub: null, 333 | ); 334 | 335 | @override 336 | void removeComponent(_i2.Entity? entity) => 337 | super.noSuchMethod( 338 | Invocation.method( 339 | #removeComponent, 340 | [entity], 341 | ), 342 | returnValueForMissingStub: null, 343 | ); 344 | 345 | @override 346 | void deleteFromWorld(_i2.Entity? entity) => super.noSuchMethod( 347 | Invocation.method( 348 | #deleteFromWorld, 349 | [entity], 350 | ), 351 | returnValueForMissingStub: null, 352 | ); 353 | } 354 | 355 | /// A class which mocks [ComponentManager]. 356 | /// 357 | /// See the documentation for Mockito's code generation for more information. 358 | class MockComponentManager extends _i1.Mock implements _i2.ComponentManager { 359 | @override 360 | _i2.World get world => (super.noSuchMethod( 361 | Invocation.getter(#world), 362 | returnValue: _FakeWorld_0( 363 | this, 364 | Invocation.getter(#world), 365 | ), 366 | returnValueForMissingStub: _FakeWorld_0( 367 | this, 368 | Invocation.getter(#world), 369 | ), 370 | ) as _i2.World); 371 | 372 | @override 373 | void registerSystem(_i2.EntitySystem? system) => super.noSuchMethod( 374 | Invocation.method( 375 | #registerSystem, 376 | [system], 377 | ), 378 | returnValueForMissingStub: null, 379 | ); 380 | 381 | @override 382 | void removeComponentsOfEntity(_i2.Entity? entity) => super.noSuchMethod( 383 | Invocation.method( 384 | #removeComponentsOfEntity, 385 | [entity], 386 | ), 387 | returnValueForMissingStub: null, 388 | ); 389 | 390 | @override 391 | List getComponentsByType() => (super.noSuchMethod( 392 | Invocation.method( 393 | #getComponentsByType, 394 | [], 395 | ), 396 | returnValue: [], 397 | returnValueForMissingStub: [], 398 | ) as List); 399 | 400 | @override 401 | List<_i2.Component> getComponentsFor(_i2.Entity? entity) => 402 | (super.noSuchMethod( 403 | Invocation.method( 404 | #getComponentsFor, 405 | [entity], 406 | ), 407 | returnValue: <_i2.Component>[], 408 | returnValueForMissingStub: <_i2.Component>[], 409 | ) as List<_i2.Component>); 410 | 411 | @override 412 | bool isUpdateNeededForSystem(_i2.EntitySystem? system) => (super.noSuchMethod( 413 | Invocation.method( 414 | #isUpdateNeededForSystem, 415 | [system], 416 | ), 417 | returnValue: false, 418 | returnValueForMissingStub: false, 419 | ) as bool); 420 | 421 | @override 422 | T? getComponent(_i2.Entity? entity) => 423 | (super.noSuchMethod( 424 | Invocation.method( 425 | #getComponent, 426 | [entity], 427 | ), 428 | returnValueForMissingStub: null, 429 | ) as T?); 430 | 431 | @override 432 | _i2.ComponentType getTypeFor(Type? typeOfComponent) => (super.noSuchMethod( 433 | Invocation.method( 434 | #getTypeFor, 435 | [typeOfComponent], 436 | ), 437 | returnValue: 0, 438 | returnValueForMissingStub: 0, 439 | ) as _i2.ComponentType); 440 | 441 | @override 442 | int getBitIndex(Type? componentType) => (super.noSuchMethod( 443 | Invocation.method( 444 | #getBitIndex, 445 | [componentType], 446 | ), 447 | returnValue: 0, 448 | returnValueForMissingStub: 0, 449 | ) as int); 450 | 451 | @override 452 | void initialize(_i2.World? world) => super.noSuchMethod( 453 | Invocation.method( 454 | #initialize, 455 | [world], 456 | ), 457 | returnValueForMissingStub: null, 458 | ); 459 | 460 | @override 461 | void added(_i2.Entity? entity) => super.noSuchMethod( 462 | Invocation.method( 463 | #added, 464 | [entity], 465 | ), 466 | returnValueForMissingStub: null, 467 | ); 468 | 469 | @override 470 | void deleted(_i2.Entity? entity) => super.noSuchMethod( 471 | Invocation.method( 472 | #deleted, 473 | [entity], 474 | ), 475 | returnValueForMissingStub: null, 476 | ); 477 | 478 | @override 479 | void destroy() => super.noSuchMethod( 480 | Invocation.method( 481 | #destroy, 482 | [], 483 | ), 484 | returnValueForMissingStub: null, 485 | ); 486 | } 487 | 488 | /// A class which mocks [Manager]. 489 | /// 490 | /// See the documentation for Mockito's code generation for more information. 491 | class MockManager extends _i1.Mock implements _i2.Manager { 492 | @override 493 | _i2.World get world => (super.noSuchMethod( 494 | Invocation.getter(#world), 495 | returnValue: _FakeWorld_0( 496 | this, 497 | Invocation.getter(#world), 498 | ), 499 | returnValueForMissingStub: _FakeWorld_0( 500 | this, 501 | Invocation.getter(#world), 502 | ), 503 | ) as _i2.World); 504 | 505 | @override 506 | void initialize(_i2.World? world) => super.noSuchMethod( 507 | Invocation.method( 508 | #initialize, 509 | [world], 510 | ), 511 | returnValueForMissingStub: null, 512 | ); 513 | 514 | @override 515 | void added(_i2.Entity? entity) => super.noSuchMethod( 516 | Invocation.method( 517 | #added, 518 | [entity], 519 | ), 520 | returnValueForMissingStub: null, 521 | ); 522 | 523 | @override 524 | void deleted(_i2.Entity? entity) => super.noSuchMethod( 525 | Invocation.method( 526 | #deleted, 527 | [entity], 528 | ), 529 | returnValueForMissingStub: null, 530 | ); 531 | 532 | @override 533 | void destroy() => super.noSuchMethod( 534 | Invocation.method( 535 | #destroy, 536 | [], 537 | ), 538 | returnValueForMissingStub: null, 539 | ); 540 | } 541 | -------------------------------------------------------------------------------- /test/dartemis/core/world_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartemis/dartemis.dart'; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'components_setup.dart'; 7 | import 'world_test.mocks.dart'; 8 | 9 | @GenerateNiceMocks( 10 | [ 11 | MockSpec(as: #MockEntitySystem2), 12 | MockSpec(), 13 | MockSpec(), 14 | MockSpec(), 15 | ], 16 | ) 17 | void main() { 18 | group('World tests', () { 19 | late World world; 20 | late EntitySystem system; 21 | late ComponentManager componentManager; 22 | setUp(() { 23 | componentManager = MockComponentManager(); 24 | system = MockEntitySystem(); 25 | 26 | when(system.passive).thenReturn(false); 27 | when(system.group).thenReturn(0); 28 | when(componentManager.isUpdateNeededForSystem(system)).thenReturn(false); 29 | 30 | world = World(componentManager: componentManager); 31 | }); 32 | test('world initializes added system', () { 33 | world 34 | ..addSystem(system) 35 | ..initialize(); 36 | 37 | // TODO(me): remove this and other ignores after https://github.com/dart-lang/sdk/issues/56819 is fixed 38 | // can't use @visibleForTesting annotation as it will complain about 39 | // calls to super in the overriding method 40 | // ignore: invalid_use_of_visible_for_overriding_member 41 | verify(system.initialize(world)).called(1); 42 | }); 43 | test('systems can not be added after calling initialize', () { 44 | world.initialize(); 45 | 46 | expect(() => world.addSystem(MockEntitySystem2()), throwsStateError); 47 | }); 48 | test("the same system can't be added twice", () { 49 | world.addSystem(system); 50 | 51 | expect(() => world.addSystem(system), throwsArgumentError); 52 | }); 53 | test('world processes added system', () { 54 | world 55 | ..addSystem(system) 56 | ..process(); 57 | 58 | // ignore: invalid_use_of_visible_for_overriding_member 59 | verify(system.process()).called(1); 60 | }); 61 | test('world processes added system', () { 62 | world 63 | ..addSystem(system) 64 | ..process(); 65 | 66 | // ignore: invalid_use_of_visible_for_overriding_member 67 | verify(system.process()).called(1); 68 | }); 69 | test('world does not process passive system', () { 70 | when(system.passive).thenReturn(true); 71 | 72 | world 73 | ..addSystem(system) 74 | ..process(); 75 | 76 | // ignore: invalid_use_of_visible_for_overriding_member 77 | verifyNever(system.process()); 78 | }); 79 | test('world processes systems by group', () { 80 | final system2 = MockEntitySystem2(); 81 | when(system2.passive).thenReturn(false); 82 | when(system2.group).thenReturn(1); 83 | when(componentManager.isUpdateNeededForSystem(system2)).thenReturn(false); 84 | 85 | world 86 | ..addSystem(system) 87 | ..addSystem(system2) 88 | ..process(); 89 | // ignore: invalid_use_of_visible_for_overriding_member 90 | verify(system.process()).called(1); 91 | verifyNever(system2.process()); 92 | 93 | world.process(1); 94 | // ignore: invalid_use_of_visible_for_overriding_member 95 | verifyNever(system.process()); 96 | verify(system2.process()).called(1); 97 | }); 98 | test('world manages time and frames by group', () { 99 | final system2 = MockEntitySystem2(); 100 | when(system2.passive).thenReturn(false); 101 | when(system2.group).thenReturn(1); 102 | when(componentManager.isUpdateNeededForSystem(system2)).thenReturn(false); 103 | 104 | world 105 | ..addSystem(system) 106 | ..addSystem(system2) 107 | ..delta = 10.0 108 | ..process() 109 | ..delta = 20.0 110 | ..process(1) 111 | ..delta = 15.0 112 | ..process(); 113 | 114 | expect(world.time(), equals(25.0)); 115 | expect(world.time(1), equals(20.0)); 116 | expect(world.frame(), equals(2)); 117 | expect(world.frame(1), equals(1)); 118 | }); 119 | test('world initializes added managers', () { 120 | final manager = MockManager(); 121 | 122 | world 123 | ..addManager(manager) 124 | ..initialize(); 125 | 126 | verify(manager.initialize(world)).called(1); 127 | }); 128 | test("the same manager can't be added twice", () { 129 | world.addManager(MockManager()); 130 | 131 | expect(() => world.addManager(MockManager()), throwsArgumentError); 132 | }); 133 | test('managers can not be added after calling initialize', () { 134 | world.initialize(); 135 | 136 | expect(() => world.addManager(TagManager()), throwsStateError); 137 | }); 138 | test('world deletes all entites', () { 139 | world 140 | ..initialize() 141 | ..createEntity() 142 | ..createEntity() 143 | ..process(); 144 | 145 | expect(world.entityManager.activeEntityCount, equals(2)); 146 | world 147 | ..deleteAllEntities() 148 | ..process(); 149 | expect(world.entityManager.activeEntityCount, equals(0)); 150 | }); 151 | test('world delete all entites should not fail if called twice', () { 152 | world 153 | ..initialize() 154 | ..createEntity() 155 | ..process() 156 | ..deleteAllEntities() 157 | ..deleteAllEntities(); 158 | }); 159 | test('world process increments frame count', () { 160 | world 161 | ..initialize() 162 | ..process(); 163 | expect(world.frame(), equals(1)); 164 | world.process(); 165 | expect(world.frame(), equals(2)); 166 | }); 167 | test('world process increments time by delta', () { 168 | world 169 | ..initialize() 170 | ..delta = 10 171 | ..process(); 172 | expect(world.time(), equals(10)); 173 | world 174 | ..delta = 20 175 | ..process(); 176 | expect(world.time(), equals(30)); 177 | }); 178 | test('world allows access to properties', () { 179 | world['key'] = 'value'; 180 | 181 | expect(world['key'], equals('value')); 182 | }); 183 | test('destroy calls destroy method on systems', () { 184 | world 185 | ..addSystem(system) 186 | ..process() 187 | ..destroy(); 188 | 189 | // ignore: invalid_use_of_visible_for_overriding_member 190 | verify(system.destroy()).called(1); 191 | }); 192 | test('destroy calls destroy method on managers', () { 193 | final manager = MockManager(); 194 | 195 | world 196 | ..addManager(manager) 197 | ..initialize() 198 | ..destroy(); 199 | 200 | verify(manager.destroy()).called(1); 201 | }); 202 | }); 203 | group('integration tests for World.process()', () { 204 | late World world; 205 | late Entity entityAB; 206 | late Entity entityAC; 207 | late EntitySystemStarter systemStarter; 208 | setUp(() { 209 | world = World(); 210 | entityAB = world.createEntity([Component0(), Component1()]); 211 | entityAC = world.createEntity(); 212 | world 213 | ..addComponent(entityAC, Component0()) 214 | ..addComponent(entityAC, PooledComponent2()); 215 | systemStarter = (es, action) { 216 | world 217 | ..addSystem(es) 218 | ..initialize() 219 | ..process(); 220 | action(); 221 | world.process(); 222 | }; 223 | }); 224 | test(''' 225 | EntitySystem which requires one Component processes entity with this component''', 226 | () { 227 | final expectedEntities = [entityAB, entityAC]; 228 | final es = 229 | TestEntitySystem(Aspect(allOf: [Component0]), expectedEntities); 230 | systemStarter(es, () {}); 231 | }); 232 | test(''' 233 | EntitySystem which required multiple Components does not process entity with a subset of those components''', 234 | () { 235 | final expectedEntities = [entityAB]; 236 | final es = TestEntitySystem( 237 | Aspect(allOf: [Component0, Component1]), 238 | expectedEntities, 239 | ); 240 | systemStarter(es, () {}); 241 | }); 242 | test(''' 243 | EntitySystem which requires one of multiple components processes entity with a subset of those components''', 244 | () { 245 | final expectedEntities = [entityAB, entityAC]; 246 | final es = TestEntitySystem( 247 | Aspect(oneOf: [Component0, Component1]), 248 | expectedEntities, 249 | ); 250 | systemStarter(es, () {}); 251 | }); 252 | test(''' 253 | EntitySystem which excludes a component does not process entity with one of those components''', 254 | () { 255 | final expectedEntities = [entityAB]; 256 | final es = TestEntitySystem( 257 | Aspect(allOf: [Component0])..exclude([PooledComponent2]), 258 | expectedEntities, 259 | ); 260 | systemStarter(es, () {}); 261 | }); 262 | test('A removed entity will not get processed', () { 263 | final expectedEntities = [entityAC]; 264 | final es = 265 | TestEntitySystem(Aspect(allOf: [Component0]), expectedEntities); 266 | systemStarter(es, () => es.deleteFromWorld(entityAB)); 267 | }); 268 | test( 269 | 'A removed entity can still be interacted with as long as the system is' 270 | ' not finished', () { 271 | final es = TestEntitySystemWithInteractingDeletedEntities(); 272 | world 273 | ..createEntity([Component0()]) 274 | ..createEntity([Component0()]); 275 | systemStarter(es, () => {}); 276 | }); 277 | test("An entity that's been deleted twice, can only be reused once", () { 278 | world 279 | ..deleteEntity(entityAB) 280 | ..deleteEntity(entityAB); 281 | final component0 = Component0(); 282 | final component1 = Component1(); 283 | 284 | world.process(); 285 | final entityA = world.createEntity([component0]); 286 | final entityB = world.createEntity([component1]); 287 | world.process(); 288 | 289 | expect(world.getComponents(entityA)[0], equals(component0)); 290 | expect(world.getComponents(entityB)[0], equals(component1)); 291 | expect(world.getComponents(entityA).length, equals(1)); 292 | expect(world.getComponents(entityB).length, equals(1)); 293 | }); 294 | test(''' 295 | Adding a component will get the entity processed''', () { 296 | final expectedEntities = [entityAC]; 297 | final es = TestEntitySystem( 298 | Aspect(allOf: [PooledComponent2]), 299 | expectedEntities, 300 | ); 301 | world 302 | ..addSystem(es) 303 | ..initialize() 304 | ..process(); 305 | es._expectedEntities = [entityAB, entityAC]; 306 | world 307 | ..addComponent(entityAB, PooledComponent2()) 308 | ..process(); 309 | }); 310 | test('world can handle more than 32 components referenced by systems', () { 311 | world.addSystem(TestEntitySystemWithMoreThan32Components()); 312 | }); 313 | test( 314 | 'world can handle entites with an id higher than 32 when spawned later', 315 | () { 316 | final es = TestEntitySystemForComponent3(); 317 | world 318 | ..addSystem(es) 319 | ..initialize() 320 | ..process(); 321 | 322 | for (var i = 0; i <= 32; i++) { 323 | world.createEntity([Component3()]); 324 | } 325 | 326 | world.process(); 327 | }); 328 | }); 329 | group('isUpdateNeededForSystem', () { 330 | late World world; 331 | late Entity entityA; 332 | late Entity entityB; 333 | late Entity entityC; 334 | late TestEntitySystem es; 335 | setUp(() { 336 | world = World(); 337 | entityA = world.createEntity([Component0(), Component32()]); 338 | entityB = world.createEntity([Component32()]); 339 | entityC = world.createEntity([Component0()]); 340 | final expectedEntities = [entityA]; 341 | es = TestEntitySystem(Aspect(allOf: [Component0]), expectedEntities); 342 | 343 | world 344 | ..addSystem(es) 345 | ..initialize() 346 | ..process(); 347 | }); 348 | test('systems should not require update when no change happened', () { 349 | expect(world.componentManager.isUpdateNeededForSystem(es), isFalse); 350 | }); 351 | test('systems should require update when new entity is added', () { 352 | world.createEntity([Component0()]); 353 | 354 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 355 | }); 356 | test('systems should require update when entity is removed', () { 357 | world 358 | ..deleteEntity(entityA) 359 | // workaround to trigger actual deletion of entities by processing a non 360 | // existent system group 361 | ..process(-1); 362 | 363 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 364 | }); 365 | test( 366 | 'systems should require update when component required by system is ' 367 | 'removed', () { 368 | world.removeComponent(entityA); 369 | 370 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 371 | }); 372 | test( 373 | 'systems should require update when component required by system is ' 374 | 'moved', () { 375 | world.moveComponent(entityA, entityC); 376 | 377 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 378 | }); 379 | test( 380 | 'systems should require update when component required by system is ' 381 | 'moved to a entity that did not have the component before', () { 382 | world.moveComponent(entityA, entityB); 383 | 384 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 385 | }); 386 | test( 387 | 'systems should not require update when component required by system ' 388 | 'is moved', () { 389 | world.moveComponent(entityA, entityB); 390 | 391 | expect(world.componentManager.isUpdateNeededForSystem(es), isFalse); 392 | }); 393 | test( 394 | 'systems should require update when component required by system is ' 395 | 'added', () { 396 | world.addComponent(entityB, Component0()); 397 | 398 | expect(world.componentManager.isUpdateNeededForSystem(es), isTrue); 399 | }); 400 | test( 401 | 'systems should not require update when unrelated component is ' 402 | 'added', () { 403 | world 404 | ..addComponent(entityA, Component32()) 405 | ..addComponent(entityB, Component32()); 406 | 407 | expect(world.componentManager.isUpdateNeededForSystem(es), isFalse); 408 | }); 409 | test( 410 | 'systems should not require update when unrelated component is ' 411 | 'removed', () { 412 | world 413 | ..removeComponent(entityA) 414 | ..removeComponent(entityB); 415 | 416 | expect(world.componentManager.isUpdateNeededForSystem(es), isFalse); 417 | }); 418 | }); 419 | } 420 | 421 | typedef EntitySystemStarter = void Function( 422 | EntitySystem es, 423 | void Function() action, 424 | ); 425 | 426 | class TestEntitySystem extends EntitySystem { 427 | bool isSetup = true; 428 | List _expectedEntities; 429 | TestEntitySystem(super.aspect, this._expectedEntities); 430 | 431 | @override 432 | void processEntities(Iterable entities) { 433 | final length = _expectedEntities.length; 434 | expect( 435 | entities.length, 436 | length, 437 | reason: 438 | '''expected $length entities, got ${entities.length} entities: [${entities.join(', ')}]''', 439 | ); 440 | for (final entity in entities) { 441 | expect(entity, isIn(_expectedEntities)); 442 | } 443 | } 444 | 445 | @override 446 | bool checkProcessing() { 447 | if (isSetup) { 448 | isSetup = false; 449 | return false; 450 | } 451 | return true; 452 | } 453 | } 454 | 455 | class TestEntitySystemWithMoreThan32Components extends EntitySystem { 456 | TestEntitySystemWithMoreThan32Components() 457 | : super( 458 | Aspect( 459 | allOf: [ 460 | Component0, 461 | Component1, 462 | PooledComponent2, 463 | Component3, 464 | Component4, 465 | Component5, 466 | Component6, 467 | Component7, 468 | Component8, 469 | Component9, 470 | Component10, 471 | Component11, 472 | Component12, 473 | Component13, 474 | Component14, 475 | Component15, 476 | Component16, 477 | Component17, 478 | Component18, 479 | Component19, 480 | Component20, 481 | Component21, 482 | Component22, 483 | Component23, 484 | Component24, 485 | Component25, 486 | Component26, 487 | Component27, 488 | Component28, 489 | Component29, 490 | Component30, 491 | Component31, 492 | Component32, 493 | ], 494 | ), 495 | ); 496 | 497 | @override 498 | void processEntities(Iterable entities) {} 499 | 500 | @override 501 | bool checkProcessing() => true; 502 | } 503 | 504 | class TestEntitySystemForComponent3 extends EntityProcessingSystem { 505 | late Mapper mapper; 506 | 507 | TestEntitySystemForComponent3() : super(Aspect(allOf: [Component3])); 508 | 509 | @override 510 | void initialize(World world) { 511 | super.initialize(world); 512 | mapper = Mapper(world); 513 | } 514 | 515 | @override 516 | void processEntity(Entity entity) { 517 | final component = mapper.getSafe(entity); 518 | 519 | expect( 520 | component, 521 | isNotNull, 522 | reason: 'component for entity $entity is null', 523 | ); 524 | } 525 | } 526 | 527 | class TestEntitySystemWithInteractingDeletedEntities extends EntitySystem { 528 | late final Mapper mapper0; 529 | TestEntitySystemWithInteractingDeletedEntities() 530 | : super(Aspect(allOf: [Component0])); 531 | 532 | @override 533 | void initialize(World world) { 534 | super.initialize(world); 535 | mapper0 = Mapper(world); 536 | } 537 | 538 | @override 539 | bool checkProcessing() => true; 540 | 541 | @override 542 | void processEntities(Iterable entities) { 543 | // some interaction that causes one entity to be deleted 544 | world.deleteEntity(entities.last); 545 | 546 | for (final entity in entities) { 547 | expect(mapper0[entity], isA()); 548 | } 549 | } 550 | } 551 | --------------------------------------------------------------------------------