├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug.md │ ├── 2_feature.md │ ├── 3_improvement.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── cicd.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── benchmark ├── object_pool_benchmark.dart ├── query_benchmark.dart └── world_benchmark.dart ├── dartdoc_options.yaml ├── doc ├── component.md ├── entity.md ├── object_pooling.md ├── query.md ├── system.md └── world.md ├── example ├── .gitignore ├── README.md ├── lib │ ├── components │ │ ├── color_component.dart │ │ ├── direction_component.dart │ │ ├── name_component.dart │ │ ├── player_component.dart │ │ ├── position_component.dart │ │ ├── render_component.dart │ │ └── velocity_component.dart │ ├── main.dart │ ├── systems │ │ ├── move_system.dart │ │ ├── player_move_system.dart │ │ └── render_system.dart │ └── utils │ │ ├── color.dart │ │ ├── game.dart │ │ ├── keyboard.dart │ │ ├── rect.dart │ │ ├── terminal.dart │ │ └── vector2.dart ├── pubspec.lock └── pubspec.yaml ├── lib ├── oxygen.dart └── src │ ├── component │ ├── component.dart │ ├── component_manager.dart │ └── value_component.dart │ ├── entity │ ├── entity.dart │ └── entity_manager.dart │ ├── pooling │ ├── object_pool.dart │ └── pool_object.dart │ ├── query │ ├── filter.dart │ ├── query.dart │ └── query_manager.dart │ ├── system │ ├── system.dart │ └── system_manager.dart │ └── world.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── components ├── component_test.dart └── hasnot_test.dart ├── pooling ├── object_pool_test.dart └── pool_object_test.dart └── system └── system_test.dart /.github/ISSUE_TEMPLATE/1_bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report. 3 | about: You are writing something with Oxygen but you are noticing strange behaviour, throws an exception, or it is buggy. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | # Current bug behaviour 13 | 14 | 15 | # Expected behaviour 16 | 17 | 18 | # Steps to reproduce 19 | 20 | 21 | 22 | # More environment information 23 | 27 | 28 | # Log information 29 | 30 | ``` 31 | 32 | ``` 33 | 34 | # More information 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request. 3 | about: Suggest a new feature for Oxygen. 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | # Problem to solve 13 | 14 | 15 | # Proposal 16 | 17 | 18 | # More information 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement suggestion. 3 | about: Something in Oxygen can be improved. 4 | title: '' 5 | labels: 'improvement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | # What could be improved 13 | 14 | 15 | # Why should this be improved 16 | 17 | 18 | # Any risks? 19 | 20 | 21 | # More information 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: I have some questions about Oxygen. 4 | url: https://stackoverflow.com/tags/oxygen 5 | about: Ask your questions on StackOverflow! The community is always willing to help out. 6 | - name: Join us on Discord! 7 | url: https://discord.gg/JUwwvNryDz 8 | about: Ask your questions in our community! 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | *Replace this paragraph with a description of what this PR is doing. If you're modifying existing behavior, describe the existing behavior, how this PR is changing it, and what motivated the change. Especially if it is a breaking change, please specify that here and what APIs where changed. * 4 | 5 | ## Checklist 6 | 7 | Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`). This will ensure a smooth and quick review process. 8 | 9 | - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. 10 | - [ ] My PR includes unit or integration tests for *all* changed/updated/fixed behaviors (See [Contributor Guide]). 11 | - [ ] All existing and new tests are passing. 12 | - [ ] I updated/added relevant documentation (doc comments with `///`). 13 | - [ ] The dart analyzer (`dart analyze`) does not report any problems on my PR. 14 | - [ ] I read and followed the [Effective Dart Style Guide]. 15 | - [ ] I have added a description of the change under `[next]` in `CHANGELOG.md`. 16 | - [ ] I am willing to follow-up on review comments in a timely manner. 17 | - [ ] I am happy with the current version of this PR and it is ready to be reviewed 18 | - [ ] If the PR still has the `Draft` status, remove it by clicking on the `Ready for review` button in this PR. 19 | 20 | ## Breaking Change 21 | 22 | Does your PR require Oxygen users to manually update their apps to accommodate your change? 23 | 24 | - [ ] Yes, this is a breaking change (please indicate a breaking change in `CHANGELOG.md`). 25 | - [ ] No, this is *not* a breaking change. 26 | 27 | ## Related Issues 28 | 29 | *Replace this paragraph with a list of issues related to this PR from the [issue database]. Indicate, which of these issues are resolved or fixed by this PR* 30 | 31 | 32 | [issue database]: https://github.com/flame-engine/oxygen/issues 33 | [Contributor Guide]: https://github.com/flame-engine/oxygen/blob/master/CONTRIBUTING.md 34 | [Effective Dart Style Guide]: https://dart.dev/guides/language/effective-dart/style 35 | [pub versioning philosophy]: https://www.dartlang.org/tools/pub/versioning -------------------------------------------------------------------------------- /.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: cicd 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | # BEGIN LINTING STAGE 12 | dartdoc: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: dart-lang/setup-dart@v1 17 | - run: dart pub get 18 | - run: dart run dartdoc --no-auto-include-dependencies --quiet 19 | 20 | format: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: dart-lang/setup-dart@v1 25 | - run: | 26 | [ -z "$(dart format . | grep "(0 changed)")" ] && exit 1 || exit 0 27 | 28 | analyze: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: dart-lang/setup-dart@v1 33 | - run: dart pub get 34 | - run: dart analyze --fatal-infos --fatal-warnings 35 | # END LINTING STAGE 36 | 37 | # BEGIN TESTING STAGE 38 | test: 39 | needs: [dartdoc, format, analyze] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: dart-lang/setup-dart@v1 44 | - run: dart pub get 45 | - run: dart test 46 | 47 | benchmark: 48 | needs: [test] 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v3 52 | - uses: dart-lang/setup-dart@v1 53 | - run: dart pub get 54 | - run: dart run benchmark 55 | # END TESTING STAGE 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | coverage/ 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "console": "terminal", 9 | "name": "example", 10 | "cwd": "example", 11 | "request": "launch", 12 | "type": "dart", 13 | "args": [ 14 | "--enable-asserts" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | - Component removal and HasNot fix 3 | - Fix so that systems can't be added twice 4 | 5 | ## 0.3.0 6 | - Fixed component removal so that it actually removes components 7 | - Fixed `HasNot` filters to properly filter after a filtered component is added 8 | - **BREAKING**: Bumped Dart to v2.15.0 to allow for constructor tear-offs 9 | 10 | ## 0.2.0 11 | - Made assertion for getting/checking/removing a `Component` stricter 12 | - **BREAKING**: Made the `priority` field on `System` final 13 | - **BREAKING**: Components will now be marked for removal. This fixes the concurrent modification error 14 | 15 | ## 0.1.0 16 | - Stable null-safety release 17 | - Added `ValueComponent` for single value components 18 | - Refactored the Component system to allow for all types to be used as init data 19 | - Initial setup 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | If you're interested in contributing to this project, here are a few ways to do so: 3 | 4 | ## Bug fixes 5 | * If you find a bug, please first check if it is already reported (duplicate issues will be closed), otherwise report it using [Github issues](https://github.com/flame-engine/oxygen/issues/new). 6 | * Issues that have already been identified as a bug will be labelled "bug". 7 | * If you'd like to submit a fix for a bug, send a [Pull Request](https://guides.github.com/activities/forking/#making-a-pull-request) from your own fork, also read the [How To](#how-to) and [Development Guidelines](#development-guidelines). 8 | * Include a test that isolates the bug and verifies that it was fixed. 9 | * Also update the example and documentation if necessary. 10 | 11 | ## New Features 12 | * If you'd like to add a feature to the library that doesn't already exist, feel free to describe the feature in a new [Github issue](https://github.com/flame-engine/oxygen/issues/new). If it is a bigger change, you can also get some initial thoughts about it in our [discord server](https://discord.gg/JUwwvNryDz). 13 | * Issues that have been identified as a feature request will be labelled "feature". 14 | * If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending too much time writing the code. In some cases, enhancements may not align well with the project objectives at the time. 15 | * Implement your code and please read the [How To](#how-to) and [Development Guidelines](#development-guidelines). 16 | * Also update the example and documentation where needed. 17 | 18 | ## Documentation & Miscellaneous 19 | * If you think the documentation could be clearer, or you have an alternative implementation of something that may have more advantages, we would love to hear it. 20 | * As always first file a report in a [Github issue](https://github.com/flame-engine/oxygen/issues/new). 21 | * Issues that have been identified as a documentation change will be labelled "documentation". 22 | * Implement the changes to the documentation, please read the [How To](#how-to) and [Development Guidelines](#development-guidelines). 23 | 24 | # Requirements 25 | For a contribution to be accepted: 26 | 27 | * Take note of the [Development Guidelines](#development-guidelines) 28 | * Code must follow existing styling conventions 29 | * Commit message should start with a [issue number](#how-to) and should also be descriptive. 30 | 31 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the issue. You can still continue to add more commits to the branch you have sent the Pull Request from. 32 | 33 | # How To 34 | * First of all [file an bug or feature report](https://github.com/flame-engine/oxygen/issues/new) on this repository. 35 | * [Fork the project](https://guides.github.com/activities/forking/#fork) on Github 36 | * Clone the forked repository to your local development machine (e.g. `git clone https://github.com//oxygen.git`) 37 | * Run `pub get` in the cloned repository to get all the dependencies 38 | * Create a new local branch based on issue number from first step (e.g. `git checkout -b 12-new-feature`) 39 | * Make your changes 40 | * When committing your changes, make sure to start the commit message with `#` (e.g. `git commit -m '#12 - New Feature added'`) 41 | * Push your new branch to your own fork into the same remote branch (e.g. `git push origin 12-new-feature`) 42 | * On Github go to the [pull request page](https://guides.github.com/activities/forking/#making-a-pull-request) on your own fork and create a merge request to this repository 43 | 44 | # Development Guidelines 45 | * Documentation should be updated. 46 | * Example application should be updated. 47 | * Format the Dart code accordingly. 48 | * Note the [`analysis_options.yaml`](https://github.com/flame-engine/oxygen/blob/master/analysis_options.yaml) and write code as stated in this file 49 | 50 | # Test generating of `dartdoc` 51 | * On local development make sure the `dartdoc` program is mentioned in your `$PATH` 52 | * `dartdoc` can be found here: `/bin/cache/dart-sdk/bin/dartdoc` or here: `/bin/dartdoc` 53 | * Generate docs with the following command: `dartdoc --no-auto-include-dependencies --quiet` 54 | * Output will be placed into `doc/api/` 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flame Engine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Oxygen 3 |

