diff --git a/analysis_options.yaml b/analysis_options.yaml index 5b6bd38..dc6c36b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,20 +1,4 @@ -# Be aware the health of this package is based on these -# analyse options, we will get the most health points if -# we do not change anything, see: -# https://pub.dev/help#health - -# Defines a default set of lint rules enforced for -# projects at Google. For details and rationale, -# see https://github.com/dart-lang/pedantic#enabled-lints. -include: package:pedantic/analysis_options.yaml - -# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. -# Uncomment to specify additional rules. -linter: - rules: - - unnecessary_brace_in_string_interps - - non_constant_identifier_names - - cancel_subscriptions +include: package:flame_lint/analysis_options.yaml analyzer: exclude: diff --git a/benchmark/component_benchmark.dart b/benchmark/component_benchmark.dart new file mode 100644 index 0000000..38c7d05 --- /dev/null +++ b/benchmark/component_benchmark.dart @@ -0,0 +1,70 @@ +// Dont use forEcah method! See details: +// https://itnext.io/comparing-darts-loops-which-is-the-fastest-731a03ad42a2 +// ignore_for_file: prefer_foreach + +import 'package:benchmark/benchmark.dart'; +import 'package:oxygen/oxygen.dart'; + +void main() { + group('Component', () { + group('With 100k entities', () { + World? world; + + Filter? filter100; + Filter? filter50; + + ComponentPool? pool100; + ComponentPool? pool50; + + setUp(() { + world = World(); + pool100 = world!.getPool(Test100Component.new); + pool50 = world!.getPool(Test50Component.new); + + for (var i = 0; i < 100000; i++) { + final entity = world!.createEntity(); + + pool100!.add(entity); + if (i.isEven) { + pool50!.add(entity); + } + } + + filter100 = world!.filter(Test100Component.new).end(); + filter50 = world!.filter(Test50Component.new).end(); + }); + + tearDown(() { + world = null; + }); + + group('100%', () { + benchmark('Iterate over 100% of the entities without getting', () { + for (final _ in filter100!) {} + }); + + benchmark('Iterate over 100% of the entities with getting', () { + for (final entity in filter100!) { + pool100!.get(entity); + } + }); + }); + + group('50%', () { + benchmark('Iterate over 50% of the entities without getting', () { + for (final _ in filter50!) {} + }); + + benchmark('Iterate over 50% of the entities with getting', () { + for (final entity in filter50!) { + pool50!.get(entity); + } + }); + }); + }); + }); +} + +class Test100Component extends Component {} + +class Test50Component extends Component {} diff --git a/benchmark/component_pool_benchmark.dart b/benchmark/component_pool_benchmark.dart new file mode 100644 index 0000000..020a4e7 --- /dev/null +++ b/benchmark/component_pool_benchmark.dart @@ -0,0 +1,53 @@ +import 'package:benchmark/benchmark.dart'; +import 'package:oxygen/oxygen.dart'; + +class TestObject extends Component { + int? value; + + @override + void init([int? data]) { + value = data ?? 0; + } + + @override + void reset() { + value = null; + } +} + +void main() { + group('ObjectPool', () { + group('creating ComponentPool', () { + late World world; + + setUp(() { + world = World(); + }); + + benchmark('with 100k instances', () { + ComponentPool(world, TestObject.new, 0, 100000, 100000); + }); + }); + + group('registered ComponentPool', () { + late World world; + final entities = []; + late ComponentPool pool; + + setUp(() { + world = World(); + pool = world.getPool(TestObject.new); + for (var i = 0; i <= 100000; i++) { + entities.add(world.createEntity()); + } + }); + + benchmark('add 100k entities', () { + final length = entities.length; + for (var i = 0; i < length; i++) { + pool.add(entities[i]); + } + }); + }); + }); +} diff --git a/benchmark/filter_benchmark.dart b/benchmark/filter_benchmark.dart new file mode 100644 index 0000000..8ec836c --- /dev/null +++ b/benchmark/filter_benchmark.dart @@ -0,0 +1,40 @@ +import 'package:benchmark/benchmark.dart'; +import 'package:oxygen/oxygen.dart'; + +class Test100Component extends Component {} + +class Test50Component extends Component {} + +void main() { + group('Filter', () { + group('With 100k entities', () { + World? world; + + setUp(() { + world = World(); + final pool100 = world!.getPool(Test100Component.new); + final pool50 = world!.getPool(Test50Component.new); + for (var i = 0; i < 100000; i++) { + final entity = world!.createEntity(); + + pool100.add(entity); + if (i.isEven) { + pool50.add(entity); + } + } + }); + + tearDown(() { + world = null; + }); + + benchmark('creating a Filter that matches 100% of all the entities', () { + world!.filter(Test100Component.new).end(); + }); + + benchmark('creating a Filter that matches 50% of all the entities', () { + world!.filter(Test50Component.new).end(); + }); + }); + }); +} diff --git a/benchmark/object_pool_benchmark.dart b/benchmark/object_pool_benchmark.dart deleted file mode 100644 index cd0667f..0000000 --- a/benchmark/object_pool_benchmark.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:oxygen/oxygen.dart'; -import 'package:benchmark/benchmark.dart'; - -class TestObject extends PoolObject { - int? value; - - @override - void init([int? data]) { - value = data ?? 0; - } - - @override - void reset() { - value = null; - } -} - -class TestPool extends ObjectPool { - TestPool({int initialSize = 100000}) : super(initialSize: initialSize); - - @override - TestObject builder() => TestObject(); -} - -void main() { - group('ObjectPool', () { - benchmark('new ObjectPool with 100000 instances', () { - TestPool(initialSize: 100000); - }); - - benchmark('new ObjectPool with 0 instances that grows to 100000', () { - final pool = TestPool(initialSize: 0); - for (var i = 0; i < 100000; i++) { - pool.acquire(); - } - }); - }); -} diff --git a/benchmark/query_benchmark.dart b/benchmark/query_benchmark.dart deleted file mode 100644 index a17a260..0000000 --- a/benchmark/query_benchmark.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:oxygen/oxygen.dart'; -import 'package:benchmark/benchmark.dart'; - -class Test100Component extends Component { - @override - void init([void data]) {} - - @override - void reset() {} -} - -class Test50Component extends Component { - @override - void init([void data]) {} - - @override - void reset() {} -} - -void main() { - group('Query', () { - group('With 100000 entities', () { - World? world; - - QueryManager? queryManager; - - setUp(() { - world = World(); - world!.registerComponent(() => Test100Component()); - world!.registerComponent(() => Test50Component()); - for (var i = 0; i < 100000; i++) { - final entity = world!.createEntity(); - - entity.add(); - if (i % 2 == 0) { - entity.add(); - } - } - queryManager = QueryManager(world!.entityManager); - }); - - tearDown(() { - world = null; - queryManager = null; - }); - - benchmark('creating a Query that matches 100% of all the entities', () { - queryManager?.createQuery([Has()]); - }); - - benchmark('creating a Query that matches 50% of all the entities', () { - queryManager?.createQuery([Has()]); - }); - }); - }); -} diff --git a/benchmark/world_benchmark.dart b/benchmark/world_benchmark.dart index b67d263..f0f42a0 100644 --- a/benchmark/world_benchmark.dart +++ b/benchmark/world_benchmark.dart @@ -1,5 +1,5 @@ -import 'package:oxygen/oxygen.dart'; import 'package:benchmark/benchmark.dart'; +import 'package:oxygen/oxygen.dart'; void main() { group('World', () { @@ -10,20 +10,240 @@ void main() { tearDownEach(() => world = null); - benchmark('World with 100000 entities', () { + benchmark('World with 100k entities', () { for (var i = 0; i < 100000; i++) { - world?.createEntity(); + world!.createEntity(); } }); }); group('With world creation', () { - benchmark('World with 100000 entities', () { + benchmark('World with 100k entities', () { + final world = World(); + for (var i = 0; i < 100000; i++) { + world.createEntity(); + } + }); + }); + + group('getPool', () { + group('with init', () { + final world = World(); + for (var i = 0; i < 100000; i++) { + world.createEntity(); + } + world.getPool(_TestComponent.new); + world.getPool(_TestComponen2.new); + world.getPool(_TestComponen3.new); + world.getPool(_TestComponen4.new); + world.getPool(_TestComponen5.new); + world.getPool(_TestComponen6.new); + world.getPool(_TestComponen7.new); + world.getPool(_TestComponen8.new); + world.getPool(_TestComponen9.new); + world.getPool(_TestComponen10.new); + world.getPool(_TestComponen11.new); + world.getPool(_TestComponen12.new); + world.getPool(_TestComponen13.new); + world.getPool(_TestComponen14.new); + world.getPool(_TestComponen15.new); + world.getPool(_TestComponen16.new); + world.getPool(_TestComponen17.new); + world.getPool(_TestComponen18.new); + world.getPool(_TestComponen19.new); + world.getPool(_TestComponen20.new); + world.getPool(_TestComponen21.new); + world.getPool(_TestComponen22.new); + world.getPool(_TestComponen23.new); + world.getPool(_TestComponen24.new); + world.getPool(_TestComponen25.new); + world.getPool(_TestComponen26.new); + world.getPool(_TestComponen27.new); + world.getPool(_TestComponen28.new); + world.getPool(_TestComponen29.new); + world.getPool(_TestComponen30.new); + world.getPool(_TestComponen31.new); + world.getPool(_TestComponen32.new); + world.getPool(_TestComponen33.new); + world.getPool(_TestComponen34.new); + world.getPool(_TestComponen35.new); + world.getPool(_TestComponen36.new); + world.getPool(_TestComponen37.new); + world.getPool(_TestComponen38.new); + world.getPool(_TestComponen39.new); + world.getPool(_TestComponen40.new); + benchmark('World with 100k entities', () { + world.getPool(_TestComponent.new); + world.getPool(_TestComponen2.new); + world.getPool(_TestComponen3.new); + world.getPool(_TestComponen4.new); + world.getPool(_TestComponen5.new); + world.getPool(_TestComponen6.new); + world.getPool(_TestComponen7.new); + world.getPool(_TestComponen8.new); + world.getPool(_TestComponen9.new); + world.getPool(_TestComponen10.new); + world.getPool(_TestComponen11.new); + world.getPool(_TestComponen12.new); + world.getPool(_TestComponen13.new); + world.getPool(_TestComponen14.new); + world.getPool(_TestComponen15.new); + world.getPool(_TestComponen16.new); + world.getPool(_TestComponen17.new); + world.getPool(_TestComponen18.new); + world.getPool(_TestComponen19.new); + world.getPool(_TestComponen20.new); + world.getPool(_TestComponen21.new); + world.getPool(_TestComponen22.new); + world.getPool(_TestComponen23.new); + world.getPool(_TestComponen24.new); + world.getPool(_TestComponen25.new); + world.getPool(_TestComponen26.new); + world.getPool(_TestComponen27.new); + world.getPool(_TestComponen28.new); + world.getPool(_TestComponen29.new); + world.getPool(_TestComponen30.new); + world.getPool(_TestComponen31.new); + world.getPool(_TestComponen32.new); + world.getPool(_TestComponen33.new); + world.getPool(_TestComponen34.new); + world.getPool(_TestComponen35.new); + world.getPool(_TestComponen36.new); + world.getPool(_TestComponen37.new); + world.getPool(_TestComponen38.new); + world.getPool(_TestComponen39.new); + world.getPool(_TestComponen40.new); + }); + }); + group('without init', () { final world = World(); for (var i = 0; i < 100000; i++) { world.createEntity(); } + + benchmark('World with 100k entities', () { + world.getPool(_TestComponent.new); + world.getPool(_TestComponen2.new); + world.getPool(_TestComponen3.new); + world.getPool(_TestComponen4.new); + world.getPool(_TestComponen5.new); + world.getPool(_TestComponen6.new); + world.getPool(_TestComponen7.new); + world.getPool(_TestComponen8.new); + world.getPool(_TestComponen9.new); + world.getPool(_TestComponen10.new); + world.getPool(_TestComponen11.new); + world.getPool(_TestComponen12.new); + world.getPool(_TestComponen13.new); + world.getPool(_TestComponen14.new); + world.getPool(_TestComponen15.new); + world.getPool(_TestComponen16.new); + world.getPool(_TestComponen17.new); + world.getPool(_TestComponen18.new); + world.getPool(_TestComponen19.new); + world.getPool(_TestComponen20.new); + world.getPool(_TestComponen21.new); + world.getPool(_TestComponen22.new); + world.getPool(_TestComponen23.new); + world.getPool(_TestComponen24.new); + world.getPool(_TestComponen25.new); + world.getPool(_TestComponen26.new); + world.getPool(_TestComponen27.new); + world.getPool(_TestComponen28.new); + world.getPool(_TestComponen29.new); + world.getPool(_TestComponen30.new); + world.getPool(_TestComponen31.new); + world.getPool(_TestComponen32.new); + world.getPool(_TestComponen33.new); + world.getPool(_TestComponen34.new); + world.getPool(_TestComponen35.new); + world.getPool(_TestComponen36.new); + world.getPool(_TestComponen37.new); + world.getPool(_TestComponen38.new); + world.getPool(_TestComponen39.new); + world.getPool(_TestComponen40.new); + }); }); }); }); } + +class _TestComponent extends Component {} + +class _TestComponen2 extends Component {} + +class _TestComponen3 extends Component {} + +class _TestComponen4 extends Component {} + +class _TestComponen5 extends Component {} + +class _TestComponen6 extends Component {} + +class _TestComponen7 extends Component {} + +class _TestComponen8 extends Component {} + +class _TestComponen9 extends Component {} + +class _TestComponen10 extends Component {} + +class _TestComponen11 extends Component {} + +class _TestComponen12 extends Component {} + +class _TestComponen13 extends Component {} + +class _TestComponen14 extends Component {} + +class _TestComponen15 extends Component {} + +class _TestComponen16 extends Component {} + +class _TestComponen17 extends Component {} + +class _TestComponen18 extends Component {} + +class _TestComponen19 extends Component {} + +class _TestComponen20 extends Component {} + +class _TestComponen21 extends Component {} + +class _TestComponen22 extends Component {} + +class _TestComponen23 extends Component {} + +class _TestComponen24 extends Component {} + +class _TestComponen25 extends Component {} + +class _TestComponen26 extends Component {} + +class _TestComponen27 extends Component {} + +class _TestComponen28 extends Component {} + +class _TestComponen29 extends Component {} + +class _TestComponen30 extends Component {} + +class _TestComponen31 extends Component {} + +class _TestComponen32 extends Component {} + +class _TestComponen33 extends Component {} + +class _TestComponen34 extends Component {} + +class _TestComponen35 extends Component {} + +class _TestComponen36 extends Component {} + +class _TestComponen37 extends Component {} + +class _TestComponen38 extends Component {} + +class _TestComponen39 extends Component {} + +class _TestComponen40 extends Component {} diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..cb861b7 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:flame_lint/analysis_options.yaml + +analyzer: + errors: + prefer_relative_imports: false + always_use_package_imports: true + prefer_foreach: false \ No newline at end of file diff --git a/example/lib/components/atack_component.dart b/example/lib/components/atack_component.dart new file mode 100644 index 0000000..394f752 --- /dev/null +++ b/example/lib/components/atack_component.dart @@ -0,0 +1,3 @@ +import 'package:oxygen/oxygen.dart'; + +class AtackComponent extends Component {} diff --git a/example/lib/components/bullet_component.dart b/example/lib/components/bullet_component.dart new file mode 100644 index 0000000..6a7ced5 --- /dev/null +++ b/example/lib/components/bullet_component.dart @@ -0,0 +1,3 @@ +import 'package:oxygen/oxygen.dart'; + +class BulletComponent extends Component {} diff --git a/example/lib/components/bullet_move_component.dart b/example/lib/components/bullet_move_component.dart new file mode 100644 index 0000000..83b51c3 --- /dev/null +++ b/example/lib/components/bullet_move_component.dart @@ -0,0 +1,3 @@ +import 'package:oxygen/oxygen.dart'; + +class BulletMoveComponent extends Component {} diff --git a/example/lib/components/color_component.dart b/example/lib/components/color_component.dart index b72e25c..7baa0c2 100644 --- a/example/lib/components/color_component.dart +++ b/example/lib/components/color_component.dart @@ -1,4 +1,13 @@ import 'package:example/utils/color.dart'; + import 'package:oxygen/oxygen.dart'; -class ColorComponent extends ValueComponent {} +class ColorComponent extends Component { + Color? color; + + @override + void init([Color? color]) => this.color = color ?? Colors.white; + + @override + void reset() => color = null; +} diff --git a/example/lib/components/direction_component.dart b/example/lib/components/direction_component.dart index b745d7c..dd1f257 100644 --- a/example/lib/components/direction_component.dart +++ b/example/lib/components/direction_component.dart @@ -1,10 +1,34 @@ +import 'package:example/utils/vector2.dart'; + import 'package:oxygen/oxygen.dart'; -enum Direction { - up, - down, - left, - right, +class DirectionComponent extends Component { + late Direction direction; + + @override + void init([Direction direction = Direction.idle]) { + this.direction = direction; + } + + @override + void reset() => direction = Direction.idle; } -class DirectionComponent extends ValueComponent {} +enum Direction { up, down, left, right, idle } + +extension DirectionVector on Direction { + Vector2 toVector() { + switch (this) { + case Direction.up: + return const Vector2(0, -1); + case Direction.down: + return const Vector2(0, 1); + case Direction.left: + return const Vector2(-1, 0); + case Direction.right: + return const Vector2(1, 0); + default: + return const Vector2.zero(); + } + } +} diff --git a/example/lib/components/name_component.dart b/example/lib/components/name_component.dart index 191859d..98b3b8c 100644 --- a/example/lib/components/name_component.dart +++ b/example/lib/components/name_component.dart @@ -1,3 +1,10 @@ import 'package:oxygen/oxygen.dart'; -class NameComponent extends ValueComponent {} +class NameComponent extends Component { + String? name; + @override + void init([String? name]) => this.name = name ?? ''; + + @override + void reset() => name = null; +} diff --git a/example/lib/components/player_component.dart b/example/lib/components/player_component.dart index 881bb00..abded32 100644 --- a/example/lib/components/player_component.dart +++ b/example/lib/components/player_component.dart @@ -1,3 +1,3 @@ import 'package:oxygen/oxygen.dart'; -class PlayerComponent extends ValueComponent {} +class PlayerComponent extends Component {} diff --git a/example/lib/components/position_component.dart b/example/lib/components/position_component.dart index 7ed68ba..0616211 100644 --- a/example/lib/components/position_component.dart +++ b/example/lib/components/position_component.dart @@ -1,7 +1,8 @@ import 'package:example/utils/vector2.dart'; + import 'package:oxygen/oxygen.dart'; -class PositionComponent extends Component { +class PositionComponent extends Component { int? x; int? y; diff --git a/example/lib/components/render_component.dart b/example/lib/components/render_component.dart index 62af53b..4f673f8 100644 --- a/example/lib/components/render_component.dart +++ b/example/lib/components/render_component.dart @@ -1,3 +1,11 @@ import 'package:oxygen/oxygen.dart'; -class RenderComponent extends ValueComponent {} +class RenderComponent extends Component { + String? char; + + @override + void init([String? char]) => this.char = char ?? ''; + + @override + void reset() => char = null; +} diff --git a/example/lib/components/unit_move_component.dart b/example/lib/components/unit_move_component.dart new file mode 100644 index 0000000..fc09188 --- /dev/null +++ b/example/lib/components/unit_move_component.dart @@ -0,0 +1,3 @@ +import 'package:oxygen/oxygen.dart'; + +class UnitMoveComponent extends Component {} diff --git a/example/lib/components/velocity_component.dart b/example/lib/components/velocity_component.dart deleted file mode 100644 index 91da662..0000000 --- a/example/lib/components/velocity_component.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:example/utils/vector2.dart'; -import 'package:oxygen/oxygen.dart'; - -class VelocityComponent extends Component { - int? x; - int? y; - - @override - void init([Vector2? data]) { - x = data?.x ?? 0; - y = data?.y ?? 0; - } - - @override - void reset() { - x = y = null; - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index d30c85f..84911ec 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,59 +1,40 @@ -import 'package:example/components/color_component.dart'; -import 'package:example/components/direction_component.dart'; -import 'package:example/components/name_component.dart'; -import 'package:example/components/player_component.dart'; -import 'package:example/components/velocity_component.dart'; -import 'package:example/systems/player_move_system.dart'; -import 'package:example/utils/color.dart'; -import 'package:example/utils/game.dart'; -import 'package:example/utils/vector2.dart'; -import 'package:example/utils/terminal.dart'; import 'package:oxygen/oxygen.dart'; -import 'systems/move_system.dart'; +import 'systems/bullet_move_system.dart'; +import 'systems/bullet_spawner_system.dart'; +import 'systems/clean_atack_system.dart'; +import 'systems/clean_bullet_system.dart'; +import 'systems/clean_move_command_system.dart'; +import 'systems/player_atack_input_system.dart'; +import 'systems/player_init_system.dart'; +import 'systems/player_move_input_system.dart'; import 'systems/render_system.dart'; -import 'components/position_component.dart'; -import 'components/render_component.dart'; +import 'systems/unit_move_system.dart'; +import 'utils/game.dart'; void main() => ExampleGame(); class ExampleGame extends Game { late World world; + late Systems systems; @override void onLoad() { world = World(); - - world.registerSystem(PlayerMoveSystem()); - world.registerSystem(MoveSystem()); - world.registerSystem(RenderSystem()); - world.registerComponent(() => DirectionComponent()); - world.registerComponent(() => VelocityComponent()); - world.registerComponent(() => PositionComponent()); - world.registerComponent(() => PlayerComponent()); - world.registerComponent(() => RenderComponent()); - world.registerComponent(() => ColorComponent()); - world.registerComponent(() => NameComponent()); - - world.createEntity() - ..add('Boi') - ..add(Direction.right) - ..add(Colors.red) - ..add('█') - ..add( - terminal.viewport.topRight.translate(-terminal.viewport.center.x, 0), - ); - - world.createEntity() - ..add('Tim') - ..add(Direction.right) - ..add() - ..add('█') - ..add(terminal.viewport.center); - - world.init(); + systems = Systems(world) + ..add(PlayerInitSystem()) + ..add(PlayerMoveInputSystem()) + ..add(PlayerAtackInputSystem()) + ..add(UnitMoveSystem()) + ..add(BulletSpawnerSystem()) + ..add(BulletMoveSystem()) + ..add(CleanUnitMoveSystem()) + ..add(CleanAtackSystem()) + ..add(CleanBulletSystem()) + ..add(RenderSystem()) + ..init(); } @override - void update(double delta) => world.execute(delta); + void update(double delta) => systems.run(delta); } diff --git a/example/lib/systems/bullet_move_system.dart b/example/lib/systems/bullet_move_system.dart new file mode 100644 index 0000000..606f44b --- /dev/null +++ b/example/lib/systems/bullet_move_system.dart @@ -0,0 +1,37 @@ +import 'package:example/components/bullet_move_component.dart'; +import 'package:example/components/direction_component.dart'; +import 'package:example/components/position_component.dart'; + +import 'package:oxygen/oxygen.dart'; + +class BulletMoveSystem implements RunSystem, InitSystem { + late final ComponentPool _positionPool; + late final ComponentPool _directionPool; + + @override + void init(Systems systems) { + _positionPool = systems.world.getPool( + PositionComponent.new, + ); + _directionPool = systems.world.getPool( + DirectionComponent.new, + ); + } + + @override + void run(Systems systems, double delta) { + final filter = systems.world + .filter(DirectionComponent.new) + .include(PositionComponent.new) + .include(BulletMoveComponent.new) + .end(); + + for (final entity in filter) { + final position = _positionPool.get(entity); + final direction = _directionPool.get(entity).direction.toVector(); + + position.x = position.x! + direction.x; + position.y = position.y! + direction.y; + } + } +} diff --git a/example/lib/systems/bullet_spawner_system.dart b/example/lib/systems/bullet_spawner_system.dart new file mode 100644 index 0000000..e0da127 --- /dev/null +++ b/example/lib/systems/bullet_spawner_system.dart @@ -0,0 +1,51 @@ +import 'package:example/components/atack_component.dart'; +import 'package:example/components/bullet_component.dart'; +import 'package:example/components/bullet_move_component.dart'; +import 'package:example/components/direction_component.dart'; +import 'package:example/components/position_component.dart'; +import 'package:example/components/render_component.dart'; + +import 'package:oxygen/oxygen.dart'; + +class BulletSpawnerSystem implements RunSystem, InitSystem { + late final ComponentPool _positionPool; + late final ComponentPool _directionPool; + late final ComponentPool _bulletMovePool; + late final ComponentPool _bulletPool; + late final ComponentPool _renderPool; + + @override + void init(Systems systems) { + _positionPool = systems.world.getPool(PositionComponent.new); + _directionPool = systems.world.getPool(DirectionComponent.new); + _bulletMovePool = systems.world.getPool(BulletMoveComponent.new); + _bulletPool = systems.world.getPool(BulletComponent.new); + _renderPool = systems.world.getPool(RenderComponent.new); + } + + @override + void run(Systems systems, double dt) { + final filter = systems.world + .filter(PositionComponent.new) + .include(AtackComponent.new) + .include(DirectionComponent.new) + .end(); + + for (final entity in filter) { + final position = _positionPool.get(entity); + final direction = _directionPool.get(entity).direction; + final spawnPosition = direction.toVector().translate( + position.x!, + position.y!, + ); + print(direction); + + final bullet = systems.world.createEntity(); + _bulletPool.add(bullet); + _bulletMovePool.add(bullet); + _positionPool.add(bullet).init(spawnPosition); + _directionPool.add(bullet).init(direction); + _renderPool.add(bullet).init('🥚'); + } + } +} diff --git a/example/lib/systems/clean_atack_system.dart b/example/lib/systems/clean_atack_system.dart new file mode 100644 index 0000000..082880e --- /dev/null +++ b/example/lib/systems/clean_atack_system.dart @@ -0,0 +1,21 @@ +import 'package:example/components/atack_component.dart'; + +import 'package:oxygen/oxygen.dart'; + +class CleanAtackSystem implements RunSystem, InitSystem { + late final ComponentPool _atackPool; + + @override + void init(Systems systems) { + _atackPool = systems.world.getPool(AtackComponent.new); + } + + @override + void run(Systems systems, double delta) { + final filter = systems.world.filter(AtackComponent.new).end(); + + for (final entity in filter) { + _atackPool.delete(entity); + } + } +} diff --git a/example/lib/systems/clean_bullet_system.dart b/example/lib/systems/clean_bullet_system.dart new file mode 100644 index 0000000..cd88e07 --- /dev/null +++ b/example/lib/systems/clean_bullet_system.dart @@ -0,0 +1,28 @@ +import 'package:example/components/bullet_component.dart'; +import 'package:example/components/position_component.dart'; +import 'package:example/utils/terminal.dart'; +import 'package:example/utils/vector2.dart'; + +import 'package:oxygen/oxygen.dart'; + +class CleanBulletSystem implements RunSystem, InitSystem { + late final ComponentPool _positionPool; + + @override + void init(Systems systems) { + _positionPool = systems.world.getPool(PositionComponent.new); + } + + @override + void run(Systems systems, double delta) { + final filter = systems.world.filter(BulletComponent.new).end(); + + for (final entity in filter) { + final position = _positionPool.get(entity); + + if (!terminal.viewport.contains(Vector2(position.x!, position.y!))) { + systems.world.deleteEntity(entity); + } + } + } +} diff --git a/example/lib/systems/clean_move_command_system.dart b/example/lib/systems/clean_move_command_system.dart new file mode 100644 index 0000000..c003754 --- /dev/null +++ b/example/lib/systems/clean_move_command_system.dart @@ -0,0 +1,20 @@ +import 'package:example/components/unit_move_component.dart'; +import 'package:oxygen/oxygen.dart'; + +class CleanUnitMoveSystem implements RunSystem, InitSystem { + late final ComponentPool _moveCommandPool; + + @override + void init(Systems systems) { + _moveCommandPool = systems.world.getPool(UnitMoveComponent.new); + } + + @override + void run(Systems systems, double delta) { + final filter = systems.world.filter(UnitMoveComponent.new).end(); + + for (final entity in filter) { + _moveCommandPool.delete(entity); + } + } +} diff --git a/example/lib/systems/move_system.dart b/example/lib/systems/move_system.dart deleted file mode 100644 index 6fa0f5b..0000000 --- a/example/lib/systems/move_system.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:example/components/position_component.dart'; -import 'package:example/components/velocity_component.dart'; -import 'package:example/utils/terminal.dart'; -import 'package:example/utils/vector2.dart'; -import 'package:oxygen/oxygen.dart'; - -class MoveSystem extends System { - late Query query; - - @override - void init() { - query = createQuery([ - Has(), - Has(), - ]); - } - - @override - void execute(delta) { - for (final entity in query.entities) { - final position = entity.get()!; - final velocity = entity.get()!; - - position.x = position.x! + velocity.x!; - position.y = position.y! + velocity.y!; - - if (!terminal.viewport.contains(Vector2(position.x!, position.y!))) { - entity.dispose(); - } - } - } -} diff --git a/example/lib/systems/player_atack_input_system.dart b/example/lib/systems/player_atack_input_system.dart new file mode 100644 index 0000000..6b4d044 --- /dev/null +++ b/example/lib/systems/player_atack_input_system.dart @@ -0,0 +1,25 @@ +import 'package:example/components/atack_component.dart'; +import 'package:example/components/player_component.dart'; +import 'package:example/utils/keyboard.dart'; + +import 'package:oxygen/oxygen.dart'; + +class PlayerAtackInputSystem implements RunSystem, InitSystem { + late final ComponentPool _atackPool; + + @override + void init(Systems systems) { + _atackPool = systems.world.getPool(AtackComponent.new); + } + + @override + void run(Systems systems, double dt) { + final filter = systems.world.filter(PlayerComponent.new).end(); + + for (final entity in filter) { + if (keyboard.isPressed(Key.space)) { + _atackPool.add(entity); + } + } + } +} diff --git a/example/lib/systems/player_init_system.dart b/example/lib/systems/player_init_system.dart new file mode 100644 index 0000000..03b55d2 --- /dev/null +++ b/example/lib/systems/player_init_system.dart @@ -0,0 +1,26 @@ +import 'package:example/components/direction_component.dart'; +import 'package:example/components/name_component.dart'; +import 'package:example/components/player_component.dart'; +import 'package:example/components/position_component.dart'; +import 'package:example/components/render_component.dart'; +import 'package:example/utils/terminal.dart'; + +import 'package:oxygen/oxygen.dart'; + +class PlayerInitSystem implements InitSystem { + @override + void init(Systems systems) { + final player = systems.world.createEntity(); + systems.world.getPool(PlayerComponent.new).add(player); + systems.world.getPool(NameComponent.new).add(player).init('Tim'); + systems.world.getPool(RenderComponent.new).add(player).init('🐥'); + systems.world + .getPool(DirectionComponent.new) + .add(player) + .init(Direction.right); + systems.world + .getPool(PositionComponent.new) + .add(player) + .init(terminal.viewport.center); + } +} diff --git a/example/lib/systems/player_move_input_system.dart b/example/lib/systems/player_move_input_system.dart new file mode 100644 index 0000000..529c15a --- /dev/null +++ b/example/lib/systems/player_move_input_system.dart @@ -0,0 +1,42 @@ +import 'package:example/components/direction_component.dart'; +import 'package:example/components/player_component.dart'; +import 'package:example/components/unit_move_component.dart'; +import 'package:example/utils/keyboard.dart'; + +import 'package:oxygen/oxygen.dart'; + +class PlayerMoveInputSystem implements RunSystem, InitSystem { + late final ComponentPool _moveCommandPool; + late final ComponentPool _directionPool; + + @override + void init(Systems systems) { + _moveCommandPool = systems.world.getPool(UnitMoveComponent.new); + _directionPool = systems.world.getPool(DirectionComponent.new); + } + + @override + void run(Systems systems, double dt) { + final filter = systems.world + .filter(PlayerComponent.new) + .include(DirectionComponent.new) + .end(); + + for (final entity in filter) { + if (keyboard.isPressed(Key.w)) { + _setDirection(entity, Direction.up); + } else if (keyboard.isPressed(Key.s)) { + _setDirection(entity, Direction.down); + } else if (keyboard.isPressed(Key.a)) { + _setDirection(entity, Direction.left); + } else if (keyboard.isPressed(Key.d)) { + _setDirection(entity, Direction.right); + } + } + } + + void _setDirection(Entity entity, Direction direction) { + _moveCommandPool.add(entity); + _directionPool.get(entity).init(direction); + } +} diff --git a/example/lib/systems/player_move_system.dart b/example/lib/systems/player_move_system.dart deleted file mode 100644 index 38d5449..0000000 --- a/example/lib/systems/player_move_system.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:example/components/color_component.dart'; -import 'package:example/components/direction_component.dart'; -import 'package:example/components/player_component.dart'; -import 'package:example/components/position_component.dart'; -import 'package:example/components/render_component.dart'; -import 'package:example/components/velocity_component.dart'; -import 'package:example/utils/color.dart'; -import 'package:example/utils/keyboard.dart'; -import 'package:example/utils/vector2.dart'; -import 'package:oxygen/oxygen.dart'; - -class PlayerMoveSystem extends System { - late Query query; - - late int _nextShot; - - @override - void init() { - _nextShot = 0; - query = createQuery([ - Has(), - Has(), - Has(), - ]); - } - - void setDirection(Direction dir, Entity entity) { - query.entities.first.get()!.value = dir; - } - - Direction getDirection(Entity entity) { - return query.entities.first.get()!.value!; - } - - @override - void execute(delta) { - final player = query.entities.first; - final position = player.get()!; - - if (keyboard.isPressed(Key.w)) { - position.y = position.y! - 1; - setDirection(Direction.up, player); - } else if (keyboard.isPressed(Key.s)) { - position.y = position.y! + 1; - setDirection(Direction.down, player); - } else if (keyboard.isPressed(Key.a)) { - position.x = position.x! - 1; - setDirection(Direction.left, player); - } else if (keyboard.isPressed(Key.d)) { - position.x = position.x! + 1; - setDirection(Direction.right, player); - } - - if (keyboard.isPressed(Key.space) && - _nextShot < DateTime.now().millisecondsSinceEpoch) { - _nextShot = DateTime.now().millisecondsSinceEpoch + 500; - final direction = getDirection(player); - world!.createEntity() - ..add( - Vector2( - direction == Direction.left - ? -1 - : direction == Direction.right - ? 1 - : 0, - direction == Direction.up - ? -1 - : direction == Direction.down - ? 1 - : 0, - ), - ) - ..add('♥') - ..add(Colors.red) - ..add(Vector2(position.x!, position.y!)); - } - } -} diff --git a/example/lib/systems/render_system.dart b/example/lib/systems/render_system.dart index 4368365..39ffc41 100644 --- a/example/lib/systems/render_system.dart +++ b/example/lib/systems/render_system.dart @@ -1,49 +1,51 @@ -import 'package:example/utils/color.dart'; import 'package:example/components/color_component.dart'; import 'package:example/components/name_component.dart'; +import 'package:example/components/position_component.dart'; +import 'package:example/components/render_component.dart'; +import 'package:example/utils/color.dart'; import 'package:example/utils/terminal.dart'; import 'package:example/utils/vector2.dart'; import 'package:oxygen/oxygen.dart'; -import '../components/render_component.dart'; -import '../components/position_component.dart'; - -class RenderSystem extends System { - late Query query; - +class RenderSystem implements RunSystem { @override - void init() { - query = createQuery([ - Has(), - Has(), - ]); - } + void run(Systems systems, double dt) { + final filter = systems.world + .filter(RenderComponent.new) + .include(PositionComponent.new) + .end(); + final positionPool = systems.world.getPool(PositionComponent.new); + final renderPool = systems.world.getPool(RenderComponent.new); + final colorPool = systems.world.getPool(ColorComponent.new); + final namePool = systems.world.getPool(NameComponent.new); - @override - void execute(delta) { - query.entities.forEach((entity) { - final position = entity.get()!; - final key = entity.get()!.value; - final color = entity.get()?.value ?? Colors.white; + for (final entity in filter) { + final position = positionPool.get(entity); + final key = renderPool.get(entity).char ?? ''; + var color = Colors.white; + if (colorPool.has(entity)) { + color = colorPool.get(entity).color!; + } terminal ..save() ..translate(position.x!, position.y!) - ..draw(key!, foregroundColor: color); - if (entity.has()) { - final name = entity.get()!.value!; + ..draw(key, foregroundColor: color); + + if (namePool.has(entity)) { + final name = namePool.get(entity).name!; terminal ..translate(-(name.length ~/ 2), 1) ..draw(name); } terminal.restore(); - }); + } - terminal.draw('delta: $delta', foregroundColor: Colors.green); + terminal.draw('delta: $dt', foregroundColor: Colors.green); terminal.draw( - 'entites: ${world!.entities.length}', + 'entites: ${systems.world.entitiesCount}', foregroundColor: Colors.green, - position: Vector2(0, 1), + position: const Vector2(0, 1), ); terminal.draw( ' W A S D | Move Tim\n' diff --git a/example/lib/systems/unit_move_system.dart b/example/lib/systems/unit_move_system.dart new file mode 100644 index 0000000..bfce910 --- /dev/null +++ b/example/lib/systems/unit_move_system.dart @@ -0,0 +1,33 @@ +import 'package:example/components/direction_component.dart'; +import 'package:example/components/position_component.dart'; +import 'package:example/components/unit_move_component.dart'; + +import 'package:oxygen/oxygen.dart'; + +class UnitMoveSystem implements RunSystem, InitSystem { + late final ComponentPool _positionPool; + late final ComponentPool _directionPool; + + @override + void init(Systems systems) { + _positionPool = systems.world.getPool(PositionComponent.new); + _directionPool = systems.world.getPool(DirectionComponent.new); + } + + @override + void run(Systems systems, double delta) { + final filter = systems.world + .filter(DirectionComponent.new) + .include(PositionComponent.new) + .include(UnitMoveComponent.new) + .end(); + + for (final entity in filter) { + final position = _positionPool.get(entity); + final direction = _directionPool.get(entity).direction.toVector(); + + position.x = position.x! + direction.x; + position.y = position.y! + direction.y; + } + } +} diff --git a/example/lib/utils/game.dart b/example/lib/utils/game.dart index 4b76684..63905ad 100644 --- a/example/lib/utils/game.dart +++ b/example/lib/utils/game.dart @@ -1,8 +1,8 @@ -import 'package:example/utils/keyboard.dart'; -import 'package:example/utils/terminal.dart'; +import 'keyboard.dart'; +import 'terminal.dart'; -const TARGET_FPS = 120; -const FRAME_TIME = 1000 ~/ TARGET_FPS; +const kTargetFps = 120; +const kFrameTime = 1000 ~/ kTargetFps; abstract class Game { final Stopwatch _stopwatch; @@ -22,14 +22,15 @@ abstract class Game { final elapsed = current - _previous; _previous = current; - update(elapsed / FRAME_TIME); + update(elapsed / kFrameTime); keyboard.clear(); terminal.clear(); terminal.render(); - Future.delayed(Duration(milliseconds: FRAME_TIME)).then((value) => loop()); + Future.delayed(const Duration(milliseconds: kFrameTime)) + .then((_) => loop()); } void onLoad(); diff --git a/example/pubspec.lock b/example/pubspec.lock index 512eb50..9e4c5ca 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,26 +1,26 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + flame_lint: + dependency: "direct dev" + description: + name: flame_lint + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" oxygen: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.1.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" + version: "0.2.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ddd1360..b97a9ef 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,11 +2,11 @@ name: example description: A simple Oxygen example. environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.16.0 <3.0.0' dependencies: oxygen: path: ../ dev_dependencies: - pedantic: ^1.11.0 \ No newline at end of file + flame_lint: ^0.0.1 \ No newline at end of file diff --git a/lib/oxygen.dart b/lib/oxygen.dart index 0f78c7a..e0d79d4 100644 --- a/lib/oxygen.dart +++ b/lib/oxygen.dart @@ -1,20 +1,12 @@ /// A lightweight Entity Component System written in Dart. library oxygen; -import 'dart:collection'; - -import 'package:meta/meta.dart'; - -part 'src/component/component_manager.dart'; -part 'src/component/component.dart'; -part 'src/component/value_component.dart'; -part 'src/entity/entity_manager.dart'; -part 'src/entity/entity.dart'; -part 'src/pooling/object_pool.dart'; -part 'src/pooling/pool_object.dart'; -part 'src/query/filter.dart'; -part 'src/query/query_manager.dart'; -part 'src/query/query.dart'; -part 'src/system/system_manager.dart'; -part 'src/system/system.dart'; -part 'src/world.dart'; +export 'src/components/component.dart'; +export 'src/components/component_pool.dart'; +export 'src/entity/entity.dart'; +export 'src/entity/entity_data.dart'; +export 'src/filter/filter.dart'; +export 'src/mask/mask.dart'; +export 'src/system/system.dart'; +export 'src/world.dart'; +export 'src/world_config.dart'; diff --git a/lib/src/component/component.dart b/lib/src/component/component.dart deleted file mode 100644 index e8ed46e..0000000 --- a/lib/src/component/component.dart +++ /dev/null @@ -1,6 +0,0 @@ -part of oxygen; - -/// A [Component] is a way to store data for an [Entity]. -/// -/// It does not define any kind of behaviour because that is handled by the systems. -abstract class Component extends PoolObject {} diff --git a/lib/src/component/component_manager.dart b/lib/src/component/component_manager.dart deleted file mode 100644 index 0d467bd..0000000 --- a/lib/src/component/component_manager.dart +++ /dev/null @@ -1,48 +0,0 @@ -part of oxygen; - -typedef ComponentBuilder = T Function(); - -/// An [ObjectPool] for a type of [Component]. -class ComponentPool extends ObjectPool { - final ComponentBuilder componentBuilder; - - ComponentPool(this.componentBuilder) : super(); - - @override - T builder() => componentBuilder(); -} - -/// Manages all the components in a [World]. -class ComponentManager { - /// The [World] which this manager belongs to. - final World world; - - /// List of registered components. - final List components = []; - - /// Map of [ObjectPool]s for each kind of registered [Component]. - final Map _componentPool = {}; - - ComponentManager(this.world); - - /// Check if a component is registered. - bool hasComponent() => components.contains(T); - - /// Register a component. - /// - /// If a component is already registered it will just return. - /// - /// The [builder] is used for pooling. - void registerComponent(T Function() builder) { - if (components.contains(T)) { - return; - } - - components.add(T); - _componentPool[T] = ComponentPool(builder); - } - - ComponentPool getComponentPool, V>() { - return _componentPool[T] as ComponentPool; - } -} diff --git a/lib/src/component/value_component.dart b/lib/src/component/value_component.dart deleted file mode 100644 index b690539..0000000 --- a/lib/src/component/value_component.dart +++ /dev/null @@ -1,12 +0,0 @@ -part of oxygen; - -/// With the [ValueComponent] you can easily define single value components. -class ValueComponent extends Component { - T? value; - - @override - void init([T? data]) => value = data; - - @override - void reset() => value = null; -} diff --git a/lib/src/components/component.dart b/lib/src/components/component.dart new file mode 100644 index 0000000..7e7951c --- /dev/null +++ b/lib/src/components/component.dart @@ -0,0 +1,18 @@ +import '../entity/entity.dart'; + +/// A [Component] is a way to store data for an [Entity]. +/// +/// It does not define any kind of behaviour because that is handled by the systems. +abstract class Component { + void init() {} + void reset() {} +} + +abstract class ComponentValue extends Component { + T? value; + + @override + void init() {} + @override + void reset() {} +} diff --git a/lib/src/components/component_pool.dart b/lib/src/components/component_pool.dart new file mode 100644 index 0000000..c3b4482 --- /dev/null +++ b/lib/src/components/component_pool.dart @@ -0,0 +1,126 @@ +import 'dart:typed_data'; + +import './component.dart'; +import '../entity/entity.dart'; +import '../helpers.dart'; +import '../world.dart'; + +typedef ComponentBuilder = T Function(); +typedef PoolBuilder = ComponentPool Function(); + +/// A ComponentPool is a way to store data for an [Entity]. +/// +/// Provides an api to add/request/remove components on an entity. +/// +/// https://austinmorlan.com/posts/entity_component_system/#the-component-array +class ComponentPool { + final int _id; + final World _world; + + /// Used to create a list of [_components] when a ComponentPool is + /// initialized, or when all elements from [_components] are assigned to + /// entities. + final ComponentBuilder _componentBuilder; + + /// List of components. Each element can be attached to one entity. + /// + /// Element index of 0 is reserved and is not used. + final List _components; + int _componentsCount = 1; + + /// Stores the index of the [Component] that is attached to the entity. + /// + /// The index is an entity. The value is the index of the [Component] stored + /// in [_components]. + Uint32List _entityToComponentIndex; + Uint32List _recycledComponentIndexes; + int _recycledIndexesCount = 0; + + int get id => _id; + + ComponentPool( + World world, + ComponentBuilder componentBuilder, + int id, + int entities, + int recycledEntities, + ) : _world = world, + _componentBuilder = componentBuilder, + _id = id, + _components = List.generate( + entities + 1, + (index) => componentBuilder(), + ), + _entityToComponentIndex = Uint32List(entities), + _recycledComponentIndexes = Uint32List(recycledEntities); + + T add(Entity entity) { + assert(_world.isEntityAliveInternal(entity), 'Entity was removed.'); + assert( + _entityToComponentIndex[entity] == 0, + 'Component $T already attached to entity.', + ); + + int index; + if (_recycledIndexesCount > 0) { + index = _recycledComponentIndexes[--_recycledIndexesCount]; + } else { + index = _componentsCount; + // Fill when _components is filled out. + if (_componentsCount == _components.length) { + _components.addAll( + Iterable.generate(_componentsCount, (i) => _componentBuilder()), + ); + } + } + _componentsCount++; + _components[index].init(); + _entityToComponentIndex[entity] = index; + _world.onEntityChangeInternal(entity, _id, true); + _world.entities[entity].componentsCount++; + + return _components[index]; + } + + void delete(Entity entity) { + assert(_world.isEntityAliveInternal(entity), 'Entity was removed.'); + + final recycledIndex = _entityToComponentIndex[entity]; + if (recycledIndex > 0) { + _world.onEntityChangeInternal(entity, _id, false); + // Fill when _recycledComponentIndexes is filled out. + if (_recycledIndexesCount == _recycledComponentIndexes.length) { + _recycledComponentIndexes = _recycledComponentIndexes.resize(); + } + _recycledComponentIndexes[_recycledIndexesCount++] = recycledIndex; + _components[recycledIndex].reset(); + _entityToComponentIndex[entity] = 0; + final entityData = _world.entities[entity]; + entityData.componentsCount--; + _componentsCount--; + if (entityData.componentsCount == 0) { + _world.deleteEntity(entity); + } + } + } + + T get(Entity entity) { + assert(_world.isEntityAliveInternal(entity), 'Entity was removed.'); + assert( + _entityToComponentIndex[entity] != 0, + 'Cant get $T. Component not attached', + ); + + return _components[_entityToComponentIndex[entity]]; + } + + bool has(Entity entity) { + assert(_world.isEntityAliveInternal(entity), 'Entity was removed.'); + + return _entityToComponentIndex[entity] > 0; + } + + void resize(int capacity) { + _entityToComponentIndex = _entityToComponentIndex.resize(capacity); + } +} diff --git a/lib/src/entity/entity.dart b/lib/src/entity/entity.dart index 6368851..3b009c2 100644 --- a/lib/src/entity/entity.dart +++ b/lib/src/entity/entity.dart @@ -1,77 +1,4 @@ -part of oxygen; - /// An Entity is a simple "container" for components. /// /// It serves no purpose apart from being an abstraction container around the components. -class Entity extends PoolObject { - /// The manager that handles all the entities. - final EntityManager _entityManager; - - /// Map of all the components added. - final Map _components = {}; - - final List _componentsToRemove = []; - - /// Set of all the component types that are added. - final Set _componentTypes = {}; - - /// Internal identifier. - int? id; - - /// Indication if this entity is no longer "in this world". - bool alive = false; - - /// Optional name to identify an entity by. - String? name; - - Entity(this._entityManager) : id = _entityManager._nextEntityId++; - - /// Retrieves a Component by Type. - /// - /// If the component is not registered, it will return `null`. - T? get() { - assert( - T != Component || T != ValueComponent, - 'An implemented Component was expected', - ); - return _components[T] as T?; - } - - /// Check if a component is added. - bool has() => _componentTypes.contains(T); - - /// Add a component. - void add, V>([V? data]) { - assert( - T != Component || T != ValueComponent, - 'An implemented Component was expected', - ); - _entityManager.addComponentToEntity(this, data); - } - - /// Remove a component. - void remove() { - assert( - T != Component || T != ValueComponent, - 'An implemented Component was expected', - ); - _entityManager.removeComponentFromEntity(this); - } - - @override - void init([String? name]) { - alive = true; - this.name = name; - } - - @override - void reset() { - id = null; - alive = false; - _components.clear(); - _componentTypes.clear(); - } - - @override - void dispose() => _entityManager.removeEntity(this); -} +typedef Entity = int; diff --git a/lib/src/entity/entity_data.dart b/lib/src/entity/entity_data.dart new file mode 100644 index 0000000..c9ae93a --- /dev/null +++ b/lib/src/entity/entity_data.dart @@ -0,0 +1,8 @@ +class EntityData { + bool isAlive; + + /// Number of components attached to the given entity. + int componentsCount; + + EntityData({this.isAlive = false, this.componentsCount = 0}); +} diff --git a/lib/src/entity/entity_manager.dart b/lib/src/entity/entity_manager.dart deleted file mode 100644 index 2a2911e..0000000 --- a/lib/src/entity/entity_manager.dart +++ /dev/null @@ -1,169 +0,0 @@ -part of oxygen; - -/// ObjectPool for entities. -class EntityPool extends ObjectPool { - /// The manager that handles all the entities. - EntityManager entityManager; - - EntityPool(this.entityManager) : super(); - - @override - Entity builder() => Entity(entityManager); -} - -/// Manages all the entities in a [World]. -/// -/// **Note**: Technically speaking we can have multiple types of entities, there -/// is nothing implemented on the [World] for it yet but this manager would be -/// able to handle that easily. -class EntityManager { - /// The [World] which this manager belongs to. - final World world; - - /// Active entities in the [world]. - final List _entities = []; - - /// Entities that have components that should be removed. - final List _entitiesWithRemovedComponents = []; - - /// Entities that are ready to be removed. - final List _entitiesToRemove = []; - - /// Entities with names are easily accesable this way. - final Map _entitiesByName = {}; - - /// The pool from which entities are pulled and released into. - late EntityPool _entityPool; - - /// The next identifier for an [Entity]. - int _nextEntityId = 0; - - /// [QueryManager] that handles the queries. - late QueryManager _queryManager; - - EntityManager(this.world) { - _entityPool = EntityPool(this); - _queryManager = QueryManager(this); - } - - /// Get an entity by name. - Entity? getEntityByName(String name) => _entitiesByName[name]; - - /// Create a new entity. - /// - /// Will acquire a new entity from the pool, and initialize it. - Entity createEntity([String? name]) { - final entity = _entityPool.acquire(name); - entity.id = _nextEntityId++; - - if (name != null) { - _entitiesByName[name] = entity; - } - - _entities.add(entity); - return entity; - } - - /// Add given component to an entity. - /// - /// If the entity already has that component it will just return. - /// - /// The [data] argument has to be of the type [V]. - void addComponentToEntity, V>(Entity entity, V? data) { - assert(T != Component, 'An implemented Component was expected'); - assert( - world.componentManager.components.contains(T), - 'Component $T has not been registered to the World', - ); - - if (entity._componentTypes.contains(T)) { - return; // Entity already has an instance of the component. - } - - final componentPool = world.componentManager.getComponentPool(); - final component = componentPool.acquire(data); - - entity._componentTypes.add(T); - entity._components[T] = component; - _queryManager._onComponentAddedToEntity(entity, T); - } - - /// Remove and dispose a component by generics. - void removeComponentFromEntity(Entity entity) { - assert(T != Component, 'An implemented Component was expected'); - assert( - world.componentManager.components.contains(T), - 'Component $T has not been registered to the World', - ); - return _markComponentForRemoval(entity, T); - } - - /// Mark a component for removal. - void _markComponentForRemoval(Entity entity, Type componentType) { - if (!entity._componentTypes.contains(componentType) || - entity._componentsToRemove.contains(componentType)) { - return; - } - _entitiesWithRemovedComponents.add(entity); - entity._componentsToRemove.add(componentType); - } - - /// Remove and dispose a component. - void _removeComponentFromEntity(Entity entity, Type componentType) { - if (!entity._componentTypes.contains(componentType)) { - return; - } - - entity._componentTypes.remove(componentType); - final component = entity._components.remove(componentType); - component?.dispose(); - - _queryManager._onComponentRemovedFromEntity(entity, componentType); - } - - /// Removes all the components the given entity has. - void _removeAllComponentFromEntity(Entity entity) { - // Make a copy so we can update this set while looping over it. - final componentsToRemove = entity._componentsToRemove.toSet(); - for (final componentType in componentsToRemove) { - _removeComponentFromEntity(entity, componentType); - } - - // Make a copy so we can update this set while looping over it. - final componentTypes = entity._componentTypes.toSet(); - for (final componentType in componentTypes) { - _removeComponentFromEntity(entity, componentType); - } - } - - /// Mark an entity for removal. - /// - /// It will be fully removed in the next execute cycle. - void removeEntity(Entity entity) { - if (!_entities.contains(entity) || _entitiesToRemove.contains(entity)) { - return; - } - - entity.alive = false; - - _entitiesToRemove.add(entity); - } - - /// Process all removed entities from the last execute cycle. - void processRemovedEntities() { - _entitiesToRemove.forEach(_releaseEntity); - _entitiesToRemove.clear(); - } - - /// Fully release and reset an entity. - void _releaseEntity(Entity entity) { - _removeAllComponentFromEntity(entity); - _queryManager._onEntityRemoved(entity); - - _entities.remove(entity); - if (_entitiesByName.containsKey(entity.name)) { - _entitiesByName.remove(entity.name); - } - entity._pool?.release(entity); - } -} diff --git a/lib/src/filter/filter.dart b/lib/src/filter/filter.dart new file mode 100644 index 0000000..756877d --- /dev/null +++ b/lib/src/filter/filter.dart @@ -0,0 +1,155 @@ +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../entity/entity.dart'; +import '../helpers.dart'; +import '../mask/mask.dart'; +import 'filter_operation.dart'; + +class Filter extends IterableBase { + final Mask _mask; + + /// Packaged entities. + Uint32List _denseEntities; + int _entitiesCount = 0; + + /// Stores index of the entity from [_denseEntities]. + /// + /// Index is incremented by 1 when added. + Uint32List _sparseEntities; + + /// List of operations performed during the iteration of the entities. + @internal + final List operations; + @internal + int operationsCount = 0; + + /// _lockCount can be > 1 when a nested iteration occurs. + @internal + int lockCount; + int get entitiesCount => _entitiesCount; + @internal + Mask get mask => _mask; + @internal + Uint32List get denseEntities => _denseEntities; + @internal + Uint32List get sparseEntities => _sparseEntities; + + Filter( + Mask mask, + int denseCapacity, + int sparseCapacity, [ + @visibleForTesting this.lockCount = 0, + ]) : _mask = mask, + _denseEntities = Uint32List(denseCapacity), + _sparseEntities = Uint32List(sparseCapacity), + // TODO(danCrane): set size of operations + operations = List.generate(512, (_) => FilterOperation()); + + @internal + void resizeSparseEntities(int capacity) { + _sparseEntities = _sparseEntities.resize(capacity); + } + + @internal + void addEntity(int entity) { + if (addOperation(true, entity)) { + return; + } + + if (_entitiesCount == _denseEntities.length) { + _denseEntities = _denseEntities.resize(); + } + _denseEntities[_entitiesCount++] = entity; + _sparseEntities[entity] = _entitiesCount; + } + + @internal + void removeEntity(int entity) { + if (addOperation(false, entity)) { + return; + } + + final idx = _sparseEntities[entity] - 1; + _sparseEntities[entity] = 0; + _entitiesCount--; + if (idx < _entitiesCount) { + _denseEntities[idx] = _denseEntities[_entitiesCount]; + _sparseEntities[_denseEntities[idx]] = idx + 1; + } + } + + /// Adds operation to [operations] during an iteration of an entity. + /// This is necessary to have an up-to-date set of entities in the filter. + @internal + bool addOperation(bool added, int entity) { + if (lockCount <= 0) { + return false; + } + + if (operationsCount == operations.length) { + operations.addAll( + Iterable.generate(operationsCount, (_) => FilterOperation()), + ); + } + final op = operations[operationsCount++]; + op.added = added; + op.entity = entity; + + return true; + } + + /// Is automatically called after completing an iteration to update the + /// [_denseEntities] and [_sparseEntities]. + @internal + void unlock() { + --lockCount; + + if (lockCount == 0 && operationsCount > 0) { + for (var i = 0; i < operationsCount; i++) { + final operation = operations[i]; + if (operation.added) { + addEntity(operation.entity); + } else { + removeEntity(operation.entity); + } + } + operationsCount = 0; + } + } + + @override + Iterator get iterator { + lockCount++; + + return FilterIterator(this); + } +} + +class FilterIterator extends Iterator { + final Filter _filter; + final Uint32List _entities; + final int _count; + int _index; + + FilterIterator(Filter filter) + : _filter = filter, + _entities = filter._denseEntities, + _count = filter._entitiesCount, + _index = -1; + + @override + Entity get current => _entities[_index]; + + @override + bool moveNext() { + if (++_index < _count) { + return true; + } else { + _filter.unlock(); + return false; + } + } +} diff --git a/lib/src/filter/filter_operation.dart b/lib/src/filter/filter_operation.dart new file mode 100644 index 0000000..2d332d7 --- /dev/null +++ b/lib/src/filter/filter_operation.dart @@ -0,0 +1,9 @@ +import 'package:meta/meta.dart'; + +@internal +class FilterOperation { + bool added; + int entity; + + FilterOperation([this.added = false, this.entity = 0]); +} diff --git a/lib/src/helpers.dart b/lib/src/helpers.dart new file mode 100644 index 0000000..af98076 --- /dev/null +++ b/lib/src/helpers.dart @@ -0,0 +1,68 @@ +import 'dart:typed_data'; +import 'package:meta/meta.dart'; + +import 'filter/filter.dart'; + +@internal +extension ResizeUint8List on Uint8List { + Uint8List resize([int? capacity]) { + var size = capacity ?? length; + + if (size <= length) { + size = length << 1; + } + + final newList = Uint8List(size); + + final max = length; + for (var i = 0; i < max; i++) { + newList[i] = this[i]; + } + + return newList; + } +} + +@internal +extension ResizeUint32List on Uint32List { + Uint32List resize([int? capacity]) { + var size = capacity ?? length; + + if (size <= length) { + size = size << 1; + } + + final newList = Uint32List(size); + + final max = length; + for (var i = 0; i < max; i++) { + newList[i] = this[i]; + } + + return newList; + } +} + +@internal +extension MaskList on Uint8List { + bool containsValue(int value, int end) { + var result = false; + + for (var i = 0; i < end; i++) { + if (this[i] == value) { + return result = true; + } + } + return result; + } + + Uint8List sortTo(int end) => + (BytesBuilder()..add(getRange(0, end).toList()..sort())).takeBytes(); +} + +@internal +extension FiltersByComponents on List> { + void resize(int? capacity) { + addAll(Iterable.generate(capacity ?? length, (_) => [])); + } +} diff --git a/lib/src/mask/mask.dart b/lib/src/mask/mask.dart new file mode 100644 index 0000000..440abed --- /dev/null +++ b/lib/src/mask/mask.dart @@ -0,0 +1,119 @@ +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../components/component.dart'; +import '../components/component_pool.dart'; +import '../filter/filter.dart'; +import '../helpers.dart'; +import 'mask_delegate.dart'; + +/// Stores lists of include and exclude [ComponentPool.id]. +class Mask { + final MaskDelegate _delegate; + + /// List of include [ComponentPool.id]. + @internal + Uint8List includeList; + @internal + int includeCount = 0; + + /// List of exclude [ComponentPool.id]. + @internal + Uint8List excludeList; + @internal + int excludeCount = 0; + + /// Unique hash based on the length of [includeList], [excludeList] and + /// [ComponentPool.id] within them. + @internal + int hash = 0; + + Mask(MaskDelegate delegate) + : _delegate = delegate, + includeList = Uint8List(8), + excludeList = Uint8List(4); + + Mask include(ComponentBuilder componentBuilder) { + final poolId = _delegate.getPool(componentBuilder).id; + + assert( + !includeList.containsValue(poolId, includeCount), + '$T is already in the include list', + ); + assert( + !excludeList.containsValue(poolId, excludeCount), + '$T is already in the exclude list', + ); + + if (includeCount == includeList.length) { + includeList = includeList.resize(); + } + + includeList[includeCount++] = poolId; + + return this; + } + + Mask exclude(ComponentBuilder componentBuilder) { + final poolId = _delegate.getPool(componentBuilder).id; + + assert( + !includeList.containsValue(poolId, includeCount), + '$T is already in the include list', + ); + assert( + !excludeList.containsValue(poolId, excludeCount), + '$T is already in the exclude list', + ); + + if (excludeCount == excludeList.length) { + excludeList = excludeList.resize(); + } + + excludeList[excludeCount++] = poolId; + + return this; + } + + /// Returns a filter with entities based on [includeList] and [excludeList]. + /// + /// capacity -- specifies the number of entities that can be in the filter. + /// This is a flexible parameter, if you exceed the [capacity] limit, + /// the list of entities will automatically be expanded. + Filter end([int capacity = 512]) { + includeList = includeList.sortTo(includeCount); + excludeList = excludeList.sortTo(excludeCount); + + hash = includeCount + excludeCount; + for (var i = 0; i < includeCount; i++) { + hash = hash * 314 + includeList[i]; + } + for (var i = 0; i < excludeCount; i++) { + hash = hash * 314 - excludeList[i]; + } + + final result = _delegate.checkFilter(this, capacity); + + if (!result.isNew) { + _recycle(); + } + + return result.filter; + } + + /// Resets the data. Must be called before release to the pool if a filter + /// with this mask exists. + void _reset() { + includeCount = 0; + excludeCount = 0; + hash = 0; + } + + /// Resets the data and release the mask back to the pool. + /// Must be called when a filter already exists. + void _recycle() { + _reset(); + _delegate.onMaskRecycle(this); + } +} diff --git a/lib/src/mask/mask_delegate.dart b/lib/src/mask/mask_delegate.dart new file mode 100644 index 0000000..5d0bdcf --- /dev/null +++ b/lib/src/mask/mask_delegate.dart @@ -0,0 +1,19 @@ +import '../components/component.dart'; +import '../components/component_pool.dart'; +import '../filter/filter.dart'; +import 'mask.dart'; + +abstract class MaskDelegate { + ComponentPool getPool( + ComponentBuilder componentBuilder, + ); + FilterCheckResult checkFilter(Mask mask, [int capacity = 512]); + void onMaskRecycle(Mask mask); +} + +class FilterCheckResult { + final Filter filter; + final bool isNew; + + FilterCheckResult(this.filter, this.isNew); +} diff --git a/lib/src/mask/mask_pool.dart b/lib/src/mask/mask_pool.dart new file mode 100644 index 0000000..541e0b1 --- /dev/null +++ b/lib/src/mask/mask_pool.dart @@ -0,0 +1,35 @@ + +import 'dart:collection'; + +import 'mask.dart'; + +typedef MaskBuilder = Mask Function(); + +class MaskPool { + final int _capacity; + final _masks = Queue(); + final MaskBuilder builder; + + int get size => _masks.length; + + MaskPool(this._capacity, this.builder) + : assert(_capacity > 0, 'Capacity must be > 0') { + _expand(); + } + + /// Take a [Mask] from the pool. + Mask take() { + if (_masks.isEmpty) { + _expand(); + } + + return _masks.removeLast(); + } + + /// Release a [Mask] back into the pool. + void release(Mask mask) => _masks.addLast(mask); + + void _expand() { + _masks.addAll(Iterable.generate(_capacity, (index) => builder())); + } +} diff --git a/lib/src/pooling/object_pool.dart b/lib/src/pooling/object_pool.dart deleted file mode 100644 index 4689d33..0000000 --- a/lib/src/pooling/object_pool.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of oxygen; - -abstract class ObjectPool, V> { - final Queue _pool = Queue(); - - int _count = 0; - - /// The total amount of the objects in the pool. - int get totalSize => _count; - - /// The amount of objects that are free to use in the pool. - int get totalFree => _pool.length; - - /// The amount of objects that are in use in the pool. - int get totalUsed => _count - _pool.length; - - ObjectPool({int? initialSize}) { - if (initialSize != null) { - expand(initialSize); - } - } - - /// Acquire a new object. - /// - /// If the pool is empty it will automically grow by 20% + 1. - /// To ensure there is always something in the pool. - /// - /// The [data] argument will be passed to [PoolObject.init] when it gets acquired. - T acquire([V? data]) { - if (_pool.isEmpty) { - expand((_count * 0.2).floor() + 1); - } - final object = _pool.removeLast(); - assert( - data == null || data is V, - '$T expects an instance of $V but received ${data.runtimeType}', - ); - return object..init(data); - } - - /// Release a object back into the pool. - void release(T item) => _pool.addLast(item..reset()); - - /// Expand the existing pool by the given count. - void expand(int count) { - for (var i = 0; i < count; i++) { - final item = builder(); - item._pool = this; - _pool.addLast(item); - } - _count += count; - } - - /// Builder for creating a new instance of a [PoolObject]. - T builder(); -} diff --git a/lib/src/pooling/pool_object.dart b/lib/src/pooling/pool_object.dart deleted file mode 100644 index 26f07aa..0000000 --- a/lib/src/pooling/pool_object.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of oxygen; - -abstract class PoolObject { - /// The pool from which the object came from. - ObjectPool? _pool; - - /// Initialize this object. - /// - /// See [ObjectPool.acquire] for more information on how this gets called. - void init([T? data]); - - /// Reset this object. - void reset(); - - /// Release this object back into the pool. - void dispose() => _pool?.release(this); -} diff --git a/lib/src/query/filter.dart b/lib/src/query/filter.dart deleted file mode 100644 index 98bc75b..0000000 --- a/lib/src/query/filter.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of oxygen; - -/// A filter allows a [Query] to be able to filter down entities. -abstract class Filter { - Filter() : assert(T != Component); - - /// Unique identifier. - String get filterId; - - Type get type => T; - - /// Method for matching an [Entity] against this filter. - bool match(Entity entity); -} - -class Has extends Filter { - @override - String get filterId => 'Has<$T>'; - - @override - bool match(Entity entity) => entity._componentTypes.contains(T); -} - -class HasNot extends Filter { - @override - String get filterId => 'HasNot<$T>'; - - @override - bool match(Entity entity) => !entity._componentTypes.contains(T); -} diff --git a/lib/src/query/query.dart b/lib/src/query/query.dart deleted file mode 100644 index f938c35..0000000 --- a/lib/src/query/query.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of oxygen; - -/// A Query is a way to retrieve entities by matching their components against the Query filters. -/// -/// They are used by systems to retrieve the entities they care about. -class Query { - /// The manager that handles all the entities. - final EntityManager entityManager; - - /// The unique filters to filter by. - final Iterable _filters; - - final List _entities = []; - - /// Entities that are found through [_filters]. - List get entities => UnmodifiableListView(_entities); - - Query(this.entityManager, this._filters) : assert(_filters.isNotEmpty) { - for (final entity in entityManager._entities) { - if (match(entity)) { - _entities.add(entity); - } - } - } - - /// Check if the given entity matches against the query. - bool match(Entity entity) => _filters.every((filter) => filter.match(entity)); -} diff --git a/lib/src/query/query_manager.dart b/lib/src/query/query_manager.dart deleted file mode 100644 index d0ca74a..0000000 --- a/lib/src/query/query_manager.dart +++ /dev/null @@ -1,66 +0,0 @@ -part of oxygen; - -/// Manages all the queries to ensure we don't duplicate lists. -class QueryManager { - /// The manager that handles all the entities. - final EntityManager entityManager; - - /// Cache of all the created queries. - /// - /// If a new [Query] is requested then [_createKey] is used to check - /// if we already have the requested query in cache and return that one instead. - final Map _queries = {}; - - QueryManager(this.entityManager); - - void _onEntityRemoved(Entity entity) { - for (final query in _queries.values) { - if (query._entities.contains(entity)) { - query._entities.remove(entity); - } - } - } - - void _onComponentAddedToEntity(Entity entity, Type componentType) { - for (final query in _queries.values) { - // Entity should only be added when all the following conditions are met: - // - the Entity matches the complete query. - // - the Entity is not already part of the query. - if (query.match(entity) && !query._entities.contains(entity)) { - query._entities.add(entity); - } - } - } - - void _onComponentRemovedFromEntity(Entity entity, Type componentType) { - for (final query in _queries.values) { - // Entity should only be removed when all the following conditions are met: - // - the Entity matches the complete query. - // - the Entity is not already part of the query. - if (!query.match(entity) && query._entities.contains(entity)) { - query._entities.remove(entity); - } - } - } - - /// Creates a unique key to identify a [Query] by. - String _createKey(Iterable filters) { - return filters.map((f) { - if (!entityManager.world.componentManager.components.contains(f.type)) { - throw Exception( - 'Tried to query on ${f.type}, but this component has not yet been registered to the World', - ); - } - return f.filterId; - }).join('-'); - } - - /// Create or retrieve a cached query. - Query createQuery(Iterable filters) { - return _queries.update( - _createKey(filters), - (value) => value, - ifAbsent: () => Query(entityManager, filters), - ); - } -} diff --git a/lib/src/system/system.dart b/lib/src/system/system.dart index 8ec87d2..09668ec 100644 --- a/lib/src/system/system.dart +++ b/lib/src/system/system.dart @@ -1,37 +1,81 @@ -part of oxygen; +import '../filter/filter.dart'; +import '../world.dart'; +import '../world_config.dart'; /// Systems contain the logic for components. /// /// They can update data stored in the components. -/// They query on components to get the entities that fit their Query. +/// They filter components to get entities matching their [Filter]. /// And they can iterate through those entities each execution frame. -abstract class System { - /// The world to which this system belongs to. - World? world; - - /// The priority of this system. - /// - /// Used to set the priority of this system compared to the other systems. - /// A System with a priority of 1 will go before a System with a priority of 2. - /// - /// It can't be changed at runtime. - final int priority; - - System({this.priority = 0}); - - /// Initialize the System. - void init(); - - /// Disposing of the System. - @mustCallSuper - void dispose() { - world = null; +abstract class BaseSystem {} + +abstract class InitSystem extends BaseSystem { + /// Called during [Systems.init]. + void init(Systems systems); +} + +abstract class RunSystem extends BaseSystem { + /// Called during [Systems.run]. + void run(Systems systems, double dt); +} + +abstract class DestroySystem extends BaseSystem { + /// Called during [Systems.destroy]. + void destroy(Systems systems); +} + +/// Manages all registered systems. +class Systems { + final World _defaultWorld; + final _worlds = {}; + final _allSystems = []; + final _runSystems = []; + var _runSystemsCount = 0; + + World get world => _defaultWorld; + + Systems(World world) : _defaultWorld = world; + + void init() { + for (final system in _allSystems) { + if (system is InitSystem) { + system.init(this); + } + if (system is RunSystem) { + _runSystems.add(system); + } + } + } + + void add(BaseSystem system) => _allSystems.add(system); + + /// It's useful to use a separate world for short-lived entity-events, since + /// each world has a size of [Config.entities] * [Config.pools]. That is, + /// if you have 100k entities for units in a world, and you suddenly create + /// one entity-events with a "Click" component, a pool with a huge size will + /// be created for that component, which will eventually lead to irrational + /// memory allocation. + void addWorld(String name, World world) => + _worlds.putIfAbsent(name, () => world); + + World getWorld(String name) => _worlds[name] ?? _defaultWorld; + + void run(double dt) { + final length = _runSystems.length; + for (var i = 0; i < length; i++) { + _runSystems[i].run(this, dt); + } } - /// Create a new Query to filter entites. - Query createQuery(Iterable filters) => - world!.entityManager._queryManager.createQuery(filters); + void destroy() { + for (var i = _allSystems.length - 1; i >= 0; i--) { + final system = _allSystems[i]; + if (system is DestroySystem) { + system.destroy(this); + } + } - /// Execute the System. - void execute(double delta); + _allSystems.clear(); + _runSystems.clear(); + } } diff --git a/lib/src/system/system_manager.dart b/lib/src/system/system_manager.dart deleted file mode 100644 index a706e5f..0000000 --- a/lib/src/system/system_manager.dart +++ /dev/null @@ -1,54 +0,0 @@ -part of oxygen; - -/// Manages all registered systems. -class SystemManager { - /// The world in which this manager lives. - final World world; - - /// All the registered systems. - final List _systems = []; - - UnmodifiableListView get systems => UnmodifiableListView(_systems); - - final Map _systemsByType = {}; - - SystemManager(this.world); - - /// Initialize all the systems that are registered. - void init() { - for (final system in _systems) { - system.init(); - } - } - - /// Register a system. - void registerSystem(T system) { - assert(system.world == null, '$T is already registered'); - system.world = world; - _systems.add(system); - _systemsByType[T] = system; - - _systems.sort((a, b) => a.priority - b.priority); - } - - /// Deregister a previously registered system. - /// - /// If the given system type is not found, it will simply return. - void deregisterSystem(Type systemType) { - if (!_systemsByType.containsKey(systemType)) { - return; - } - final system = _systemsByType.remove(systemType); - system?.dispose(); - _systems.remove(system); - - _systems.sort((a, b) => a.priority - b.priority); - } - - /// Execute all the systems that are registered. - void _execute(double delta) { - for (final system in _systems) { - system.execute(delta); - } - } -} diff --git a/lib/src/world.dart b/lib/src/world.dart index dbdd2ec..746ef13 100644 --- a/lib/src/world.dart +++ b/lib/src/world.dart @@ -1,67 +1,277 @@ -part of oxygen; +import 'dart:typed_data'; -class World { - final HashMap _storedItems = HashMap(); +import 'package:meta/meta.dart'; - UnmodifiableListView get entities { - return UnmodifiableListView(entityManager._entities); +import 'components/component.dart'; +import 'components/component_pool.dart'; +import 'entity/entity.dart'; +import 'entity/entity_data.dart'; +import 'filter/filter.dart'; +import 'helpers.dart'; +import 'mask/mask.dart'; +import 'mask/mask_delegate.dart'; +import 'mask/mask_pool.dart'; +import 'world_config.dart'; + +class World implements MaskDelegate { + final Config _config; + @internal + final List entities; + int _entitiesCount; + @internal + Uint32List recycledEntities; + @internal + int recycledEntitiesCount; + final List _pools; + int _poolsCount; + @internal + final Map poolCached; + @internal + final Map hashedFilters; + @internal + final List allFilters; + @internal + final List> filtersByIncludedComponents; + @internal + final List> filtersByExcludedComponents; + bool _destroyed; + late final MaskPool _maskPool; + + int getComponentCount(Entity entity) => entities[entity].componentsCount; + bool isUsedEntity(Entity entity) => entities[entity].isAlive; + int get entitiesCount => _entitiesCount - recycledEntitiesCount; + int get worldSize => entities.length; + int get poolsCount => _poolsCount; + bool get isAlive => !_destroyed; + + World([this._config = const Config()]) + : entities = List.generate(_config.entities, (_) => EntityData()), + recycledEntities = Uint32List(_config.recycledEntities), + _entitiesCount = 0, + recycledEntitiesCount = 0, + _pools = [], + poolCached = {}, + filtersByIncludedComponents = List.generate(_config.pools, (_) => []), + filtersByExcludedComponents = List.generate(_config.pools, (_) => []), + _poolsCount = 0, + hashedFilters = {}, + allFilters = [], + _destroyed = false { + // TODO(danCrane): capacity maskPool + _maskPool = MaskPool(64, () => Mask(this)); } - late EntityManager entityManager; + /// Returns the [ComponentPool]. + @override + ComponentPool getPool( + ComponentBuilder compoenentBuilder, + ) { + return poolCached.putIfAbsent(compoenentBuilder, () { + final pool = ComponentPool( + this, + compoenentBuilder, + _poolsCount, + entities.length, + _config.poolRecycled, + ); - late ComponentManager componentManager; + // TODO(dan): need test + if (filtersByIncludedComponents.length == _poolsCount || + filtersByExcludedComponents.length == _poolsCount) { + filtersByIncludedComponents.resize(_poolsCount); + filtersByExcludedComponents.resize(_poolsCount); + } - late SystemManager systemManager; + _pools.add(pool); + _poolsCount++; - World() { - entityManager = EntityManager(this); - componentManager = ComponentManager(this); - systemManager = SystemManager(this); + return pool; + }) as ComponentPool; } - /// Store extra data. - void store(String key, T item) => _storedItems[key] = item; + Entity createEntity() { + Entity entity; + if (recycledEntitiesCount > 0) { + entity = recycledEntities[--recycledEntitiesCount]; + final entityData = entities[entity]; + entityData.isAlive = true; + } else { + // New entity. + if (_entitiesCount == entities.length) { + // Resize entities and component pools. + entities.addAll(List.generate(_entitiesCount, (_) => EntityData())); + for (var i = 0; i < _poolsCount; i++) { + _pools[i].resize(_entitiesCount); + } + final length = allFilters.length; + for (var i = 0; i < length; i++) { + allFilters[i].resizeSparseEntities(_entitiesCount); + } + } + entity = _entitiesCount++; + entities[entity].isAlive = true; + } - /// Retrieve extra data. - T? retrieve(String key) => _storedItems[key]; + return entity; + } - /// Remove extra data. - void remove(String key) => _storedItems[key] = null; + void deleteEntity(Entity entity) { + final entityData = entities[entity]; - @mustCallSuper + if (!entityData.isAlive) { + return; + } - /// Initialize the World. - /// - /// Will initialize all the registered [System]s. - void init() { - systemManager.init(); + // Delete components. + if (entityData.componentsCount > 0) { + var index = 0; + // TODO(dan): need more tests + while (entityData.componentsCount > 0 && index < _poolsCount) { + if (_pools[index].has(entity)) { + _pools[index].delete(entity); + } + index++; + } + + return; + } + + entityData.isAlive = false; + if (recycledEntitiesCount == recycledEntities.length) { + recycledEntities = recycledEntities.resize(); + } + recycledEntities[recycledEntitiesCount++] = entity; } - /// Register a [System]. - /// - /// Keep in mind you can't share the same instance of system across multiple worlds. - void registerSystem(T system) { - systemManager.registerSystem(system); + Mask filter(ComponentBuilder componentBuilder) { + return _maskPool.take()..include(componentBuilder); } - /// Deregister a registered [System]. - void deregisterSystem() { - systemManager.deregisterSystem(T); + void destroy() { + _destroyed = true; + for (var i = _entitiesCount - 1; i >= 0; i--) { + final entityData = entities[i]; + + if (entityData.componentsCount > 0) { + deleteEntity(i); + } + } + _pools.clear(); + poolCached.clear(); + hashedFilters.clear(); + allFilters.clear(); + filtersByIncludedComponents.clear(); + filtersByExcludedComponents.clear(); } - /// Register a [Component] builder. - void registerComponent, V>( - ComponentBuilder builder, - ) { - componentManager.registerComponent(builder); + @internal + bool isEntityAliveInternal(Entity entity) => + entity >= 0 && entity < _entitiesCount && entities[entity].isAlive; + + @internal + void onEntityChangeInternal(Entity entity, int componentId, bool added) { + final includeList = filtersByIncludedComponents[componentId]; + final excludeList = filtersByExcludedComponents[componentId]; + if (added) { + // Add component. + if (includeList.isNotEmpty) { + for (final filter in includeList) { + if (isMaskCompatible(filter.mask, entity)) { + filter.addEntity(entity); + } + } + } + if (excludeList.isNotEmpty) { + for (final filter in excludeList) { + if (isMaskCompatibleWithout(filter.mask, entity, componentId)) { + filter.removeEntity(entity); + } + } + } + } else { + // Remove component. + if (includeList.isNotEmpty) { + for (final filter in includeList) { + if (isMaskCompatible(filter.mask, entity)) { + filter.removeEntity(entity); + } + } + } + if (excludeList.isNotEmpty) { + for (final filter in excludeList) { + if (isMaskCompatibleWithout(filter.mask, entity, componentId)) { + filter.addEntity(entity); + } + } + } + } } - /// Create a new [Entity]. - Entity createEntity([String? name]) => entityManager.createEntity(name); + /// Checks if the [Entity] has in the mask. + @internal + bool isMaskCompatible(Mask mask, Entity entity) { + for (var i = 0; i < mask.includeCount; i++) { + if (!_pools[mask.includeList[i]].has(entity)) { + return false; + } + } + for (var i = 0; i < mask.excludeCount; i++) { + if (_pools[mask.excludeList[i]].has(entity)) { + return false; + } + } + return true; + } - /// Execute everything in the World once. - void execute(double delta) { - systemManager._execute(delta); - entityManager.processRemovedEntities(); + /// Checks if the [Entity] has in the mask without component. + @internal + bool isMaskCompatibleWithout(Mask mask, Entity entity, int componentId) { + for (var i = 0; i < mask.includeCount; i++) { + final poolId = mask.includeList[i]; + if (poolId == componentId || !_pools[poolId].has(entity)) { + return false; + } + } + for (var i = 0; i < mask.excludeCount; i++) { + final poolId = mask.excludeList[i]; + if (poolId != componentId && _pools[poolId].has(entity)) { + return false; + } + } + return true; } + + @override + @internal + FilterCheckResult checkFilter(Mask mask, [int capacity = 512]) { + final hash = mask.hash; + final exists = hashedFilters[hash]; + + if (exists != null) { + return FilterCheckResult(exists, false); + } + + final filter = Filter(mask, capacity, entities.length); + hashedFilters[hash] = filter; + allFilters.add(filter); + + for (var i = 0; i < mask.includeCount; i++) { + filtersByIncludedComponents[mask.includeList[i]].add(filter); + } + for (var i = 0; i < mask.excludeCount; i++) { + filtersByExcludedComponents[mask.excludeList[i]].add(filter); + } + for (var entity = 0; entity < _entitiesCount; entity++) { + final entityData = entities[entity]; + if (entityData.componentsCount > 0 && isMaskCompatible(mask, entity)) { + filter.addEntity(entity); + } + } + + return FilterCheckResult(filter, true); + } + + @override + @internal + void onMaskRecycle(Mask mask) => _maskPool.release(mask); } diff --git a/lib/src/world_config.dart b/lib/src/world_config.dart new file mode 100644 index 0000000..bf0e7aa --- /dev/null +++ b/lib/src/world_config.dart @@ -0,0 +1,15 @@ +class Config { + final int entities; + final int recycledEntities; + final int pools; + final int poolCapacity; + final int poolRecycled; + + const Config({ + this.entities = 512, + this.recycledEntities = 512, + this.pools = 512, + this.poolCapacity = 512, + this.poolRecycled = 512, + }); +} diff --git a/pubspec.lock b/pubspec.lock index efd83b0..43731ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,28 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "39.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "4.0.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.3.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.9.0" benchmark: dependency: "direct dev" description: @@ -49,161 +49,147 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" + version: "1.3.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.1" coverage: dependency: transitive description: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.15.2" + version: "1.2.0" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "3.0.2" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "6.1.2" + flame_lint: + dependency: "direct dev" + description: + name: flame_lint + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.2" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.2.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.4" - intl: - dependency: transitive - description: - name: intl - url: "https://pub.dartlang.org" - source: hosted - version: "0.16.1" + version: "4.0.0" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "1.0.3" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.7" - node_interop: - dependency: transitive - description: - name: node_interop - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - node_io: - dependency: transitive + version: "1.0.2" + mocktail: + dependency: "direct dev" description: - name: node_io + name: mocktail url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "0.3.0" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.12" + version: "2.0.1" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.0.2" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.0" + version: "1.8.1" pool: dependency: transitive description: @@ -217,35 +203,35 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.1.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.9" + version: "1.3.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.9+1" + version: "1.1.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "1.0.1" source_map_stack_trace: dependency: transitive description: @@ -266,7 +252,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -301,21 +287,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.16.5" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.3.15" + version: "0.4.13" typed_data: dependency: transitive description: @@ -329,34 +315,34 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "8.3.0" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "2.2.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "1.0.1" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index c2e8f01..c17d638 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,12 +4,13 @@ version: 0.2.0 homepage: https://github.com/flame-engine/oxygen environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.16.0 <3.0.0" dependencies: - meta: ^1.3.0 + meta: ^1.7.0 dev_dependencies: - test: ^1.16.5 benchmark: ^0.3.0 - pedantic: ^1.11.0 + flame_lint: ^0.0.1 + mocktail: ^0.3.0 + test: ^1.20.2 diff --git a/test/components/component_pool_test.dart b/test/components/component_pool_test.dart new file mode 100644 index 0000000..5672dd2 --- /dev/null +++ b/test/components/component_pool_test.dart @@ -0,0 +1,240 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:oxygen/oxygen.dart'; +import 'package:test/test.dart'; + +class MockWorld extends Mock implements World {} + +class MockComponent extends Mock implements Component {} + +void main() { + late World world; + late Component component; + + setUp(() { + world = MockWorld(); + component = MockComponent(); + }); + + group('Pool', () { + group('add', () { + test( + 'should be throwed assertion exception when entity was removed', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(false); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(() => pool.add(0), throwsA(isA())); + }, + ); + + test( + 'should be throwed assertion exception when entity was attached', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + + expect(() => pool.add(0), throwsA(isA())); + }, + ); + + test('should be call init method of component', () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, () => component, 0, 10); + pool.add(0); + + verify(component.init).called(1); + }); + + test('should be call world.onEntityChangeInternal', () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + + verify(() => world.onEntityChangeInternal(0, 0, true)).called(1); + }); + }); + + group('delete', () { + test( + 'should be throwed assertion exception when entity was removed', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(false); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(() => pool.delete(0), throwsA(isA())); + }, + ); + + test( + 'should be call world.onEntityChangeInternal', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + pool.delete(0); + + verify(() => world.onEntityChangeInternal(0, 0, false)).called(1); + }, + ); + + test( + 'should be call world.deleteEntity', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + pool.delete(0); + + verify(() => world.deleteEntity(0)).called(1); + }, + ); + + test( + 'should be delete entity from component', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + pool.delete(0); + + expect(pool.has(0), isFalse); + }, + ); + }); + + group('get', () { + test( + 'should be throwed assertion exception when entity was removed', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(false); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(() => pool.get(0), throwsA(isA())); + }, + ); + + test( + 'should be throwed assertion exception when component is not attached', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(() => pool.get(0), throwsA(isA())); + }, + ); + + test( + 'should be get component', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + + expect(pool.get(0), isA<_TestComponent>()); + expect(pool.get(0).x, equals(10)); + }, + ); + }); + + group('has', () { + test( + 'should be throwed assertion exception when entity was removed', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(false); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(() => pool.has(0), throwsA(isA())); + }, + ); + + test( + 'should be returned false when entity dosnt attached to component', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + + expect(pool.has(0), isFalse); + }, + ); + + test( + 'should be returned true when entity is attached to component', + () { + when(() => world.isEntityAliveInternal(0)).thenReturn(true); + when(() => world.entities[0].isAlive).thenReturn(true); + when(() => world.entities).thenReturn( + [EntityData(isAlive: true)], + ); + + final pool = ComponentPool(world, _TestComponent.new, 0, 10); + pool.add(0); + + expect(pool.has(0), isTrue); + }, + ); + }); + }); +} + +class _TestComponent extends Component { + int x = 0; + @override + void init() { + x = 10; + } + + @override + void reset() { + x = -10; + } +} diff --git a/test/filter/filter_test.dart b/test/filter/filter_test.dart new file mode 100644 index 0000000..7e8b508 --- /dev/null +++ b/test/filter/filter_test.dart @@ -0,0 +1,203 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:oxygen/oxygen.dart'; +import 'package:test/test.dart'; + +class MockMask extends Mock implements Mask {} + +void main() { + late Mask mask; + + setUp(() { + mask = MockMask(); + }); + + group('Filter', () { + group('addEntity', () { + test('should be added entities to denseEntities', () { + final filter = Filter(mask, 10, 10); + const entity1 = 1; + const entity2 = 3; + const entity3 = 2; + + filter + ..addEntity(entity1) + ..addEntity(entity2) + ..addEntity(entity3); + + expect(filter.denseEntities[0], equals(entity1)); + expect(filter.denseEntities[1], equals(entity2)); + expect(filter.denseEntities[2], equals(entity3)); + expect(filter.entitiesCount, equals(3)); + expect(filter, hasLength(3)); + }); + + test( + 'must be added entity index +1 from denseEntities to the sparseEntities', + () { + final filter = Filter(mask, 10, 10); + const entity1 = 1; + const entity2 = 3; + const entity3 = 2; + + filter + ..addEntity(entity1) + ..addEntity(entity2) + ..addEntity(entity3); + + expect(filter.sparseEntities[entity1], equals(1)); + expect(filter.sparseEntities[entity2], equals(2)); + expect(filter.sparseEntities[entity3], equals(3)); + expect(filter.entitiesCount, equals(3)); + expect(filter, hasLength(3)); + }, + ); + + test('should not be added entity when the filter is blocked', () { + final filter = Filter(mask, 10, 10, 1); + const entity1 = 1; + const entity2 = 2; + + filter + ..addEntity(entity1) + ..addEntity(entity2); + + expect(filter.entitiesCount, equals(0)); + expect(filter, hasLength(0)); + expect(filter.denseEntities[0], equals(0)); + expect(filter.denseEntities[1], equals(0)); + expect(filter.sparseEntities[entity1], equals(0)); + expect(filter.sparseEntities[entity2], equals(0)); + }); + + test('should be resized denseEntities', () { + final filter = Filter(mask, 1, 2); + + expect(filter.denseEntities, hasLength(1)); + + filter + ..addEntity(0) + ..addEntity(1); + + expect(filter.denseEntities, hasLength(2)); + }); + }); + + group('removeEntity', () { + test('should be removed entity', () { + final filter = Filter(mask, 4, 4); + const entity = 1; + + filter.addEntity(entity); + expect(filter.entitiesCount, equals(1)); + expect(filter.length, equals(1)); + + filter.removeEntity(entity); + expect(filter.entitiesCount, equals(0)); + expect(filter, hasLength(0)); + }); + test( + 'must be packed entities when entity was remover from the middle', + () { + final filter = Filter(mask, 10, 10); + const entity1 = 1; + const entity2 = 3; + const entity3 = 2; + const entity4 = 4; + + filter + ..addEntity(entity1) + ..addEntity(entity2) + ..addEntity(entity3) + ..addEntity(entity4); + + expect(filter.denseEntities[0], equals(entity1)); + expect(filter.denseEntities[1], equals(entity2)); + expect(filter.denseEntities[2], equals(entity3)); + expect(filter.denseEntities[3], equals(entity4)); + + filter.removeEntity(entity2); + expect(filter.denseEntities[0], equals(entity1)); + expect(filter.denseEntities[1], equals(entity4)); + expect(filter.denseEntities[2], equals(entity3)); + }, + ); + }); + + group('addOperation', () { + test('should be returned false when lockCount <= 0', () { + final filter = Filter(mask, 4, 4); + + expect(filter.addOperation(true, 0), equals(false)); + }); + + test('should be change operationsCount', () { + final filter = Filter(mask, 4, 4, 1)..addOperation(true, 0); + + expect(filter.operationsCount, equals(1)); + }); + + test('should be added new operations', () { + final filter = Filter(mask, 4, 4, 1) + ..addOperation(true, 0) + ..addOperation(false, 1); + + expect(filter.operations[0].added, equals(true)); + expect(filter.operations[0].entity, equals(0)); + expect(filter.operations[1].added, equals(false)); + expect(filter.operations[1].entity, equals(1)); + }); + }); + + group('unlock', () { + test('should be decreased lockCount', () { + final filter = Filter(mask, 10, 10, 1); + + expect(filter.lockCount, equals(1)); + filter.unlock(); + expect(filter.lockCount, equals(0)); + }); + + test( + 'should be processed operations list when lockCount == 0 && ' + 'operationsCount > 0', + () { + final filter = Filter(mask, 10, 10, 1); + const entity1 = 1; + const entity2 = 2; + const entity3 = 4; + + filter + ..addEntity(entity1) + ..addEntity(entity2) + ..addEntity(entity3); + + expect(filter.operationsCount, equals(3)); + + filter.unlock(); + expect(filter.lockCount, equals(0)); + }, + ); + }); + test( + 'should be increase [lockCount] during iteration', + () { + final filter = Filter(mask, 10, 10); + + filter + ..addEntity(1) + ..addEntity(3) + ..addEntity(5); + + for (final _ in filter) { + expect(filter.lockCount, equals(1)); + for (final _ in filter) { + expect(filter.lockCount, equals(2)); + for (final _ in filter) { + expect(filter.lockCount, equals(3)); + } + } + } + }, + ); + }); +} diff --git a/test/mask/mask_pool_test.dart b/test/mask/mask_pool_test.dart new file mode 100644 index 0000000..33db3fd --- /dev/null +++ b/test/mask/mask_pool_test.dart @@ -0,0 +1,45 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:oxygen/oxygen.dart'; +import 'package:oxygen/src/mask/mask_pool.dart'; +import 'package:test/test.dart'; + +class MockMask extends Mock implements Mask {} + +void main() { + late Mask mask; + + setUp(() { + mask = MockMask(); + }); + + group('MaskPool', () { + test('must be throwed assertation error when capacity is < 1', () { + expect(() => MaskPool(0, () => mask), throwsA(isA())); + }); + + test('must be filled when the it is initialized', () { + final pool = MaskPool(10, () => mask); + + expect(pool.size, equals(10)); + }); + + test('take method should be generate mask when pool is empty', () { + final pool = MaskPool(2, () => mask); + + pool.take(); + pool.take(); + expect(pool.size, equals(0)); + pool.take(); + expect(pool.size, equals(1)); + }); + + test('should be release mask', () { + final pool = MaskPool(10, () => mask); + + final takedMask = pool.take(); + expect(pool.size, equals(9)); + pool.release(takedMask); + expect(pool.size, equals(10)); + }); + }); +} diff --git a/test/mask/mask_test.dart b/test/mask/mask_test.dart new file mode 100644 index 0000000..7afc83d --- /dev/null +++ b/test/mask/mask_test.dart @@ -0,0 +1,226 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:oxygen/oxygen.dart'; +import 'package:oxygen/src/mask/mask_delegate.dart'; + +import 'package:test/test.dart'; + +class MockWorld extends Mock implements World {} + +class MockFilter extends Mock implements Filter {} + +void main() { + group('Mask', () { + late World world; + late Filter filter; + + setUp(() { + world = MockWorld(); + filter = MockFilter(); + + when(() => world.getPool<_Component1>()).thenReturn( + ComponentPool(world, _Component1.new, 0, 10), + ); + when(() => world.getPool<_Component2>()).thenReturn( + ComponentPool(world, _Component2.new, 1, 10), + ); + when(() => world.getPool<_Component3>()).thenReturn( + ComponentPool(world, _Component3.new, 2, 10), + ); + when(() => world.getPool<_Component4>()).thenReturn( + ComponentPool(world, _Component4.new, 3, 10), + ); + when(() => world.getPool<_Component5>()).thenReturn( + ComponentPool(world, _Component5.new, 4, 10), + ); + when(() => world.getPool<_Component6>()).thenReturn( + ComponentPool(world, _Component6.new, 5, 10), + ); + when(() => world.getPool<_Component7>()).thenReturn( + ComponentPool(world, _Component7.new, 6, 10), + ); + when(() => world.getPool<_Component8>()).thenReturn( + ComponentPool(world, _Component8.new, 7, 10), + ); + when(() => world.getPool<_Component9>()).thenReturn( + ComponentPool(world, _Component9.new, 8, 10), + ); + when(() => world.getPool<_Component10>()).thenReturn( + ComponentPool(world, _Component10.new, 9, 10), + ); + }); + + group('include', () { + test( + 'should be thrown assertation error when include list already has ' + 'component', + () { + final mask = Mask(world)..include<_Component1>(); + + expect( + () => mask.include<_Component1>(), + throwsA(isA()), + ); + }, + ); + + test( + 'should be thrown assertation error when exclude list already has ' + 'component', + () { + final mask = Mask(world)..exclude<_Component1>(); + + expect( + () => mask.include<_Component1>(), + throwsA(isA()), + ); + }, + ); + + test('should be added component id to include list', () { + final mask = Mask(world) + ..include<_Component1>() + ..include<_Component5>(); + + expect(mask.includeList[0], equals(0)); + expect(mask.includeList[1], equals(4)); + expect(mask.includeCount, equals(2)); + }); + + test('should be resize when include list is fool', () { + final mask = Mask(world) + ..include<_Component1>() + ..include<_Component2>() + ..include<_Component3>() + ..include<_Component4>() + ..include<_Component5>() + ..include<_Component6>() + ..include<_Component7>() + ..include<_Component8>() + ..include<_Component9>(); + + expect(mask.includeCount, equals(9)); + expect(mask.includeList.length, equals(16)); + }); + }); + + group('exclude', () { + test( + 'should be thrown assertation error when include list already has ' + 'component', + () { + final mask = Mask(world)..include<_Component1>(); + + expect( + () => mask.exclude<_Component1>(), + throwsA(isA()), + ); + }, + ); + + test( + 'should be thrown assertation error when exclude list already has ' + 'component', + () { + final mask = Mask(world)..exclude<_Component1>(); + + expect( + () => mask.exclude<_Component1>(), + throwsA(isA()), + ); + }, + ); + + test('should be added component id to include list', () { + final mask = Mask(world) + ..exclude<_Component1>() + ..exclude<_Component5>(); + + expect(mask.excludeList[0], equals(0)); + expect(mask.excludeList[1], equals(4)); + expect(mask.excludeCount, equals(2)); + }); + + test('should be resize when exclude list is fool', () { + final mask = Mask(world) + ..exclude<_Component1>() + ..exclude<_Component2>() + ..exclude<_Component3>() + ..exclude<_Component4>() + ..exclude<_Component5>(); + + expect(mask.excludeCount, equals(5)); + expect(mask.excludeList.length, equals(8)); + }); + }); + + group('end', () { + test('should be called world.getFilterInternal method', () { + final mask = Mask(world); + when(() => world.checkFilter(mask)).thenReturn( + FilterCheckResult(filter, true), + ); + + mask + ..include<_Component1>() + ..end(); + + verify(() => world.checkFilter(mask)).called(1); + }); + + test('should calculate hash', () { + final mask = Mask(world); + when(() => world.checkFilter(mask)).thenReturn( + FilterCheckResult(filter, true), + ); + + mask + ..include<_Component1>() + ..exclude<_Component2>() + ..end(); + + expect(mask.hash, equals(197191)); + }); + + test( + 'should reset the data and release to the pool when the filter already ' + 'exists', + () { + final mask = Mask(world); + when(() => world.checkFilter(mask)).thenReturn( + FilterCheckResult(filter, false), + ); + + mask + ..include<_Component1>() + ..exclude<_Component2>() + ..end(); + + expect(mask.includeCount, equals(0)); + expect(mask.excludeCount, equals(0)); + expect(mask.hash, equals(0)); + verify(() => world.onMaskRecycle(mask)).called(1); + }, + ); + }); + }); +} + +class _Component1 extends Component {} + +class _Component2 extends Component {} + +class _Component3 extends Component {} + +class _Component4 extends Component {} + +class _Component5 extends Component {} + +class _Component6 extends Component {} + +class _Component7 extends Component {} + +class _Component8 extends Component {} + +class _Component9 extends Component {} + +class _Component10 extends Component {} diff --git a/test/pooling/object_pool_test.dart b/test/pooling/object_pool_test.dart deleted file mode 100644 index 762531c..0000000 --- a/test/pooling/object_pool_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:oxygen/oxygen.dart'; - -class TestObject extends PoolObject { - int? value; - - @override - void init([int? data]) { - value = data ?? 0; - } - - @override - void reset() { - value = null; - } -} - -class TestPool extends ObjectPool { - TestPool({int? initialSize}) : super(initialSize: initialSize); - - @override - TestObject builder() => TestObject(); -} - -void main() { - group('ObjectPool', () { - test('Should construct a pool with the initialSize', () { - const initialSize = 1; - final pool = TestPool(initialSize: initialSize); - - expect(pool.totalSize, initialSize); - expect(pool.totalFree, initialSize); - expect(pool.totalUsed, 0); - }); - - test('Should acquire an instance from the pool and release it', () { - const initialSize = 1; - const instanceValue = 5; - - final pool = TestPool(initialSize: initialSize); - final instance = pool.acquire(instanceValue); - - expect(instance.value, instanceValue); - expect(pool.totalSize, initialSize); - expect(pool.totalFree, initialSize - 1); - expect(pool.totalUsed, initialSize); - - pool.release(instance); - - expect(instance.value, null); - expect(pool.totalSize, initialSize); - expect(pool.totalFree, initialSize); - expect(pool.totalUsed, 0); - }); - - test('Should automatically expand by 20% + 1 when pool is empty', () { - const initialSize = 10; - final pool = TestPool(initialSize: initialSize); - List.generate(initialSize, (index) => pool.acquire()); - - expect(pool.totalSize, initialSize); - expect(pool.totalFree, 0); - expect(pool.totalUsed, initialSize); - - final expandingValue = (initialSize * 0.2).floor() + 1; - final expectedSize = initialSize + expandingValue; - final leftOver = expandingValue - 1; // 1 because we acquire once. - - pool.acquire(); - - expect(pool.totalSize, expectedSize); - expect(pool.totalFree, leftOver); - expect(pool.totalUsed, expectedSize - leftOver); - }); - }); -} diff --git a/test/pooling/pool_object_test.dart b/test/pooling/pool_object_test.dart deleted file mode 100644 index 24568f5..0000000 --- a/test/pooling/pool_object_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:oxygen/oxygen.dart'; - -class TestObject extends PoolObject { - @override - void init([int? data]) {} - - @override - void reset() {} -} - -class TestPool extends ObjectPool { - TestPool({int? initialSize}) : super(initialSize: initialSize); - - @override - TestObject builder() => TestObject(); -} - -const initialSize = 1; - -void main() { - group('PoolObject', () { - late TestPool pool; - - setUp(() { - pool = TestPool(initialSize: initialSize); - }); - - test('Should be able to dispose itself', () { - final instance = pool.acquire(); - - expect(pool.totalSize, initialSize); - expect(pool.totalFree, initialSize - 1); - expect(pool.totalUsed, initialSize); - - instance.dispose(); - - expect(pool.totalSize, initialSize); - expect(pool.totalFree, initialSize); - expect(pool.totalUsed, 0); - }); - }); -} diff --git a/test/src/helpers_test.dart b/test/src/helpers_test.dart new file mode 100644 index 0000000..a2604de --- /dev/null +++ b/test/src/helpers_test.dart @@ -0,0 +1,114 @@ +import 'dart:typed_data'; + +import 'package:oxygen/src/helpers.dart'; +import 'package:test/test.dart'; + +void main() { + group('ResizeUint8List', () { + test('should be set new size', () { + final list = Uint8List(4); + + expect(list.resize(6), hasLength(6)); + }); + + test( + 'should be set length << 1 when size is not passed', + () { + final list = Uint8List(4); + + expect(list.resize(), hasLength(8)); + }, + ); + + test( + 'should be set length << 1 when the transmitted size is less than the ' + 'current list length', + () { + final list = Uint8List(4); + + expect(list.resize(2), hasLength(8)); + }, + ); + }); + + group('ResizeUint32List', () { + test('should be set new size', () { + final list = Uint8List(4); + + expect(list.resize(6), hasLength(6)); + }); + + test( + 'should be set length << 1 when size is not passed', + () { + final list = Uint8List(4); + + expect(list.resize(), hasLength(8)); + }, + ); + + test( + 'should be set length << 1 when the transmitted size is less than the ' + 'current list length', + () { + final list = Uint8List(4); + + expect(list.resize(2), hasLength(8)); + }, + ); + }); + + group('MaskList', () { + group('containsValue', () { + test('should be returned true when value contains before limiting', () { + final list = Uint8List(8)..[3] = 1; + + expect(list.containsValue(1, 8), isTrue); + }); + + test('should be returned false when value is not contains ', () { + final list = Uint8List(8)..[3] = 1; + + expect(list.containsValue(2, 8), false); + }); + + test( + 'should be returned false when value is not contains before limiting', + () { + final list = Uint8List(8)..[3] = 1; + + expect(list.containsValue(1, 2), isFalse); + }); + }); + + group('sortTo', () { + test('should be sorted to the limit', () { + final list = Uint8List(4) + ..[0] = 3 + ..[1] = 2 + ..[3] = 1; + final expectedValue = Uint8List(4) + ..[0] = 0 + ..[1] = 1 + ..[2] = 2 + ..[3] = 3; + + expect(list.sortTo(4), equals(expectedValue)); + }); + + test('should be trimmed to the transferred size ', () { + final list = Uint8List(4) + ..[0] = 3 + ..[1] = 1 + ..[2] = 2; + final expectedValue = Uint8List(3) + ..[0] = 1 + ..[1] = 2 + ..[2] = 3; + + expect(list.sortTo(3), equals(expectedValue)); + expect(list.sortTo(3), hasLength(3)); + }); + }); + }); +} diff --git a/test/system/system_test.dart b/test/system/system_test.dart index 0dfc0ba..411f743 100644 --- a/test/system/system_test.dart +++ b/test/system/system_test.dart @@ -1,64 +1,101 @@ +import 'package:mocktail/mocktail.dart'; import 'package:oxygen/oxygen.dart'; import 'package:test/test.dart'; -class SystemA extends System { - SystemA() : super(priority: 2); +class MockWorld extends Mock implements World {} - @override - void execute(double delta) {} +class MockInitSystem extends Mock implements InitSystem {} - @override - void init() {} -} +class MockRunSystem extends Mock implements RunSystem {} -class SystemB extends System { - SystemB() : super(priority: 1); +class MockDestroySystem extends Mock implements DestroySystem {} - @override - void execute(double delta) {} +void main() { + late World world; + late InitSystem initSystem1; + late InitSystem initSystem2; + late RunSystem runSystem1; + late RunSystem runSystem2; + late DestroySystem destroySystem1; + late DestroySystem destroySystem2; - @override - void init() {} -} + setUp(() { + world = MockWorld(); + initSystem1 = MockInitSystem(); + initSystem2 = MockInitSystem(); + runSystem1 = MockRunSystem(); + runSystem2 = MockRunSystem(); + destroySystem1 = MockDestroySystem(); + destroySystem2 = MockDestroySystem(); + }); -class SystemC extends System { - SystemC() : super(priority: 0); + group('Systems', () { + test('should be call init method of all InitSystem', () { + final systems = Systems(world) + ..add(initSystem1) + ..add(initSystem2) + ..add(runSystem1) + ..add(runSystem2) + ..add(destroySystem1) + ..add(destroySystem2); + when(() => initSystem1.init(systems)).thenReturn(null); + when(() => initSystem2.init(systems)).thenReturn(null); - @override - void execute(double delta) {} + systems.init(); - @override - void init() {} -} + verify(() => initSystem1.init(systems)).called(1); + verify(() => initSystem2.init(systems)).called(1); -class SystemD extends System { - SystemD() : super(priority: 1); - @override - void execute(double delta) {} + verifyNever(() => runSystem1.run(systems, 0)); + verifyNever(() => runSystem2.run(systems, 0)); + verifyNever(() => destroySystem1.destroy(systems)); + verifyNever(() => destroySystem1.destroy(systems)); + }); - @override - void init() {} -} + test('should be call run method of all RunSystem', () { + final systems = Systems(world) + ..add(initSystem1) + ..add(initSystem2) + ..add(runSystem1) + ..add(runSystem2) + ..add(destroySystem1) + ..add(destroySystem2); + when(() => runSystem1.run(systems, 0)).thenReturn(null); + when(() => runSystem1.run(systems, 0)).thenReturn(null); -void main() { - group('System', () { - test('Priority should be in the right order', () { - final systemA = SystemA(); - final systemB = SystemB(); - final systemC = SystemC(); - final systemD = SystemD(); - - final world = World() - ..registerSystem(systemA) - ..registerSystem(systemB) - ..registerSystem(systemC) - ..registerSystem(systemD) - ..init(); - - expect( - world.systemManager.systems, - equals([systemC, systemB, systemD, systemA]), - ); + systems + ..init() + ..run(0); + + verify(() => runSystem1.run(systems, 0)).called(1); + verify(() => runSystem2.run(systems, 0)).called(1); + + verify(() => initSystem1.init(systems)).called(1); + verify(() => initSystem2.init(systems)).called(1); + verifyNever(() => destroySystem1.destroy(systems)); + verifyNever(() => destroySystem1.destroy(systems)); + }); + + test('should be call destroy method of all DestroySystem', () { + final systems = Systems(world) + ..add(initSystem1) + ..add(initSystem2) + ..add(runSystem1) + ..add(runSystem2) + ..add(destroySystem1) + ..add(destroySystem2); + when(() => destroySystem1.destroy(systems)).thenReturn(null); + when(() => destroySystem2.destroy(systems)).thenReturn(null); + + systems.destroy(); + + verify(() => destroySystem1.destroy(systems)).called(1); + verify(() => destroySystem2.destroy(systems)).called(1); + + verifyNever(() => initSystem1.init(systems)); + verifyNever(() => initSystem2.init(systems)); + verifyNever(() => runSystem1.run(systems, 0)); + verifyNever(() => runSystem2.run(systems, 0)); }); }); } diff --git a/test/world_test.dart b/test/world_test.dart new file mode 100644 index 0000000..7d06849 --- /dev/null +++ b/test/world_test.dart @@ -0,0 +1,333 @@ +import 'dart:typed_data'; + +import 'package:mocktail/mocktail.dart'; +import 'package:oxygen/oxygen.dart'; +import 'package:test/test.dart'; + +class MockMask extends Mock implements Mask {} + +void main() { + group('Worlds', () { + group('registerPool', () { + test('should be added to poolCached', () { + final world = World(const Config(entities: 1, pools: 1)); + + expect(world.poolCached, hasLength(0)); + expect(world.poolsCount, equals(0)); + expect(world.filtersByIncludedComponents, hasLength(1)); + expect(world.filtersByExcludedComponents, hasLength(1)); + world.registerPool(_Component1.new); + + expect(world.poolCached, hasLength(1)); + expect(world.poolsCount, equals(1)); + }); + + test( + 'must resize filtersByIncludedComponents and ' + 'filtersByExcludedComponents when their size equals pools count', + () { + final world = World(const Config(entities: 1, pools: 1)); + + expect(world.filtersByIncludedComponents, hasLength(1)); + expect(world.filtersByExcludedComponents, hasLength(1)); + world.registerPool(_Component1.new); + world.registerPool(_Component2.new); + + expect(world.filtersByIncludedComponents, hasLength(2)); + expect(world.filtersByExcludedComponents, hasLength(2)); + }, + ); + }); + + group('getPool', () { + test('should returned pool', () { + final world = World()..registerPool(_Component1.new); + + expect( + world.getPool(_Component1.new), + isA>(), + ); + }); + + test( + 'should be throw assert error when pool is not registered', + () { + final world = World(Config(pools: 1, entities: 1)); + + world.getPool(_Component1.new); + world.getPool(_Component2.new); + world.getPool(_Component3.new); + world.getPool(_Component4.new); + world.getPool(_Component5.new); + world.getPool(_Component6.new); + world.getPool(_Component7.new); + world.getPool(_Component8.new); + + + // expect(world.getPool<_Component1>, throwsA(isA())); + // expect(world.getPool<_Component1>, throwsA(isA())); + }, + ); + }); + + group('createEntity', () { + test('should be create new entity', () { + final world = World(); + + final entity = world.createEntity(); + expect(entity, equals(0)); + }); + + test('should be change entity data', () { + final world = World(); + + final entity = world.createEntity(); + + expect(world.entities[entity].isAlive, isTrue); + }); + + test( + 'should be changed entity.isAlive when created from recycledEntities', + () { + final world = World(); + final entity = world.createEntity(); + world.deleteEntity(entity); + + expect(world.entities[entity].isAlive, isFalse); + + final newEntity = world.createEntity(); + + expect(world.entities[newEntity].isAlive, isTrue); + }, + ); + }); + + group('deleteEntity', () { + test('should be change entity.isAlive', () { + final world = World(); + final entity1 = world.createEntity(); + final entity2 = world.createEntity(); + + expect(world.entities[entity1].isAlive, isTrue); + expect(world.entities[entity2].isAlive, isTrue); + world.deleteEntity(entity1); + world.deleteEntity(entity2); + expect(world.entities[entity1].isAlive, isFalse); + expect(world.entities[entity2].isAlive, isFalse); + }); + + test('should be add entity to recycledEntities list', () { + final world = World(); + final entity1 = world.createEntity(); + final entity2 = world.createEntity(); + + world.deleteEntity(entity1); + world.deleteEntity(entity2); + expect(world.recycledEntities[0], entity1); + expect(world.recycledEntities[1], entity2); + }); + + test('should be nothing when entity is < 0', () { + final world = World(); + final entity = world.createEntity(); + + expect(world.entities[entity].isAlive, isTrue); + world.deleteEntity(entity); + expect(world.entities[entity].isAlive, isFalse); + expect(world.recycledEntitiesCount, 1); + + world.deleteEntity(entity); + expect(world.entities[entity].isAlive, isFalse); + expect(world.recycledEntitiesCount, 1); + }); + + test('should be resize recycledEntities', () { + final world = World( + const Config( + entities: 1, + pools: 1, + recycledEntities: 1, + poolCapacity: 1, + ), + ); + final entity1 = world.createEntity(); + final entity2 = world.createEntity(); + expect(world.recycledEntities, hasLength(1)); + + world.deleteEntity(entity1); + world.deleteEntity(entity2); + expect(world.recycledEntities, hasLength(2)); + }); + + test('should be delete components from entity', () { + final world = World(); + final pool1 = world.registerPool(_Component1.new); + final pool2 = world.registerPool(_Component2.new); + final entity1 = world.createEntity(); + final entity2 = world.createEntity(); + pool1 + ..add(entity1) + ..add(entity2); + pool2.add(entity2); + + expect(world.entities[entity1].componentsCount, 1); + expect(world.entities[entity2].componentsCount, 2); + expect(pool1.has(entity1), isTrue); + expect(pool1.has(entity2), isTrue); + expect(pool2.has(entity1), isFalse); + expect(pool2.has(entity2), isTrue); + + world.deleteEntity(entity1); + expect(world.entities[entity1].componentsCount, 0); + world.deleteEntity(entity2); + expect(world.entities[entity2].componentsCount, 0); + }); + }); + + // TODO(danCrane): complete the tests + group('onEntityChangeInternal', () {}); + + group('checkFilter', () { + test( + 'should return a filter that is already registered, regardless of the ' + 'order of the added components', + () { + final world = World(); + world.registerPool(_Component1.new); + world.registerPool(_Component2.new); + world.registerPool(_Component3.new); + world.registerPool(_Component4.new); + + final filter = world + .filter<_Component1>(_Component1.new) + .include<_Component2>(_Component2.new) + .exclude<_Component3>(_Component3.new) + .exclude<_Component4>(_Component4.new) + .end(); + + final filter2 = world + .filter<_Component2>(_Component2.new) + .include<_Component1>(_Component1.new) + .exclude<_Component4>(_Component4.new) + .exclude<_Component3>(_Component3.new) + .end(); + + expect(filter, equals(filter2)); + }, + ); + + test('should be added entities when created filter', () { + final world = World(); + final pool = world.registerPool(_Component1.new); + final e1 = world.createEntity(); + world.createEntity(); + final e3 = world.createEntity(); + pool + ..add(e1) + ..add(e3); + + final filter = world.filter<_Component1>(_Component1.new).end(); + + expect(filter, hasLength(2)); + expect(filter.denseEntities[0], equals(e1)); + expect(filter.denseEntities[1], equals(e3)); + }); + + test('should be added filter to filters lists', () { + final world = World(); + final pool1 = world.registerPool(_Component1.new); + final pool2 = world.registerPool(_Component2.new); + + expect(world.filtersByIncludedComponents[pool1.id], isEmpty); + expect(world.filtersByExcludedComponents[pool2.id], isEmpty); + final filter = world + .filter<_Component1>(_Component1.new) + .exclude<_Component2>(_Component2.new) + .end(); + expect(world.filtersByIncludedComponents[pool1.id], hasLength(1)); + expect( + world.filtersByIncludedComponents[pool1.id].first, + equals(filter), + ); + expect(world.filtersByExcludedComponents[pool2.id], hasLength(1)); + expect( + world.filtersByExcludedComponents[pool2.id].first, + equals(filter), + ); + }); + }); + + // TODO(danCrane): complete the tests + group('isMaskCompatible', () { + test('should be returned true when entity is compatible with mask', () { + final world = World(); + final entity = world.createEntity(); + + final p1 = world.registerPool(_Component1.new); + final p2 = world.registerPool(_Component2.new); + + p1.add(entity); + + final mask = MockMask(); + when(() => mask.includeCount).thenReturn(1); + when(() => mask.excludeCount).thenReturn(1); + when(() => mask.includeList).thenReturn(Uint8List(4)..[0] = p1.id); + when(() => mask.excludeList).thenReturn(Uint8List(4)..[0] = p2.id); + + expect(world.isMaskCompatible(mask, entity), isTrue); + }); + + test( + 'should be returned false when entity is not compatible with mask', + () { + final world = World(); + final entity = world.createEntity(); + + final p1 = world.registerPool(_Component1.new); + final p2 = world.registerPool(_Component2.new); + + final mask = MockMask(); + when(() => mask.includeCount).thenReturn(1); + when(() => mask.excludeCount).thenReturn(1); + when(() => mask.includeList).thenReturn(Uint8List(4)..[0] = p1.id); + when(() => mask.excludeList).thenReturn(Uint8List(4)..[0] = p2.id); + + expect(world.isMaskCompatible(mask, entity), isFalse); + }, + ); + }); + + // TODO(danCrane): complete the tests + group('isMaskCompatibleWithout', () { + test('should be returned true when entity is compatible with mask', () { + final world = World(); + final entity = world.createEntity(); + + final p1 = world.registerPool(_Component1.new); + final p2 = world.registerPool(_Component2.new); + + p1.add(entity); + + final mask = MockMask(); + when(() => mask.includeCount).thenReturn(1); + when(() => mask.excludeCount).thenReturn(1); + when(() => mask.includeList).thenReturn(Uint8List(1)..[0] = p1.id); + when(() => mask.excludeList).thenReturn(Uint8List(1)..[0] = p2.id); + + expect(world.isMaskCompatibleWithout(mask, entity, p2.id), isTrue); + }); + }); + }); +} + +class _Component1 extends Component {} + +class _Component2 extends Component {} + +class _Component3 extends Component {} + +class _Component4 extends Component {} +class _Component5 extends Component {} +class _Component6 extends Component {} +class _Component7 extends Component {} +class _Component8 extends Component {}