├── .codecov.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── config.yml ├── pull_request_template.md └── workflows │ ├── ci-linux.yml │ ├── ci-macos.yml │ ├── ci-wasm.yml │ ├── ci-windows.yml │ ├── documentation.yml │ └── markdown-link-check.yml ├── .gitignore ├── .sourcery.yml ├── .sourceryTests.yml ├── .spi.yml ├── .swift-version ├── .swiftformat ├── .swiftlint.yml ├── Benchmarks ├── Benchmarks │ └── ECSBenchmark │ │ ├── Base.swift │ │ └── OneDimensionBenchmarks.swift ├── Package.swift └── README.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Mintfile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── FirebladeECS │ ├── CodingStrategy.swift │ ├── Component.swift │ ├── ComponentIdentifier.swift │ ├── Documentation.docc │ ├── Documentation.md │ └── GettingStartedWithFirebladeECS.md │ ├── Entity+Component.swift │ ├── Entity.swift │ ├── EntityIdentifier.swift │ ├── EntityIdentifierGenerator.swift │ ├── FSM.swift │ ├── Family+Coding.swift │ ├── Family.swift │ ├── FamilyDecoding.swift │ ├── FamilyEncoding.swift │ ├── FamilyMemberBuilder.swift │ ├── FamilyRequirementsManaging.swift │ ├── FamilyTraitSet.swift │ ├── Foundation+Extensions.swift │ ├── Generated │ ├── .gitkeep │ └── Family.generated.swift │ ├── Hashing.swift │ ├── ManagedContiguousArray.swift │ ├── Nexus+Component.swift │ ├── Nexus+ComponentsBuilder.swift │ ├── Nexus+Entity.swift │ ├── Nexus+Family.swift │ ├── Nexus+Internal.swift │ ├── Nexus.swift │ ├── NexusEvent.swift │ ├── NexusEventDelegate.swift │ ├── Single.swift │ ├── Stencils │ └── Family.stencil │ └── UnorderedSparseSet.swift ├── Tests ├── FirebladeECSPerformanceTests │ ├── Base.swift │ ├── ComponentPerformanceTests.swift │ ├── HashingPerformanceTests.swift │ ├── TypeIdentifierPerformanceTests.swift │ └── TypedFamilyPerformanceTests.swift └── FirebladeECSTests │ ├── Base.swift │ ├── ComponentIdentifierTests.swift │ ├── ComponentTests.swift │ ├── EntityCreationTests.swift │ ├── EntityIdGenTests.swift │ ├── EntityTests.swift │ ├── FSMTests.swift │ ├── FamilyCodingTests.swift │ ├── FamilyTests.swift │ ├── FamilyTraitsTests.swift │ ├── Generated │ ├── .gitkeep │ └── FamilyTests.generated.swift │ ├── HashingTests.swift │ ├── NexusEventDelegateTests.swift │ ├── NexusTests.swift │ ├── SingleTests.swift │ ├── SparseSetTests.swift │ ├── Stencils │ └── FamilyTests.stencil │ └── SystemsTests.swift └── renovate.json /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/" 3 | - "Benchmarks/" 4 | 5 | comment: 6 | layout: header, changes, diff 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ctreffs] 2 | custom: ['https://www.paypal.com/donate?hosted_button_id=GCG3K54SKRALQ'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Something isn't working as expected, create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | 17 | 18 | ### Bug Description 19 | 20 | *A clear and concise description of what the bug is. 21 | Replace this paragraph with a short description of the incorrect behavior. 22 | (If this is a regression, please note the last version of the package that exhibited the correct behavior in addition to your current version.)* 23 | 24 | ### Information 25 | 26 | - **Package version:** What tag or branch of this package are you using? e.g. tag `1.2.3` or branch `main` 27 | - **Platform version:** Please tell us the version number of your operating system. e.g. `macOS 11.2.3` or `Ubuntu 20.04` 28 | - **Swift version:** Paste the output of `swift --version` here. 29 | 30 | ### Checklist 31 | 32 | - [ ] If possible, I've reproduced the issue using the `main`/`master` branch of this package. 33 | - [ ] I've searched for existing issues under the issues tab. 34 | - [ ] The bug is reproducible 35 | 36 | ### Steps to Reproduce 37 | 38 | *Steps to reproduce the behavior:* 39 | 40 | 1. Go to '...' 41 | 2. '....' 42 | 43 | *Replace this paragraph with an explanation of how to reproduce the incorrect behavior. 44 | Include a simple code example, if possible.* 45 | 46 | ### Expected behavior 47 | 48 | *A clear and concise description of what you expected to happen. 49 | Describe what you expect to happen.* 50 | 51 | ### Actual behavior 52 | 53 | *Describe or copy/paste the behavior you observe.* 54 | 55 | ### Screenshots 56 | 57 | If applicable, add screenshots to help explain your problem. 58 | 59 | ### Additional context 60 | 61 | *Add any other context about the problem here.* 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: A suggestion for a new feature or idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | 13 | 14 | ### Feature request 15 | 16 | *Replace this paragraph with a description of your proposed feature. 17 | A clear and concise description of what the idea or problem is you want to solve. 18 | Please be sure to describe some concrete use cases for the new feature -- be as specific as possible. 19 | Provide links to existing issues or external references/discussions, if appropriate.* 20 | 21 | ### Describe the solution you'd like 22 | 23 | *A clear and concise description of what you want to happen.* 24 | 25 | ### Describe alternatives you've considered 26 | 27 | *A clear and concise description of any alternative solutions or features you've considered.* 28 | 29 | ### Additional context 30 | 31 | *Add any other context or screenshots about the feature request here.* 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/JMM7W6pCCc 5 | about: Questions or comments about using Fireblade? Ask here! 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Description 13 | 14 | *Replace this paragraph with a description of your changes and rationale. 15 | Provide links to an existing issue or external references/discussions, if appropriate.* 16 | 17 | ### Detailed Design 18 | 19 | *Include any additional information about the design here. At minimum, describe a synopsis of any public API additions.* 20 | 21 | ```swift 22 | /// The new feature implemented by this pull request. 23 | public struct Example: Collection { 24 | } 25 | ``` 26 | 27 | ### Documentation 28 | 29 | *How has the new feature been documented? 30 | Have the relevant portions of the guides in the Documentation folder been updated in addition to symbol-level documentation?* 31 | 32 | ### Testing 33 | 34 | *How is the new feature tested? 35 | Please ensure CI is not broken* 36 | 37 | ### Performance 38 | 39 | *How did you verify the new feature performs as expected?* 40 | 41 | ### Source Impact 42 | 43 | *What is the impact of this change on existing users of this package? Does it deprecate or remove any existing API?* 44 | 45 | ### Checklist 46 | 47 | - [ ] I've read the [Contribution Guidelines](https://github.com/fireblade-engine/ecs/blob/master/CONTRIBUTING.md) 48 | - [ ] I've followed the coding style of the rest of the project. 49 | - [ ] I've added tests covering all new code paths my change adds to the project (to the extent possible). 50 | - [ ] I've added benchmarks covering new functionality (if appropriate). 51 | - [ ] I've verified that my change does not break any existing tests or introduce unexpected benchmark regressions. 52 | - [ ] I've updated the documentation (if appropriate). 53 | -------------------------------------------------------------------------------- /.github/workflows/ci-linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | swift: ["latest"] 15 | container: 16 | image: swift:${{ matrix.swift }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Test 22 | run: swift test -c release --enable-xctest --parallel --xunit-output .build/xUnit-output.xml 23 | 24 | - name: Upload test artifacts 25 | if: failure() 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: test-artifacts-linux-${{ matrix.swift }}-${{ github.run_id }} 29 | path: | 30 | .build/*.yaml 31 | .build/*.xml 32 | .build/*.json 33 | .build/*.txt 34 | .build/**/*.xctest 35 | .build/**/*.json 36 | .build/**/*.txt 37 | if-no-files-found: warn 38 | include-hidden-files: true 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 12 | CODECOV_XCODE_VERSION: "16.0" # Xcode version used to generate code coverage 13 | 14 | jobs: 15 | macos: 16 | runs-on: ${{ matrix.config.os }} 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | config: 21 | - { os: "macos-14", xcode: "15.4" } 22 | - { os: "macos-15", xcode: "16.0" } 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Select Xcode ${{ matrix.config.xcode }} 28 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.config.xcode }}.app 29 | 30 | - name: Test 31 | run: swift test -c release --parallel --xunit-output .build/xUnit-output.xml --enable-code-coverage 32 | env: 33 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.config.xcode }}.app/Contents/Developer 34 | 35 | - name: Upload test artifacts 36 | if: failure() 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: test-artifacts-${{ matrix.config.xcode }}-${{ github.run_id }} 40 | path: | 41 | .build/*.yaml 42 | .build/*.xml 43 | .build/*.json 44 | .build/*.txt 45 | .build/**/*.xctest 46 | .build/**/*.json 47 | .build/**/*.txt 48 | if-no-files-found: warn 49 | include-hidden-files: true 50 | 51 | # Only run coverage steps if the CODECOV_TOKEN is available and the matrix.xcode matches CODECOV_XCODE_VERSION 52 | - name: Generate coverage report 53 | if: env.CODECOV_TOKEN != '' && matrix.config.xcode == env.CODECOV_XCODE_VERSION 54 | run: xcrun llvm-cov export -format="lcov" .build/**/*PackageTests.xctest/Contents/MacOS/*PackageTests -instr-profile .build/**/codecov/default.profdata > coverage.lcov 55 | 56 | - name: Upload code coverage report 57 | if: env.CODECOV_TOKEN != '' && matrix.config.xcode == env.CODECOV_XCODE_VERSION 58 | uses: codecov/codecov-action@v5.4.0 59 | with: 60 | token: ${{ env.CODECOV_TOKEN }} 61 | files: coverage.lcov 62 | fail_ci_if_error: true 63 | -------------------------------------------------------------------------------- /.github/workflows/ci-wasm.yml: -------------------------------------------------------------------------------- 1 | name: WASM 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | wasm: 11 | runs-on: ubuntu-latest 12 | container: swift:6.0.3 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: swiftwasm/setup-swiftwasm@v2 16 | - run: swift build --swift-sdk wasm32-unknown-wasi 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | defaults: 10 | run: # Use powershell because bash is not supported: https://github.com/compnerd/gha-setup-swift/issues/18#issuecomment-1705524890 11 | shell: pwsh 12 | 13 | jobs: 14 | windows: 15 | runs-on: windows-2019 # Windows SDK lower than 10.0.26100 is needed until https://github.com/swiftlang/swift/pull/79751 released! 16 | steps: 17 | 18 | - name: Setup VS Dev Environment 19 | uses: seanmiddleditch/gha-setup-vsdevenv@v5 20 | 21 | - name: Setup 22 | uses: compnerd/gha-setup-swift@v0.3.0 23 | with: 24 | branch: swift-5.10-release 25 | tag: 5.10-RELEASE 26 | 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Test 31 | run: swift test -c release --parallel --xunit-output .build/xUnit-output.xml 32 | 33 | - name: Upload test artifacts 34 | if: failure() 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: test-artifacts-windows-${{ github.run_id }} 38 | path: | 39 | .build/*.yaml 40 | .build/*.xml 41 | .build/*.json 42 | .build/*.txt 43 | .build/**/*.xctest 44 | .build/**/*.json 45 | .build/**/*.txt 46 | if-no-files-found: warn 47 | include-hidden-files: true 48 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | # Build and deploy DocC to GitHub pages. Based off of pointfreeco/swift-composable-architecture: 2 | # https://github.com/pointfreeco/swift-composable-architecture/blob/main/.github/workflows/documentation.yml 3 | name: Documentation 4 | 5 | on: 6 | release: 7 | types: 8 | - published 9 | push: 10 | branches: 11 | - master 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: docs-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | documentation: 20 | runs-on: macos-15 21 | steps: 22 | - name: Select Xcode 16.0 23 | run: sudo xcode-select -s /Applications/Xcode_16.0.app 24 | 25 | - name: Checkout Package 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Checkout gh-pages Branch 31 | uses: actions/checkout@v4 32 | with: 33 | ref: gh-pages 34 | path: docs 35 | 36 | - name: Build documentation 37 | run: > 38 | rm -rf docs/.git; 39 | rm -rf docs/master; 40 | git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.*" | tail -n +6 | xargs -I {} rm -rf {}; 41 | 42 | for tag in $(echo "master"; git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.*" | head -6); 43 | do 44 | if [ -d "docs/$tag/data/documentation/firebladeecs" ] 45 | then 46 | echo "✅ Documentation for "$tag" already exists."; 47 | else 48 | echo "⏳ Generating documentation for FirebladeECS @ "$tag" release."; 49 | rm -rf "docs/$tag"; 50 | 51 | git checkout .; 52 | git checkout "$tag"; 53 | 54 | DOCC_JSON_PRETTYPRINT=YES \ 55 | swift package \ 56 | --allow-writing-to-directory docs/"$tag" \ 57 | generate-documentation \ 58 | --fallback-bundle-identifier com.github.fireblade-engine.FirebladeECS \ 59 | --target FirebladeECS \ 60 | --output-path docs/"$tag" \ 61 | --transform-for-static-hosting \ 62 | --hosting-base-path ecs/"$tag" \ 63 | && echo "✅ Documentation generated for FirebladeECS @ "$tag" release." \ 64 | || echo "⚠️ Documentation skipped for FirebladeECS @ "$tag"."; 65 | fi; 66 | done 67 | 68 | - name: Fix permissions 69 | run: 'sudo chown -R $USER docs' 70 | 71 | - name: Publish documentation to GitHub Pages 72 | uses: JamesIves/github-pages-deploy-action@v4.7.3 73 | with: 74 | branch: gh-pages 75 | folder: docs 76 | single-commit: true 77 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check.yml: -------------------------------------------------------------------------------- 1 | name: Check markdown links 2 | 3 | on: push 4 | 5 | jobs: 6 | markdown-link-check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: markdown-link-check 12 | uses: gaurav-nelson/github-action-markdown-link-check@1.0.16 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !default.mode1v3 2 | !default.mode2v3 3 | !default.pbxuser 4 | !default.perspectivev3 5 | *.DS_Store 6 | *.dSYM 7 | *.dSYM.zip 8 | *.hmap 9 | *.ipa 10 | *.mode1v3 11 | *.mode2v3 12 | *.moved-aside 13 | *.pbxuser 14 | *.perspectivev3 15 | *.rb 16 | *.sh 17 | *.xccheckout 18 | *.xcconfig 19 | *.xcscmblueprint 20 | ._* 21 | .apdisk 22 | .AppleDB 23 | .AppleDesktop 24 | .AppleDouble 25 | .build/ 26 | .com.apple.timemachine.donotpresent 27 | .DocumentRevisions-V100 28 | .DS_Store 29 | .fseventsd 30 | .idea/ 31 | .LSOverride 32 | .Spotlight-V100 33 | .swiftpm/ 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .vscode/ 38 | /*.xcodeproj 39 | /.build 40 | /Packages 41 | build/ 42 | DerivedData/ 43 | docs/ 44 | fastlane/Preview.html 45 | fastlane/report.xml 46 | fastlane/screenshots 47 | fastlane/test_output 48 | Gemfile* 49 | Icon 50 | Network Trash Folder 51 | Packages 52 | playground.xcworkspace 53 | Temporary Items 54 | timeline.xctimeline 55 | xcuserdata 56 | xcuserdata/Package.resolveddocs/ 57 | -------------------------------------------------------------------------------- /.sourcery.yml: -------------------------------------------------------------------------------- 1 | sources: # you can provide either single path or several paths using `-` 2 | - Sources 3 | templates: # as well as for templates 4 | - Sources/FirebladeECS/Stencils 5 | output: # note that there is no `-` here as only single output path is supported 6 | Sources/FirebladeECS/Generated 7 | -------------------------------------------------------------------------------- /.sourceryTests.yml: -------------------------------------------------------------------------------- 1 | sources: # you can provide either single path or several paths using `-` 2 | - Sources 3 | templates: # as well as for templates 4 | - Tests/FirebladeECSTests/Stencils 5 | output: # note that there is no `-` here as only single output path is supported 6 | Tests/FirebladeECSTests/Generated 7 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [FirebladeECS] 5 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.8 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | --exclude Sources/**/*.generated.swift 3 | --exclude Sources/FirebladeECS/Entity+Component.swift # problems with self.get { } 4 | --exclude Tests/**/*.swift 5 | 6 | # format options 7 | --extensionacl on-declarations 8 | --stripunusedargs closure-only 9 | --commas inline 10 | --self remove 11 | --selfrequired get 12 | --disable preferKeyPath 13 | --disable opaqueGenericParameters -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | excluded: 4 | - .build 5 | - Tests 6 | identifier_name: 7 | excluded: 8 | - id 9 | line_length: 220 10 | number_separator: 11 | minimum_length: 5 12 | analyzer_rules: 13 | - explicit_self 14 | - unused_declaration 15 | - unused_import 16 | opt_in_rules: 17 | - array_init 18 | - attributes 19 | - closure_body_length 20 | - closure_end_indentation 21 | - closure_spacing 22 | - collection_alignment 23 | - conditional_returns_on_newline 24 | - contains_over_filter_count 25 | - contains_over_filter_is_empty 26 | - contains_over_first_not_nil 27 | - contains_over_range_nil_comparison 28 | - convenience_type 29 | - discouraged_object_literal 30 | - discouraged_optional_boolean 31 | - discouraged_optional_collection 32 | - empty_collection_literal 33 | - empty_count 34 | - empty_string 35 | - empty_xctest_method 36 | - enum_case_associated_values_count 37 | - expiring_todo 38 | - explicit_init 39 | - explicit_top_level_acl 40 | - fallthrough 41 | - fatal_error_message 42 | - file_header 43 | - file_name_no_space 44 | - first_where 45 | - flatmap_over_map_reduce 46 | - force_unwrapping 47 | - function_default_parameter_at_end 48 | - identical_operands 49 | - implicit_return 50 | - implicitly_unwrapped_optional 51 | - joined_default_parameter 52 | - last_where 53 | - legacy_multiple 54 | - legacy_random 55 | - let_var_whitespace 56 | - literal_expression_end_indentation 57 | - lower_acl_than_parent 58 | - modifier_order 59 | - multiline_arguments 60 | - multiline_function_chains 61 | - multiline_parameters 62 | - nimble_operator 63 | - no_extension_access_modifier 64 | - nslocalizedstring_key 65 | - nslocalizedstring_require_bundle 66 | - number_separator 67 | - object_literal 68 | - operator_usage_whitespace 69 | - optional_enum_case_matching 70 | - overridden_super_call 71 | - override_in_extension 72 | - pattern_matching_keywords 73 | - prefer_self_type_over_type_of_self 74 | - prefixed_toplevel_constant 75 | - private_action 76 | - private_outlet 77 | - prohibited_interface_builder 78 | - prohibited_super_call 79 | - quick_discouraged_call 80 | - quick_discouraged_focused_test 81 | - quick_discouraged_pending_test 82 | - raw_value_for_camel_cased_codable_enum 83 | - reduce_into 84 | - redundant_nil_coalescing 85 | - redundant_type_annotation 86 | - required_enum_case 87 | - single_test_class 88 | - sorted_first_last 89 | - sorted_imports 90 | - static_operator 91 | - strict_fileprivate 92 | - strong_iboutlet 93 | - switch_case_on_newline 94 | - toggle_bool 95 | - trailing_closure 96 | - unavailable_function 97 | - unneeded_parentheses_in_closure_argument 98 | - untyped_error_in_catch 99 | - vertical_parameter_alignment_on_call 100 | - vertical_whitespace_between_cases 101 | - vertical_whitespace_closing_braces 102 | - vertical_whitespace_opening_braces 103 | - xct_specific_matcher 104 | - yoda_condition 105 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks/ECSBenchmark/Base.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | import FirebladeECS 9 | 10 | class EmptyComponent: Component {} 11 | 12 | class Name: Component { 13 | var name: String 14 | init(name: String) { 15 | self.name = name 16 | } 17 | } 18 | 19 | class Position: Component { 20 | var x: Int 21 | var y: Int 22 | init(x: Int, y: Int) { 23 | self.x = x 24 | self.y = y 25 | } 26 | } 27 | 28 | class Velocity: Component { 29 | var a: Float 30 | init(a: Float) { 31 | self.a = a 32 | } 33 | } 34 | 35 | class Party: Component { 36 | var partying: Bool 37 | init(partying: Bool) { 38 | self.partying = partying 39 | } 40 | } 41 | 42 | class Color: Component { 43 | var r: UInt8 = 0 44 | var g: UInt8 = 0 45 | var b: UInt8 = 0 46 | } 47 | 48 | class ExampleSystem { 49 | private let family: Family2 50 | 51 | init(nexus: Nexus) { 52 | family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) 53 | } 54 | 55 | func update(deltaT _: Double) { 56 | for (position, velocity) in family { 57 | position.x *= 2 58 | velocity.a *= 2 59 | } 60 | } 61 | } 62 | 63 | final class SingleGameState: SingleComponent { 64 | var shouldQuit: Bool = false 65 | var playerHealth: Int = 67 66 | } 67 | 68 | func setUpNexus() -> Nexus { 69 | let numEntities = 10000 70 | let nexus = Nexus() 71 | 72 | for i in 0 ..< numEntities { 73 | nexus.createEntity().assign(Position(x: 1 + i, y: 2 + i), 74 | Name(name: "myName\(i)"), 75 | Velocity(a: 3.14), 76 | EmptyComponent(), 77 | Color()) 78 | } 79 | 80 | precondition(nexus.numEntities == numEntities) 81 | // precondition(nexus.numFamilies == 1) 82 | precondition(nexus.numComponents == numEntities * 5) 83 | 84 | return nexus 85 | } 86 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks/ECSBenchmark/OneDimensionBenchmarks.swift: -------------------------------------------------------------------------------- 1 | // swiftformat:disable preferForLoop 2 | import Benchmark 3 | import FirebladeECS 4 | 5 | // derived from FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift in the parent project 6 | 7 | let benchmarks = { 8 | Benchmark("TraitMatching") { benchmark in 9 | let nexus = setUpNexus() 10 | let a = nexus.createEntity() 11 | a.assign(Position(x: 1, y: 2)) 12 | a.assign(Name(name: "myName")) 13 | a.assign(Velocity(a: 3.14)) 14 | a.assign(EmptyComponent()) 15 | 16 | let isMatch = nexus.family(requiresAll: Position.self, Velocity.self, 17 | excludesAll: Party.self) 18 | 19 | for _ in benchmark.scaledIterations { 20 | blackHole( 21 | isMatch.canBecomeMember(a) 22 | ) 23 | } 24 | } 25 | 26 | Benchmark("TypedFamilyEntities") { benchmark in 27 | let nexus = setUpNexus() 28 | let family = nexus.family(requires: Position.self, excludesAll: Party.self) 29 | for _ in benchmark.scaledIterations { 30 | blackHole( 31 | family 32 | .entities 33 | .forEach { (entity: Entity) in 34 | _ = entity 35 | } 36 | ) 37 | } 38 | } 39 | 40 | Benchmark("TypedFamilyOneComponent") { benchmark in 41 | let nexus = setUpNexus() 42 | let family = nexus.family(requires: Position.self, excludesAll: Party.self) 43 | for _ in benchmark.scaledIterations { 44 | blackHole( 45 | family 46 | .forEach { (position: Position) in 47 | _ = position 48 | } 49 | ) 50 | } 51 | } 52 | 53 | Benchmark("TypedFamilyEntityOneComponent") { benchmark in 54 | let nexus = setUpNexus() 55 | let family = nexus.family(requires: Position.self, excludesAll: Party.self) 56 | for _ in benchmark.scaledIterations { 57 | blackHole( 58 | family 59 | .entityAndComponents 60 | .forEach { (entity: Entity, position: Position) in 61 | _ = entity 62 | _ = position 63 | } 64 | ) 65 | } 66 | } 67 | 68 | Benchmark("TypedFamilyTwoComponents") { benchmark in 69 | let nexus = setUpNexus() 70 | let family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: Party.self) 71 | for _ in benchmark.scaledIterations { 72 | blackHole( 73 | family 74 | .forEach { (position: Position, velocity: Velocity) in 75 | _ = position 76 | _ = velocity 77 | } 78 | ) 79 | } 80 | } 81 | Benchmark("TypedFamilyEntityTwoComponents") { benchmark in 82 | let nexus = setUpNexus() 83 | let family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: Party.self) 84 | for _ in benchmark.scaledIterations { 85 | blackHole( 86 | family 87 | .entityAndComponents 88 | .forEach { (entity: Entity, position: Position, velocity: Velocity) in 89 | _ = entity 90 | _ = position 91 | _ = velocity 92 | } 93 | ) 94 | } 95 | } 96 | 97 | Benchmark("TypedFamilyThreeComponents") { benchmark in 98 | let nexus = setUpNexus() 99 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, excludesAll: Party.self) 100 | for _ in benchmark.scaledIterations { 101 | blackHole( 102 | family 103 | .forEach { (position: Position, velocity: Velocity, name: Name) in 104 | _ = position 105 | _ = velocity 106 | _ = name 107 | } 108 | ) 109 | } 110 | } 111 | Benchmark("TypedFamilyEntityThreeComponents") { benchmark in 112 | let nexus = setUpNexus() 113 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, excludesAll: Party.self) 114 | for _ in benchmark.scaledIterations { 115 | blackHole( 116 | family 117 | .entityAndComponents 118 | .forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name) in 119 | _ = entity 120 | _ = position 121 | _ = velocity 122 | _ = name 123 | } 124 | ) 125 | } 126 | } 127 | 128 | Benchmark("TypedFamilyFourComponents") { benchmark in 129 | let nexus = setUpNexus() 130 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, excludesAll: Party.self) 131 | for _ in benchmark.scaledIterations { 132 | blackHole( 133 | family 134 | .forEach { (position: Position, velocity: Velocity, name: Name, color: Color) in 135 | _ = position 136 | _ = velocity 137 | _ = name 138 | _ = color 139 | } 140 | ) 141 | } 142 | } 143 | 144 | Benchmark("TypedFamilyEntityFourComponents") { benchmark in 145 | let nexus = setUpNexus() 146 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, excludesAll: Party.self) 147 | for _ in benchmark.scaledIterations { 148 | blackHole( 149 | family 150 | .entityAndComponents 151 | .forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name, color: Color) in 152 | _ = entity 153 | _ = position 154 | _ = velocity 155 | _ = name 156 | _ = color 157 | } 158 | ) 159 | } 160 | } 161 | 162 | Benchmark("TypedFamilyFiveComponents") { benchmark in 163 | let nexus = setUpNexus() 164 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, EmptyComponent.self, excludesAll: Party.self) 165 | 166 | for _ in benchmark.scaledIterations { 167 | blackHole( 168 | family 169 | .forEach { (position: Position, velocity: Velocity, name: Name, color: Color, empty: EmptyComponent) in 170 | _ = position 171 | _ = velocity 172 | _ = name 173 | _ = color 174 | _ = empty 175 | } 176 | ) 177 | } 178 | } 179 | 180 | Benchmark("TypedFamilyEntityFiveComponents") { benchmark in 181 | let nexus = setUpNexus() 182 | let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, EmptyComponent.self, excludesAll: Party.self) 183 | 184 | for _ in benchmark.scaledIterations { 185 | blackHole(family 186 | .entityAndComponents 187 | .forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name, color: Color, empty: EmptyComponent) in 188 | _ = entity 189 | _ = position 190 | _ = velocity 191 | _ = name 192 | _ = color 193 | _ = empty 194 | } 195 | ) 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Benchmarks/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ECSBenchmarks", 7 | platforms: [ 8 | .iOS(.v16), 9 | .macOS(.v13) 10 | ], 11 | dependencies: [ 12 | .package(path: "../"), 13 | .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.29.2")) 14 | ], 15 | targets: [ 16 | .executableTarget( 17 | name: "ECSBenchmark", 18 | dependencies: [ 19 | .product(name: "FirebladeECS", package: "ecs"), 20 | .product(name: "Benchmark", package: "package-benchmark") 21 | ], 22 | path: "Benchmarks/ECSBenchmark", 23 | plugins: [ 24 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 25 | ] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for FirebladeECS 2 | 3 | Originally seeded by replicating performance tests into a new form leveraging [package-benchmark](https://swiftpackageindex.com/ordo-one/package-benchmark/) [Documentation](https://swiftpackageindex.com/ordo-one/package-benchmark/main/documentation/benchmark). 4 | 5 | To run all the available benchmarks: 6 | 7 | swift package benchmark --format markdown 8 | 9 | For more help on the package-benchmark SwiftPM plugin: 10 | 11 | swift package benchmark help 12 | 13 | Creating a local baseline: 14 | 15 | swift package --allow-writing-to-package-directory benchmark baseline update dev 16 | swift package benchmark baseline list 17 | 18 | Comparing to a the baseline 'alpha' 19 | 20 | swift package benchmark baseline compare dev 21 | 22 | For more details on creating and comparing baselines, read [Creating and Comparing Benchmark Baselines](https://swiftpackageindex.com/ordo-one/package-benchmark/main/documentation/benchmark/creatingandcomparingbaselines). 23 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is a list of the people responsible for ensuring that contributions 2 | # to this projected are reviewed, either by themselves or by someone else. 3 | # They are also the gatekeepers for their part of this project, with the final 4 | # word on what goes in or not. 5 | # The code owners file uses a .gitignore-like syntax to specify which parts of 6 | # the codebase is associated with an owner. See 7 | # 8 | # for details. 9 | # The following lines are used by GitHub to automatically recommend reviewers. 10 | # Each line is a file pattern followed by one or more owners. 11 | 12 | * @ctreffs 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement 63 | e.g. via [content abuse report][ref-report-abuse]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][ref-homepage-cc], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | . 126 | Translations are available at 127 | . 128 | 129 | 130 | 131 | [ref-homepage-cc]: https://www.contributor-covenant.org 132 | [ref-report-abuse]: https://docs.github.com/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam#reporting-an-issue-or-pull-request 133 | [ref-gh-coc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project 134 | [ref-gh-abuse]: https://docs.github.com/en/communities/moderating-comments-and-conversations/managing-how-contributors-report-abuse-in-your-organizations-repository 135 | [ref-coc-guide]: https://opensource.guide/code-of-conduct/ 136 | 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 💁 Contributing to this project 2 | 3 | 4 | > First off, thank you for considering contributing to this project. 5 | > It’s [people like you][ref-contributors] that keep this project alive and make it great! 6 | > Thank you! 🙏💜🎉👍 7 | 8 | The following is a set of **guidelines for contributing** to this project. 9 | Use your best judgment and feel free to propose changes to this document in a pull request. 10 | 11 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 12 | 13 | ### 💡 Your contribution - the sky is the limit 🌈 14 | 15 | This is an open source project and we love to receive contributions from our community — [**you**][ref-contributors]! 16 | 17 | There are many ways to contribute, from writing __tutorials__ or __blog posts__, improving the [__documentation__][ref-documentation], submitting [__bug reports__][ref-issues-new] and [__enhancement__][ref-pull-request-new] or 18 | [__writing code__][ref-pull-request-new] which can be incorporated into the repository itself. 19 | 20 | When contributing to this project, please feel free to discuss the changes and ideas you wish to contribute with the repository owners before making a change by opening a [new issue][ref-issues-new] and add the **feature request** tag to that issue. 21 | 22 | Note that we have a [code of conduct][ref-code-of-conduct], please follow it in all your interactions with the project. 23 | 24 | ### 🐞 You want to report a bug or file an issue? 25 | 26 | 1. Ensure that it was **not already reported** and is being worked on by checking [open issues][ref-issues]. 27 | 2. Create a [new issue][ref-issues-new] with a **clear and descriptive title** 28 | 3. Write a **detailed comment** with as much relevant information as possible including 29 | - *how to reproduce* the bug 30 | - a *code sample* or an *executable test case* demonstrating the expected behavior that is not occurring 31 | - any *files that could help* trace it down (i.e. logs) 32 | 33 | ### 🩹 You wrote a patch that fixes an issue? 34 | 35 | 1. Open a [new pull request (PR)][ref-pull-request-new] with the patch. 36 | 2. Ensure the PR description clearly describes the problem and solution. 37 | 3. Link the relevant **issue** if applicable ([how to link issues in PRs][ref-pull-request-how-to]). 38 | 4. Ensure that [**no tests are failing**][ref-gh-actions] and **coding conventions** are met 39 | 5. Submit the patch and await review. 40 | 41 | ### 🎁 You want to suggest or contribute a new feature? 42 | 43 | That's great, thank you! You rock 🤘 44 | 45 | If you want to dive deep and help out with development on this project, then first get the project [installed locally][ref-readme]. 46 | After that is done we suggest you have a look at tickets in our [issue tracker][ref-issues]. 47 | You can start by looking through the beginner or help-wanted issues: 48 | - [__Good first issues__][ref-issues-first] are issues which should only require a few lines of code, and a test or two. 49 | - [__Help wanted issues__][ref-issues-help] are issues which should be a bit more involved than beginner issues. 50 | These are meant to be a great way to get a smooth start and won't put you in front of the most complex parts of the system. 51 | 52 | If you are up to more challenging tasks with a bigger scope, then there are a set of tickets with a __feature__, __enhancement__ or __improvement__ tag. 53 | These tickets have a general overview and description of the work required to finish. 54 | If you want to start somewhere, this would be a good place to start. 55 | That said, these aren't necessarily the easiest tickets. 56 | 57 | For any new contributions please consider these guidelines: 58 | 59 | 1. Open a [new pull request (PR)][ref-pull-request-new] with a **clear and descriptive title** 60 | 2. Write a **detailed comment** with as much relevant information as possible including: 61 | - What your feature is intended to do? 62 | - How it can be used? 63 | - What alternatives where considered, if any? 64 | - Has this feature impact on performance or stability of the project? 65 | 66 | #### Your contribution responsibilities 67 | 68 | Don't be intimidated by these responsibilities, they are easy to meet if you take your time to develop your feature 😌 69 | 70 | - [x] Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 71 | - [x] Ensure (cross-)platform compatibility for every change that's accepted. An addition should not reduce the number of platforms that the project supports. 72 | - [x] Ensure **coding conventions** are met. Lint your code with the project's default tools. Project wide commands are available through the [Makefile][ref-makefile] in the repository root. 73 | - [x] Add tests for your feature that prove it's working as expected. Code coverage should not drop below its previous value. 74 | - [x] Ensure none of the existing tests are failing after adding your changes. 75 | - [x] Document your public API code and ensure to add code comments where necessary. 76 | 77 | 78 | ### ⚙️ How to set up the environment 79 | 80 | Please consult the [README][ref-readme] for installation instructions. 81 | 82 | 83 | 84 | [ref-code-of-conduct]: https://github.com/fireblade-engine/ecs/blob/master/CODE_OF_CONDUCT.md 85 | [ref-contributors]: https://github.com/fireblade-engine/ecs/graphs/contributors 86 | [ref-documentation]: https://github.com/fireblade-engine/ecs/wiki 87 | [ref-gh-actions]: https://github.com/fireblade-engine/ecs/actions 88 | [ref-issues-first]: https://github.com/fireblade-engine/ecs/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue" 89 | [ref-issues-help]: https://github.com/fireblade-engine/ecs/issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted" 90 | [ref-issues-new]: https://github.com/fireblade-engine/ecs/issues/new/choose 91 | [ref-issues]: https://github.com/fireblade-engine/ecs/issues 92 | [ref-pull-request-how-to]: https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls 93 | [ref-pull-request-new]: https://github.com/fireblade-engine/ecs/compare 94 | [ref-readme]: https://github.com/fireblade-engine/ecs/blob/master/README.md 95 | [ref-makefile]: https://github.com/fireblade-engine/ecs/blob/master/Makefile 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 Christian Treffs 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SWIFT_PACKAGE_VERSION := $(shell swift package tools-version) 2 | 3 | # Lint fix and format code. 4 | .PHONY: lint-fix 5 | swiftlint: 6 | mint run swiftlint lint --fix --config .swiftlint.yml --format --quiet 7 | swiftformat: 8 | mint run swiftformat . --swiftversion ${SWIFT_PACKAGE_VERSION} 9 | lint-fix: swiftlint swiftformat 10 | 11 | # Generate code 12 | .PHONY: generate-code 13 | generate-code: 14 | mint run sourcery --quiet --config ./.sourcery.yml 15 | mint run sourcery --quiet --config ./.sourceryTests.yml 16 | 17 | # Run pre-push tasks 18 | .PHONY: pre-push 19 | pre-push: generate-code lint-fix 20 | 21 | .PHONY: precommit 22 | precommit: pre-push 23 | 24 | .PHONY: setup-brew 25 | setup-brew: 26 | @which -s brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 27 | @brew update 28 | 29 | .PHONY: install-dependencies-macOS 30 | install-dependencies-macOS: setup-brew 31 | brew install mint 32 | mint bootstrap 33 | 34 | .PHONY: setupEnvironment 35 | setupEnvironment: install-dependencies-macOS 36 | 37 | # Build debug version 38 | .PHONY: build-debug 39 | build-debug: 40 | swift build -c debug 41 | 42 | # Build release version 43 | .PHONY: build-release 44 | build-release: 45 | swift build -c release --skip-update 46 | 47 | # Test links in README 48 | # requires 49 | .PHONY: testReadme 50 | testReadme: 51 | markdown-link-check -p -v ./README.md 52 | 53 | # Delete package build artifacts. 54 | .PHONY: clean 55 | clean: clean-sourcery 56 | swift package clean 57 | 58 | # Clean sourcery cache 59 | .PHONY: clean-sourcery 60 | clean-sourcery: 61 | rm -rdf ${HOME}/Library/Caches/Sourcery 62 | 63 | # Preview DocC documentation 64 | .PHONY: preview-docs 65 | preview-docs: 66 | swift package --disable-sandbox preview-documentation --target FirebladeECS 67 | 68 | # Preview DocC documentation with analysis/warnings and overview of coverage 69 | .PHONY: preview-analysis-docs 70 | preview-analysis-docs: 71 | swift package --disable-sandbox preview-documentation --target FirebladeECS --analyze --experimental-documentation-coverage --level brief 72 | 73 | # Generates a plain DocC archive in the .build directory 74 | .PHONY: generate-docs 75 | generate-docs: 76 | DOCC_JSON_PRETTYPRINT=YES \ 77 | swift package \ 78 | generate-documentation \ 79 | --fallback-bundle-identifier com.github.fireblade-engine.FirebladeECS \ 80 | --target FirebladeECS \ 81 | 82 | # Generates documentation pages suitable to push/host on github pages (or another static site) 83 | # Expected location, if set up, would be: 84 | # https://fireblade-engine.github.io/FirebladeECS/documentation/FirebladeECS/ 85 | .PHONY: generate-docs-githubpages 86 | generate-docs-githubpages: 87 | DOCC_JSON_PRETTYPRINT=YES \ 88 | swift package \ 89 | --allow-writing-to-directory ./docs \ 90 | generate-documentation \ 91 | --fallback-bundle-identifier com.github.fireblade-engine.FirebladeECS \ 92 | --target FirebladeECS \ 93 | --output-path ./docs \ 94 | --transform-for-static-hosting \ 95 | --hosting-base-path 'FirebladeECS' -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | realm/SwiftLint@0.57.0 2 | nicklockwood/SwiftFormat@0.55.0 3 | krzysztofzablocki/Sourcery@2.2.5 4 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-docc-plugin", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-docc-plugin", 7 | "state" : { 8 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 9 | "version" : "1.4.3" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-docc-symbolkit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 16 | "state" : { 17 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 18 | "version" : "1.0.0" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "FirebladeECS", 6 | products: [ 7 | .library(name: "FirebladeECS", 8 | targets: ["FirebladeECS"]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.3") 12 | ], 13 | targets: [ 14 | .target(name: "FirebladeECS", 15 | exclude: ["Stencils/Family.stencil"]), 16 | .testTarget(name: "FirebladeECSTests", 17 | dependencies: ["FirebladeECS"], 18 | exclude: ["Stencils/FamilyTests.stencil"]), 19 | .testTarget(name: "FirebladeECSPerformanceTests", 20 | dependencies: ["FirebladeECS"]) 21 | ], 22 | swiftLanguageVersions: [.v5] 23 | ) 24 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/CodingStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodingStrategy.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 05.08.20. 6 | // 7 | 8 | public protocol CodingStrategy { 9 | func codingKey(for componentType: C.Type) -> DynamicCodingKey where C: Component 10 | } 11 | 12 | public struct DynamicCodingKey: CodingKey { 13 | public var intValue: Int? 14 | public var stringValue: String 15 | 16 | public init?(intValue: Int) { self.intValue = intValue; stringValue = "\(intValue)" } 17 | public init?(stringValue: String) { self.stringValue = stringValue } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Component.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 08.10.17. 6 | // 7 | 8 | /// **Component** 9 | /// 10 | /// A component represents the raw data for one aspect of an entity. 11 | public protocol Component: AnyObject { 12 | /// Unique, immutable identifier of this component type. 13 | static var identifier: ComponentIdentifier { get } 14 | 15 | /// Unique, immutable identifier of this component type. 16 | var identifier: ComponentIdentifier { get } 17 | } 18 | 19 | extension Component { 20 | public static var identifier: ComponentIdentifier { ComponentIdentifier(Self.self) } 21 | @inline(__always) 22 | public var identifier: ComponentIdentifier { Self.identifier } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/ComponentIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentIdentifier.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 20.08.19. 6 | // 7 | 8 | /// Identifies a component by it's meta type 9 | public struct ComponentIdentifier { 10 | public typealias Identifier = Int 11 | public let id: Identifier 12 | } 13 | 14 | extension ComponentIdentifier { 15 | @usableFromInline 16 | init(_ componentType: (some Component).Type) { 17 | id = Self.makeRuntimeHash(componentType) 18 | } 19 | 20 | /// object identifier hash (only stable during runtime) - arbitrary hash is ok. 21 | static func makeRuntimeHash(_ componentType: (some Component).Type) -> Identifier { 22 | ObjectIdentifier(componentType).hashValue 23 | } 24 | } 25 | 26 | extension ComponentIdentifier: Equatable {} 27 | extension ComponentIdentifier: Hashable {} 28 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``FirebladeECS`` 2 | 3 | Seamlessly, consistently, and asynchronously replicate data. 4 | 5 | ## Overview 6 | 7 | This is a **dependency free**, **lightweight**, **fast** and **easy to use** [Entity-Component System](https://en.wikipedia.org/wiki/Entity_component_system) implementation in Swift. 8 | An ECS comprises entities composed from components of data, with systems which operate on the components. 9 | 10 | Fireblade ECS is available for all platforms that support [Swift 5.8](https://swift.org/) and higher and the [Swift Package Manager (SPM)](https://github.com/apple/swift-package-manager). 11 | It is developed and maintained as part of the [Fireblade Game Engine project](https://github.com/fireblade-engine). 12 | 13 | For a more detailed example of FirebladeECS in action, see the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo). 14 | 15 | ## Topics 16 | 17 | ### Essentials 18 | 19 | - 20 | - ``Nexus`` 21 | - ``NexusEvent`` 22 | - ``NexusEventDelegate`` 23 | 24 | ### Entities 25 | 26 | - ``Entity`` 27 | - ``EntityState`` 28 | - ``EntityStateMachine`` 29 | - ``EntityCreated`` 30 | - ``EntityDestroyed`` 31 | - ``EntityComponentHash`` 32 | - ``EntityIdentifier`` 33 | - ``EntityIdentifierGenerator`` 34 | - ``DefaultEntityIdGenerator`` 35 | - ``LinearIncrementingEntityIdGenerator`` 36 | 37 | ### Components 38 | 39 | - ``Component`` 40 | - ``ComponentAdded`` 41 | - ``ComponentRemoved`` 42 | - ``ComponentProvider`` 43 | - ``ComponentsBuilder-4co42`` 44 | - ``ComponentsBuilder`` 45 | - ``ComponentInstanceProvider`` 46 | - ``ComponentIdentifier`` 47 | - ``ComponentInitializable`` 48 | - ``ComponentTypeHash`` 49 | - ``ComponentTypeProvider`` 50 | - ``ComponentSingletonProvider`` 51 | - ``SingleComponent`` 52 | - ``EntityComponentHash`` 53 | - ``StateComponentMapping`` 54 | - ``DynamicComponentProvider`` 55 | - ``RequiringComponents1`` 56 | - ``RequiringComponents2`` 57 | - ``RequiringComponents3`` 58 | - ``RequiringComponents4`` 59 | - ``RequiringComponents5`` 60 | - ``RequiringComponents6`` 61 | - ``RequiringComponents7`` 62 | - ``RequiringComponents8`` 63 | - ``DefaultInitializable`` 64 | - ``SingleComponent`` 65 | 66 | ### Systems 67 | 68 | - ``Family`` 69 | - ``FamilyEncoding`` 70 | - ``FamilyDecoding`` 71 | - ``FamilyMemberAdded`` 72 | - ``FamilyMemberRemoved`` 73 | - ``FamilyMemberBuilder-3f2i6`` 74 | - ``FamilyMemberBuilder`` 75 | - ``FamilyTraitSet`` 76 | - ``Requires1`` 77 | - ``Requires2`` 78 | - ``Requires3`` 79 | - ``Requires4`` 80 | - ``Requires5`` 81 | - ``Requires6`` 82 | - ``Requires7`` 83 | - ``Requires8`` 84 | - ``Single`` 85 | - ``Family1`` 86 | - ``Family2`` 87 | - ``Family3`` 88 | - ``Family4`` 89 | - ``Family5`` 90 | - ``Family6`` 91 | - ``Family7`` 92 | - ``Family8`` 93 | - ``FamilyRequirementsManaging`` 94 | 95 | ### Coding Strategies 96 | 97 | - ``CodingStrategy`` 98 | - ``DefaultCodingStrategy`` 99 | - ``TopLevelDecoder`` 100 | - ``TopLevelEncoder`` 101 | - ``DynamicCodingKey`` 102 | 103 | ### Supporting Types 104 | 105 | - ``ManagedContiguousArray`` 106 | - ``UnorderedSparseSet`` 107 | 108 | ### Hash Functions 109 | 110 | - ``hash(combine:)`` 111 | - ``hash(combine:_:)`` 112 | - ``StringHashing`` 113 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Documentation.docc/GettingStartedWithFirebladeECS.md: -------------------------------------------------------------------------------- 1 | # Getting started with Fireblade ECS 2 | 3 | Learn the API and key types Fireblade provides to compose your game or app logic. 4 | 5 | ## Overview 6 | 7 | Fireblade ECS is a dependency free, Swift language implementation of an Entity-Component-System ([ECS](https://en.wikipedia.org/wiki/Entity_component_system)). 8 | An ECS comprises entities composed from components of data, with systems which operate on the components. 9 | 10 | Extend the following lines in your `Package.swift` file or use it to create a new project. 11 | 12 | ```swift 13 | // swift-tools-version:5.8 14 | 15 | import PackageDescription 16 | 17 | let package = Package( 18 | name: "YourPackageName", 19 | dependencies: [ 20 | .package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.17.5") 21 | ], 22 | targets: [ 23 | .target( 24 | name: "YourTargetName", 25 | dependencies: ["FirebladeECS"]) 26 | ] 27 | ) 28 | 29 | ``` 30 | 31 | This article introduces you to the key concepts of Fireblade ECS's API. 32 | For a more detailed example, see the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo). 33 | 34 | ### 🏛️ Nexus 35 | 36 | The core element in the Fireblade-ECS is the [Nexus](https://en.wiktionary.org/wiki/nexus#Noun). 37 | It acts as a centralized way to store, access and manage entities and their components. 38 | A single `Nexus` may (theoretically) hold up to 4294967295 `Entities` at a time. 39 | You may use more than one `Nexus` at a time. 40 | 41 | Initialize a `Nexus` with 42 | 43 | ```swift 44 | let nexus = Nexus() 45 | ``` 46 | 47 | ### 👤 Entities 48 | 49 | then create entities by letting the `Nexus` generate them. 50 | 51 | ```swift 52 | // an entity without components 53 | let newEntity = nexus.createEntity() 54 | ``` 55 | 56 | To define components, conform your class to the `Component` protocol 57 | 58 | ```swift 59 | final class Position: Component { 60 | var x: Int = 0 61 | var y: Int = 0 62 | } 63 | ``` 64 | and assign instances of it to an `Entity` with 65 | 66 | ```swift 67 | let position = Position(x: 1, y: 2) 68 | entity.assign(position) 69 | ``` 70 | 71 | You can be more efficient by assigning components while creating an entity. 72 | 73 | ```swift 74 | // an entity with two components assigned. 75 | nexus.createEntity { 76 | Position(x: 1, y: 2) 77 | Color(.red) 78 | } 79 | 80 | // bulk create entities with multiple components assigned. 81 | nexus.createEntities(count: 100) { _ in 82 | Position() 83 | Color() 84 | } 85 | 86 | ``` 87 | ### 👪 Families 88 | 89 | This ECS uses a grouping approach for entities with the same component types to optimize cache locality and ease up access to them. 90 | Entities with the __same component types__ may belong to one `Family`. 91 | A `Family` has entities as members and component types as family traits. 92 | 93 | Create a family by calling `.family` with a set of traits on the nexus. 94 | A family that contains only entities with a `Movement` and `PlayerInput` component, but no `Texture` component is created by 95 | 96 | ```swift 97 | let family = nexus.family(requiresAll: Movement.self, PlayerInput.self, 98 | excludesAll: Texture.self) 99 | ``` 100 | 101 | These entities are cached in the nexus for efficient access and iteration. 102 | Families conform to the [Sequence](https://developer.apple.com/documentation/swift/sequence) protocol so that members (components) 103 | may be iterated and accessed like any other sequence in Swift. 104 | Access a family's components directly on the family instance. To get family entities and access components at the same time call `family.entityAndComponents`. 105 | If you are only interested in a family's entities call `family.entities`. 106 | 107 | ```swift 108 | class PlayerMovementSystem { 109 | let family = nexus.family(requiresAll: Movement.self, PlayerInput.self, 110 | excludesAll: Texture.self) 111 | 112 | func update() { 113 | family 114 | .forEach { (mov: Movement, input: PlayerInput) in 115 | 116 | // position & velocity component for the current entity 117 | 118 | // get properties 119 | _ = mov.position 120 | _ = mov.velocity 121 | 122 | // set properties 123 | mov.position.x = mov.position.x + 3.0 124 | ... 125 | 126 | // current input command for the given entity 127 | _ = input.command 128 | ... 129 | 130 | } 131 | } 132 | 133 | func update2() { 134 | family 135 | .entityAndComponents 136 | .forEach { (entity: Entity, mov: Movement, input: PlayerInput) in 137 | 138 | // the current entity instance 139 | _ = entity 140 | 141 | // position & velocity component for the current entity 142 | 143 | // get properties 144 | _ = mov.position 145 | _ = mov.velocity 146 | 147 | 148 | } 149 | } 150 | 151 | func update3() { 152 | family 153 | .entities 154 | .forEach { (entity: Entity) in 155 | 156 | // the current entity instance 157 | _ = entity 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | ### 🧑 Singles 164 | 165 | A `Single` on the other hand is a special kind of family that holds exactly **one** entity with exactly **one** component for the entire lifetime of the Nexus. This may come in handy if you have components that have a [Singleton](https://en.wikipedia.org/wiki/Singleton_(mathematics)) character. Single components must conform to the `SingleComponent` protocol and will not be available through regular family iteration. 166 | 167 | ```swift 168 | final class GameState: SingleComponent { 169 | var quitGame: Bool = false 170 | } 171 | class GameLogicSystem { 172 | let gameState: Single 173 | 174 | init(nexus: Nexus) { 175 | gameState = nexus.single(GameState.self) 176 | } 177 | 178 | func update() { 179 | // update your game sate here 180 | gameState.component.quitGame = true 181 | 182 | // entity access is provided as well 183 | _ = gameState.entity 184 | } 185 | } 186 | 187 | ``` 188 | 189 | ### 🔗 Serialization 190 | 191 | 192 | To serialize/deserialize entities you must conform their assigned components to the `Codable` protocol. 193 | Conforming components can then be serialized per family like this: 194 | 195 | ```swift 196 | // MyComponent and YourComponent both conform to Component and Codable protocols. 197 | let nexus = Nexus() 198 | let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self) 199 | 200 | // JSON encode entities from given family. 201 | var jsonEncoder = JSONEncoder() 202 | let encodedData = try family.encodeMembers(using: &jsonEncoder) 203 | 204 | // Decode entities into given family from JSON. 205 | // The decoded entities will be added to the nexus. 206 | var jsonDecoder = JSONDecoder() 207 | let newEntities = try family.decodeMembers(from: jsonData, using: &jsonDecoder) 208 | 209 | ``` 210 | 211 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Entity+Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity+Component.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 22.10.17. 6 | // 7 | 8 | extension Entity { 9 | @inlinable 10 | public func get() -> C? where C: Component { 11 | nexus.get(safe: identifier) 12 | } 13 | 14 | @inlinable 15 | public func get(component compType: A.Type = A.self) -> A? where A: Component { 16 | nexus.get(safe: identifier) 17 | } 18 | 19 | @inlinable 20 | public func get(components _: A.Type, _: B.Type) -> (A?, B?) where A: Component, B: Component { 21 | let compA: A? = get(component: A.self) 22 | let compB: B? = get(component: B.self) 23 | return (compA, compB) 24 | } 25 | 26 | // swiftlint:disable large_tuple 27 | @inlinable 28 | public func get(components _: A.Type, _: B.Type, _: C.Type) -> (A?, B?, C?) where A: Component, B: Component, C: Component { 29 | let compA: A? = get(component: A.self) 30 | let compB: B? = get(component: B.self) 31 | let compC: C? = get(component: C.self) 32 | return (compA, compB, compC) 33 | } 34 | 35 | /// Get or set component instance by type via subscript. 36 | /// 37 | /// **Behavior:** 38 | /// - If `Comp` is a component type that is currently *not* assigned to this entity, 39 | /// the new instance will be assigned to this entity. 40 | /// - If `Comp` is already assinged to this entity nothing happens. 41 | /// - If `Comp` is set to `nil` and an instance of `Comp` is assigned to this entity, 42 | /// `Comp` will be removed from this entity. 43 | @inlinable 44 | public subscript(_ componentType: Comp.Type) -> Comp? where Comp: Component { 45 | get { self.get(component: componentType) } 46 | nonmutating set { 47 | guard let newComponent = newValue else { 48 | self.remove(Comp.self) 49 | return 50 | } 51 | if self.get(component: componentType) === newComponent { 52 | return 53 | } 54 | self.assign(newComponent) 55 | } 56 | } 57 | 58 | /// Get the value of a component using the key Path to the property in the component. 59 | /// 60 | /// A `Comp` instance must be assigned to this entity! 61 | /// - Parameter componentKeyPath: The `KeyPath` to the property of the given component. 62 | @inlinable 63 | public func get(valueAt componentKeyPath: KeyPath) -> Value where Comp: Component { 64 | self.get(component: Comp.self)![keyPath: componentKeyPath] 65 | } 66 | 67 | /// Get the value of a component using the key Path to the property in the component. 68 | /// 69 | /// A `Comp` instance must be assigned to this entity! 70 | /// - Parameter componentKeyPath: The `KeyPath` to the property of the given component. 71 | @inlinable 72 | public func get(valueAt componentKeyPath: KeyPath) -> Value? where Comp: Component { 73 | self.get(component: Comp.self)![keyPath: componentKeyPath] 74 | } 75 | 76 | /// Get the value of a component using the key Path to the property in the component. 77 | @inlinable 78 | public subscript(_ componentKeyPath: KeyPath) -> Value where Comp: Component { 79 | self.get(valueAt: componentKeyPath) 80 | } 81 | 82 | /// Get the value of a component using the key Path to the property in the component. 83 | @inlinable 84 | public subscript(_ componentKeyPath: KeyPath) -> Value? where Comp: Component { 85 | self.get(valueAt: componentKeyPath) 86 | } 87 | 88 | /// Set the value of a component using the key path to the property in the component. 89 | /// 90 | /// **Behavior:** 91 | /// - If `Comp` is a component type that is currently *not* assigned to this entity, 92 | /// a new instance of `Comp` will be default initialized and `newValue` will be set at the given keyPath. 93 | /// 94 | /// - Parameters: 95 | /// - newValue: The value to set. 96 | /// - componentKeyPath: The `ReferenceWritableKeyPath` to the property of the given component. 97 | /// - Returns: Returns true if an action was performed, false otherwise. 98 | @inlinable 99 | @discardableResult 100 | public func set(value newValue: Value, for componentKeyPath: ReferenceWritableKeyPath) -> Bool where Comp: Component & DefaultInitializable { 101 | guard has(Comp.self) else { 102 | let newInstance = Comp() 103 | newInstance[keyPath: componentKeyPath] = newValue 104 | return nexus.assign(component: newInstance, entityId: identifier) 105 | } 106 | 107 | get(component: Comp.self)![keyPath: componentKeyPath] = newValue 108 | return true 109 | } 110 | 111 | /// Set the value of a component using the key path to the property in the component. 112 | /// 113 | /// **Behavior:** 114 | /// - If `Comp` is a component type that is currently *not* assigned to this entity, 115 | /// a new instance of `Comp` will be default initialized and `newValue` will be set at the given keyPath. 116 | /// 117 | /// - Parameters: 118 | /// - newValue: The value to set. 119 | /// - componentKeyPath: The `ReferenceWritableKeyPath` to the property of the given component. 120 | /// - Returns: Returns true if an action was performed, false otherwise. 121 | @inlinable 122 | @discardableResult 123 | public func set(value newValue: Value?, for componentKeyPath: ReferenceWritableKeyPath) -> Bool where Comp: Component & DefaultInitializable { 124 | guard has(Comp.self) else { 125 | let newInstance = Comp() 126 | newInstance[keyPath: componentKeyPath] = newValue 127 | return nexus.assign(component: newInstance, entityId: identifier) 128 | } 129 | 130 | get(component: Comp.self)![keyPath: componentKeyPath] = newValue 131 | return true 132 | } 133 | 134 | /// Set the value of a component using the key path to the property in the component. 135 | /// 136 | /// **Behavior:** 137 | /// - If `Comp` is a component type that is currently *not* assigned to this entity, 138 | /// a new instance of `Comp` will be default initialized and `newValue` will be set at the given keyPath. 139 | @inlinable 140 | public subscript(_ componentKeyPath: ReferenceWritableKeyPath) -> Value where Comp: Component & DefaultInitializable { 141 | get { self.get(valueAt: componentKeyPath) } 142 | nonmutating set { self.set(value: newValue, for: componentKeyPath) } 143 | } 144 | 145 | /// Set the value of a component using the key path to the property in the component. 146 | /// 147 | /// **Behavior:** 148 | /// - If `Comp` is a component type that is currently *not* assigned to this entity, 149 | /// a new instance of `Comp` will be default initialized and `newValue` will be set at the given keyPath. 150 | @inlinable 151 | public subscript(_ componentKeyPath: ReferenceWritableKeyPath) -> Value? where Comp: Component & DefaultInitializable { 152 | get { self.get(valueAt: componentKeyPath) } 153 | nonmutating set { self.set(value: newValue, for: componentKeyPath) } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 08.10.17. 6 | // 7 | 8 | /// **Entity** 9 | /// 10 | /// An entity is a general purpose object. 11 | /// It only consists of a unique id (EntityIdentifier). 12 | /// Components can be assigned to an entity to give it behavior or functionality. 13 | /// An entity creates the relationship between all it's assigned components. 14 | public struct Entity { 15 | @usableFromInline unowned let nexus: Nexus 16 | 17 | /// The unique entity identifier. 18 | public private(set) var identifier: EntityIdentifier 19 | 20 | init(nexus: Nexus, id: EntityIdentifier) { 21 | self.nexus = nexus 22 | identifier = id 23 | } 24 | 25 | /// Returns the number of components for this entity. 26 | public var numComponents: Int { 27 | nexus.count(components: identifier) 28 | } 29 | 30 | @discardableResult 31 | public func createEntity() -> Entity { 32 | nexus.createEntity() 33 | } 34 | 35 | @discardableResult 36 | public func createEntity(with components: Component...) -> Entity { 37 | createEntity(with: components) 38 | } 39 | 40 | @discardableResult 41 | public func createEntity(with components: some Collection) -> Entity { 42 | nexus.createEntity(with: components) 43 | } 44 | 45 | /// Checks if a component with given type is assigned to this entity. 46 | /// - Parameter type: the component type. 47 | public func has(_ type: (some Component).Type) -> Bool { 48 | has(type.identifier) 49 | } 50 | 51 | /// Checks if a component with a given component identifier is assigned to this entity. 52 | /// - Parameter compId: the component identifier. 53 | public func has(_ compId: ComponentIdentifier) -> Bool { 54 | nexus.has(componentId: compId, entityId: identifier) 55 | } 56 | 57 | /// Checks if this entity has any components. 58 | public var hasComponents: Bool { 59 | nexus.count(components: identifier) > 0 60 | } 61 | 62 | /// Add one or more components to this entity. 63 | /// - Parameter components: one or more components. 64 | @discardableResult 65 | public func assign(_ components: Component...) -> Entity { 66 | assign(components) 67 | return self 68 | } 69 | 70 | /// Add a component to this entity. 71 | /// - Parameter component: a component. 72 | @discardableResult 73 | public func assign(_ component: Component) -> Entity { 74 | nexus.assign(component: component, to: self) 75 | return self 76 | } 77 | 78 | /// Add a typed component to this entity. 79 | /// - Parameter component: the typed component. 80 | @discardableResult 81 | public func assign(_ component: some Component) -> Entity { 82 | assign(component) 83 | return self 84 | } 85 | 86 | @discardableResult 87 | public func assign(_ components: some Collection) -> Entity { 88 | nexus.assign(components: components, to: self) 89 | return self 90 | } 91 | 92 | /// Remove a component from this entity. 93 | /// - Parameter component: the component. 94 | @discardableResult 95 | public func remove(_ component: some Component) -> Entity { 96 | remove(component.identifier) 97 | } 98 | 99 | /// Remove a component by type from this entity. 100 | /// - Parameter compType: the component type. 101 | @discardableResult 102 | public func remove(_ compType: (some Component).Type) -> Entity { 103 | remove(compType.identifier) 104 | } 105 | 106 | /// Remove a component by id from this entity. 107 | /// - Parameter compId: the component id. 108 | @discardableResult 109 | public func remove(_ compId: ComponentIdentifier) -> Entity { 110 | nexus.remove(component: compId, from: identifier) 111 | return self 112 | } 113 | 114 | /// Remove all components from this entity. 115 | public func removeAll() { 116 | nexus.removeAll(components: identifier) 117 | } 118 | 119 | /// Destroy this entity. 120 | public func destroy() { 121 | nexus.destroy(entity: self) 122 | } 123 | 124 | /// Returns an iterator over all components of this entity. 125 | @inlinable 126 | public func makeComponentsIterator() -> ComponentsIterator { 127 | ComponentsIterator(nexus: nexus, entityIdentifier: identifier) 128 | } 129 | } 130 | 131 | extension Entity { 132 | public struct ComponentsIterator: IteratorProtocol { 133 | private var iterator: IndexingIterator<[Component]>? 134 | 135 | @usableFromInline 136 | init(nexus: Nexus, entityIdentifier: EntityIdentifier) { 137 | iterator = nexus.get(components: entityIdentifier)? 138 | .map { nexus.get(unsafe: $0, for: entityIdentifier) } 139 | .makeIterator() 140 | } 141 | 142 | public mutating func next() -> Component? { 143 | iterator?.next() 144 | } 145 | } 146 | } 147 | 148 | extension Entity.ComponentsIterator: LazySequenceProtocol {} 149 | extension Entity.ComponentsIterator: Sequence {} 150 | 151 | extension Entity: Equatable { 152 | public static func == (lhs: Entity, rhs: Entity) -> Bool { 153 | lhs.nexus === rhs.nexus && lhs.identifier == rhs.identifier 154 | } 155 | } 156 | 157 | extension Entity: CustomStringConvertible { 158 | public var description: String { 159 | "" 160 | } 161 | } 162 | 163 | extension Entity: CustomDebugStringConvertible { 164 | public var debugDescription: String { 165 | "" 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/EntityIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityIdentifier.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 08.10.17. 6 | // 7 | 8 | /// **EntityIdentifier** 9 | /// 10 | /// An entity identifier represents the unique identity of an entity. 11 | public struct EntityIdentifier { 12 | /// Entity identifier type. 13 | /// 14 | /// Provides 4294967295 unique identifiers. 15 | public typealias Identifier = UInt32 16 | 17 | /// The entity identifier. 18 | public let id: Identifier 19 | 20 | @inlinable 21 | public init(_ id: Identifier) { 22 | self.init(rawValue: id) 23 | } 24 | } 25 | 26 | extension EntityIdentifier: Equatable {} 27 | extension EntityIdentifier: Hashable {} 28 | 29 | extension EntityIdentifier: RawRepresentable { 30 | /// The entity identifier represented as a raw value. 31 | @inline(__always) 32 | public var rawValue: Identifier { id } 33 | 34 | @inlinable 35 | public init(rawValue: Identifier) { 36 | id = rawValue 37 | } 38 | } 39 | 40 | extension EntityIdentifier: ExpressibleByIntegerLiteral { 41 | public init(integerLiteral value: Identifier) { 42 | self.init(value) 43 | } 44 | } 45 | 46 | extension EntityIdentifier { 47 | /// Invalid entity identifier 48 | /// 49 | /// Used to represent an invalid entity identifier. 50 | public static let invalid = EntityIdentifier(.max) 51 | } 52 | 53 | extension EntityIdentifier { 54 | /// Provides the entity identifier as an index 55 | /// 56 | /// This is a convenience property for collection indexing and does not represent the raw identifier. 57 | /// 58 | /// Use `id` or `rawValue` instead. 59 | @inline(__always) 60 | public var index: Int { Int(id) } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/EntityIdentifierGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityIdentifierGenerator.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 26.06.20. 6 | // 7 | 8 | /// **Entity Identifier Generator** 9 | /// 10 | /// An entity identifier generator provides new entity identifiers on entity creation. 11 | /// It also allows entity ids to be marked as unused (to be re-usable). 12 | /// 13 | /// You should strive to keep entity ids tightly packed around `EntityIdentifier.Identifier.min` since it has an influence on the underlying memory layout. 14 | public protocol EntityIdentifierGenerator { 15 | /// Initialize the generator providing entity ids to begin with when creating new entities. 16 | /// 17 | /// Entity ids provided should be passed to `nextId()` in last out order up until the collection is empty. 18 | /// The default is an empty collection. 19 | /// - Parameter initialEntityIds: The entity ids to start providing up until the collection is empty (in last out order). 20 | init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier 21 | 22 | /// Provides the next unused entity identifier. 23 | /// 24 | /// The provided entity identifier must be unique during runtime. 25 | func nextId() -> EntityIdentifier 26 | 27 | /// Marks the given entity identifier as free and ready for re-use. 28 | /// 29 | /// Unused entity identifiers will again be provided with `nextId()`. 30 | /// - Parameter entityId: The entity id to be marked as unused. 31 | func markUnused(entityId: EntityIdentifier) 32 | } 33 | 34 | /// A default entity identifier generator implementation. 35 | public typealias DefaultEntityIdGenerator = LinearIncrementingEntityIdGenerator 36 | 37 | /// **Linear incrementing entity id generator** 38 | /// 39 | /// This entity id generator creates linearly incrementing entity ids 40 | /// unless an entity is marked as unused then the marked id is returned next in a FIFO order. 41 | /// 42 | /// Furthermore it respects order of entity ids on initialization, meaning the provided ids on initialization will be provided in order 43 | /// until all are in use. After that the free entities start at the lowest available id increasing linearly skipping already in-use entity ids. 44 | public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator { 45 | @usableFromInline 46 | final class Storage { 47 | @usableFromInline var stack: [EntityIdentifier.Identifier] 48 | @usableFromInline var count: Int { stack.count } 49 | 50 | @usableFromInline 51 | init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier { 52 | let initialInUse: [EntityIdentifier.Identifier] = initialEntityIds.map(\.id) 53 | let maxInUseValue = initialInUse.max() ?? 0 54 | let inUseSet = Set(initialInUse) // a set of all eIds in use 55 | let allSet = Set(0 ... maxInUseValue) // all eIds from 0 to including maxInUseValue 56 | let freeSet = allSet.subtracting(inUseSet) // all "holes" / unused / free eIds 57 | let initialFree = Array(freeSet).sorted().reversed() // order them to provide them linear increasing after all initially used are provided. 58 | stack = initialFree + initialInUse 59 | } 60 | 61 | @usableFromInline 62 | init() { 63 | stack = [0] 64 | } 65 | 66 | @usableFromInline 67 | func nextId() -> EntityIdentifier { 68 | guard stack.count == 1 else { 69 | return EntityIdentifier(stack.removeLast()) 70 | } 71 | defer { stack[0] += 1 } 72 | return EntityIdentifier(stack[0]) 73 | } 74 | 75 | @usableFromInline 76 | func markUnused(entityId: EntityIdentifier) { 77 | stack.append(entityId.id) 78 | } 79 | } 80 | 81 | @usableFromInline let storage: Storage 82 | @usableFromInline var count: Int { storage.count } 83 | 84 | @inlinable 85 | public init(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier { 86 | storage = Storage(startProviding: initialEntityIds) 87 | } 88 | 89 | @inlinable 90 | public init() { 91 | storage = Storage() 92 | } 93 | 94 | @inline(__always) 95 | public func nextId() -> EntityIdentifier { 96 | storage.nextId() 97 | } 98 | 99 | @inline(__always) 100 | public func markUnused(entityId: EntityIdentifier) { 101 | storage.markUnused(entityId: entityId) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Family+Coding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Family+Coding.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 22.07.20. 6 | // 7 | 8 | private struct FamilyMemberContainer where R: FamilyRequirementsManaging { 9 | let components: [R.Components] 10 | } 11 | 12 | extension CodingUserInfoKey { 13 | static let nexusCodingStrategy = CodingUserInfoKey(rawValue: "nexusCodingStrategy").unsafelyUnwrapped 14 | } 15 | 16 | // MARK: - encoding 17 | 18 | extension FamilyMemberContainer: Encodable where R: FamilyEncoding { 19 | func encode(to encoder: Encoder) throws { 20 | let strategy = encoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy() 21 | var familyContainer = encoder.unkeyedContainer() 22 | try R.encode(componentsArray: components, into: &familyContainer, using: strategy) 23 | } 24 | } 25 | 26 | public protocol TopLevelEncoder { 27 | /// The type this encoder produces. 28 | associatedtype Output 29 | 30 | /// Encodes an instance of the indicated type. 31 | /// 32 | /// - Parameter value: The instance to encode. 33 | func encode(_ value: T) throws -> Self.Output where T: Encodable 34 | 35 | /// Contextual user-provided information for use during decoding. 36 | var userInfo: [CodingUserInfoKey: Any] { get set } 37 | } 38 | 39 | extension Family where R: FamilyEncoding { 40 | /// Encode family members (entities) to data using a given encoder. 41 | /// 42 | /// The encoded members will *NOT* be removed from the nexus and will also stay present in this family. 43 | /// - Parameter encoder: The data encoder. Data encoder respects the coding strategy set at `nexus.codingStrategy`. 44 | /// - Returns: The encoded data. 45 | public func encodeMembers(using encoder: inout Encoder) throws -> Encoder.Output where Encoder: TopLevelEncoder { 46 | encoder.userInfo[.nexusCodingStrategy] = nexus.codingStrategy 47 | let components = [R.Components](self) 48 | let container = FamilyMemberContainer(components: components) 49 | return try encoder.encode(container) 50 | } 51 | } 52 | 53 | // MARK: - decoding 54 | 55 | extension FamilyMemberContainer: Decodable where R: FamilyDecoding { 56 | init(from decoder: Decoder) throws { 57 | var familyContainer = try decoder.unkeyedContainer() 58 | let strategy = decoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy() 59 | components = try R.decode(componentsIn: &familyContainer, using: strategy) 60 | } 61 | } 62 | 63 | public protocol TopLevelDecoder { 64 | /// The type this decoder accepts. 65 | associatedtype Input 66 | 67 | /// Decodes an instance of the indicated type. 68 | func decode(_ type: T.Type, from: Self.Input) throws -> T where T: Decodable 69 | 70 | /// Contextual user-provided information for use during decoding. 71 | var userInfo: [CodingUserInfoKey: Any] { get set } 72 | } 73 | 74 | extension Family where R: FamilyDecoding { 75 | /// Decode family members (entities) from given data using a decoder. 76 | /// 77 | /// The decoded members will be added to the nexus and will be present in this family. 78 | /// - Parameters: 79 | /// - data: The data decoded by decoder. An unkeyed container of family members (keyed component containers) is expected. 80 | /// - decoder: The decoder to use for decoding family member data. Decoder respects the coding strategy set at `nexus.codingStrategy`. 81 | /// - Returns: returns the newly added entities. 82 | @discardableResult 83 | public func decodeMembers(from data: Decoder.Input, using decoder: inout Decoder) throws -> [Entity] where Decoder: TopLevelDecoder { 84 | decoder.userInfo[.nexusCodingStrategy] = nexus.codingStrategy 85 | let familyMembers = try decoder.decode(FamilyMemberContainer.self, from: data) 86 | return familyMembers.components 87 | .map { createMember(with: $0) } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Family.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Family.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 21.08.19. 6 | // 7 | 8 | public struct Family where R: FamilyRequirementsManaging { 9 | @usableFromInline unowned let nexus: Nexus 10 | public let traits: FamilyTraitSet 11 | 12 | public init(nexus: Nexus, requiresAll: @autoclosure () -> R.ComponentTypes, excludesAll: [Component.Type]) { 13 | let required = R(requiresAll()) 14 | self.nexus = nexus 15 | let traits = FamilyTraitSet(requiresAll: required.componentTypes, excludesAll: excludesAll) 16 | self.traits = traits 17 | nexus.onFamilyInit(traits: traits) 18 | } 19 | 20 | @inlinable var memberIds: UnorderedSparseSet { 21 | nexus.members(withFamilyTraits: traits) 22 | } 23 | 24 | /// Returns the number of family member entities. 25 | @inlinable public var count: Int { 26 | memberIds.count 27 | } 28 | 29 | /// True if this family has no members; false otherwise. 30 | @inlinable public var isEmpty: Bool { 31 | memberIds.isEmpty 32 | } 33 | 34 | @inlinable 35 | public func canBecomeMember(_ entity: Entity) -> Bool { 36 | nexus.canBecomeMember(entity, in: traits) 37 | } 38 | 39 | @inlinable 40 | public func isMember(_ entity: Entity) -> Bool { 41 | nexus.isMember(entity, in: traits) 42 | } 43 | 44 | /// Destroy all member entities of this family. 45 | /// - Returns: True if entities where destroyed, false otherwise. 46 | @discardableResult 47 | public func destroyMembers() -> Bool { 48 | entities.reduce(!isEmpty) { $0 && nexus.destroy(entity: $1) } 49 | } 50 | 51 | /// Create a member entity with the given components assigned. 52 | /// - Parameter builder: The family member builder. 53 | /// - Returns: The newly created member entity. 54 | @discardableResult 55 | public func createMember(@FamilyMemberBuilder using builder: () -> R.Components) -> Entity { 56 | createMember(with: builder()) 57 | } 58 | } 59 | 60 | extension Family: Equatable { 61 | public static func == (lhs: Family, rhs: Family) -> Bool { 62 | lhs.nexus === rhs.nexus && 63 | lhs.traits == rhs.traits 64 | } 65 | } 66 | 67 | extension Family: Sequence { 68 | public func makeIterator() -> ComponentsIterator { 69 | ComponentsIterator(family: self) 70 | } 71 | } 72 | 73 | extension Family: LazySequenceProtocol {} 74 | 75 | // MARK: - components iterator 76 | 77 | extension Family { 78 | public struct ComponentsIterator: IteratorProtocol { 79 | @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator 80 | @usableFromInline unowned let nexus: Nexus 81 | 82 | public init(family: Family) { 83 | nexus = family.nexus 84 | memberIdsIterator = family.memberIds.makeIterator() 85 | } 86 | 87 | public mutating func next() -> R.Components? { 88 | guard let entityId: EntityIdentifier = memberIdsIterator.next() else { 89 | return nil 90 | } 91 | 92 | return R.components(nexus: nexus, entityId: entityId) 93 | } 94 | } 95 | } 96 | 97 | extension Family.ComponentsIterator: LazySequenceProtocol {} 98 | extension Family.ComponentsIterator: Sequence {} 99 | 100 | // MARK: - entity iterator 101 | 102 | extension Family { 103 | @inlinable public var entities: EntityIterator { 104 | EntityIterator(family: self) 105 | } 106 | 107 | public struct EntityIterator: IteratorProtocol { 108 | @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator 109 | @usableFromInline unowned let nexus: Nexus 110 | 111 | public init(family: Family) { 112 | nexus = family.nexus 113 | memberIdsIterator = family.memberIds.makeIterator() 114 | } 115 | 116 | public mutating func next() -> Entity? { 117 | guard let entityId = memberIdsIterator.next() else { 118 | return nil 119 | } 120 | return Entity(nexus: nexus, id: entityId) 121 | } 122 | } 123 | } 124 | 125 | extension Family.EntityIterator: LazySequenceProtocol {} 126 | extension Family.EntityIterator: Sequence {} 127 | 128 | // MARK: - entity component iterator 129 | 130 | extension Family { 131 | @inlinable public var entityAndComponents: EntityComponentIterator { 132 | EntityComponentIterator(family: self) 133 | } 134 | 135 | public struct EntityComponentIterator: IteratorProtocol { 136 | @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator 137 | @usableFromInline unowned let nexus: Nexus 138 | 139 | public init(family: Family) { 140 | nexus = family.nexus 141 | memberIdsIterator = family.memberIds.makeIterator() 142 | } 143 | 144 | public mutating func next() -> R.EntityAndComponents? { 145 | guard let entityId = memberIdsIterator.next() else { 146 | return nil 147 | } 148 | return R.entityAndComponents(nexus: nexus, entityId: entityId) 149 | } 150 | } 151 | } 152 | 153 | extension Family.EntityComponentIterator: LazySequenceProtocol {} 154 | extension Family.EntityComponentIterator: Sequence {} 155 | 156 | // MARK: - member creation 157 | 158 | extension Family { 159 | /// Create a new entity with components required by this family. 160 | /// 161 | /// Since the created entity will meet the requirements of this family it 162 | /// will automatically become member of this family. 163 | /// - Parameter components: The components required by this family. 164 | /// - Returns: The newly created entity. 165 | @discardableResult 166 | public func createMember(with components: R.Components) -> Entity { 167 | R.createMember(nexus: nexus, components: components) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/FamilyDecoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyDecoding.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 05.08.20. 6 | // 7 | 8 | public protocol FamilyDecoding: FamilyRequirementsManaging { 9 | static func decode(componentsIn unkeyedContainer: inout UnkeyedDecodingContainer, using strategy: CodingStrategy) throws -> [Components] 10 | static func decode(componentsIn container: KeyedDecodingContainer, using strategy: CodingStrategy) throws -> Components 11 | } 12 | 13 | extension FamilyDecoding { 14 | public static func decode(componentsIn unkeyedContainer: inout UnkeyedDecodingContainer, using strategy: CodingStrategy) throws -> [Components] { 15 | var components = [Components]() 16 | if let count = unkeyedContainer.count { 17 | components.reserveCapacity(count) 18 | } 19 | while !unkeyedContainer.isAtEnd { 20 | let container = try unkeyedContainer.nestedContainer(keyedBy: DynamicCodingKey.self) 21 | let comps = try Self.decode(componentsIn: container, using: strategy) 22 | components.append(comps) 23 | } 24 | return components 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/FamilyEncoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyEncoding.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 05.08.20. 6 | // 7 | 8 | public protocol FamilyEncoding: FamilyRequirementsManaging { 9 | static func encode(componentsArray: [Components], into container: inout UnkeyedEncodingContainer, using strategy: CodingStrategy) throws 10 | static func encode(components: Components, into container: inout KeyedEncodingContainer, using strategy: CodingStrategy) throws 11 | } 12 | 13 | extension FamilyEncoding { 14 | public static func encode(componentsArray: [Components], into container: inout UnkeyedEncodingContainer, using strategy: CodingStrategy) throws { 15 | for comps in componentsArray { 16 | var container = container.nestedContainer(keyedBy: DynamicCodingKey.self) 17 | try Self.encode(components: comps, into: &container, using: strategy) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/FamilyMemberBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyMemberBuilder.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 07.08.20. 6 | // 7 | 8 | #if swift(<5.4) 9 | @_functionBuilder 10 | public enum FamilyMemberBuilder where R: FamilyRequirementsManaging {} 11 | #else 12 | @resultBuilder 13 | public enum FamilyMemberBuilder where R: FamilyRequirementsManaging {} 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/FamilyRequirementsManaging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyRequirementsManaging.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 21.08.19. 6 | // 7 | 8 | public protocol FamilyRequirementsManaging { 9 | associatedtype Components 10 | associatedtype ComponentTypes 11 | associatedtype EntityAndComponents 12 | 13 | init(_ types: ComponentTypes) 14 | 15 | var componentTypes: [Component.Type] { get } 16 | 17 | static func components(nexus: Nexus, entityId: EntityIdentifier) -> Components 18 | static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> EntityAndComponents 19 | static func createMember(nexus: Nexus, components: Components) -> Entity 20 | } 21 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/FamilyTraitSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyTraitSet.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | public struct FamilyTraitSet { 9 | public let requiresAll: Set 10 | public let excludesAll: Set 11 | 12 | public let setHash: Int 13 | 14 | public init(requiresAll: [Component.Type], excludesAll: [Component.Type]) { 15 | let requiresAll = Set(requiresAll.map { $0.identifier }) 16 | let excludesAll = Set(excludesAll.map { $0.identifier }) 17 | 18 | assert(FamilyTraitSet.isValid(requiresAll: requiresAll, excludesAll: excludesAll), "invalid family trait created - requiresAll: \(requiresAll), excludesAll: \(excludesAll)") 19 | 20 | self.requiresAll = requiresAll 21 | self.excludesAll = excludesAll 22 | setHash = FirebladeECS.hash(combine: [requiresAll, excludesAll]) 23 | } 24 | 25 | @inlinable 26 | public func isMatch(components: Set) -> Bool { 27 | hasAll(components) && hasNone(components) 28 | } 29 | 30 | @inlinable 31 | public func hasAll(_ components: Set) -> Bool { 32 | requiresAll.isSubset(of: components) 33 | } 34 | 35 | @inlinable 36 | public func hasNone(_ components: Set) -> Bool { 37 | excludesAll.isDisjoint(with: components) 38 | } 39 | 40 | @inlinable 41 | public static func isValid(requiresAll: Set, excludesAll: Set) -> Bool { 42 | !requiresAll.isEmpty && 43 | requiresAll.isDisjoint(with: excludesAll) 44 | } 45 | } 46 | 47 | extension FamilyTraitSet: Equatable { 48 | public static func == (lhs: FamilyTraitSet, rhs: FamilyTraitSet) -> Bool { 49 | lhs.setHash == rhs.setHash 50 | } 51 | } 52 | 53 | extension FamilyTraitSet: Hashable { 54 | public func hash(into hasher: inout Hasher) { 55 | hasher.combine(setHash) 56 | } 57 | } 58 | 59 | extension FamilyTraitSet: CustomStringConvertible { 60 | @inlinable public var description: String { 61 | "" 62 | } 63 | } 64 | 65 | extension FamilyTraitSet: CustomDebugStringConvertible { 66 | @inlinable public var debugDescription: String { 67 | "" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Foundation+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Foundation+Extensions.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 22.07.20. 6 | // 7 | 8 | #if canImport(Foundation) 9 | import Foundation 10 | 11 | extension JSONEncoder: TopLevelEncoder {} 12 | extension JSONDecoder: TopLevelDecoder {} 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireblade-engine/ecs/5a7e7f6f2c5fa10b6a90d940c6efcf6a72581635/Sources/FirebladeECS/Generated/.gitkeep -------------------------------------------------------------------------------- /Sources/FirebladeECS/Hashing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hashing.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 16.10.17. 6 | // 7 | 8 | #if arch(x86_64) || arch(arm64) || arch(powerpc64) || arch(powerpc64le) || arch(s390x) // 64 bit 9 | private let kFibA: UInt = 0x9E37_79B9_7F4A_7C15 // = 11400714819323198485 aka Fibonacci Hash a value for 2^64; calculate by: 2^64 / (golden ratio) 10 | #elseif arch(i386) || arch(arm) || os(watchOS) || arch(wasm32) // 32 bit 11 | private let kFibA: UInt = 0x9E37_79B9 // = 2654435769 aka Fibonacci Hash a value for 2^32; calculate by: 2^32 / (golden ratio) 12 | #else 13 | #error("unsupported architecture") 14 | #endif 15 | 16 | /// entity id ^ component identifier hash 17 | public typealias EntityComponentHash = Int 18 | 19 | /// component object identifier hash value 20 | public typealias ComponentTypeHash = Int 21 | 22 | // MARK: - hash combine 23 | 24 | /// Calculates the combined hash of two values. 25 | /// 26 | /// This implementation is based on boost::hash_combine. 27 | /// It produces the same result for the same combination of seed and value during the single run of a program. 28 | /// 29 | /// - Parameters: 30 | /// - seed: seed hash. 31 | /// - value: value to be combined with seed hash. 32 | /// - Returns: combined hash value. 33 | public func hash(combine seed: Int, _ value: Int) -> Int { 34 | /// http://www.boost.org/doc/libs/1_65_1/doc/html/hash/combine.html 35 | /// http://www.boost.org/doc/libs/1_65_1/doc/html/hash/reference.html#boost.hash_combine 36 | /// http://www.boost.org/doc/libs/1_65_1/boost/functional/hash/hash.hpp 37 | /// http://book.huihoo.com/data-structures-and-algorithms-with-object-oriented-design-patterns-in-c++/html/page214.html 38 | /// https://stackoverflow.com/a/35991300 39 | /// https://stackoverflow.com/a/4948967 40 | /* 41 | let phi = (1.0 + sqrt(5.0)) / 2 // golden ratio 42 | let a32 = pow(2.0,32.0) / phi 43 | let a64 = pow(2.0,64.0) / phi 44 | */ 45 | var uSeed = UInt(bitPattern: seed) 46 | let uValue = UInt(bitPattern: value) 47 | uSeed ^= uValue &+ kFibA &+ (uSeed << 6) &+ (uSeed >> 2) 48 | return Int(bitPattern: uSeed) 49 | } 50 | 51 | /// Calculates the combined hash value of the elements. 52 | /// 53 | /// This implementation is based on boost::hash_range. 54 | /// The hash value this method computes is sensitive to the order of the elements. 55 | /// - Parameter hashValues: sequence of hash values to combine. 56 | /// - Returns: combined hash value. 57 | public func hash(combine hashValues: H) -> Int where H.Element: Hashable { 58 | /// http://www.boost.org/doc/libs/1_65_1/doc/html/hash/reference.html#boost.hash_range_idp517643120 59 | hashValues.reduce(0) { hash(combine: $0, $1.hashValue) } 60 | } 61 | 62 | // MARK: - entity component hash 63 | 64 | extension EntityComponentHash { 65 | static func compose(entityId: EntityIdentifier, componentTypeHash: ComponentTypeHash) -> EntityComponentHash { 66 | let entityIdSwapped = UInt(entityId.id).byteSwapped // needs to be 64 bit 67 | let componentTypeHashUInt = UInt(bitPattern: componentTypeHash) 68 | let hashUInt: UInt = componentTypeHashUInt ^ entityIdSwapped 69 | return Int(bitPattern: hashUInt) 70 | } 71 | 72 | static func decompose(_ hash: EntityComponentHash, with entityId: EntityIdentifier) -> ComponentTypeHash { 73 | let entityIdSwapped = UInt(entityId.id).byteSwapped 74 | let entityIdSwappedInt = Int(bitPattern: entityIdSwapped) 75 | return hash ^ entityIdSwappedInt 76 | } 77 | 78 | static func decompose(_ hash: EntityComponentHash, with componentTypeHash: ComponentTypeHash) -> EntityIdentifier { 79 | let entityId: Int = (hash ^ componentTypeHash).byteSwapped 80 | return EntityIdentifier(UInt32(truncatingIfNeeded: entityId)) 81 | } 82 | } 83 | 84 | // MARK: - string hashing 85 | 86 | /// A type that provides stable hash values for String. 87 | /// 88 | /// The details are based on [StackOverflow Q&A on String hashing in Swift](https://stackoverflow.com/a/52440609) 89 | public enum StringHashing { 90 | /// *Warren Stringer djb2* 91 | /// 92 | /// Implementation from 93 | public static func singer_djb2(_ utf8String: String) -> UInt64 { 94 | var hash: UInt64 = 5381 95 | var iter = utf8String.unicodeScalars.makeIterator() 96 | while let char = iter.next() { 97 | hash = 127 * (hash & 0xFF_FFFF_FFFF_FFFF) &+ UInt64(char.value) 98 | } 99 | return hash 100 | } 101 | 102 | /// *Dan Bernstein djb2* 103 | /// 104 | /// This algorithm (k=33) was first reported by Dan Bernstein many years ago in `comp.lang.c`. 105 | /// Another version of this algorithm (now favored by Bernstein) uses xor: `hash(i) = hash(i - 1) * 33 ^ str[i];` 106 | /// The magic of number 33 (why it works better than many other constants, prime or not) has never been adequately explained. 107 | /// 108 | /// 109 | public static func bernstein_djb2(_ string: String) -> UInt64 { 110 | var hash: UInt64 = 5381 111 | var iter = string.unicodeScalars.makeIterator() 112 | while let char = iter.next() { 113 | hash = (hash << 5) &+ hash &+ UInt64(char.value) 114 | } 115 | return hash 116 | } 117 | 118 | /// *sdbm* 119 | /// 120 | /// This algorithm was created for sdbm (a public-domain reimplementation of ndbm) database library. 121 | /// It was found to do well in scrambling bits, causing better distribution of the keys and fewer splits. 122 | /// It also happens to be a good general hashing function with good distribution. 123 | /// 124 | /// 125 | public static func sdbm(_ string: String) -> UInt64 { 126 | var hash: UInt64 = 0 127 | var iter = string.unicodeScalars.makeIterator() 128 | while let char = iter.next() { 129 | hash = (UInt64(char.value) &+ (hash << 6) &+ (hash << 16)) 130 | } 131 | return hash 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/ManagedContiguousArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedContiguousArray.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 28.10.17. 6 | // 7 | /// A type that provides a managed contiguous array of elements that you provide. 8 | public struct ManagedContiguousArray { 9 | public typealias Index = Int 10 | 11 | @usableFromInline let chunkSize: Int 12 | @usableFromInline var size: Int = 0 13 | @usableFromInline var store: ContiguousArray = [] 14 | 15 | /// Creates a new array. 16 | /// - Parameter minCount: The minimum number of elements, which defaults to `4096`. 17 | public init(minCount: Int = 4096) { 18 | chunkSize = minCount 19 | store = ContiguousArray(repeating: nil, count: minCount) 20 | } 21 | 22 | /// The number of elements in the array. 23 | @inline(__always) 24 | public var count: Int { 25 | size 26 | } 27 | 28 | /// Inserts an element into the managed array. 29 | /// - Parameters: 30 | /// - element: The element to insert 31 | /// - index: The location at which to insert the element. 32 | /// - Returns: `true` to indicate the element was inserted. 33 | @discardableResult 34 | @inlinable 35 | public mutating func insert(_ element: Element, at index: Int) -> Bool { 36 | if needsToGrow(index) { 37 | grow(to: index) 38 | } 39 | if store[index] == nil { 40 | size += 1 41 | } 42 | store[index] = element 43 | return true 44 | } 45 | 46 | /// Returns a Boolean value that indicates whether the index location holds an element. 47 | /// - Parameter index: The index location in the contiguous array to inspect. 48 | @inlinable 49 | public func contains(_ index: Index) -> Bool { 50 | if store.count <= index { 51 | return false 52 | } 53 | return store[index] != nil 54 | } 55 | 56 | /// Retrieves the value at the index location you provide. 57 | /// - Parameter index: The index location. 58 | /// - Returns: The element at the index location, or `nil`. 59 | @inline(__always) 60 | public func get(at index: Index) -> Element? { 61 | store[index] 62 | } 63 | 64 | /// Unsafely retrieves the value at the index location you provide. 65 | /// - Parameter index: The index location. 66 | /// - Returns: The element at the index location. 67 | @inline(__always) 68 | public func get(unsafeAt index: Index) -> Element { 69 | store[index].unsafelyUnwrapped 70 | } 71 | 72 | /// Removes the object at the index location you provide. 73 | /// - Parameter index: The index location. 74 | /// - Returns: `true` to indicate the element was removed. 75 | @discardableResult 76 | @inlinable 77 | public mutating func remove(at index: Index) -> Bool { 78 | if store[index] != nil { 79 | size -= 1 80 | } 81 | store[index] = nil 82 | if size == 0 { 83 | clear() 84 | } 85 | return true 86 | } 87 | 88 | /// Clears the array of all elements. 89 | /// - Parameter keepingCapacity: A Boolean value that indicates whether to keep the capacity of the array. 90 | @inlinable 91 | public mutating func clear(keepingCapacity: Bool = false) { 92 | size = 0 93 | store.removeAll(keepingCapacity: keepingCapacity) 94 | } 95 | 96 | /// Returns a Boolean value that indicates if the array needs to grow to insert another item. 97 | /// - Parameter index: The index location to check. 98 | @inlinable 99 | func needsToGrow(_ index: Index) -> Bool { 100 | index > store.count - 1 101 | } 102 | 103 | /// Expands the contiguous array to encompass the index location you provide. 104 | /// - Parameter index: The index location. 105 | @inlinable 106 | mutating func grow(to index: Index) { 107 | let newCapacity: Int = calculateCapacity(to: index) 108 | let newCount: Int = newCapacity - store.count 109 | store += ContiguousArray(repeating: nil, count: newCount) 110 | } 111 | 112 | /// Returns the capacity of the array to the index location you provide. 113 | /// - Parameter index: The index location 114 | @inlinable 115 | func calculateCapacity(to index: Index) -> Int { 116 | let delta = Float(index) / Float(chunkSize) 117 | let multiplier = Int(delta.rounded(.up)) + 1 118 | return multiplier * chunkSize 119 | } 120 | } 121 | 122 | // MARK: - Equatable 123 | 124 | extension ManagedContiguousArray: Equatable where Element: Equatable { 125 | public static func == (lhs: ManagedContiguousArray, rhs: ManagedContiguousArray) -> Bool { 126 | lhs.store == rhs.store 127 | } 128 | } 129 | 130 | // MARK: - Codable 131 | 132 | extension ManagedContiguousArray: Codable where Element: Codable {} 133 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus+Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus+Component.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 13.10.17. 6 | // 7 | 8 | extension Nexus { 9 | public final var numComponents: Int { 10 | componentsByType.reduce(0) { $0 + $1.value.count } 11 | } 12 | 13 | public final func has(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> Bool { 14 | guard let uniforms = componentsByType[componentId] else { 15 | return false 16 | } 17 | return uniforms.contains(entityId.index) 18 | } 19 | 20 | public final func count(components entityId: EntityIdentifier) -> Int { 21 | componentIdsByEntity[entityId]?.count ?? 0 22 | } 23 | 24 | @discardableResult 25 | public final func assign(component: Component, to entity: Entity) -> Bool { 26 | let entityId: EntityIdentifier = entity.identifier 27 | return assign(component: component, entityId: entityId) 28 | } 29 | 30 | @discardableResult 31 | public final func assign(component: some Component, to entity: Entity) -> Bool { 32 | assign(component: component, to: entity) 33 | } 34 | 35 | @discardableResult 36 | public final func assign(components: some Collection, to entity: Entity) -> Bool { 37 | assign(components: components, to: entity.identifier) 38 | } 39 | 40 | @inlinable 41 | public final func get(safe componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { 42 | guard let uniformComponents = componentsByType[componentId], uniformComponents.contains(entityId.index) else { 43 | return nil 44 | } 45 | return uniformComponents.get(at: entityId.index) 46 | } 47 | 48 | @inlinable 49 | public final func get(unsafe componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component { 50 | let uniformComponents = componentsByType[componentId].unsafelyUnwrapped 51 | return uniformComponents.get(unsafeAt: entityId.index) 52 | } 53 | 54 | @inlinable 55 | public final func get(safe componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> C? where C: Component { 56 | get(safe: componentId, for: entityId) as? C 57 | } 58 | 59 | @inlinable 60 | public final func get(safe entityId: EntityIdentifier) -> C? where C: Component { 61 | get(safe: C.identifier, for: entityId) 62 | } 63 | 64 | @inlinable 65 | public final func get(unsafe entityId: EntityIdentifier) -> C where C: Component { 66 | let component: Component = get(unsafe: C.identifier, for: entityId) 67 | // components are guaranteed to be reference types so unsafeDowncast is applicable here 68 | return unsafeDowncast(component, to: C.self) 69 | } 70 | 71 | @inlinable 72 | public final func get(components entityId: EntityIdentifier) -> Set? { 73 | componentIdsByEntity[entityId] 74 | } 75 | 76 | @discardableResult 77 | public final func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool { 78 | // delete component instance 79 | componentsByType[componentId]?.remove(at: entityId.index) 80 | // un-assign component from entity 81 | componentIdsByEntity[entityId]?.remove(componentId) 82 | 83 | update(familyMembership: entityId) 84 | 85 | delegate?.nexusEvent(ComponentRemoved(component: componentId, from: entityId)) 86 | return true 87 | } 88 | 89 | @discardableResult 90 | public final func removeAll(components entityId: EntityIdentifier) -> Bool { 91 | guard let allComponents = get(components: entityId) else { 92 | delegate?.nexusNonFatalError("clearing components form entity \(entityId) with no components") 93 | return false 94 | } 95 | var iter = allComponents.makeIterator() 96 | var removedAll = true 97 | while let component = iter.next() { 98 | removedAll = removedAll && remove(component: component, from: entityId) 99 | } 100 | return removedAll 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus+ComponentsBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus+ComponentsBuilder.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 30.07.20. 6 | // 7 | 8 | #if swift(<5.4) 9 | @_functionBuilder 10 | public enum ComponentsBuilder {} 11 | #else 12 | @resultBuilder 13 | public enum ComponentsBuilder {} 14 | #endif 15 | 16 | extension ComponentsBuilder { 17 | public static func buildBlock(_ components: Component...) -> [Component] { 18 | components 19 | } 20 | 21 | public struct Context { 22 | /// The index of the newly created entity. 23 | /// 24 | /// This is **NOT** equal to the entity identifier. 25 | public let index: Int 26 | } 27 | } 28 | 29 | extension Nexus { 30 | /// Create an entity assigning one component. 31 | /// 32 | /// Usage: 33 | /// ``` 34 | /// let newEntity = nexus.createEntity { 35 | /// Position(x: 1, y: 2) 36 | /// } 37 | /// ``` 38 | /// - Parameter builder: The component builder. 39 | /// - Returns: The newly created entity with the provided component assigned. 40 | @discardableResult 41 | public func createEntity(@ComponentsBuilder using builder: () -> Component) -> Entity { 42 | createEntity(with: builder()) 43 | } 44 | 45 | /// Create an entity assigning multiple components. 46 | /// 47 | /// Usage: 48 | /// ``` 49 | /// let newEntity = nexus.createEntity { 50 | /// Position(x: 1, y: 2) 51 | /// Name(name: "Some name") 52 | /// } 53 | /// ``` 54 | /// - Parameter builder: The component builder. 55 | /// - Returns: The newly created entity with the provided components assigned. 56 | @discardableResult 57 | public func createEntity(@ComponentsBuilder using builder: () -> [Component]) -> Entity { 58 | createEntity(with: builder()) 59 | } 60 | 61 | /// Create multiple entities assigning one component each. 62 | /// 63 | /// Usage: 64 | /// ``` 65 | /// let newEntities = nexus.createEntities(count: 100) { ctx in 66 | /// Velocity(a: Float(ctx.index)) 67 | /// } 68 | /// ``` 69 | /// - Parameters: 70 | /// - count: The count of entities to create. 71 | /// - builder: The component builder providing context. 72 | /// - Returns: The newly created entities with the provided component assigned. 73 | @discardableResult 74 | public func createEntities(count: Int, @ComponentsBuilder using builder: (ComponentsBuilder.Context) -> Component) -> [Entity] { 75 | (0 ..< count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) } 76 | } 77 | 78 | /// Create multiple entities assigning multiple components each. 79 | /// 80 | /// Usage: 81 | /// ``` 82 | /// let newEntities = nexus.createEntities(count: 100) { ctx in 83 | /// Position(x: ctx.index, y: ctx.index) 84 | /// Name(name: "\(ctx.index)") 85 | /// } 86 | /// ``` 87 | /// - Parameters: 88 | /// - count: The count of entities to create. 89 | /// - builder: The component builder providing context. 90 | /// - Returns: The newly created entities with the provided components assigned. 91 | @discardableResult 92 | public func createEntities(count: Int, @ComponentsBuilder using builder: (ComponentsBuilder.Context) -> [Component] = { _ in [] }) -> [Entity] { 93 | (0 ..< count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus+Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus+Entity.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 13.10.17. 6 | // 7 | 8 | extension Nexus { 9 | @discardableResult 10 | public func createEntity() -> Entity { 11 | let entityId: EntityIdentifier = entityIdGenerator.nextId() 12 | componentIdsByEntity[entityId] = [] 13 | delegate?.nexusEvent(EntityCreated(entityId: entityId)) 14 | return Entity(nexus: self, id: entityId) 15 | } 16 | 17 | @discardableResult 18 | public func createEntity(with components: Component...) -> Entity { 19 | let newEntity = createEntity() 20 | assign(components: components, to: newEntity.identifier) 21 | return newEntity 22 | } 23 | 24 | @discardableResult 25 | public func createEntity(with components: some Collection) -> Entity { 26 | let entity = createEntity() 27 | assign(components: components, to: entity.identifier) 28 | return entity 29 | } 30 | 31 | /// Number of entities in nexus. 32 | public var numEntities: Int { 33 | componentIdsByEntity.keys.count 34 | } 35 | 36 | /// Creates an iterator over all entities in the nexus. 37 | /// 38 | /// Entity order is not guaranteed to stay the same over iterations. 39 | public func makeEntitiesIterator() -> EntitiesIterator { 40 | EntitiesIterator(nexus: self) 41 | } 42 | 43 | public func exists(entity entityId: EntityIdentifier) -> Bool { 44 | componentIdsByEntity.keys.contains(entityId) 45 | } 46 | 47 | public func entity(from entityId: EntityIdentifier) -> Entity { 48 | Entity(nexus: self, id: entityId) 49 | } 50 | 51 | @discardableResult 52 | public func destroy(entity: Entity) -> Bool { 53 | destroy(entityId: entity.identifier) 54 | } 55 | 56 | @discardableResult 57 | public func destroy(entityId: EntityIdentifier) -> Bool { 58 | guard componentIdsByEntity.keys.contains(entityId) else { 59 | delegate?.nexusNonFatalError("EntityRemove failure: no entity \(entityId) to remove") 60 | return false 61 | } 62 | 63 | if removeAll(components: entityId) { 64 | update(familyMembership: entityId) 65 | } 66 | 67 | if let index = componentIdsByEntity.index(forKey: entityId) { 68 | componentIdsByEntity.remove(at: index) 69 | } 70 | 71 | entityIdGenerator.markUnused(entityId: entityId) 72 | 73 | delegate?.nexusEvent(EntityDestroyed(entityId: entityId)) 74 | return true 75 | } 76 | } 77 | 78 | // MARK: - entities iterator 79 | 80 | extension Nexus { 81 | public struct EntitiesIterator: IteratorProtocol { 82 | private var iterator: AnyIterator 83 | 84 | @usableFromInline 85 | init(nexus: Nexus) { 86 | var iter = nexus.componentIdsByEntity.keys.makeIterator() 87 | iterator = AnyIterator { 88 | guard let entityId = iter.next() else { 89 | return nil 90 | } 91 | return Entity(nexus: nexus, id: entityId) 92 | } 93 | } 94 | 95 | public func next() -> Entity? { 96 | iterator.next() 97 | } 98 | } 99 | } 100 | 101 | extension Nexus.EntitiesIterator: LazySequenceProtocol {} 102 | extension Nexus.EntitiesIterator: Sequence {} 103 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus+Family.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus+Family.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 13.10.17. 6 | // 7 | 8 | extension Nexus { 9 | public final var numFamilies: Int { 10 | familyMembersByTraits.keys.count 11 | } 12 | 13 | public func canBecomeMember(_ entity: Entity, in traits: FamilyTraitSet) -> Bool { 14 | guard let componentIds = componentIdsByEntity[entity.identifier] else { 15 | assertionFailure("no component set defined for entity: \(entity)") 16 | return false 17 | } 18 | return traits.isMatch(components: componentIds) 19 | } 20 | 21 | public func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { 22 | familyMembersByTraits[traits] ?? UnorderedSparseSet() 23 | } 24 | 25 | public func isMember(_ entity: Entity, in family: FamilyTraitSet) -> Bool { 26 | isMember(entity.identifier, in: family) 27 | } 28 | 29 | public func isMember(_ entityId: EntityIdentifier, in family: FamilyTraitSet) -> Bool { 30 | isMember(entity: entityId, inFamilyWithTraits: family) 31 | } 32 | 33 | public func isMember(entity entityId: EntityIdentifier, inFamilyWithTraits traits: FamilyTraitSet) -> Bool { 34 | members(withFamilyTraits: traits).contains(entityId.id) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus+Internal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus+Internal.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 14.02.19. 6 | // 7 | 8 | extension Nexus { 9 | @usableFromInline 10 | @discardableResult 11 | func assign(components: some Collection, to entityId: EntityIdentifier) -> Bool { 12 | var iter = components.makeIterator() 13 | while let component = iter.next() { 14 | let componentId = component.identifier 15 | // test if component is already assigned 16 | guard !has(componentId: componentId, entityId: entityId) else { 17 | delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") 18 | assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") 19 | return false 20 | } 21 | 22 | // add component instances to uniform component stores 23 | insertComponentInstance(component, componentId, entityId) 24 | 25 | // assigns the component id to the entity id 26 | assign(componentId, entityId) 27 | } 28 | 29 | // Update entity membership 30 | update(familyMembership: entityId) 31 | return true 32 | } 33 | 34 | @usableFromInline 35 | func assign(component: Component, entityId: EntityIdentifier) -> Bool { 36 | let componentId = component.identifier 37 | 38 | // test if component is already assigned 39 | guard !has(componentId: componentId, entityId: entityId) else { 40 | delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") 41 | assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") 42 | return false 43 | } 44 | 45 | // add component instances to uniform component stores 46 | insertComponentInstance(component, componentId, entityId) 47 | 48 | // assigns the component id to the entity id 49 | assign(componentId, entityId) 50 | 51 | // Update entity membership 52 | update(familyMembership: entityId) 53 | return true 54 | } 55 | 56 | @usableFromInline 57 | func insertComponentInstance(_ component: Component, _ componentId: ComponentIdentifier, _ entityId: EntityIdentifier) { 58 | if componentsByType[componentId] == nil { 59 | componentsByType[componentId] = ManagedContiguousArray() 60 | } 61 | componentsByType[componentId]?.insert(component, at: entityId.index) 62 | } 63 | 64 | @usableFromInline 65 | func assign(_ componentId: ComponentIdentifier, _ entityId: EntityIdentifier) { 66 | let (inserted, _) = componentIdsByEntity[entityId]!.insert(componentId) 67 | if inserted { 68 | delegate?.nexusEvent(ComponentAdded(component: componentId, toEntity: entityId)) 69 | } 70 | } 71 | 72 | @usableFromInline 73 | func update(familyMembership entityId: EntityIdentifier) { 74 | // FIXME: iterating all families is costly for many families 75 | // FIXME: this could be parallelized 76 | var iter = familyMembersByTraits.keys.makeIterator() 77 | while let traits = iter.next() { 78 | update(membership: traits, for: entityId) 79 | } 80 | } 81 | 82 | /// will be called on family init 83 | func onFamilyInit(traits: FamilyTraitSet) { 84 | guard familyMembersByTraits[traits] == nil else { 85 | return 86 | } 87 | 88 | familyMembersByTraits[traits] = UnorderedSparseSet() 89 | update(familyMembership: traits) 90 | } 91 | 92 | func update(familyMembership traits: FamilyTraitSet) { 93 | // FIXME: iterating all entities is costly for many entities 94 | var iter = componentIdsByEntity.keys.makeIterator() 95 | while let entityId = iter.next() { 96 | update(membership: traits, for: entityId) 97 | } 98 | } 99 | 100 | func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) { 101 | guard let componentIds = componentIdsByEntity[entityId] else { 102 | // no components - so skip 103 | return 104 | } 105 | 106 | let isMember: Bool = isMember(entity: entityId, inFamilyWithTraits: traits) 107 | if !exists(entity: entityId), isMember { 108 | remove(entityWithId: entityId, fromFamilyWithTraits: traits) 109 | return 110 | } 111 | 112 | let isMatch: Bool = traits.isMatch(components: componentIds) 113 | 114 | switch (isMatch, isMember) { 115 | case (true, false): 116 | add(entityWithId: entityId, toFamilyWithTraits: traits) 117 | delegate?.nexusEvent(FamilyMemberAdded(member: entityId, toFamily: traits)) 118 | 119 | case (false, true): 120 | remove(entityWithId: entityId, fromFamilyWithTraits: traits) 121 | delegate?.nexusEvent(FamilyMemberRemoved(member: entityId, from: traits)) 122 | 123 | default: 124 | break 125 | } 126 | } 127 | 128 | func add(entityWithId entityId: EntityIdentifier, toFamilyWithTraits traits: FamilyTraitSet) { 129 | familyMembersByTraits[traits].unsafelyUnwrapped.insert(entityId, at: entityId.id) 130 | } 131 | 132 | func remove(entityWithId entityId: EntityIdentifier, fromFamilyWithTraits traits: FamilyTraitSet) { 133 | familyMembersByTraits[traits].unsafelyUnwrapped.remove(at: entityId.id) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Nexus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nexus.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | public final class Nexus { 9 | /// - Key: ComponentIdentifier aka component type. 10 | /// - Value: Array of component instances of same type (uniform). 11 | /// New component instances are appended. 12 | @usableFromInline final var componentsByType: [ComponentIdentifier: ManagedContiguousArray] 13 | 14 | /// - Key: EntityIdentifier aka entity index 15 | /// - Value: Set of unique component types (ComponentIdentifier). 16 | /// Each element is a component identifier associated with this entity. 17 | @usableFromInline final var componentIdsByEntity: [EntityIdentifier: Set] 18 | 19 | /// - Key: FamilyTraitSet aka component types that make up one distinct family. 20 | /// - Value: Tightly packed EntityIdentifiers that represent the association of an entity to the family. 21 | @usableFromInline final var familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet] 22 | 23 | /// The entity identifier generator responsible for providing unique ids for entities during runtime. 24 | /// 25 | /// Provide a custom implementation prior to entity creation. 26 | /// Defaults to `DefaultEntityIdGenerator`. 27 | public final var entityIdGenerator: EntityIdentifierGenerator 28 | 29 | /// The coding strategy used to encode/decode entities from/into families. 30 | /// 31 | /// Provide a custom implementation prior to encoding/decoding. 32 | /// Defaults to `DefaultCodingStrategy`. 33 | public final var codingStrategy: CodingStrategy 34 | 35 | public final weak var delegate: NexusEventDelegate? 36 | 37 | public convenience init() { 38 | self.init(componentsByType: [:], 39 | componentsByEntity: [:], 40 | entityIdGenerator: DefaultEntityIdGenerator(), 41 | familyMembersByTraits: [:], 42 | codingStrategy: DefaultCodingStrategy()) 43 | } 44 | 45 | init(componentsByType: [ComponentIdentifier: ManagedContiguousArray], 46 | componentsByEntity: [EntityIdentifier: Set], 47 | entityIdGenerator: EntityIdentifierGenerator, 48 | familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet], 49 | codingStrategy: CodingStrategy) 50 | { 51 | self.componentsByType = componentsByType 52 | componentIdsByEntity = componentsByEntity 53 | self.familyMembersByTraits = familyMembersByTraits 54 | self.entityIdGenerator = entityIdGenerator 55 | self.codingStrategy = codingStrategy 56 | } 57 | 58 | deinit { 59 | clear() 60 | } 61 | 62 | public final func clear() { 63 | componentsByType.removeAll() 64 | componentIdsByEntity.removeAll() 65 | familyMembersByTraits.removeAll() 66 | } 67 | } 68 | 69 | // MARK: - CustomDebugStringConvertible 70 | 71 | extension Nexus: CustomDebugStringConvertible { 72 | public var debugDescription: String { 73 | "" 74 | } 75 | } 76 | 77 | // MARK: - default coding strategy 78 | 79 | public struct DefaultCodingStrategy: CodingStrategy { 80 | public init() {} 81 | 82 | public func codingKey(for componentType: C.Type) -> DynamicCodingKey where C: Component { 83 | DynamicCodingKey(stringValue: "\(C.self)").unsafelyUnwrapped 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/NexusEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NexusEvent.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 08.10.17. 6 | // 7 | 8 | public protocol NexusEvent {} 9 | 10 | public struct EntityCreated: NexusEvent { 11 | public let entityId: EntityIdentifier 12 | } 13 | 14 | public struct EntityDestroyed: NexusEvent { 15 | public let entityId: EntityIdentifier 16 | } 17 | 18 | public struct ComponentAdded: NexusEvent { 19 | public let component: ComponentIdentifier 20 | public let toEntity: EntityIdentifier 21 | } 22 | 23 | public struct ComponentRemoved: NexusEvent { 24 | public let component: ComponentIdentifier 25 | public let from: EntityIdentifier 26 | } 27 | 28 | public struct FamilyMemberAdded: NexusEvent { 29 | public let member: EntityIdentifier 30 | public let toFamily: FamilyTraitSet 31 | } 32 | 33 | public struct FamilyMemberRemoved: NexusEvent { 34 | public let member: EntityIdentifier 35 | public let from: FamilyTraitSet 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/NexusEventDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NexusEventDelegate.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 20.08.19. 6 | // 7 | 8 | public protocol NexusEventDelegate: AnyObject { 9 | func nexusEvent(_ event: NexusEvent) 10 | func nexusNonFatalError(_ message: String) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Single.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Single.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 13.02.19. 6 | // 7 | 8 | public protocol SingleComponent: Component { 9 | init() 10 | } 11 | 12 | public struct Single where A: SingleComponent { 13 | public let nexus: Nexus 14 | public let traits: FamilyTraitSet 15 | public let entityId: EntityIdentifier 16 | } 17 | 18 | extension Single: Equatable { 19 | public static func == (lhs: Single, rhs: Single) -> Bool { 20 | lhs.traits == rhs.traits && 21 | lhs.entityId == rhs.entityId && 22 | lhs.nexus === rhs.nexus 23 | } 24 | } 25 | 26 | extension Single where A: SingleComponent { 27 | @inlinable public var component: A { 28 | // Since we guarantee that the component will always be present by managing the complete lifecycle of the entity 29 | // and component assignment we may unsafelyUnwrap here. 30 | // Since components will always be of reference type (class) we may use unsafeDowncast here for performance reasons. 31 | nexus.get(unsafe: entityId) 32 | } 33 | 34 | public var entity: Entity { 35 | Entity(nexus: nexus, id: entityId) 36 | } 37 | } 38 | 39 | extension Nexus { 40 | public func single(_ component: S.Type) -> Single where S: SingleComponent { 41 | let family = family(requires: S.self) 42 | precondition(family.count <= 1, "Singleton count of \(S.self) must be 0 or 1: \(family.count)") 43 | let entityId: EntityIdentifier 44 | if family.isEmpty { 45 | entityId = createEntity(with: S()).identifier 46 | } else { 47 | entityId = family.memberIds.first.unsafelyUnwrapped 48 | } 49 | return Single(nexus: self, traits: family.traits, entityId: entityId) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/Stencils/Family.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable file_length 2 | // swiftlint:disable function_parameter_count 3 | // swiftlint:disable large_tuple 4 | // swiftlint:disable line_length 5 | // swiftlint:disable multiline_parameters 6 | {% for idx in 1...8 %} 7 | {% map 1...idx into components using index %}Comp{{ index }}{% endmap %} 8 | {% set CompParams %}{{components|join: ", "}}{% endset %} 9 | {% map components into compWhere using comp %}{{ comp }}: Component{% endmap %} 10 | {% set CompsWhere %}{{compWhere|join: ", "}}{% endset %} 11 | {% map components into compEncodable using comp %}{{ comp }}: Encodable{% endmap %} 12 | {% set CompsWhereEncodable %}{{compEncodable|join: ", "}}{% endset %} 13 | {% map components into compsDecodable using comp %}{{ comp }}: Decodable{% endmap %} 14 | {% set CompsWhereDecodable %}{{compsDecodable|join: ", "}}{% endset %} 15 | {% map components into compTypes using comp %}{{ comp }}.Type{% endmap %} 16 | {% set CompsTypes %}{{compTypes|join: ", "}}{% endset %} 17 | {% map components into compSelf using comp %}{{ comp }}.self{% endmap %} 18 | {% set CompsSelf %}{{compSelf|join: ", "}}{% endset %} 19 | {% map components into compsLowercased using comp %}{{ comp|lowercase }}{% endmap %} 20 | {% set CompsLowercased %}{{compsLowercased|join: ", "}}{% endset %} 21 | {% map components into compsTuple using comp %}components.{{ maploop.counter }}{% endmap %} 22 | {% set CompsTuple %}{{compsTuple|join: ", "}}{% endset %} 23 | {% map components into compsTypeParams using comp %}{% if not maploop.first %}_ {% endif %}{{ comp|lowercase }}: {{ comp }}.Type{% endmap %} 24 | {% set CompsTypeParams %}{{compsTypeParams|join: ", "}}{% endset %} 25 | {% map components into compsNamedParams using comp %}{% if not maploop.first %}_ {% endif %}{{ comp|lowercase }}: {{ comp }}{% endmap %} 26 | {% set CompsNamedParams %}{{compsNamedParams|join: ", "}}{% endset %} 27 | {% map components into compsNamedRParams using comp %}_ {{ comp|lowercase }}: R.{{ comp }}{% endmap %} 28 | {% set CompsNamedRParams %}{{compsNamedRParams|join: ", "}}{% endset %} 29 | 30 | // MARK: - Family {{ idx }} 31 | 32 | public typealias Family{{ idx }}<{{ CompParams }}> = Family> where {{ CompsWhere }} 33 | 34 | public protocol RequiringComponents{{ idx }}: FamilyRequirementsManaging where Components == ({{ CompParams }}) { 35 | {% for comp in components %} 36 | associatedtype {{ comp }}: Component 37 | {% endfor %} 38 | } 39 | 40 | public struct Requires{{ idx }}<{{ CompParams }}>: FamilyRequirementsManaging where {{ CompsWhere }} { 41 | public let componentTypes: [Component.Type] 42 | 43 | public init(_ components: ({{ CompsTypes }})) { 44 | componentTypes = [{{ CompsSelf}}] 45 | } 46 | 47 | public static func components(nexus: Nexus, entityId: EntityIdentifier) -> ({{ CompParams }}) { 48 | {% for comp in components %} 49 | let {{ comp|lowercase }}: {{ comp }} = nexus.get(unsafe: entityId) 50 | {% endfor %} 51 | return ({{ CompsLowercased }}) 52 | } 53 | 54 | public static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> (Entity, {{ CompParams }}) { 55 | let entity: Entity = Entity(nexus: nexus, id: entityId) 56 | {% for comp in components %} 57 | let {{ comp|lowercase }}: {{ comp }} = nexus.get(unsafe: entityId) 58 | {% endfor %} 59 | return (entity, {{ CompsLowercased }}) 60 | } 61 | 62 | public static func createMember(nexus: Nexus, components: ({{ CompParams }})) -> Entity { 63 | {% if compEncodable.count == 1 %}nexus.createEntity(with: components){% else %}nexus.createEntity(with: {{ CompsTuple }}){% endif %} 64 | } 65 | } 66 | 67 | extension Requires{{ idx }}: RequiringComponents{{ idx }} { } 68 | 69 | extension FamilyMemberBuilder where R: RequiringComponents{{ idx }} { 70 | public static func buildBlock({{ CompsNamedRParams }}) -> (R.Components) { 71 | return ({{ CompsLowercased }}) 72 | } 73 | } 74 | 75 | extension Requires{{ idx }}: FamilyEncoding where {{ CompsWhereEncodable }} { 76 | public static func encode(components: ({{ CompParams }}), into container: inout KeyedEncodingContainer, using strategy: CodingStrategy) throws { 77 | {% if compEncodable.count == 1 %} 78 | try container.encode(components, forKey: strategy.codingKey(for: {{ CompsSelf }})) 79 | {% else %} 80 | {% for comp in compSelf %} 81 | try container.encode(components.{{ forloop.counter0 }}, forKey: strategy.codingKey(for: {{ comp }})) 82 | {% endfor %} 83 | {% endif %} 84 | } 85 | } 86 | 87 | extension Requires{{ idx }}: FamilyDecoding where {{ CompsWhereDecodable }} { 88 | public static func decode(componentsIn container: KeyedDecodingContainer, using strategy: CodingStrategy) throws -> ({{ CompParams }}) { 89 | {% for comp in components %} 90 | let {{ comp|lowercase }} = try container.decode({{ comp }}.self, forKey: strategy.codingKey(for: {{ comp }}.self)) 91 | {% endfor %} 92 | {% if compEncodable.count == 1 %} 93 | return {{ CompsLowercased }} 94 | {% else %} 95 | return Components({{ CompsLowercased }}) 96 | {% endif %} 97 | } 98 | } 99 | 100 | extension Nexus { 101 | /// Create a family of entities (aka members) having {{ components.count }} required components. 102 | /// 103 | /// A family is a collection of entities with uniform component types per entity. 104 | /// Entities that are be part of this family will have at least the {{ components.count }} required components, 105 | /// but may have more components assigned. 106 | /// 107 | /// A family is just a view on (component) data, creating them is cheap. 108 | /// Use them to iterate efficiently over entities with the same components assigned. 109 | /// Families with the same requirements provide a view on the same collection of entities (aka members). 110 | /// A family conforms to the `LazySequenceProtocol` and therefore can be accessed like any other (lazy) sequence. 111 | /// 112 | /// **General usage** 113 | /// ```swift 114 | /// let family = nexus.family({% if components.count == 1 %}requires{% else %}requiresAll{%endif%}: {{ CompsSelf }}) 115 | /// // iterate each entity's components 116 | /// family.forEach { ({{ CompsLowercased }}) in 117 | /// ... 118 | /// } 119 | /// ``` 120 | /// **Caveats** 121 | /// - Component types must be unique per family 122 | /// - Component type order is arbitrary 123 | /// 124 | /// - Parameters: 125 | {% for comp in compsLowercased %} 126 | /// - {{ comp }}: Component type {{ forloop.counter }} required by members of this family. 127 | {% endfor %} 128 | /// - excludedComponents: All component types that must not be assigned to an entity in this family. 129 | /// - Returns: The family of entities having {{ components.count }} required components each. 130 | public func family<{{ CompParams }}>( 131 | {% if components.count == 1 %}requires{% else %}requiresAll{%endif%} {{ CompsTypeParams }}, 132 | excludesAll excludedComponents: Component.Type... 133 | ) -> Family{{ idx }}<{{ CompParams }}> where {{ CompsWhere }} { 134 | Family{{ idx }}<{{ CompParams }}>( 135 | nexus: self, 136 | requiresAll: ({{ CompsLowercased }}), 137 | excludesAll: excludedComponents 138 | ) 139 | } 140 | } 141 | {% endfor %} 142 | -------------------------------------------------------------------------------- /Sources/FirebladeECS/UnorderedSparseSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnorderedSparseSet.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 30.10.17. 6 | // 7 | 8 | /// An unordered sparse set. 9 | /// 10 | /// - `Element`: the element (instance) to store. 11 | /// - `Key`: the unique, hashable datastructure to use as a key to retrieve 12 | /// an element from the sparse set. 13 | /// 14 | /// See for a reference implementation. 15 | public struct UnorderedSparseSet { 16 | // swiftlint:disable nesting 17 | @usableFromInline 18 | final class Storage { 19 | /// An index into the dense store. 20 | @usableFromInline 21 | typealias DenseIndex = Int 22 | 23 | /// A sparse store holding indices into the dense mapped to key. 24 | @usableFromInline 25 | typealias SparseStore = [Key: DenseIndex] 26 | 27 | /// A dense store holding all the entries. 28 | @usableFromInline 29 | typealias DenseStore = ContiguousArray 30 | 31 | @usableFromInline 32 | struct Entry { 33 | @usableFromInline let key: Key 34 | @usableFromInline let element: Element 35 | 36 | @usableFromInline 37 | init(key: Key, element: Element) { 38 | self.key = key 39 | self.element = element 40 | } 41 | } 42 | 43 | @usableFromInline var dense: DenseStore 44 | @usableFromInline var sparse: SparseStore 45 | 46 | @usableFromInline 47 | init(sparse: SparseStore, dense: DenseStore) { 48 | self.sparse = sparse 49 | self.dense = dense 50 | } 51 | 52 | @usableFromInline 53 | convenience init() { 54 | self.init(sparse: [:], dense: []) 55 | } 56 | 57 | @usableFromInline var count: Int { dense.count } 58 | @usableFromInline var isEmpty: Bool { dense.isEmpty } 59 | 60 | @inlinable var first: Element? { 61 | dense.first?.element 62 | } 63 | 64 | @inlinable 65 | func findIndex(at key: Key) -> Int? { 66 | guard let denseIndex = sparse[key], denseIndex < count else { 67 | return nil 68 | } 69 | return denseIndex 70 | } 71 | 72 | @inlinable 73 | func findElement(at key: Key) -> Element? { 74 | guard let denseIndex = findIndex(at: key) else { 75 | return nil 76 | } 77 | let entry = dense[denseIndex] 78 | assert(entry.key == key, "entry.key and findIndex(at: key) must be equal!") 79 | return entry.element 80 | } 81 | 82 | @inlinable 83 | func insert(_ element: Element, at key: Key) -> Bool { 84 | if let denseIndex = findIndex(at: key) { 85 | dense[denseIndex] = Entry(key: key, element: element) 86 | return false 87 | } 88 | 89 | let nIndex = dense.count 90 | dense.append(Entry(key: key, element: element)) 91 | sparse.updateValue(nIndex, forKey: key) 92 | return true 93 | } 94 | 95 | @inlinable 96 | func remove(at key: Key) -> Entry? { 97 | guard let denseIndex = findIndex(at: key) else { 98 | return nil 99 | } 100 | 101 | let removed = swapRemove(at: denseIndex) 102 | if !dense.isEmpty, denseIndex < dense.count { 103 | let swappedElement = dense[denseIndex] 104 | sparse[swappedElement.key] = denseIndex 105 | } 106 | sparse[key] = nil 107 | return removed 108 | } 109 | 110 | /// Removes an element from the set and returns it in O(1). 111 | /// The removed element is replaced with the last element of the set. 112 | /// 113 | /// - Parameter denseIndex: the dense index 114 | /// - Returns: the element entry 115 | @inlinable 116 | func swapRemove(at denseIndex: Int) -> Entry { 117 | dense.swapAt(denseIndex, dense.count - 1) 118 | return dense.removeLast() 119 | } 120 | 121 | @inlinable 122 | func removeAll(keepingCapacity: Bool = false) { 123 | sparse.removeAll(keepingCapacity: keepingCapacity) 124 | dense.removeAll(keepingCapacity: keepingCapacity) 125 | } 126 | 127 | @inlinable 128 | func makeIterator() -> IndexingIterator> { 129 | dense.makeIterator() 130 | } 131 | } 132 | 133 | /// Creates a new sparse set. 134 | public init() { 135 | self.init(storage: Storage()) 136 | } 137 | 138 | @usableFromInline 139 | init(storage: Storage) { 140 | self.storage = storage 141 | } 142 | 143 | @usableFromInline let storage: Storage 144 | 145 | /// The size of the set. 146 | public var count: Int { storage.count } 147 | /// A Boolean value that indicates whether the set is empty. 148 | public var isEmpty: Bool { storage.isEmpty } 149 | 150 | /// Returns a Boolean value that indicates whether the key is included in the set. 151 | /// - Parameter key: The key to inspect. 152 | @inlinable 153 | public func contains(_ key: Key) -> Bool { 154 | storage.findIndex(at: key) != nil 155 | } 156 | 157 | /// Inset an element for a given key into the set in O(1). 158 | /// 159 | /// Elements at previously set keys will be replaced. 160 | /// 161 | /// - Parameters: 162 | /// - element: The element. 163 | /// - key: The key. 164 | /// - Returns: `true` if new, `false` if replaced. 165 | @discardableResult 166 | public func insert(_ element: Element, at key: Key) -> Bool { 167 | storage.insert(element, at: key) 168 | } 169 | 170 | /// Get the element for the given key in O(1). 171 | /// 172 | /// - Parameter key: The key. 173 | /// - Returns: the element or `nil` if the key wasn't found. 174 | @inlinable 175 | public func get(at key: Key) -> Element? { 176 | storage.findElement(at: key) 177 | } 178 | 179 | /// Unsafely gets the element for the given key, 180 | /// - Parameter key: The key. 181 | /// - Returns: The element. 182 | @inlinable 183 | public func get(unsafeAt key: Key) -> Element { 184 | storage.findElement(at: key).unsafelyUnwrapped 185 | } 186 | 187 | /// Removes the element entry for given key in O(1). 188 | /// 189 | /// - Parameter key: the key 190 | /// - Returns: removed value or nil if key not found. 191 | @discardableResult 192 | public func remove(at key: Key) -> Element? { 193 | storage.remove(at: key)?.element 194 | } 195 | 196 | /// Removes all keys and elements from the set. 197 | /// - Parameter keepingCapacity: A Boolean value that indicates whether the set should maintain it's capacity. 198 | @inlinable 199 | public func removeAll(keepingCapacity: Bool = false) { 200 | storage.removeAll(keepingCapacity: keepingCapacity) 201 | } 202 | 203 | /// The first element of the set. 204 | @inlinable public var first: Element? { 205 | storage.first 206 | } 207 | } 208 | 209 | extension UnorderedSparseSet where Key == Int { 210 | /// Retrieve or set an element using the key. 211 | @inlinable 212 | public subscript(key: Key) -> Element { 213 | get { 214 | get(unsafeAt: key) 215 | } 216 | 217 | nonmutating set(newValue) { 218 | insert(newValue, at: key) 219 | } 220 | } 221 | } 222 | 223 | // MARK: - Sequence 224 | 225 | extension UnorderedSparseSet: Sequence { 226 | public func makeIterator() -> ElementIterator { 227 | ElementIterator(self) 228 | } 229 | 230 | // MARK: - UnorderedSparseSetIterator 231 | 232 | public struct ElementIterator: IteratorProtocol { 233 | var iterator: IndexingIterator> 234 | 235 | public init(_ sparseSet: UnorderedSparseSet) { 236 | iterator = sparseSet.storage.makeIterator() 237 | } 238 | 239 | public mutating func next() -> Element? { 240 | iterator.next()?.element 241 | } 242 | } 243 | } 244 | 245 | extension UnorderedSparseSet.ElementIterator: LazySequenceProtocol {} 246 | extension UnorderedSparseSet.ElementIterator: Sequence {} 247 | 248 | // MARK: - Equatable 249 | 250 | extension UnorderedSparseSet.Storage.Entry: Equatable where Element: Equatable {} 251 | extension UnorderedSparseSet.Storage: Equatable where Element: Equatable { 252 | @usableFromInline 253 | static func == (lhs: UnorderedSparseSet.Storage, rhs: UnorderedSparseSet.Storage) -> Bool { 254 | lhs.dense == rhs.dense && lhs.sparse == rhs.sparse 255 | } 256 | } 257 | 258 | extension UnorderedSparseSet: Equatable where Element: Equatable { 259 | public static func == (lhs: UnorderedSparseSet, rhs: UnorderedSparseSet) -> Bool { 260 | lhs.storage == rhs.storage 261 | } 262 | } 263 | 264 | // MARK: - Codable 265 | 266 | extension UnorderedSparseSet.Storage.Entry: Codable where Element: Codable {} 267 | extension UnorderedSparseSet.Storage: Codable where Element: Codable {} 268 | extension UnorderedSparseSet: Codable where Element: Codable {} 269 | -------------------------------------------------------------------------------- /Tests/FirebladeECSPerformanceTests/Base.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | import FirebladeECS 9 | 10 | class EmptyComponent: Component { 11 | } 12 | 13 | class Name: Component { 14 | var name: String 15 | init(name: String) { 16 | self.name = name 17 | } 18 | } 19 | 20 | class Position: Component { 21 | var x: Int 22 | var y: Int 23 | init(x: Int, y: Int) { 24 | self.x = x 25 | self.y = y 26 | } 27 | } 28 | 29 | class Velocity: Component { 30 | var a: Float 31 | init(a: Float) { 32 | self.a = a 33 | } 34 | } 35 | 36 | class Party: Component { 37 | var partying: Bool 38 | init(partying: Bool) { 39 | self.partying = partying 40 | } 41 | } 42 | 43 | class Color: Component { 44 | var r: UInt8 = 0 45 | var g: UInt8 = 0 46 | var b: UInt8 = 0 47 | } 48 | 49 | class ExampleSystem { 50 | private let family: Family2 51 | 52 | init(nexus: Nexus) { 53 | family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) 54 | } 55 | 56 | func update(deltaT: Double) { 57 | family.forEach { (position: Position, velocity: Velocity) in 58 | position.x *= 2 59 | velocity.a *= 2 60 | } 61 | } 62 | } 63 | 64 | final class SingleGameState: SingleComponent { 65 | var shouldQuit: Bool = false 66 | var playerHealth: Int = 67 67 | } 68 | -------------------------------------------------------------------------------- /Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentIdentifierTests.swift 3 | // FirebladeECSPerformanceTests 4 | // 5 | // Created by Christian Treffs on 14.02.19. 6 | // 7 | 8 | import FirebladeECS 9 | import XCTest 10 | 11 | class ComponentIdentifierTests: XCTestCase { 12 | /// release: 0.034 sec 13 | /// debug: 0.456 sec 14 | func testMeasureStaticComponentIdentifier() { 15 | let number: Int = 1_000_000 16 | measure { 17 | for _ in 0..([14_561_291, 26_451_562, 34_562_182, 488_972_556, 5_128_426_962, 68_211_812]) 17 | let b = Set([1_083_838, 912_312, 83_333, 71_234_555, 4_343_234]) 18 | let c = Set([3_410_346_899_765, 90_000_002, 12_212_321, 71, 6_123_345_676_543]) 19 | 20 | let input: ContiguousArray = ContiguousArray(arrayLiteral: a.hashValue, b.hashValue, c.hashValue) 21 | measure { 22 | for _ in 0..<1_000_000 { 23 | let hashRes: Int = FirebladeECS.hash(combine: input) 24 | _ = hashRes 25 | } 26 | } 27 | } 28 | 29 | /// release: 0.494 sec 30 | /// debug: 1.026 sec 31 | func testMeasureSetOfSetHash() { 32 | let a = Set([14_561_291, 26_451_562, 34_562_182, 488_972_556, 5_128_426_962, 68_211_812]) 33 | let b = Set([1_083_838, 912_312, 83_333, 71_234_555, 4_343_234]) 34 | let c = Set([3_410_346_899_765, 90_000_002, 12_212_321, 71, 6_123_345_676_543]) 35 | 36 | let input = Set>(arrayLiteral: a, b, c) 37 | measure { 38 | for _ in 0..<1_000_000 { 39 | let hash: Int = input.hashValue 40 | _ = hash 41 | } 42 | } 43 | } 44 | 45 | /// release: 0.098 sec 46 | /// debug: 16.702 sec 47 | func testMeasureBernsteinDjb2() throws { 48 | #if !DEBUG 49 | let string = "The quick brown fox jumps over the lazy dog" 50 | measure { 51 | for _ in 0..<1_000_000 { 52 | let hash = StringHashing.bernstein_djb2(string) 53 | _ = hash 54 | } 55 | } 56 | #endif 57 | } 58 | 59 | /// release: 0.087 sec 60 | /// debug: 2.613 sec 61 | func testMeasureSingerDjb2() throws { 62 | let string = "The quick brown fox jumps over the lazy dog" 63 | measure { 64 | for _ in 0..<1_000_000 { 65 | let hash = StringHashing.singer_djb2(string) 66 | _ = hash 67 | } 68 | } 69 | } 70 | 71 | /// release: 0.088 sec 72 | /// debug: 30.766 sec 73 | func testMeasureSDBM() throws { 74 | #if !DEBUG 75 | let string = "The quick brown fox jumps over the lazy dog" 76 | measure { 77 | for _ in 0..<1_000_000 { 78 | let hash = StringHashing.sdbm(string) 79 | _ = hash 80 | } 81 | } 82 | #endif 83 | } 84 | 85 | /// release: 0.036 sec 86 | /// debug: 0.546 sec 87 | func testMeasureSwiftHasher() throws { 88 | #if !DEBUG 89 | let string = "The quick brown fox jumps over the lazy dog" 90 | measure { 91 | for _ in 0..<1_000_000 { 92 | var hasher = Hasher() 93 | hasher.combine(string) 94 | let hash = hasher.finalize() 95 | _ = hash 96 | } 97 | } 98 | #endif 99 | } 100 | } 101 | #else 102 | #warning("Skipping HashingPerformanceTests") 103 | #endif 104 | -------------------------------------------------------------------------------- /Tests/FirebladeECSPerformanceTests/TypeIdentifierPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeIdentifierPerformanceTests.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 05.10.19. 6 | // 7 | 8 | #if os(macOS) 9 | import FirebladeECS 10 | import XCTest 11 | 12 | final class TypeIdentifierPerformanceTests: XCTestCase { 13 | let maxIterations: Int = 100_000 14 | 15 | // release: 0.000 sec 16 | // debug: 0.051 sec 17 | func testPerformanceObjectIdentifier() { 18 | measure { 19 | for _ in 0.. Bool { 30 | lhs.int == rhs.int && 31 | lhs.float == rhs.float && 32 | lhs.string == rhs.string 33 | } 34 | } 35 | 36 | final class Name: Component, DefaultInitializable { 37 | var name: String 38 | init(name: String) { 39 | self.name = name 40 | } 41 | 42 | convenience init() { 43 | self.init(name: "") 44 | } 45 | } 46 | 47 | final class Position: Component, DefaultInitializable { 48 | var x: Int 49 | var y: Int 50 | 51 | convenience init() { 52 | self.init(x: 0, y: 0) 53 | } 54 | 55 | init(x: Int, y: Int) { 56 | self.x = x 57 | self.y = y 58 | } 59 | } 60 | extension Position: Codable { } 61 | 62 | final class Velocity: Component, DefaultInitializable { 63 | var a: Float 64 | 65 | init(a: Float) { 66 | self.a = a 67 | } 68 | 69 | convenience init() { 70 | self.init(a: 0) 71 | } 72 | } 73 | 74 | final class Party: Component { 75 | var partying: Bool 76 | 77 | init(partying: Bool) { 78 | self.partying = partying 79 | } 80 | } 81 | extension Party: Codable { } 82 | 83 | final class Color: Component { 84 | var r: UInt8 85 | var g: UInt8 86 | var b: UInt8 87 | 88 | init(r: UInt8 = 0, g: UInt8 = 0, b: UInt8 = 0) { 89 | self.r = r 90 | self.g = g 91 | self.b = b 92 | } 93 | } 94 | extension Color: Codable { } 95 | 96 | class Index: Component { 97 | var index: Int 98 | 99 | init(index: Int) { 100 | self.index = index 101 | } 102 | } 103 | 104 | final class MyComponent: Component { 105 | var name: String 106 | var flag: Bool 107 | 108 | init(name: String, flag: Bool) { 109 | self.name = name 110 | self.flag = flag 111 | } 112 | } 113 | extension MyComponent: Decodable { } 114 | extension MyComponent: Encodable { } 115 | 116 | final class YourComponent: Component { 117 | var number: Float 118 | 119 | init(number: Float) { 120 | self.number = number 121 | } 122 | } 123 | extension YourComponent: Decodable { } 124 | extension YourComponent: Encodable { } 125 | 126 | final class SingleGameState: SingleComponent { 127 | var shouldQuit: Bool = false 128 | var playerHealth: Int = 67 129 | } 130 | 131 | class ExampleSystem { 132 | private let family: Family2 133 | 134 | init(nexus: Nexus) { 135 | family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) 136 | } 137 | 138 | func update(deltaT: Double) { 139 | family.forEach { (position: Position, velocity: Velocity) in 140 | position.x *= 2 141 | velocity.a *= 2 142 | } 143 | } 144 | } 145 | 146 | class ColorSystem { 147 | let nexus: Nexus 148 | lazy var colors = nexus.family(requires: Color.self) 149 | 150 | init(nexus: Nexus) { 151 | self.nexus = nexus 152 | } 153 | 154 | func update() { 155 | colors 156 | .forEach { (color: Color) in 157 | color.r = 1 158 | color.g = 2 159 | color.b = 3 160 | } 161 | } 162 | } 163 | 164 | class PositionSystem { 165 | let positions: Family1 166 | 167 | var velocity: Double = 4.0 168 | 169 | init(nexus: Nexus) { 170 | positions = nexus.family(requires: Position.self) 171 | } 172 | 173 | func randNorm() -> Double { 174 | 4.0 175 | } 176 | 177 | func update() { 178 | positions 179 | .forEach { [unowned self](pos: Position) in 180 | let deltaX: Double = self.velocity * ((self.randNorm() * 2) - 1) 181 | let deltaY: Double = self.velocity * ((self.randNorm() * 2) - 1) 182 | let x = pos.x + Int(deltaX) 183 | let y = pos.y + Int(deltaY) 184 | 185 | pos.x = x 186 | pos.y = y 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/ComponentIdentifierTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentIdentifierTests.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 05.10.19. 6 | // 7 | import XCTest 8 | 9 | final class ComponentIdentifierTests: XCTestCase { 10 | func testMirrorAsStableIdentifier() { 11 | let m = String(reflecting: Position.self) 12 | let identifier: String = m 13 | XCTAssertEqual(identifier, "FirebladeECSTests.Position") 14 | } 15 | 16 | func testStringDescribingAsStableIdentifier() { 17 | let s = String(describing: Position.self) 18 | let identifier: String = s 19 | XCTAssertEqual(identifier, "Position") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/ComponentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentTests.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 21.10.17. 6 | // 7 | 8 | import FirebladeECS 9 | import XCTest 10 | 11 | class ComponentTests: XCTestCase { 12 | func testComponentIdentifier() { 13 | XCTAssertEqual(Position.identifier, Position.identifier) 14 | XCTAssertEqual(Velocity.identifier, Velocity.identifier) 15 | XCTAssertNotEqual(Velocity.identifier, Position.identifier) 16 | 17 | let p1 = Position(x: 1, y: 2) 18 | let v1 = Velocity(a: 3.14) 19 | XCTAssertEqual(p1.identifier, Position.identifier) 20 | XCTAssertEqual(v1.identifier, Velocity.identifier) 21 | XCTAssertNotEqual(v1.identifier, p1.identifier) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/EntityCreationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityCreationTests.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 30.07.20. 6 | // 7 | 8 | import FirebladeECS 9 | import XCTest 10 | 11 | final class EntityCreationTests: XCTestCase { 12 | func testCreateEntityOneComponent() throws { 13 | let nexus = Nexus() 14 | let entity = nexus.createEntity { 15 | Position(x: 1, y: 2) 16 | } 17 | 18 | XCTAssertEqual(entity[\Position.x], 1) 19 | XCTAssertEqual(entity[\Position.y], 2) 20 | 21 | XCTAssertEqual(nexus.numEntities, 1) 22 | XCTAssertEqual(nexus.numComponents, 1) 23 | XCTAssertEqual(nexus.numFamilies, 0) 24 | } 25 | 26 | func testCreateEntityMultipleComponents() throws { 27 | let nexus = Nexus() 28 | 29 | let entity = nexus.createEntity { 30 | Position(x: 1, y: 2) 31 | Name(name: "Hello") 32 | } 33 | 34 | XCTAssertEqual(entity[\Position.x], 1) 35 | XCTAssertEqual(entity[\Position.y], 2) 36 | 37 | XCTAssertEqual(entity[\Name.name], "Hello") 38 | 39 | XCTAssertEqual(nexus.numEntities, 1) 40 | XCTAssertEqual(nexus.numComponents, 2) 41 | XCTAssertEqual(nexus.numFamilies, 0) 42 | } 43 | 44 | func testBulkCreateEntitiesOneComponent() throws { 45 | let nexus = Nexus() 46 | 47 | let entities = nexus.createEntities(count: 100) { ctx in 48 | Velocity(a: Float(ctx.index)) 49 | } 50 | 51 | XCTAssertEqual(entities[0][\Velocity.a], 0) 52 | XCTAssertEqual(entities[99][\Velocity.a], 99) 53 | 54 | XCTAssertEqual(nexus.numEntities, 100) 55 | XCTAssertEqual(nexus.numComponents, 100) 56 | XCTAssertEqual(nexus.numFamilies, 0) 57 | } 58 | 59 | func testBulkCreateEntitiesMultipleComponents() throws { 60 | let nexus = Nexus() 61 | 62 | let entities = nexus.createEntities(count: 100) { ctx in 63 | Position(x: ctx.index, y: ctx.index) 64 | Name(name: "\(ctx.index)") 65 | } 66 | 67 | XCTAssertEqual(entities[0][\Position.x], 0) 68 | XCTAssertEqual(entities[0][\Position.y], 0) 69 | XCTAssertEqual(entities[0][\Name.name], "0") 70 | XCTAssertEqual(entities[99][\Position.x], 99) 71 | XCTAssertEqual(entities[99][\Position.y], 99) 72 | XCTAssertEqual(entities[99][\Name.name], "99") 73 | 74 | XCTAssertEqual(nexus.numEntities, 100) 75 | XCTAssertEqual(nexus.numComponents, 200) 76 | XCTAssertEqual(nexus.numFamilies, 0) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/EntityIdGenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityIdGenTests.swift 3 | // 4 | // 5 | // Created by Christian Treffs on 21.08.20. 6 | // 7 | 8 | import FirebladeECS 9 | import XCTest 10 | 11 | final class EntityIdGenTests: XCTestCase { 12 | var gen: EntityIdentifierGenerator! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | gen = DefaultEntityIdGenerator() 17 | } 18 | 19 | func testGeneratorDefaultInit() { 20 | XCTAssertEqual(gen.nextId(), 0) 21 | } 22 | 23 | func testGeneratorWithDefaultEmptyCollection() { 24 | gen = DefaultEntityIdGenerator(startProviding: []) 25 | XCTAssertEqual(gen.nextId(), 0) 26 | XCTAssertEqual(gen.nextId(), 1) 27 | } 28 | 29 | func testLinearIncrement() { 30 | for i in 0..<1_000_000 { 31 | XCTAssertEqual(gen.nextId(), EntityIdentifier(EntityIdentifier.Identifier(i))) 32 | } 33 | } 34 | 35 | func testGenerateWithInitialIds() { 36 | let initialIds: [EntityIdentifier] = [2, 4, 11, 3, 0, 304] 37 | gen = DefaultEntityIdGenerator(startProviding: initialIds) 38 | 39 | let generatedIds: [EntityIdentifier] = (0..(_ other: OtherSequence, by areEquivalent: (Element, OtherSequence.Element) throws -> Bool) rethrows -> Bool where OtherSequence: Sequence { 175 | for element in self { 176 | if try !other.contains(where: { try areEquivalent(element, $0) }) { 177 | return false 178 | } 179 | } 180 | return true 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/FamilyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamilyTests.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | @testable import FirebladeECS 9 | import XCTest 10 | 11 | class FamilyTests: XCTestCase { 12 | var nexus: Nexus! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | nexus = Nexus() 17 | } 18 | 19 | override func tearDown() { 20 | nexus = nil 21 | super.tearDown() 22 | } 23 | 24 | func createDefaultEntity() { 25 | let e = nexus.createEntity() 26 | e.assign(Position(x: 1, y: 2)) 27 | e.assign(Color()) 28 | } 29 | 30 | func testFamilyCreation() { 31 | let family = nexus.family(requires: Position.self, 32 | excludesAll: Name.self) 33 | 34 | XCTAssertTrue(family.nexus === self.nexus) 35 | XCTAssertEqual(nexus.numFamilies, 1) 36 | XCTAssertEqual(nexus.numComponents, 0) 37 | XCTAssertEqual(nexus.numEntities, 0) 38 | XCTAssertFalse(family.traits.description.isEmpty) 39 | XCTAssertFalse(family.traits.debugDescription.isEmpty) 40 | 41 | let traits = FamilyTraitSet(requiresAll: [Position.self], excludesAll: [Name.self]) 42 | XCTAssertEqual(family.traits, traits) 43 | } 44 | 45 | func testFamilyReuse() { 46 | let familyA = nexus.family(requires: Position.self, 47 | excludesAll: Name.self) 48 | 49 | let familyB = nexus.family(requires: Position.self, 50 | excludesAll: Name.self) 51 | 52 | XCTAssertEqual(nexus.numFamilies, 1) 53 | XCTAssertEqual(nexus.numComponents, 0) 54 | 55 | XCTAssertEqual(familyA, familyB) 56 | } 57 | 58 | func testFamilyAbandoned() { 59 | XCTAssertEqual(nexus.numFamilies, 0) 60 | XCTAssertEqual(nexus.numComponents, 0) 61 | XCTAssertEqual(nexus.numEntities, 0) 62 | _ = nexus.family(requires: Position.self) 63 | XCTAssertEqual(nexus.numFamilies, 1) 64 | XCTAssertEqual(nexus.numComponents, 0) 65 | XCTAssertEqual(nexus.numEntities, 0) 66 | let entity = nexus.createEntity() 67 | XCTAssertFalse(entity.has(Position.self)) 68 | XCTAssertEqual(nexus.numFamilies, 1) 69 | XCTAssertEqual(nexus.numComponents, 0) 70 | XCTAssertEqual(nexus.numEntities, 1) 71 | entity.assign(Position(x: 1, y: 1)) 72 | XCTAssertTrue(entity.has(Position.self)) 73 | XCTAssertEqual(nexus.numFamilies, 1) 74 | XCTAssertEqual(nexus.numComponents, 1) 75 | XCTAssertEqual(nexus.numEntities, 1) 76 | entity.remove(Position.self) 77 | XCTAssertEqual(nexus.numFamilies, 1) 78 | XCTAssertEqual(nexus.numComponents, 0) 79 | XCTAssertEqual(nexus.numEntities, 1) 80 | nexus.destroy(entity: entity) 81 | XCTAssertEqual(nexus.numFamilies, 1) 82 | XCTAssertEqual(nexus.numComponents, 0) 83 | XCTAssertEqual(nexus.numEntities, 0) 84 | } 85 | 86 | func testFamilyLateMember() { 87 | let eEarly = nexus.createEntity(with: Position(x: 1, y: 2)) 88 | XCTAssertEqual(nexus.numFamilies, 0) 89 | XCTAssertEqual(nexus.numComponents, 1) 90 | XCTAssertEqual(nexus.numEntities, 1) 91 | let family = nexus.family(requires: Position.self) 92 | XCTAssertEqual(nexus.numFamilies, 1) 93 | XCTAssertEqual(nexus.numComponents, 1) 94 | XCTAssertEqual(nexus.numEntities, 1) 95 | let eLate = nexus.createEntity(with: Position(x: 1, y: 2)) 96 | XCTAssertEqual(nexus.numFamilies, 1) 97 | XCTAssertEqual(nexus.numComponents, 2) 98 | XCTAssertEqual(nexus.numEntities, 2) 99 | XCTAssertTrue(family.isMember(eEarly)) 100 | XCTAssertTrue(family.isMember(eLate)) 101 | } 102 | 103 | func testFamilyExchange() { 104 | let number: Int = 10 105 | 106 | for i in 0.. Int { 13 | let upperBound: Int = 44 14 | let range = UInt32.min...UInt32.max 15 | let high = UInt(UInt32.random(in: range)) << UInt(upperBound) 16 | let low = UInt(UInt32.random(in: range)) 17 | XCTAssertTrue(high.leadingZeroBitCount < 64 - upperBound) 18 | XCTAssertTrue(high.trailingZeroBitCount >= upperBound) 19 | XCTAssertTrue(low.leadingZeroBitCount >= 32) 20 | XCTAssertTrue(low.trailingZeroBitCount <= 32) 21 | let rand: UInt = high | low 22 | let cH = Int(bitPattern: rand) 23 | return cH 24 | } 25 | 26 | func testCollisionsInCritialRange() { 27 | var hashSet: Set = Set() 28 | 29 | var range: CountableRange = 0 ..< 1_000_000 30 | 31 | let maxComponents: Int = 1000 32 | let components: [Int] = (0.. () 217 | var onNonFatal: (String) -> () 218 | 219 | init(onEvent: @escaping (NexusEvent) -> Void = { _ in }, 220 | onNonFatal: @escaping (String) -> Void = { _ in }) { 221 | self.onEvent = onEvent 222 | self.onNonFatal = onNonFatal 223 | } 224 | 225 | func nexusEvent(_ event: NexusEvent) { 226 | onEvent(event) 227 | } 228 | 229 | func nexusNonFatalError(_ message: String) { 230 | onNonFatal(message) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/NexusTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NexusTests.swift 3 | // FirebladeECS 4 | // 5 | // Created by Christian Treffs on 09.10.17. 6 | // 7 | 8 | @testable import FirebladeECS 9 | import XCTest 10 | 11 | class NexusTests: XCTestCase { 12 | var nexus: Nexus! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | nexus = Nexus() 17 | } 18 | 19 | override func tearDown() { 20 | nexus = nil 21 | super.tearDown() 22 | } 23 | 24 | func testEntityCreate() { 25 | XCTAssertEqual(nexus.numEntities, 0) 26 | 27 | let e0 = nexus.createEntity() 28 | 29 | XCTAssertEqual(e0.identifier.id, 0) 30 | XCTAssertEqual(nexus.numEntities, 1) 31 | 32 | let e1 = nexus.createEntity(with: Name(name: "Entity 1")) 33 | 34 | XCTAssert(e1.identifier.id == 1) 35 | XCTAssert(nexus.numEntities == 2) 36 | XCTAssertFalse(nexus.debugDescription.isEmpty) 37 | } 38 | 39 | func testEntityDestroy() { 40 | testEntityCreate() 41 | XCTAssertEqual(nexus.numEntities, 2) 42 | 43 | let e1 = nexus.entity(from: EntityIdentifier(1)) 44 | XCTAssertTrue(nexus.exists(entity: EntityIdentifier(1))) 45 | XCTAssertEqual(e1.identifier.id, 1) 46 | 47 | XCTAssertTrue(nexus.destroy(entity: e1)) 48 | XCTAssertFalse(nexus.destroy(entity: e1)) 49 | 50 | XCTAssertFalse(nexus.exists(entity: EntityIdentifier(1))) 51 | 52 | XCTAssertEqual(nexus.numEntities, 1) 53 | 54 | XCTAssertEqual(nexus.numEntities, 1) 55 | 56 | nexus.clear() 57 | 58 | XCTAssertEqual(nexus.numEntities, 0) 59 | } 60 | 61 | func testComponentCreation() { 62 | XCTAssert(nexus.numEntities == 0) 63 | 64 | let e0: Entity = nexus.createEntity() 65 | 66 | let p0 = Position(x: 1, y: 2) 67 | 68 | e0.assign(p0) 69 | // component collision: e0.assign(p0) 70 | 71 | XCTAssert(e0.hasComponents) 72 | XCTAssert(e0.numComponents == 1) 73 | 74 | let rP0: Position = e0.get(component: Position.self)! 75 | XCTAssert(rP0.x == 1) 76 | XCTAssert(rP0.y == 2) 77 | } 78 | 79 | func testComponentDeletion() { 80 | let identifier: EntityIdentifier = nexus.createEntity().identifier 81 | 82 | let e0 = nexus.entity(from: identifier) 83 | 84 | XCTAssert(e0.numComponents == 0) 85 | e0.remove(Position.self) 86 | XCTAssert(e0.numComponents == 0) 87 | 88 | let n0 = Name(name: "myName") 89 | let p0 = Position(x: 99, y: 111) 90 | 91 | e0.assign(n0) 92 | XCTAssert(e0.numComponents == 1) 93 | XCTAssert(e0.hasComponents) 94 | 95 | e0.remove(Name.self) 96 | 97 | XCTAssert(e0.numComponents == 0) 98 | XCTAssert(!e0.hasComponents) 99 | 100 | e0.assign(p0) 101 | 102 | XCTAssert(e0.numComponents == 1) 103 | XCTAssert(e0.hasComponents) 104 | 105 | e0.remove(p0) 106 | 107 | XCTAssert(e0.numComponents == 0) 108 | XCTAssert(!e0.hasComponents) 109 | 110 | e0.assign(n0) 111 | e0.assign(p0) 112 | 113 | XCTAssert(e0.numComponents == 2) 114 | let (name, position) = e0.get(components: Name.self, Position.self) 115 | 116 | XCTAssert(name?.name == "myName") 117 | XCTAssert(position?.x == 99) 118 | XCTAssert(position?.y == 111) 119 | 120 | e0.destroy() 121 | 122 | XCTAssert(e0.numComponents == 0) 123 | } 124 | 125 | func testComponentRetrieval() { 126 | let pos = Position(x: 1, y: 2) 127 | let name = Name(name: "myName") 128 | let vel = Velocity(a: 3) 129 | let entity = nexus.createEntity(with: pos, name, vel) 130 | 131 | let (rPos, rName, rVel) = entity.get(components: Position.self, Name.self, Velocity.self) 132 | 133 | XCTAssertTrue(rPos === pos) 134 | XCTAssertTrue(rName === name) 135 | XCTAssertTrue(rVel === vel) 136 | } 137 | 138 | func testComponentUniqueness() { 139 | let a = nexus.createEntity() 140 | let b = nexus.createEntity() 141 | let c = nexus.createEntity() 142 | 143 | XCTAssert(nexus.numEntities == 3) 144 | 145 | a.assign(Position(x: 0, y: 0)) 146 | b.assign(Position(x: 0, y: 0)) 147 | c.assign(Position(x: 0, y: 0)) 148 | 149 | let pA: Position = a.get()! 150 | let pB: Position = b.get()! 151 | 152 | pA.x = 23 153 | pA.y = 32 154 | 155 | XCTAssert(pB.x != pA.x) 156 | XCTAssert(pB.y != pA.y) 157 | } 158 | 159 | func testEntityIteration() { 160 | nexus.createEntities(count: 1000) { ctx in Position(x: ctx.index, y: ctx.index) } 161 | 162 | let entityArray = [Entity](nexus.makeEntitiesIterator()).lazy 163 | 164 | XCTAssertEqual(entityArray.count, 1000) 165 | 166 | XCTAssertTrue(entityArray.contains(where: { $0.identifier.index == 0 })) 167 | XCTAssertTrue(entityArray.contains(where: { $0.identifier.index == 999 })) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/SingleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleTests.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 13.02.19. 6 | // 7 | 8 | @testable import FirebladeECS 9 | import XCTest 10 | 11 | class SingleTests: XCTestCase { 12 | var nexus: Nexus! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | nexus = Nexus() 17 | } 18 | 19 | override func tearDown() { 20 | nexus = nil 21 | super.tearDown() 22 | } 23 | 24 | func testSingleCreation() { 25 | let single = nexus.single(SingleGameState.self) 26 | XCTAssertTrue(single.nexus === self.nexus) 27 | XCTAssertEqual(single.traits.requiresAll.count, 1) 28 | XCTAssertEqual(single.traits.excludesAll.count, 0) 29 | 30 | XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1) 31 | XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1) 32 | 33 | let traits = FamilyTraitSet(requiresAll: [SingleGameState.self], excludesAll: []) 34 | XCTAssertEqual(single.traits, traits) 35 | } 36 | 37 | func testSingleReuse() { 38 | let singleA = nexus.single(SingleGameState.self) 39 | 40 | let singleB = nexus.single(SingleGameState.self) 41 | 42 | XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1) 43 | XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1) 44 | 45 | XCTAssertEqual(singleA, singleB) 46 | } 47 | 48 | func testSingleEntityAndComponentCreation() { 49 | let single = nexus.single(SingleGameState.self) 50 | let gameState = SingleGameState() 51 | XCTAssertNotNil(single.entity) 52 | XCTAssertNotNil(single.component) 53 | XCTAssertEqual(single.component.shouldQuit, gameState.shouldQuit) 54 | XCTAssertEqual(single.component.playerHealth, gameState.playerHealth) 55 | } 56 | 57 | func testSingleCreationOnExistingFamilyMember() { 58 | _ = nexus.createEntity(with: Position(x: 1, y: 2)) 59 | let singleGame = SingleGameState() 60 | _ = nexus.createEntity(with: singleGame) 61 | let single = nexus.single(SingleGameState.self) 62 | XCTAssertTrue(singleGame === single.component) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/FirebladeECSTests/SystemsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemsTests.swift 3 | // FirebladeECSTests 4 | // 5 | // Created by Christian Treffs on 10.05.18. 6 | // 7 | 8 | @testable import FirebladeECS 9 | import XCTest 10 | 11 | class SystemsTests: XCTestCase { 12 | var nexus: Nexus! 13 | var colorSystem: ColorSystem! 14 | var positionSystem: PositionSystem! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | nexus = Nexus() 19 | colorSystem = ColorSystem(nexus: nexus) 20 | positionSystem = PositionSystem(nexus: nexus) 21 | } 22 | 23 | override func tearDown() { 24 | colorSystem = nil 25 | positionSystem = nil 26 | nexus = nil 27 | super.tearDown() 28 | } 29 | 30 | func testSystemsUpdate() { 31 | let num: Int = 10_000 32 | 33 | colorSystem.update() 34 | positionSystem.update() 35 | 36 | let posTraits = positionSystem.positions.traits 37 | 38 | XCTAssertEqual(nexus.numEntities, 0) 39 | XCTAssertEqual(colorSystem.colors.memberIds.count, 0) 40 | XCTAssertEqual(positionSystem.positions.memberIds.count, 0) 41 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, 0) 42 | 43 | batchCreateEntities(count: num) 44 | 45 | XCTAssertEqual(nexus.numEntities, num) 46 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) 47 | XCTAssertEqual(colorSystem.colors.memberIds.count, num) 48 | XCTAssertEqual(positionSystem.positions.memberIds.count, num) 49 | 50 | colorSystem.update() 51 | positionSystem.update() 52 | 53 | XCTAssertEqual(nexus.numEntities, num) 54 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) 55 | XCTAssertEqual(colorSystem.colors.memberIds.count, num) 56 | XCTAssertEqual(positionSystem.positions.memberIds.count, num) 57 | 58 | batchCreateEntities(count: num) 59 | 60 | XCTAssertEqual(nexus.numEntities, num * 2) 61 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) 62 | XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) 63 | XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) 64 | 65 | colorSystem.update() 66 | positionSystem.update() 67 | 68 | XCTAssertEqual(nexus.numEntities, num * 2) 69 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) 70 | XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) 71 | XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) 72 | 73 | batchDestroyEntities(count: num) 74 | 75 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) 76 | XCTAssertEqual(nexus.numEntities, num) 77 | XCTAssertEqual(colorSystem.colors.memberIds.count, num) 78 | XCTAssertEqual(positionSystem.positions.memberIds.count, num) 79 | 80 | colorSystem.update() 81 | positionSystem.update() 82 | 83 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) 84 | XCTAssertEqual(nexus.numEntities, num) 85 | XCTAssertEqual(colorSystem.colors.memberIds.count, num) 86 | XCTAssertEqual(positionSystem.positions.memberIds.count, num) 87 | 88 | batchCreateEntities(count: num) 89 | 90 | XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) 91 | XCTAssertEqual(nexus.numEntities, num * 2) 92 | XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) 93 | XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) 94 | } 95 | 96 | func createDefaultEntity() { 97 | let e = nexus.createEntity() 98 | e.assign(Position(x: 1, y: 2)) 99 | e.assign(Color()) 100 | } 101 | 102 | func batchCreateEntities(count: Int) { 103 | for _ in 0..[0-9.]+)" 73 | ], 74 | "datasourceTemplate": "github-releases", 75 | "depNameTemplate": "swiftlang/swift", 76 | "extractVersionTemplate": "^swift-(?.*)-RELEASE$" 77 | }, 78 | { 79 | "customType": "regex", 80 | "description": "Update .swift-version", 81 | "fileMatch": ".swift-version", 82 | "matchStrings": [ 83 | "(?[0-9.]+)" 84 | ], 85 | "datasourceTemplate": "github-releases", 86 | "depNameTemplate": "swiftlang/swift", 87 | "extractVersionTemplate": "^swift-(?.*)-RELEASE$" 88 | } 89 | ], 90 | "labels": [ 91 | "dependencies", 92 | "renovate" 93 | ] 94 | } --------------------------------------------------------------------------------