4 | 5 |

6 | A lightweight Entity Component System framework written in Dart. 7 |

8 | 9 |

10 | 11 | cicd 12 | 13 | 14 |

15 | 16 | ## About Oxygen 17 | 18 | Oxygen is a lightweight Entity Component System framework written in Dart, with a focus on performance and ease of use. Oxygen is by design agnostic, and any game engine you want to use can be used with Oxygen. 19 | 20 | ## Goals 21 | 22 | Oxygen is heavily inspired by [ECSY](https://ecsyjs.github.io/ecsy/), and because of that it shares the same design principals. The main goal for Oxygen is to be lightweight, performant and simple to use. With APIs that try and help you make good use of the ECS design pattern, without restricting you in building your logic. 23 | 24 | ## Documentation 25 | 26 | See the [documentation](https://github.com/flame-engine/oxygen/blob/master/doc) for more information about how to work with Oxygen. 27 | 28 | ## Contributing 29 | 30 | For contributing see our [Contributing guidelines](https://github.com/flame-engine/oxygen/blob/master/CONTRIBUTING.md). Please read this carefully as it will answer most of your contributing-related questions. 31 | 32 | ## Credits 33 | - Inspired by [ecsy](https://ecsyjs.github.io/ecsy/) 34 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Be aware the health of this package is based on these 2 | # analyse options, we will get the most health points if 3 | # we do not change anything, see: 4 | # https://pub.dev/help#health 5 | 6 | # Defines a default set of lint rules enforced for 7 | # projects at Google. For details and rationale, 8 | # see https://github.com/dart-lang/pedantic#enabled-lints. 9 | include: package:pedantic/analysis_options.yaml 10 | 11 | # For lint rules and documentation, see http://dart-lang.github.io/linter/lints. 12 | # Uncomment to specify additional rules. 13 | linter: 14 | rules: 15 | - unnecessary_brace_in_string_interps 16 | - non_constant_identifier_names 17 | - cancel_subscriptions 18 | 19 | analyzer: 20 | exclude: 21 | - example/** # Needed for CI/CD.. 22 | -------------------------------------------------------------------------------- /benchmark/object_pool_benchmark.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:benchmark/benchmark.dart'; 3 | 4 | class TestObject extends PoolObject { 5 | int? value; 6 | 7 | @override 8 | void init([int? data]) { 9 | value = data ?? 0; 10 | } 11 | 12 | @override 13 | void reset() { 14 | value = null; 15 | } 16 | } 17 | 18 | class TestPool extends ObjectPool { 19 | TestPool({int initialSize = 100000}) : super(initialSize: initialSize); 20 | 21 | @override 22 | TestObject builder() => TestObject(); 23 | } 24 | 25 | void main() { 26 | group('ObjectPool', () { 27 | benchmark('new ObjectPool with 100000 instances', () { 28 | TestPool(initialSize: 100000); 29 | }); 30 | 31 | benchmark('new ObjectPool with 0 instances that grows to 100000', () { 32 | final pool = TestPool(initialSize: 0); 33 | for (var i = 0; i < 100000; i++) { 34 | pool.acquire(); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /benchmark/query_benchmark.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:benchmark/benchmark.dart'; 3 | 4 | class Test100Component extends Component { 5 | @override 6 | void init([void data]) {} 7 | 8 | @override 9 | void reset() {} 10 | } 11 | 12 | class Test50Component extends Component { 13 | @override 14 | void init([void data]) {} 15 | 16 | @override 17 | void reset() {} 18 | } 19 | 20 | void main() { 21 | group('Query', () { 22 | group('With 100000 entities', () { 23 | World? world; 24 | 25 | QueryManager? queryManager; 26 | 27 | setUp(() { 28 | world = World(); 29 | world!.registerComponent(() => Test100Component()); 30 | world!.registerComponent(() => Test50Component()); 31 | for (var i = 0; i < 100000; i++) { 32 | final entity = world!.createEntity(); 33 | 34 | entity.add(); 35 | if (i % 2 == 0) { 36 | entity.add(); 37 | } 38 | } 39 | queryManager = QueryManager(world!.entityManager); 40 | }); 41 | 42 | tearDown(() { 43 | world = null; 44 | queryManager = null; 45 | }); 46 | 47 | benchmark('creating a Query that matches 100% of all the entities', () { 48 | queryManager?.createQuery([Has()]); 49 | }); 50 | 51 | benchmark('creating a Query that matches 50% of all the entities', () { 52 | queryManager?.createQuery([Has()]); 53 | }); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /benchmark/world_benchmark.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:benchmark/benchmark.dart'; 3 | 4 | void main() { 5 | group('World', () { 6 | group('Without world creation', () { 7 | World? world; 8 | 9 | setUpEach(() => world = World()); 10 | 11 | tearDownEach(() => world = null); 12 | 13 | benchmark('World with 100000 entities', () { 14 | for (var i = 0; i < 100000; i++) { 15 | world?.createEntity(); 16 | } 17 | }); 18 | }); 19 | 20 | group('With world creation', () { 21 | benchmark('World with 100000 entities', () { 22 | final world = World(); 23 | for (var i = 0; i < 100000; i++) { 24 | world.createEntity(); 25 | } 26 | }); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /dartdoc_options.yaml: -------------------------------------------------------------------------------- 1 | dartdoc: 2 | include: ['oxygen'] 3 | errors: 4 | - ambiguous-doc-reference 5 | - ambiguous-reexport 6 | - broken-link 7 | - category-order-gives-missing-package-name 8 | - deprecated 9 | - ignored-canonical-for 10 | - missing-from-search-index 11 | - no-canonical-found 12 | - no-library-level-docs 13 | - not-implemented 14 | - orphaned-file 15 | - reexported 16 | - private-api-across-packages 17 | - unknown-file 18 | - unknown-macro 19 | - unresolved-doc-reference 20 | -------------------------------------------------------------------------------- /doc/component.md: -------------------------------------------------------------------------------- 1 | # Component 2 | 3 | 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. A Component can be anything you want, there is no defacto standard for implementing one: 4 | ```dart 5 | class YourComponent extends Component { 6 | int? yourProperty; 7 | 8 | @override 9 | void init([data]) { 10 | yourProperty = 10; 11 | } 12 | 13 | @override 14 | void reset() { 15 | yourProperty = null; 16 | } 17 | } 18 | 19 | void main() { 20 | // ... 21 | final yourEntity = world.createEntity() 22 | ..add(); 23 | // ... 24 | } 25 | ``` 26 | > Components have their own object pool, and each instance will be acquired and released from that pool when needed. The `init` method will be called on acquire, and the `reset` method on release. Therefore components do not use constructors. See [Object Pooling](./object_pooling.md) for more information. 27 | 28 | ## Initializing a Component with data 29 | 30 | You can also pass data to the `init` method. To ensure proper handling of the data you first have to tell the Component what kind of init data it will receive: 31 | ```dart 32 | class YourComponent extends Component { 33 | int? yourProperty; 34 | 35 | @override 36 | void init([int? data]) { 37 | yourProperty = data; 38 | } 39 | 40 | @override 41 | void reset() { 42 | yourProperty = null; 43 | } 44 | } 45 | ``` 46 | 47 | You can then add your Component to an Entity and pass along the required data: 48 | ```dart 49 | void main() { 50 | // ... 51 | final yourEntity = world.createEntity() 52 | ..add(10) // With data 53 | ..add(); // Without data 54 | // ... 55 | } 56 | ``` 57 | 58 | The data can be `null` and only an instance of the given type or a type that extends from that type is allowed. If any other type is given an assertion exception will be thrown. 59 | 60 | ## Single value components 61 | 62 | Whenever you need to define a Component that only holds a single value you can make use of the `ValueComponent` class. With the `ValueComponent` you can easily define single value components: 63 | ```dart 64 | class PositionComponent extends ValueComponent {} 65 | 66 | void main() { 67 | // ... 68 | final entity = world.createEntity() 69 | ..add(Position(0, 0)); 70 | // ... 71 | // You can then retrieve the position value. 72 | final position = entity.get()?.value; 73 | // ... 74 | } 75 | ``` 76 | 77 | ## Registering a Component 78 | 79 | When you want to create an Entity with certain components in a World, you first have to let your World know which components there are. You do that by registering it using a `builder` closure: 80 | ```dart 81 | world.registerComponent(() => YourComponent()); 82 | ``` 83 | 84 | This `builder` closure will be automatically called whenever the pool for this component is empty and new instances are required. See [Component Pooling](./object_pooling.md#component-pooling) for more information. 85 | -------------------------------------------------------------------------------- /doc/entity.md: -------------------------------------------------------------------------------- 1 | # Entity 2 | 3 | An Entity is a simple "container" for components. It serves no purpose apart from being an abstraction container around the components. 4 | 5 | ## Creating an Entity 6 | 7 | You can create an entity by using the [World](./world.md): 8 | ```dart 9 | final entity = world.createEntity('Optional name'); 10 | ``` 11 | 12 | ## Removing an Entity 13 | ```dart 14 | entity.dispose(); 15 | ``` 16 | 17 | This will mark the Entity for removal but won't be immediately disposed until the end of the last update cycle. This allows systems to react to the Entity. 18 | 19 | ## Adding Components 20 | 21 | After you have created an entity you can easily add new components: 22 | ```dart 23 | entity 24 | ..add(someInitData); 25 | ..add(someOtherInitData); 26 | ``` 27 | 28 | Only one instance of the same Component can be added to an Entity. After adding it to an Entity all the queries that are subscribed will be updated. 29 | 30 | For more information about components and how they work see the [Component documentation](./component.md). 31 | 32 | ## Retrieving Components 33 | ```dart 34 | final yourComponent = entity.get(); 35 | yourComponent?.property = someValue; 36 | ``` 37 | 38 | ## Checking if it has a Component 39 | ```dart 40 | if (entity.has()) { 41 | final yourComponent = entity.get()!; 42 | // ... 43 | } 44 | ``` 45 | 46 | ## Removing Components 47 | ```dart 48 | entity.remove(); 49 | ``` 50 | 51 | This will mark the Component for removal but won't be immediately disposed until the end of the last update cycle. This allows systems to react to the data inside the Component. -------------------------------------------------------------------------------- /doc/object_pooling.md: -------------------------------------------------------------------------------- 1 | # Object Pooling 2 | From [Wikipedia](https://en.wikipedia.org/wiki/Object_pool_pattern): 3 | > The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use – a "pool" – rather than allocating and destroying them on demand. A client of the pool will request an object from the pool and perform operations on the returned object. When the client has finished, it returns the object to the pool rather than destroying it; this can be done manually or automatically. 4 | 5 | In this implementation the object pool works as explained above, and it is able to grown on-demand. By passing a `builder` closure to the constructor it can create new objects when there are no more objects in a pool. It will automatically grow by 20% whenever a pool is empty, ensuring there will always be objects to return. 6 | 7 | There is currently no [high water mark](https://en.wikipedia.org/wiki/High_water_mark) in place, so the pool will grow indefinitely. 8 | 9 | ## Component Pooling 10 | Each Component has its own object pool, Oxygen does this to ensure there is no overhead for creating new instances. This is especially useful when your game is constantly adding and removing components from entities. It also ensures the garbage collector won't have to be called quite as often, ensuring a better performance (this is especially true for the web). 11 | 12 | So whenever a Component is added to an Entity: 13 | ```dart 14 | entity.add(); 15 | ``` 16 | it will try and reuse a `AComponent` instance, from the `AComponent` pool. It won't allocate a new instance if there are still instances left in the pool, it will be returned to the pool by calling: 17 | ```dart 18 | entity.remove(); 19 | ``` 20 | -------------------------------------------------------------------------------- /doc/query.md: -------------------------------------------------------------------------------- 1 | # Query 2 | 3 | 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: 4 | ```dart 5 | class YourSystem extends System { 6 | late Query query; 7 | 8 | @override 9 | void init() { 10 | query = createQuery([Has()]); 11 | } 12 | 13 | @override 14 | void execute(double delta) { 15 | query.entities.forEach((entity) { 16 | final yourComponent = entity.get(); 17 | // ... 18 | }); 19 | } 20 | } 21 | ``` 22 | 23 | A query will always be updated with entities that match its filters. So whenever an entity's component list changes it will be reflected in all the queries. 24 | 25 | ## Filters 26 | 27 | Queries are able to use filters to define how the entities should be matched. 28 | 29 | ### Has filter 30 | 31 | This filter checks if an entity has the given component: 32 | ```dart 33 | final query = createQuery([ 34 | Has(), 35 | Has(), 36 | ]); 37 | 38 | // query.entities will contain all the entities that have both YourComponentA and YourComponentB. 39 | ``` 40 | 41 | ### HasNot filter 42 | 43 | This filter checks if an entity does **not** have the given component: 44 | ```dart 45 | final query = createQuery([ 46 | Has(), 47 | Has(), 48 | HasNot(), 49 | ]); 50 | 51 | // query.entities will contain all the entities that have both YourComponentA and YourComponentB and NOT YourComponentC. 52 | ``` -------------------------------------------------------------------------------- /doc/system.md: -------------------------------------------------------------------------------- 1 | # System 2 | 3 | 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. And they can iterate through those entities each execution frame. 4 | 5 | A System is build up as followed: 6 | ```dart 7 | class YourSystem extends System { 8 | @override 9 | void init() { } 10 | 11 | @override 12 | void execute(double delta) { } 13 | } 14 | ``` 15 | 16 | The `init()` method will be called whenever the World that [the System is registered to](./world.md#registering-a-system) is [initialized](./world.md#initializing). And the `execute(delta)` method will be called every time the World gets [executed](./world.md#executing). 17 | 18 | ## Registering a System 19 | 20 | To register a System to the world you can pass an instance like this: 21 | ```dart 22 | world.registerSystem(YourSystem()); 23 | ``` 24 | 25 | Keep in mind, you **cannot** reuse system instances over multiple worlds, it will throw an assertion error if that happens. Just pass a new instance to the `registerSystem` method for each world that you want your system to be part of. 26 | 27 | ## Deregistering a System 28 | 29 | To deregister a System from the world: 30 | ```dart 31 | world.deregisterSystem(); 32 | ``` 33 | 34 | ## Execution order 35 | 36 | Systems are executed in the order they are registed to the World. But each system can define it's own priority to ensure it gets executed before others: 37 | ```dart 38 | class YourSystem extends System { 39 | @override 40 | final priority = 2; 41 | 42 | @override 43 | void init() { } 44 | 45 | @override 46 | void execute(double delta) { } 47 | } 48 | ``` 49 | 50 | ## Queries 51 | 52 | A System can create queries on entities to filter through them based on their components: 53 | ```dart 54 | class YourSystem extends System { 55 | late Query query; 56 | 57 | @override 58 | void init() { 59 | query = createQuery([Has()]); 60 | } 61 | 62 | @override 63 | void execute(double delta) { 64 | query.entities.forEach((entity) { 65 | final yourComponent = entity.get(); 66 | // ... 67 | }); 68 | } 69 | } 70 | ``` 71 | 72 | For more information about queries and how they work see the [Query documentation](./query.md). 73 | -------------------------------------------------------------------------------- /doc/world.md: -------------------------------------------------------------------------------- 1 | # World 2 | 3 | A World is a container for Entities, Components and Systems (ECS). So a World is quite important for the ECS, but you are not restricted to a single world. You can have multiple worlds running at the same time, or just a few and switch certain worlds off and on depending on your gameplay: 4 | ```dart 5 | final world = World(); 6 | ``` 7 | 8 | ## Registering a Component 9 | 10 | See [Registering a Component](./component.md#registering-a-component) for more information. 11 | 12 | ## Registering a System 13 | 14 | See [Registering a System](./system.md#registering-a-system) for more information. 15 | 16 | ### Deregister a System. 17 | 18 | See [Deregistering a System](./system.md#deregistering-a-system) for more information. 19 | 20 | ## Creating an Entity 21 | 22 | When you have registered all your components you can then start creating entities in your world: 23 | ```dart 24 | world.createEntity('Optional name'); 25 | ``` 26 | 27 | ## Initializing 28 | 29 | After adding all your components and systems you can initialize the world: 30 | ```dart 31 | world.init(); 32 | ``` 33 | 34 | This will initialize all the systems and make sure everything is in place. 35 | 36 | ## Executing 37 | 38 | Now you can execute the world by passing the delta since the last execution time: 39 | ```dart 40 | world.execute(delta); 41 | ``` 42 | 43 | This will run everything in the world once. You would normally call this somewhere in your game loop. 44 | 45 | ## Storing data 46 | 47 | It is also possible to store extra data in a World. This data will be accessible to anyone who has access to the World. It allows for passing data between systems or passing references that multiple systems need: 48 | ```dart 49 | // Storing 50 | world.store('yourKey', yourValue); 51 | 52 | // Retrieving 53 | final yourValue = world.retrieve('yourKey'); 54 | 55 | // Removing 56 | world.remove('yourKey'); 57 | ``` -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build outputs 6 | build/ 7 | 8 | # Directory created by dartdoc 9 | doc/api/ 10 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # A simple Oxygen example 2 | 3 | ## Run 4 | 5 | ``` 6 | dart --enable-asserts lib/main.dart 7 | ``` -------------------------------------------------------------------------------- /example/lib/components/color_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/color.dart'; 2 | import 'package:oxygen/oxygen.dart'; 3 | 4 | class ColorComponent extends ValueComponent {} 5 | -------------------------------------------------------------------------------- /example/lib/components/direction_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | 3 | enum Direction { 4 | up, 5 | down, 6 | left, 7 | right, 8 | } 9 | 10 | class DirectionComponent extends ValueComponent {} 11 | -------------------------------------------------------------------------------- /example/lib/components/name_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | 3 | class NameComponent extends ValueComponent {} 4 | -------------------------------------------------------------------------------- /example/lib/components/player_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | 3 | class PlayerComponent extends ValueComponent {} 4 | -------------------------------------------------------------------------------- /example/lib/components/position_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/vector2.dart'; 2 | import 'package:oxygen/oxygen.dart'; 3 | 4 | class PositionComponent extends Component { 5 | int? x; 6 | int? y; 7 | 8 | @override 9 | void init([Vector2? data]) { 10 | x = data?.x ?? 0; 11 | y = data?.y ?? 0; 12 | } 13 | 14 | @override 15 | void reset() { 16 | x = y = null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/lib/components/render_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | 3 | class RenderComponent extends ValueComponent {} 4 | -------------------------------------------------------------------------------- /example/lib/components/velocity_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/vector2.dart'; 2 | import 'package:oxygen/oxygen.dart'; 3 | 4 | class VelocityComponent extends Component { 5 | int? x; 6 | int? y; 7 | 8 | @override 9 | void init([Vector2? data]) { 10 | x = data?.x ?? 0; 11 | y = data?.y ?? 0; 12 | } 13 | 14 | @override 15 | void reset() { 16 | x = y = null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/components/color_component.dart'; 2 | import 'package:example/components/direction_component.dart'; 3 | import 'package:example/components/name_component.dart'; 4 | import 'package:example/components/player_component.dart'; 5 | import 'package:example/components/velocity_component.dart'; 6 | import 'package:example/systems/player_move_system.dart'; 7 | import 'package:example/utils/color.dart'; 8 | import 'package:example/utils/game.dart'; 9 | import 'package:example/utils/vector2.dart'; 10 | import 'package:example/utils/terminal.dart'; 11 | import 'package:oxygen/oxygen.dart'; 12 | 13 | import 'systems/move_system.dart'; 14 | import 'systems/render_system.dart'; 15 | import 'components/position_component.dart'; 16 | import 'components/render_component.dart'; 17 | 18 | void main() => ExampleGame(); 19 | 20 | class ExampleGame extends Game { 21 | late World world; 22 | 23 | @override 24 | void onLoad() { 25 | world = World(); 26 | 27 | world.registerSystem(PlayerMoveSystem()); 28 | world.registerSystem(MoveSystem()); 29 | world.registerSystem(RenderSystem()); 30 | world.registerComponent(() => DirectionComponent()); 31 | world.registerComponent(() => VelocityComponent()); 32 | world.registerComponent(() => PositionComponent()); 33 | world.registerComponent(() => PlayerComponent()); 34 | world.registerComponent(() => RenderComponent()); 35 | world.registerComponent(() => ColorComponent()); 36 | world.registerComponent(() => NameComponent()); 37 | 38 | world.createEntity() 39 | ..add('Boi') 40 | ..add(Direction.right) 41 | ..add(Colors.red) 42 | ..add('█') 43 | ..add( 44 | terminal.viewport.topRight.translate(-terminal.viewport.center.x, 0), 45 | ); 46 | 47 | world.createEntity() 48 | ..add('Tim') 49 | ..add(Direction.right) 50 | ..add() 51 | ..add('█') 52 | ..add(terminal.viewport.center); 53 | 54 | world.init(); 55 | } 56 | 57 | @override 58 | void update(double delta) => world.execute(delta); 59 | } 60 | -------------------------------------------------------------------------------- /example/lib/systems/move_system.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/components/position_component.dart'; 2 | import 'package:example/components/velocity_component.dart'; 3 | import 'package:example/utils/terminal.dart'; 4 | import 'package:example/utils/vector2.dart'; 5 | import 'package:oxygen/oxygen.dart'; 6 | 7 | class MoveSystem extends System { 8 | late Query query; 9 | 10 | @override 11 | void init() { 12 | query = createQuery([ 13 | Has(), 14 | Has(), 15 | ]); 16 | } 17 | 18 | @override 19 | void execute(delta) { 20 | for (final entity in query.entities) { 21 | final position = entity.get()!; 22 | final velocity = entity.get()!; 23 | 24 | position.x = position.x! + velocity.x!; 25 | position.y = position.y! + velocity.y!; 26 | 27 | if (!terminal.viewport.contains(Vector2(position.x!, position.y!))) { 28 | entity.dispose(); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/systems/player_move_system.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/components/color_component.dart'; 2 | import 'package:example/components/direction_component.dart'; 3 | import 'package:example/components/player_component.dart'; 4 | import 'package:example/components/position_component.dart'; 5 | import 'package:example/components/render_component.dart'; 6 | import 'package:example/components/velocity_component.dart'; 7 | import 'package:example/utils/color.dart'; 8 | import 'package:example/utils/keyboard.dart'; 9 | import 'package:example/utils/vector2.dart'; 10 | import 'package:oxygen/oxygen.dart'; 11 | 12 | class PlayerMoveSystem extends System { 13 | late Query query; 14 | 15 | late int _nextShot; 16 | 17 | @override 18 | void init() { 19 | _nextShot = 0; 20 | query = createQuery([ 21 | Has(), 22 | Has(), 23 | Has(), 24 | ]); 25 | } 26 | 27 | void setDirection(Direction dir, Entity entity) { 28 | query.entities.first.get()!.value = dir; 29 | } 30 | 31 | Direction getDirection(Entity entity) { 32 | return query.entities.first.get()!.value!; 33 | } 34 | 35 | @override 36 | void execute(delta) { 37 | final player = query.entities.first; 38 | final position = player.get()!; 39 | 40 | if (keyboard.isPressed(Key.w)) { 41 | position.y = position.y! - 1; 42 | setDirection(Direction.up, player); 43 | } else if (keyboard.isPressed(Key.s)) { 44 | position.y = position.y! + 1; 45 | setDirection(Direction.down, player); 46 | } else if (keyboard.isPressed(Key.a)) { 47 | position.x = position.x! - 1; 48 | setDirection(Direction.left, player); 49 | } else if (keyboard.isPressed(Key.d)) { 50 | position.x = position.x! + 1; 51 | setDirection(Direction.right, player); 52 | } 53 | 54 | if (keyboard.isPressed(Key.space) && 55 | _nextShot < DateTime.now().millisecondsSinceEpoch) { 56 | _nextShot = DateTime.now().millisecondsSinceEpoch + 500; 57 | final direction = getDirection(player); 58 | world!.createEntity() 59 | ..add( 60 | Vector2( 61 | direction == Direction.left 62 | ? -1 63 | : direction == Direction.right 64 | ? 1 65 | : 0, 66 | direction == Direction.up 67 | ? -1 68 | : direction == Direction.down 69 | ? 1 70 | : 0, 71 | ), 72 | ) 73 | ..add('♥') 74 | ..add(Colors.red) 75 | ..add(Vector2(position.x!, position.y!)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/lib/systems/render_system.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/color.dart'; 2 | import 'package:example/components/color_component.dart'; 3 | import 'package:example/components/name_component.dart'; 4 | import 'package:example/utils/terminal.dart'; 5 | import 'package:example/utils/vector2.dart'; 6 | import 'package:oxygen/oxygen.dart'; 7 | 8 | import '../components/render_component.dart'; 9 | import '../components/position_component.dart'; 10 | 11 | class RenderSystem extends System { 12 | late Query query; 13 | 14 | @override 15 | void init() { 16 | query = createQuery([ 17 | Has(), 18 | Has(), 19 | ]); 20 | } 21 | 22 | @override 23 | void execute(delta) { 24 | query.entities.forEach((entity) { 25 | final position = entity.get()!; 26 | final key = entity.get()!.value; 27 | final color = entity.get()?.value ?? Colors.white; 28 | 29 | terminal 30 | ..save() 31 | ..translate(position.x!, position.y!) 32 | ..draw(key!, foregroundColor: color); 33 | if (entity.has()) { 34 | final name = entity.get()!.value!; 35 | terminal 36 | ..translate(-(name.length ~/ 2), 1) 37 | ..draw(name); 38 | } 39 | terminal.restore(); 40 | }); 41 | 42 | terminal.draw('delta: $delta', foregroundColor: Colors.green); 43 | terminal.draw( 44 | 'entites: ${world!.entities.length}', 45 | foregroundColor: Colors.green, 46 | position: Vector2(0, 1), 47 | ); 48 | terminal.draw( 49 | ' W A S D | Move Tim\n' 50 | ' Space | Shoot', 51 | foregroundColor: Colors.green, 52 | position: terminal.viewport.bottomLeft.translate(0, -2), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/lib/utils/color.dart: -------------------------------------------------------------------------------- 1 | class Colors { 2 | static const Color black = Color(0x000000); 3 | static const Color white = Color(0xFFFFFF); 4 | static const Color red = Color(0xFF0000); 5 | static const Color green = Color(0x00FF00); 6 | static const Color blue = Color(0x0000FF); 7 | 8 | const Colors._(); 9 | } 10 | 11 | class Color { 12 | final int value; 13 | 14 | const Color(this.value); 15 | 16 | String toRGB() { 17 | final r = (value >> 16) & 255; 18 | final g = (value >> 8) & 255; 19 | final b = value & 255; 20 | return '$r;$g;$b'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/lib/utils/game.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/keyboard.dart'; 2 | import 'package:example/utils/terminal.dart'; 3 | 4 | const TARGET_FPS = 120; 5 | const FRAME_TIME = 1000 ~/ TARGET_FPS; 6 | 7 | abstract class Game { 8 | final Stopwatch _stopwatch; 9 | 10 | late int _previous; 11 | 12 | Game() : _stopwatch = Stopwatch()..start() { 13 | onLoad(); 14 | 15 | _previous = _stopwatch.elapsedMilliseconds; 16 | 17 | loop(); 18 | } 19 | 20 | void loop() { 21 | final current = _stopwatch.elapsedMilliseconds; 22 | final elapsed = current - _previous; 23 | _previous = current; 24 | 25 | update(elapsed / FRAME_TIME); 26 | 27 | keyboard.clear(); 28 | 29 | terminal.clear(); 30 | terminal.render(); 31 | 32 | Future.delayed(Duration(milliseconds: FRAME_TIME)).then((value) => loop()); 33 | } 34 | 35 | void onLoad(); 36 | 37 | void update(double delta); 38 | } 39 | -------------------------------------------------------------------------------- /example/lib/utils/keyboard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | enum Key { 4 | w, 5 | a, 6 | s, 7 | d, 8 | space, 9 | } 10 | 11 | final keyboard = Keyboard._(); 12 | 13 | class Keyboard { 14 | final List _keysPressed = []; 15 | 16 | bool isPressed(Key key) => _keysPressed.contains(key); 17 | 18 | Keyboard._() { 19 | stdin.echoMode = false; 20 | stdin.lineMode = false; 21 | stdin.listen((event) { 22 | switch (event.first) { 23 | case 32: 24 | _keysPressed.add(Key.space); 25 | break; 26 | case 119: 27 | _keysPressed.add(Key.w); 28 | break; 29 | case 97: 30 | _keysPressed.add(Key.a); 31 | break; 32 | case 115: 33 | _keysPressed.add(Key.s); 34 | break; 35 | case 100: 36 | _keysPressed.add(Key.d); 37 | break; 38 | } 39 | }); 40 | } 41 | 42 | void clear() => _keysPressed.clear(); 43 | } 44 | -------------------------------------------------------------------------------- /example/lib/utils/rect.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/utils/vector2.dart'; 2 | 3 | class Rect { 4 | final int left; 5 | final int top; 6 | final int right; 7 | final int bottom; 8 | 9 | int get width => right - left; 10 | int get height => bottom - top; 11 | 12 | Vector2 get topLeft => Vector2(left, top); 13 | Vector2 get topRight => Vector2(left + width, top); 14 | Vector2 get bottomLeft => Vector2(left, top + height); 15 | Vector2 get bottomRight => Vector2(left + width, top + height); 16 | Vector2 get center => Vector2(left + width ~/ 2, top + height ~/ 2); 17 | 18 | const Rect.fromLTRB(this.left, this.top, this.right, this.bottom); 19 | 20 | const Rect.fromLTWH(int left, int top, int width, int height) 21 | : this.fromLTRB(left, top, left + width, top + height); 22 | 23 | bool contains(Vector2 position) { 24 | return position.x >= left && 25 | position.x < right && 26 | position.y >= top && 27 | position.y < bottom; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/lib/utils/terminal.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:example/utils/color.dart'; 4 | import 'package:example/utils/vector2.dart'; 5 | import 'package:example/utils/rect.dart'; 6 | 7 | const ESCAPE = '\x1B'; 8 | 9 | final Terminal terminal = Terminal._(); 10 | 11 | class Terminal { 12 | Rect get viewport => Rect.fromLTWH( 13 | 0, 14 | 0, 15 | stdout.terminalColumns, 16 | stdout.terminalLines, 17 | ); 18 | 19 | bool hideCursor = true; 20 | 21 | late List buffer; 22 | 23 | Terminal._() { 24 | if (!stdout.supportsAnsiEscapes) { 25 | throw Exception( 26 | 'Sorry only terminals that support ANSI escapes are supported', 27 | ); 28 | } 29 | buffer = [if (hideCursor) _escape('?25l') else _escape('?25h')]; 30 | 31 | ProcessSignal.sigint.watch().listen((ProcessSignal signal) { 32 | clear(); 33 | stdout.write(_escape('?25h')); 34 | exit(0); 35 | }); 36 | } 37 | 38 | final List _positions = [Vector2.zero()]; 39 | Vector2 get _position => _positions.last; 40 | set _position(Vector2 position) { 41 | _positions.last = position; 42 | } 43 | 44 | void translate(int x, int y) { 45 | _position = _position.translate(x, y); 46 | buffer.add(_escape('${_position.y};${_position.x}H')); 47 | } 48 | 49 | String _escape(String data) => '$ESCAPE[$data'; 50 | 51 | void draw( 52 | String data, { 53 | Vector2 position = const Vector2.zero(), 54 | Color foregroundColor = Colors.white, 55 | Color backgroundColor = Colors.black, 56 | }) { 57 | position = _position.translate(position.x, position.y); 58 | buffer.addAll([ 59 | _escape('${position.y + 1};${position.x}H'), 60 | _escape('38;2;${foregroundColor.toRGB()}m'), 61 | _escape('48;2;${backgroundColor.toRGB()}m'), 62 | data, 63 | ]); 64 | } 65 | 66 | void clear() => stdout.write('$ESCAPE[2J$ESCAPE[0;0H'); 67 | 68 | void save() => _positions.add(_position); 69 | 70 | void restore() => _positions.removeLast(); 71 | 72 | void render() { 73 | for (final line in buffer) { 74 | stdout.write(line); 75 | } 76 | buffer = [if (hideCursor) _escape('?25l') else _escape('?25h')]; 77 | _position = Vector2.zero(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/lib/utils/vector2.dart: -------------------------------------------------------------------------------- 1 | class Vector2 { 2 | final int x; 3 | final int y; 4 | 5 | const Vector2(this.x, this.y); 6 | 7 | const Vector2.zero() 8 | : x = 0, 9 | y = 0; 10 | 11 | Vector2 translate(int x, int y) => Vector2(this.x + x, this.y + y); 12 | 13 | Vector2 operator +(Vector2 other) { 14 | return Vector2(x + other.x, y + other.y); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | meta: 5 | dependency: transitive 6 | description: 7 | name: meta 8 | sha256: "98a7492d10d7049ea129fd4e50f7cdd2d5008522b1dfa1148bbbc542b9dd21f7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "1.3.0" 12 | oxygen: 13 | dependency: "direct main" 14 | description: 15 | path: ".." 16 | relative: true 17 | source: path 18 | version: "0.3.1" 19 | pedantic: 20 | dependency: "direct dev" 21 | description: 22 | name: pedantic 23 | sha256: "8f6460c77a98ad2807cd3b98c67096db4286f56166852d0ce5951bb600a63594" 24 | url: "https://pub.dev" 25 | source: hosted 26 | version: "1.11.0" 27 | sdks: 28 | dart: ">=2.15.0 <4.0.0" 29 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A simple Oxygen example. 3 | 4 | environment: 5 | sdk: '>=2.12.0 <3.0.0' 6 | 7 | dependencies: 8 | oxygen: 9 | path: ../ 10 | 11 | dev_dependencies: 12 | pedantic: ^1.11.0 -------------------------------------------------------------------------------- /lib/oxygen.dart: -------------------------------------------------------------------------------- 1 | /// A lightweight Entity Component System written in Dart. 2 | library oxygen; 3 | 4 | import 'dart:collection'; 5 | 6 | import 'package:meta/meta.dart'; 7 | 8 | part 'src/component/component_manager.dart'; 9 | part 'src/component/component.dart'; 10 | part 'src/component/value_component.dart'; 11 | part 'src/entity/entity_manager.dart'; 12 | part 'src/entity/entity.dart'; 13 | part 'src/pooling/object_pool.dart'; 14 | part 'src/pooling/pool_object.dart'; 15 | part 'src/query/filter.dart'; 16 | part 'src/query/query_manager.dart'; 17 | part 'src/query/query.dart'; 18 | part 'src/system/system_manager.dart'; 19 | part 'src/system/system.dart'; 20 | part 'src/world.dart'; 21 | -------------------------------------------------------------------------------- /lib/src/component/component.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// A [Component] is a way to store data for an [Entity]. 4 | /// 5 | /// It does not define any kind of behaviour because that is handled by the systems. 6 | abstract class Component extends PoolObject {} 7 | -------------------------------------------------------------------------------- /lib/src/component/component_manager.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | typedef ComponentBuilder = T Function(); 4 | 5 | /// An [ObjectPool] for a type of [Component]. 6 | class ComponentPool extends ObjectPool { 7 | final ComponentBuilder componentBuilder; 8 | 9 | ComponentPool(this.componentBuilder) : super(); 10 | 11 | @override 12 | T builder() => componentBuilder(); 13 | } 14 | 15 | /// Manages all the components in a [World]. 16 | class ComponentManager { 17 | /// The [World] which this manager belongs to. 18 | final World world; 19 | 20 | /// List of registered components. 21 | final List components = []; 22 | 23 | /// Map of [ObjectPool]s for each kind of registered [Component]. 24 | final Map _componentPool = {}; 25 | 26 | ComponentManager(this.world); 27 | 28 | /// Check if a component is registered. 29 | bool hasComponent() => components.contains(T); 30 | 31 | /// Register a component. 32 | /// 33 | /// If a component is already registered it will just return. 34 | /// 35 | /// The [builder] is used for pooling. 36 | void registerComponent(T Function() builder) { 37 | if (components.contains(T)) { 38 | return; 39 | } 40 | 41 | components.add(T); 42 | _componentPool[T] = ComponentPool(builder); 43 | } 44 | 45 | ComponentPool getComponentPool, V>() { 46 | return _componentPool[T] as ComponentPool; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/component/value_component.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// With the [ValueComponent] you can easily define single value components. 4 | class ValueComponent extends Component { 5 | T? value; 6 | 7 | @override 8 | void init([T? data]) => value = data; 9 | 10 | @override 11 | void reset() => value = null; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/entity/entity.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// An Entity is a simple "container" for components. 4 | /// 5 | /// It serves no purpose apart from being an abstraction container around the components. 6 | class Entity extends PoolObject { 7 | /// The manager that handles all the entities. 8 | final EntityManager _entityManager; 9 | 10 | /// Map of all the components added. 11 | final Map _components = {}; 12 | 13 | final Set _componentsToRemove = {}; 14 | 15 | /// Set of all the component types that are added. 16 | final Set _componentTypes = {}; 17 | 18 | /// Internal identifier. 19 | int? id; 20 | 21 | /// Indication if this entity is no longer "in this world". 22 | bool alive = false; 23 | 24 | /// Optional name to identify an entity by. 25 | String? name; 26 | 27 | Entity(this._entityManager) : id = _entityManager._nextEntityId++; 28 | 29 | /// Retrieves a Component by Type. 30 | /// 31 | /// If the component is not registered, it will return `null`. 32 | T? get() { 33 | assert( 34 | T != Component || T != ValueComponent, 35 | 'An implemented Component was expected', 36 | ); 37 | return _components[T] as T?; 38 | } 39 | 40 | /// Check if a component is added. 41 | bool has() => _componentTypes.contains(T); 42 | 43 | /// Add a component. 44 | void add, V>([V? data]) { 45 | assert( 46 | T != Component || T != ValueComponent, 47 | 'An implemented Component was expected', 48 | ); 49 | _entityManager.addComponentToEntity(this, data); 50 | } 51 | 52 | /// Remove a component. 53 | void remove() { 54 | assert( 55 | T != Component || T != ValueComponent, 56 | 'An implemented Component was expected', 57 | ); 58 | _entityManager.removeComponentFromEntity(this); 59 | } 60 | 61 | @override 62 | void init([String? name]) { 63 | alive = true; 64 | this.name = name; 65 | } 66 | 67 | @override 68 | void reset() { 69 | id = null; 70 | alive = false; 71 | _components.clear(); 72 | _componentTypes.clear(); 73 | } 74 | 75 | @override 76 | void dispose() => _entityManager.removeEntity(this); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/entity/entity_manager.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// ObjectPool for entities. 4 | class EntityPool extends ObjectPool { 5 | /// The manager that handles all the entities. 6 | EntityManager entityManager; 7 | 8 | EntityPool(this.entityManager) : super(); 9 | 10 | @override 11 | Entity builder() => Entity(entityManager); 12 | } 13 | 14 | /// Manages all the entities in a [World]. 15 | /// 16 | /// **Note**: Technically speaking we can have multiple types of entities, there 17 | /// is nothing implemented on the [World] for it yet but this manager would be 18 | /// able to handle that easily. 19 | class EntityManager { 20 | /// The [World] which this manager belongs to. 21 | final World world; 22 | 23 | /// Active entities in the [world]. 24 | final List _entities = []; 25 | 26 | /// Entities that have components that should be removed. 27 | final List _entitiesWithRemovedComponents = []; 28 | 29 | /// Entities that are ready to be removed. 30 | final List _entitiesToRemove = []; 31 | 32 | /// Entities with names are easily accesable this way. 33 | final Map _entitiesByName = {}; 34 | 35 | /// The pool from which entities are pulled and released into. 36 | late EntityPool _entityPool; 37 | 38 | /// The next identifier for an [Entity]. 39 | int _nextEntityId = 0; 40 | 41 | /// [QueryManager] that handles the queries. 42 | late QueryManager _queryManager; 43 | 44 | EntityManager(this.world) { 45 | _entityPool = EntityPool(this); 46 | _queryManager = QueryManager(this); 47 | } 48 | 49 | /// Get an entity by name. 50 | Entity? getEntityByName(String name) => _entitiesByName[name]; 51 | 52 | /// Create a new entity. 53 | /// 54 | /// Will acquire a new entity from the pool, and initialize it. 55 | Entity createEntity([String? name]) { 56 | final entity = _entityPool.acquire(name); 57 | entity.id = _nextEntityId++; 58 | 59 | if (name != null) { 60 | _entitiesByName[name] = entity; 61 | } 62 | 63 | _entities.add(entity); 64 | return entity; 65 | } 66 | 67 | /// Add given component to an entity. 68 | /// 69 | /// If the entity already has that component it will just return. 70 | /// 71 | /// The [data] argument has to be of the type [V]. 72 | void addComponentToEntity, V>(Entity entity, V? data) { 73 | assert(T != Component, 'An implemented Component was expected'); 74 | assert( 75 | world.componentManager.components.contains(T), 76 | 'Component $T has not been registered to the World', 77 | ); 78 | 79 | if (entity._componentTypes.contains(T)) { 80 | return; // Entity already has an instance of the component. 81 | } 82 | 83 | final componentPool = world.componentManager.getComponentPool(); 84 | final component = componentPool.acquire(data); 85 | 86 | entity._componentTypes.add(T); 87 | entity._components[T] = component; 88 | _queryManager._onComponentOfEntityChanged(entity, T); 89 | } 90 | 91 | /// Remove and dispose a component by generics. 92 | void removeComponentFromEntity(Entity entity) { 93 | assert(T != Component, 'An implemented Component was expected'); 94 | assert( 95 | world.componentManager.components.contains(T), 96 | 'Component $T has not been registered to the World', 97 | ); 98 | return _markComponentForRemoval(entity, T); 99 | } 100 | 101 | /// Mark a component for removal. 102 | void _markComponentForRemoval(Entity entity, Type componentType) { 103 | if (!entity._componentTypes.contains(componentType) || 104 | entity._componentsToRemove.contains(componentType)) { 105 | return; 106 | } 107 | _entitiesWithRemovedComponents.add(entity); 108 | entity._componentsToRemove.add(componentType); 109 | } 110 | 111 | /// Remove and dispose a component. 112 | void _removeComponentFromEntity(Entity entity, Type componentType) { 113 | if (!entity._componentTypes.contains(componentType)) { 114 | return; 115 | } 116 | entity._componentsToRemove.remove(componentType); 117 | entity._componentTypes.remove(componentType); 118 | final component = entity._components.remove(componentType); 119 | component?.dispose(); 120 | _queryManager._onComponentOfEntityChanged(entity, componentType); 121 | } 122 | 123 | /// Removes all the components the given entity has. 124 | void _removeAllComponentFromEntity(Entity entity) { 125 | // Make a copy so we can update this set while looping over it. 126 | final componentsToRemove = entity._componentsToRemove.toSet(); 127 | for (final componentType in componentsToRemove) { 128 | _removeComponentFromEntity(entity, componentType); 129 | } 130 | 131 | // Make a copy so we can update this set while looping over it. 132 | final componentTypes = entity._componentTypes.toSet(); 133 | for (final componentType in componentTypes) { 134 | _removeComponentFromEntity(entity, componentType); 135 | } 136 | } 137 | 138 | /// Mark an entity for removal. 139 | /// 140 | /// It will be fully removed in the next execute cycle. 141 | void removeEntity(Entity entity) { 142 | if (!_entities.contains(entity) || _entitiesToRemove.contains(entity)) { 143 | return; 144 | } 145 | 146 | entity.alive = false; 147 | 148 | _entitiesToRemove.add(entity); 149 | } 150 | 151 | /// Process all removed entities from the last execute cycle. 152 | void processRemovedEntities() { 153 | _entitiesToRemove.forEach(_releaseEntity); 154 | _entitiesToRemove.clear(); 155 | } 156 | 157 | /// Process all removed components from the last execute cycle. 158 | void processRemovedComponents() { 159 | _entitiesWithRemovedComponents.forEach(_releaseComponentsFromEntity); 160 | _entitiesWithRemovedComponents.clear(); 161 | } 162 | 163 | /// Fully release and reset an component. 164 | void _releaseComponentsFromEntity(Entity entity) { 165 | // Make a copy so we can update this set while looping over it. 166 | final components = entity._componentsToRemove.toList(); 167 | components 168 | .forEach((component) => _removeComponentFromEntity(entity, component)); 169 | } 170 | 171 | /// Fully release and reset an entity. 172 | void _releaseEntity(Entity entity) { 173 | _removeAllComponentFromEntity(entity); 174 | _queryManager._onEntityRemoved(entity); 175 | 176 | _entities.remove(entity); 177 | if (_entitiesByName.containsKey(entity.name)) { 178 | _entitiesByName.remove(entity.name); 179 | } 180 | entity._pool?.release(entity); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/src/pooling/object_pool.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | abstract class ObjectPool, V> { 4 | final Queue _pool = Queue(); 5 | 6 | int _count = 0; 7 | 8 | /// The total amount of the objects in the pool. 9 | int get totalSize => _count; 10 | 11 | /// The amount of objects that are free to use in the pool. 12 | int get totalFree => _pool.length; 13 | 14 | /// The amount of objects that are in use in the pool. 15 | int get totalUsed => _count - _pool.length; 16 | 17 | ObjectPool({int? initialSize}) { 18 | if (initialSize != null) { 19 | expand(initialSize); 20 | } 21 | } 22 | 23 | /// Acquire a new object. 24 | /// 25 | /// If the pool is empty it will automically grow by 20% + 1. 26 | /// To ensure there is always something in the pool. 27 | /// 28 | /// The [data] argument will be passed to [PoolObject.init] when it gets acquired. 29 | T acquire([V? data]) { 30 | if (_pool.isEmpty) { 31 | expand((_count * 0.2).floor() + 1); 32 | } 33 | final object = _pool.removeLast(); 34 | return object..init(data); 35 | } 36 | 37 | /// Release a object back into the pool. 38 | void release(T item) => _pool.addLast(item..reset()); 39 | 40 | /// Expand the existing pool by the given count. 41 | void expand(int count) { 42 | for (var i = 0; i < count; i++) { 43 | final item = builder(); 44 | item._pool = this; 45 | _pool.addLast(item); 46 | } 47 | _count += count; 48 | } 49 | 50 | /// Builder for creating a new instance of a [PoolObject]. 51 | T builder(); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/pooling/pool_object.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | abstract class PoolObject { 4 | /// The pool from which the object came from. 5 | ObjectPool? _pool; 6 | 7 | /// Initialize this object. 8 | /// 9 | /// See [ObjectPool.acquire] for more information on how this gets called. 10 | void init([T? data]); 11 | 12 | /// Reset this object. 13 | void reset(); 14 | 15 | /// Release this object back into the pool. 16 | void dispose() => _pool?.release(this); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/query/filter.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// A filter allows a [Query] to be able to filter down entities. 4 | abstract class Filter { 5 | Filter() : assert(T != Component); 6 | 7 | /// Unique identifier. 8 | String get filterId; 9 | 10 | Type get type => T; 11 | 12 | /// Method for matching an [Entity] against this filter. 13 | bool match(Entity entity); 14 | } 15 | 16 | class Has extends Filter { 17 | @override 18 | String get filterId => 'Has<$T>'; 19 | 20 | @override 21 | bool match(Entity entity) => entity._componentTypes.contains(T); 22 | } 23 | 24 | class HasNot extends Filter { 25 | @override 26 | String get filterId => 'HasNot<$T>'; 27 | 28 | @override 29 | bool match(Entity entity) => !entity._componentTypes.contains(T); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/query/query.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// A Query is a way to retrieve entities by matching their components against the Query filters. 4 | /// 5 | /// They are used by systems to retrieve the entities they care about. 6 | class Query { 7 | /// The manager that handles all the entities. 8 | final EntityManager entityManager; 9 | 10 | /// The unique filters to filter by. 11 | final Iterable _filters; 12 | 13 | final List _entities = []; 14 | 15 | /// Entities that are found through [_filters]. 16 | List get entities => List.unmodifiable(_entities); 17 | 18 | Query(this.entityManager, this._filters) : assert(_filters.isNotEmpty) { 19 | for (final entity in entityManager._entities) { 20 | if (match(entity)) { 21 | _entities.add(entity); 22 | } 23 | } 24 | } 25 | 26 | /// Check if the given entity matches against the query. 27 | bool match(Entity entity) => _filters.every((filter) => filter.match(entity)); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/query/query_manager.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// Manages all the queries to ensure we don't duplicate lists. 4 | class QueryManager { 5 | /// The manager that handles all the entities. 6 | final EntityManager entityManager; 7 | 8 | /// Cache of all the created queries. 9 | /// 10 | /// If a new [Query] is requested then [_createKey] is used to check 11 | /// if we already have the requested query in cache and return that one instead. 12 | final Map _queries = {}; 13 | 14 | QueryManager(this.entityManager); 15 | 16 | void _onEntityRemoved(Entity entity) { 17 | for (final query in _queries.values) { 18 | if (query._entities.contains(entity)) { 19 | query._entities.remove(entity); 20 | } 21 | } 22 | } 23 | 24 | void _onComponentOfEntityChanged(Entity entity, Type componentType) { 25 | for (final query in _queries.values) { 26 | // Entity should only be added when all the following conditions are met: 27 | // - the Entity matches the complete query. 28 | // - the Entity is not already part of the query. 29 | if (query.match(entity) && !query._entities.contains(entity)) { 30 | query._entities.add(entity); 31 | } 32 | 33 | // Entity should only be removed when all the following conditions are met: 34 | // - the Entity matches the complete query. 35 | // - the Entity is not already part of the query. 36 | if (!query.match(entity) && query._entities.contains(entity)) { 37 | query._entities.remove(entity); 38 | } 39 | } 40 | } 41 | 42 | /// Creates a unique key to identify a [Query] by. 43 | String _createKey(Iterable filters) { 44 | return filters.map((f) { 45 | if (!entityManager.world.componentManager.components.contains(f.type)) { 46 | throw Exception( 47 | 'Tried to query on ${f.type}, but this component has not yet been registered to the World', 48 | ); 49 | } 50 | return f.filterId; 51 | }).join('-'); 52 | } 53 | 54 | /// Create or retrieve a cached query. 55 | Query createQuery(Iterable filters) { 56 | return _queries.update( 57 | _createKey(filters), 58 | (value) => value, 59 | ifAbsent: () => Query(entityManager, filters), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/system/system.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// Systems contain the logic for components. 4 | /// 5 | /// They can update data stored in the components. 6 | /// They query on components to get the entities that fit their Query. 7 | /// And they can iterate through those entities each execution frame. 8 | abstract class System { 9 | /// The world to which this system belongs to. 10 | World? world; 11 | 12 | /// The priority of this system. 13 | /// 14 | /// Used to set the priority of this system compared to the other systems. 15 | /// A System with a priority of 1 will go before a System with a priority of 2. 16 | /// 17 | /// It can't be changed at runtime. 18 | final int priority; 19 | 20 | System({this.priority = 0}); 21 | 22 | /// Initialize the System. 23 | void init(); 24 | 25 | /// Disposing of the System. 26 | @mustCallSuper 27 | void dispose() { 28 | world = null; 29 | } 30 | 31 | /// Create a new Query to filter entites. 32 | Query createQuery(Iterable filters) => 33 | world!.entityManager._queryManager.createQuery(filters); 34 | 35 | /// Execute the System. 36 | void execute(double delta); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/system/system_manager.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | /// Manages all registered systems. 4 | class SystemManager { 5 | /// The world in which this manager lives. 6 | final World world; 7 | 8 | /// All the registered systems. 9 | final List _systems = []; 10 | 11 | UnmodifiableListView get systems => UnmodifiableListView(_systems); 12 | 13 | final Map _systemsByType = {}; 14 | 15 | SystemManager(this.world); 16 | 17 | /// Initialize all the systems that are registered. 18 | void init() { 19 | for (final system in _systems) { 20 | system.init(); 21 | } 22 | } 23 | 24 | /// Register a system. 25 | /// 26 | /// If a given system type has already been added, it will simply return. 27 | void registerSystem(T system) { 28 | assert(system.world == null, '$T is already registered'); 29 | if (_systemsByType.containsKey(system.runtimeType)) { 30 | return; 31 | } 32 | system.world = world; 33 | _systems.add(system); 34 | _systemsByType[T] = system; 35 | 36 | _systems.sort((a, b) => a.priority - b.priority); 37 | } 38 | 39 | /// Deregister a previously registered system. 40 | /// 41 | /// If the given system type is not found, it will simply return. 42 | void deregisterSystem(Type systemType) { 43 | if (!_systemsByType.containsKey(systemType)) { 44 | return; 45 | } 46 | final system = _systemsByType.remove(systemType); 47 | system?.dispose(); 48 | _systems.remove(system); 49 | 50 | _systems.sort((a, b) => a.priority - b.priority); 51 | } 52 | 53 | /// Execute all the systems that are registered. 54 | void _execute(double delta) { 55 | for (final system in _systems) { 56 | system.execute(delta); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/world.dart: -------------------------------------------------------------------------------- 1 | part of oxygen; 2 | 3 | class World { 4 | final HashMap _storedItems = HashMap(); 5 | 6 | UnmodifiableListView get entities { 7 | return UnmodifiableListView(entityManager._entities); 8 | } 9 | 10 | late EntityManager entityManager; 11 | 12 | late ComponentManager componentManager; 13 | 14 | late SystemManager systemManager; 15 | 16 | World() { 17 | entityManager = EntityManager(this); 18 | componentManager = ComponentManager(this); 19 | systemManager = SystemManager(this); 20 | } 21 | 22 | /// Store extra data. 23 | void store(String key, T item) => _storedItems[key] = item; 24 | 25 | /// Retrieve extra data. 26 | T? retrieve(String key) => _storedItems[key]; 27 | 28 | /// Remove extra data. 29 | void remove(String key) => _storedItems[key] = null; 30 | 31 | @mustCallSuper 32 | 33 | /// Initialize the World. 34 | /// 35 | /// Will initialize all the registered [System]s. 36 | void init() { 37 | systemManager.init(); 38 | } 39 | 40 | /// Register a [System]. 41 | /// 42 | /// Keep in mind you can't share the same instance of system across multiple worlds. 43 | void registerSystem(T system) { 44 | systemManager.registerSystem(system); 45 | } 46 | 47 | /// Deregister a registered [System]. 48 | void deregisterSystem() { 49 | systemManager.deregisterSystem(T); 50 | } 51 | 52 | /// Register a [Component] builder. 53 | void registerComponent, V>( 54 | ComponentBuilder builder, 55 | ) { 56 | componentManager.registerComponent(builder); 57 | } 58 | 59 | /// Create a new [Entity]. 60 | Entity createEntity([String? name]) => entityManager.createEntity(name); 61 | 62 | /// Execute everything in the World once. 63 | void execute(double delta) { 64 | systemManager._execute(delta); 65 | entityManager.processRemovedEntities(); 66 | entityManager.processRemovedComponents(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "61.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "5.13.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | benchmark: 37 | dependency: "direct dev" 38 | description: 39 | name: benchmark 40 | sha256: cb3eeea01e3f054df76ee9775ca680f3afa5f19f39b2bb426ba78ba27654493b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "0.3.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "5bbf32bc9e518d41ec49718e2931cd4527292c9b0c6d2dffcf7fe6b9a8a8cf72" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.0" 52 | charcode: 53 | dependency: transitive 54 | description: 55 | name: charcode 56 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | cli_util: 61 | dependency: transitive 62 | description: 63 | name: cli_util 64 | sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "0.4.1" 68 | collection: 69 | dependency: transitive 70 | description: 71 | name: collection 72 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.18.0" 76 | convert: 77 | dependency: transitive 78 | description: 79 | name: convert 80 | sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "3.0.1" 84 | coverage: 85 | dependency: transitive 86 | description: 87 | name: coverage 88 | sha256: ad538fa2e8f6b828d54c04a438af816ce814de404690136d3b9dfb3a436cd01c 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.0.3" 92 | crypto: 93 | dependency: transitive 94 | description: 95 | name: crypto 96 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "3.0.3" 100 | csslib: 101 | dependency: transitive 102 | description: 103 | name: csslib 104 | sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.0.0" 108 | dartdoc: 109 | dependency: "direct dev" 110 | description: 111 | name: dartdoc 112 | sha256: d9bab893c9f42615f62bf2d3ff2b89d5905952d1d42cc7890003537249cb472e 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "6.3.0" 116 | file: 117 | dependency: transitive 118 | description: 119 | name: file 120 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "7.0.0" 124 | frontend_server_client: 125 | dependency: transitive 126 | description: 127 | name: frontend_server_client 128 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "3.2.0" 132 | glob: 133 | dependency: transitive 134 | description: 135 | name: glob 136 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.2" 140 | html: 141 | dependency: transitive 142 | description: 143 | name: html 144 | sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "0.15.4" 148 | http_multi_server: 149 | dependency: transitive 150 | description: 151 | name: http_multi_server 152 | sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "3.0.1" 156 | http_parser: 157 | dependency: transitive 158 | description: 159 | name: http_parser 160 | sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "4.0.0" 164 | io: 165 | dependency: transitive 166 | description: 167 | name: io 168 | sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.0.3" 172 | js: 173 | dependency: transitive 174 | description: 175 | name: js 176 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "0.6.7" 180 | logging: 181 | dependency: transitive 182 | description: 183 | name: logging 184 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.2.0" 188 | markdown: 189 | dependency: transitive 190 | description: 191 | name: markdown 192 | sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "7.2.2" 196 | matcher: 197 | dependency: transitive 198 | description: 199 | name: matcher 200 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "0.12.16+1" 204 | meta: 205 | dependency: "direct main" 206 | description: 207 | name: meta 208 | sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "1.12.0" 212 | mime: 213 | dependency: transitive 214 | description: 215 | name: mime 216 | sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.0.1" 220 | node_preamble: 221 | dependency: transitive 222 | description: 223 | name: node_preamble 224 | sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.0.1" 228 | package_config: 229 | dependency: transitive 230 | description: 231 | name: package_config 232 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "2.1.0" 236 | path: 237 | dependency: transitive 238 | description: 239 | name: path 240 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.9.0" 244 | pedantic: 245 | dependency: "direct dev" 246 | description: 247 | name: pedantic 248 | sha256: "8f6460c77a98ad2807cd3b98c67096db4286f56166852d0ce5951bb600a63594" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.11.0" 252 | pool: 253 | dependency: transitive 254 | description: 255 | name: pool 256 | sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.5.0" 260 | pub_semver: 261 | dependency: transitive 262 | description: 263 | name: pub_semver 264 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "2.1.4" 268 | shelf: 269 | dependency: transitive 270 | description: 271 | name: shelf 272 | sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.2.0" 276 | shelf_packages_handler: 277 | dependency: transitive 278 | description: 279 | name: shelf_packages_handler 280 | sha256: e0b44ebddec91e70a713e13adf93c1b2100821303b86a18e1ef1d082bd8bd9b8 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "3.0.0" 284 | shelf_static: 285 | dependency: transitive 286 | description: 287 | name: shelf_static 288 | sha256: "4a0d12cd512aa4fc55fed5f6280f02ef183f47ba29b4b0dfd621b1c99b7e6361" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.1.0" 292 | shelf_web_socket: 293 | dependency: transitive 294 | description: 295 | name: shelf_web_socket 296 | sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "1.0.1" 300 | source_map_stack_trace: 301 | dependency: transitive 302 | description: 303 | name: source_map_stack_trace 304 | sha256: "8c463326277f68a628abab20580047b419c2ff66756fd0affd451f73f9508c11" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "2.1.0" 308 | source_maps: 309 | dependency: transitive 310 | description: 311 | name: source_maps 312 | sha256: "52de2200bb098de739794c82d09c41ac27b2e42fd7e23cce7b9c74bf653c7296" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "0.10.10" 316 | source_span: 317 | dependency: transitive 318 | description: 319 | name: source_span 320 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.10.0" 324 | stack_trace: 325 | dependency: transitive 326 | description: 327 | name: stack_trace 328 | sha256: f8d9f247e2f9f90e32d1495ff32dac7e4ae34ffa7194c5ff8fcc0fd0e52df774 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "1.10.0" 332 | stream_channel: 333 | dependency: transitive 334 | description: 335 | name: stream_channel 336 | sha256: db47e4797198ee601990820437179bb90219f918962318d494ada2b4b11e6f6d 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "2.1.0" 340 | string_scanner: 341 | dependency: transitive 342 | description: 343 | name: string_scanner 344 | sha256: dd11571b8a03f7cadcf91ec26a77e02bfbd6bbba2a512924d3116646b4198fc4 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.1.0" 348 | term_glyph: 349 | dependency: transitive 350 | description: 351 | name: term_glyph 352 | sha256: a88162591b02c1f3a3db3af8ce1ea2b374bd75a7bb8d5e353bcfbdc79d719830 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "1.2.0" 356 | test: 357 | dependency: "direct dev" 358 | description: 359 | name: test 360 | sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "1.25.2" 364 | test_api: 365 | dependency: transitive 366 | description: 367 | name: test_api 368 | sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "0.7.0" 372 | test_core: 373 | dependency: transitive 374 | description: 375 | name: test_core 376 | sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "0.6.0" 380 | typed_data: 381 | dependency: transitive 382 | description: 383 | name: typed_data 384 | sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "1.3.0" 388 | vm_service: 389 | dependency: transitive 390 | description: 391 | name: vm_service 392 | sha256: "35ef1bbae978d7158e09c98dcdfe8673b58a30eb53e82833cc027e0aab2d5213" 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "7.5.0" 396 | watcher: 397 | dependency: transitive 398 | description: 399 | name: watcher 400 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "1.1.0" 404 | web_socket_channel: 405 | dependency: transitive 406 | description: 407 | name: web_socket_channel 408 | sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "2.1.0" 412 | webkit_inspection_protocol: 413 | dependency: transitive 414 | description: 415 | name: webkit_inspection_protocol 416 | sha256: "5adb6ab8ed14e22bb907aae7338f0c206ea21e7a27004e97664b16c120306f00" 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "1.0.0" 420 | yaml: 421 | dependency: transitive 422 | description: 423 | name: yaml 424 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 425 | url: "https://pub.dev" 426 | source: hosted 427 | version: "3.1.2" 428 | sdks: 429 | dart: ">=3.2.0 <4.0.0" 430 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: oxygen 2 | description: A lightweight Entity Component System framework written in Dart 3 | version: 0.3.1 4 | homepage: https://github.com/flame-engine/oxygen 5 | 6 | environment: 7 | sdk: ">=2.15.0 <3.0.0" 8 | 9 | dependencies: 10 | meta: ^1.3.0 11 | 12 | dev_dependencies: 13 | dartdoc: ^6.3.0 14 | test: ^1.16.5 15 | benchmark: ^0.3.0 16 | pedantic: ^1.11.0 17 | -------------------------------------------------------------------------------- /test/components/component_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class TestComponent extends ValueComponent {} 5 | 6 | class ComponentAdderSystem extends System { 7 | late Query query; 8 | @override 9 | void init() { 10 | query = createQuery([HasNot()]); 11 | } 12 | 13 | @override 14 | void execute(double delta) { 15 | for (final entity in query.entities) { 16 | entity.add(); 17 | } 18 | } 19 | } 20 | 21 | class ComponentRemoverSystem extends System { 22 | late Query query; 23 | @override 24 | void init() { 25 | query = createQuery([Has()]); 26 | } 27 | 28 | @override 29 | void execute(double delta) { 30 | for (final entity in query.entities) { 31 | entity.remove(); 32 | } 33 | } 34 | } 35 | 36 | void main() { 37 | group('Component', () { 38 | test('Components should be added and removed from entities.', () { 39 | final world = World(); 40 | world.registerComponent(TestComponent.new); 41 | world.registerSystem(ComponentRemoverSystem()); 42 | world.registerSystem(ComponentAdderSystem()); 43 | var testEntity = world.createEntity('Test Entity'); 44 | world.init(); 45 | 46 | expect( 47 | false, 48 | testEntity.has(), 49 | reason: 'Entity has component it shouldnt.', 50 | ); 51 | 52 | world.execute(1); 53 | 54 | testEntity = world.entities.first; 55 | expect( 56 | true, 57 | testEntity.has(), 58 | reason: 'Entity does not have required component.', 59 | ); 60 | 61 | world.execute(1); 62 | 63 | testEntity = world.entities.first; 64 | expect( 65 | false, 66 | testEntity.has(), 67 | reason: 'Entity has component it shouldnt.', 68 | ); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/components/hasnot_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class ComponentA extends ValueComponent {} 5 | 6 | class ComponentB extends ValueComponent {} 7 | 8 | class InitSystem extends System { 9 | late final Query initQuery; 10 | late final Query query; 11 | @override 12 | void init() { 13 | initQuery = createQuery([Has(), HasNot()]); 14 | query = createQuery([Has(), Has()]); 15 | } 16 | 17 | @override 18 | void execute(double delta) { 19 | for (final entity in initQuery.entities) { 20 | entity.add(); 21 | } 22 | } 23 | } 24 | 25 | void main() { 26 | group('Component', () { 27 | test('HasNot should filter newly added components.', () { 28 | final world = World(); 29 | world.registerComponent(() => ComponentA()); 30 | world.registerComponent(() => ComponentB()); 31 | final initSystem = InitSystem(); 32 | world.registerSystem(initSystem); 33 | world.init(); 34 | 35 | final entity = world.createEntity('Test')..add(); 36 | 37 | expect( 38 | initSystem.initQuery.entities.length, 39 | 1, 40 | reason: 'initQuery should have the single added entity', 41 | ); 42 | expect( 43 | initSystem.query.entities.length, 44 | 0, 45 | reason: 'query should not have the single added entity', 46 | ); 47 | 48 | world.execute(1); 49 | 50 | expect( 51 | entity.has(), 52 | true, 53 | reason: 'Entity should have ComponentB', 54 | ); 55 | expect( 56 | initSystem.initQuery.entities.length, 57 | 0, 58 | reason: 59 | 'initQuery should not have the entity after ComponentB was added.', 60 | ); 61 | expect( 62 | initSystem.query.entities.length, 63 | 1, 64 | reason: 'query should have the entity after ComponentB was added.', 65 | ); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/pooling/object_pool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'package:oxygen/oxygen.dart'; 4 | 5 | class TestObject extends PoolObject { 6 | int? value; 7 | 8 | @override 9 | void init([int? data]) { 10 | value = data ?? 0; 11 | } 12 | 13 | @override 14 | void reset() { 15 | value = null; 16 | } 17 | } 18 | 19 | class TestPool extends ObjectPool { 20 | TestPool({int? initialSize}) : super(initialSize: initialSize); 21 | 22 | @override 23 | TestObject builder() => TestObject(); 24 | } 25 | 26 | void main() { 27 | group('ObjectPool', () { 28 | test('Should construct a pool with the initialSize', () { 29 | const initialSize = 1; 30 | final pool = TestPool(initialSize: initialSize); 31 | 32 | expect(pool.totalSize, initialSize); 33 | expect(pool.totalFree, initialSize); 34 | expect(pool.totalUsed, 0); 35 | }); 36 | 37 | test('Should acquire an instance from the pool and release it', () { 38 | const initialSize = 1; 39 | const instanceValue = 5; 40 | 41 | final pool = TestPool(initialSize: initialSize); 42 | final instance = pool.acquire(instanceValue); 43 | 44 | expect(instance.value, instanceValue); 45 | expect(pool.totalSize, initialSize); 46 | expect(pool.totalFree, initialSize - 1); 47 | expect(pool.totalUsed, initialSize); 48 | 49 | pool.release(instance); 50 | 51 | expect(instance.value, null); 52 | expect(pool.totalSize, initialSize); 53 | expect(pool.totalFree, initialSize); 54 | expect(pool.totalUsed, 0); 55 | }); 56 | 57 | test('Should automatically expand by 20% + 1 when pool is empty', () { 58 | const initialSize = 10; 59 | final pool = TestPool(initialSize: initialSize); 60 | List.generate(initialSize, (index) => pool.acquire()); 61 | 62 | expect(pool.totalSize, initialSize); 63 | expect(pool.totalFree, 0); 64 | expect(pool.totalUsed, initialSize); 65 | 66 | final expandingValue = (initialSize * 0.2).floor() + 1; 67 | final expectedSize = initialSize + expandingValue; 68 | final leftOver = expandingValue - 1; // 1 because we acquire once. 69 | 70 | pool.acquire(); 71 | 72 | expect(pool.totalSize, expectedSize); 73 | expect(pool.totalFree, leftOver); 74 | expect(pool.totalUsed, expectedSize - leftOver); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /test/pooling/pool_object_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'package:oxygen/oxygen.dart'; 4 | 5 | class TestObject extends PoolObject { 6 | @override 7 | void init([int? data]) {} 8 | 9 | @override 10 | void reset() {} 11 | } 12 | 13 | class TestPool extends ObjectPool { 14 | TestPool({int? initialSize}) : super(initialSize: initialSize); 15 | 16 | @override 17 | TestObject builder() => TestObject(); 18 | } 19 | 20 | const initialSize = 1; 21 | 22 | void main() { 23 | group('PoolObject', () { 24 | late TestPool pool; 25 | 26 | setUp(() { 27 | pool = TestPool(initialSize: initialSize); 28 | }); 29 | 30 | test('Should be able to dispose itself', () { 31 | final instance = pool.acquire(); 32 | 33 | expect(pool.totalSize, initialSize); 34 | expect(pool.totalFree, initialSize - 1); 35 | expect(pool.totalUsed, initialSize); 36 | 37 | instance.dispose(); 38 | 39 | expect(pool.totalSize, initialSize); 40 | expect(pool.totalFree, initialSize); 41 | expect(pool.totalUsed, 0); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/system/system_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:oxygen/oxygen.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | class SystemA extends System { 5 | SystemA() : super(priority: 2); 6 | 7 | @override 8 | void execute(double delta) {} 9 | 10 | @override 11 | void init() {} 12 | } 13 | 14 | class SystemB extends System { 15 | SystemB() : super(priority: 1); 16 | 17 | @override 18 | void execute(double delta) {} 19 | 20 | @override 21 | void init() {} 22 | } 23 | 24 | class SystemC extends System { 25 | SystemC() : super(priority: 0); 26 | 27 | @override 28 | void execute(double delta) {} 29 | 30 | @override 31 | void init() {} 32 | } 33 | 34 | class SystemD extends System { 35 | SystemD() : super(priority: 1); 36 | @override 37 | void execute(double delta) {} 38 | 39 | @override 40 | void init() {} 41 | } 42 | 43 | void main() { 44 | group('System', () { 45 | group('registerSystem', () { 46 | test('Priority should be in the right order', () { 47 | final systemA = SystemA(); 48 | final systemB = SystemB(); 49 | final systemC = SystemC(); 50 | final systemD = SystemD(); 51 | 52 | final world = World() 53 | ..registerSystem(systemA) 54 | ..registerSystem(systemB) 55 | ..registerSystem(systemC) 56 | ..registerSystem(systemD) 57 | ..init(); 58 | 59 | expect( 60 | world.systemManager.systems, 61 | equals([systemC, systemB, systemD, systemA]), 62 | ); 63 | }); 64 | 65 | test('Should not be added if the system type is already registered', () { 66 | final world = World() 67 | ..registerSystem(SystemA()) 68 | ..registerSystem(SystemA()) 69 | ..registerSystem(SystemA()) 70 | ..registerSystem(SystemA()) 71 | ..init(); 72 | 73 | expect(world.systemManager.systems, hasLength(1)); 74 | }); 75 | 76 | test( 77 | 'The "system.world == null assertion" should occur when adding a ' 78 | 'system with an initialized world', () { 79 | final system = SystemA(); 80 | final world = World()..registerSystem(system); 81 | 82 | expect( 83 | () => world.registerSystem(system), 84 | throwsA(isA()), 85 | ); 86 | }); 87 | }); 88 | }); 89 | } 90 | --------------------------------------------------------------------------------