Merge branch 'master' into feature/serialization

# Conflicts:
#	Tests/FirebladeECSTests/XCTestManifests.swift
This commit is contained in:
Christian Treffs 2024-10-21 13:48:25 +02:00
commit e02bf4f8b8
No known key found for this signature in database
GPG Key ID: 49A4B4B460BE3ED4
55 changed files with 1030 additions and 390 deletions

View File

@ -1,5 +1,6 @@
ignore:
- "Tests/"
- "Benchmarks/"
comment:
layout: header, changes, diff
layout: header, changes, diff

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: [ctreffs]
custom: ['https://www.paypal.com/donate?hosted_button_id=GCG3K54SKRALQ']

61
.github/ISSUE_TEMPLATE/BUG_REPORT.md vendored Normal file
View File

@ -0,0 +1,61 @@
---
name: 🐛 Bug Report
about: Something isn't working as expected, create a report to help us improve
labels: bug
---
<!--
Thanks for contributing to this project!
Before you submit your issue, please replace each paragraph
below with the relevant details for your bug, and complete
the steps in the checklist by placing an 'x' in each box:
- [x] I've completed this task
- [ ] This task isn't completed
-->
### Bug Description
*A clear and concise description of what the bug is.
Replace this paragraph with a short description of the incorrect behavior.
(If this is a regression, please note the last version of the package that exhibited the correct behavior in addition to your current version.)*
### Information
- **Package version:** What tag or branch of this package are you using? e.g. tag `1.2.3` or branch `main`
- **Platform version:** Please tell us the version number of your operating system. e.g. `macOS 11.2.3` or `Ubuntu 20.04`
- **Swift version:** Paste the output of `swift --version` here.
### Checklist
- [ ] If possible, I've reproduced the issue using the `main`/`master` branch of this package.
- [ ] I've searched for existing issues under the issues tab.
- [ ] The bug is reproducible
### Steps to Reproduce
*Steps to reproduce the behavior:*
1. Go to '...'
2. '....'
*Replace this paragraph with an explanation of how to reproduce the incorrect behavior.
Include a simple code example, if possible.*
### Expected behavior
*A clear and concise description of what you expected to happen.
Describe what you expect to happen.*
### Actual behavior
*Describe or copy/paste the behavior you observe.*
### Screenshots
If applicable, add screenshots to help explain your problem.
### Additional context
*Add any other context about the problem here.*

View File

@ -0,0 +1,31 @@
---
name: 💡 Feature Request
about: A suggestion for a new feature or idea for this project
labels: enhancement
---
<!--
Thanks for contributing to this project!
Before you submit your issue, please replace the paragraphs
below with information about your proposed feature.
-->
### Feature request
*Replace this paragraph with a description of your proposed feature.
A clear and concise description of what the idea or problem is you want to solve.
Please be sure to describe some concrete use cases for the new feature -- be as specific as possible.
Provide links to existing issues or external references/discussions, if appropriate.*
### Describe the solution you'd like
*A clear and concise description of what you want to happen.*
### Describe alternatives you've considered
*A clear and concise description of any alternative solutions or features you've considered.*
### Additional context
*Add any other context or screenshots about the feature request here.*

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/JMM7W6pCCc
about: Questions or comments about using Fireblade? Ask here!

52
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,52 @@
<!--
Thanks for contributing to this project!
Before you submit your request, please replace each paragraph
below with the relevant details, and complete the steps in the
checklist by placing an 'x' in each box:
- [x] I've completed this task
- [ ] This task isn't completed
-->
### Description
*Replace this paragraph with a description of your changes and rationale.
Provide links to an existing issue or external references/discussions, if appropriate.*
### Detailed Design
*Include any additional information about the design here. At minimum, describe a synopsis of any public API additions.*
```swift
/// The new feature implemented by this pull request.
public struct Example: Collection {
}
```
### Documentation
*How has the new feature been documented?
Have the relevant portions of the guides in the Documentation folder been updated in addition to symbol-level documentation?*
### Testing
*How is the new feature tested?
Please ensure CI is not broken*
### Performance
*How did you verify the new feature performs as expected?*
### Source Impact
*What is the impact of this change on existing users of this package? Does it deprecate or remove any existing API?*
### Checklist
- [ ] I've read the [Contribution Guidelines](https://github.com/fireblade-engine/ecs/blob/master/CONTRIBUTING.md)
- [ ] I've followed the coding style of the rest of the project.
- [ ] I've added tests covering all new code paths my change adds to the project (to the extent possible).
- [ ] I've added benchmarks covering new functionality (if appropriate).
- [ ] I've verified that my change does not break any existing tests or introduce unexpected benchmark regressions.
- [ ] I've updated the documentation (if appropriate).

39
.github/workflows/ci-linux.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Linux
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
linux-test-build-release:
runs-on: ubuntu-latest
strategy:
matrix:
swift: ["latest"]
container:
image: swift:${{ matrix.swift }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Test
run: swift test -c release --enable-xctest --parallel --xunit-output .build/xUnit-output.xml
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4.4.3
with:
name: test-artifacts-linux-${{ matrix.swift }}-${{ github.run_id }}
path: |
.build/*.yaml
.build/*.xml
.build/*.json
.build/*.txt
.build/**/*.xctest
.build/**/*.json
.build/**/*.txt
if-no-files-found: warn
include-hidden-files: true

60
.github/workflows/ci-macos.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: macOS
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
CODECOV_XCODE_VERSION: "15.4" # Xcode version used to generate code coverage
jobs:
macos-test-build-release-xcode:
runs-on: macOS-latest
strategy:
matrix:
xcode: ["14.3.1", "15.4", "16.0"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Test
run: swift test -c release --parallel --xunit-output .build/xUnit-output.xml --enable-code-coverage
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4.4.3
with:
name: test-artifacts-${{ matrix.xcode }}-${{ github.run_id }}
path: |
.build/*.yaml
.build/*.xml
.build/*.json
.build/*.txt
.build/**/*.xctest
.build/**/*.json
.build/**/*.txt
if-no-files-found: warn
include-hidden-files: true
# Only run coverage steps if the CODECOV_TOKEN is available and the matrix.xcode matches CODECOV_XCODE_VERSION
- name: Generate coverage report
if: env.CODECOV_TOKEN != '' && matrix.xcode == env.CODECOV_XCODE_VERSION
run: xcrun llvm-cov export -format="lcov" .build/**/*PackageTests.xctest/Contents/MacOS/*PackageTests -instr-profile .build/**/codecov/default.profdata > coverage.lcov
- name: Upload code coverage report
if: env.CODECOV_TOKEN != '' && matrix.xcode == env.CODECOV_XCODE_VERSION
uses: codecov/codecov-action@v4.6.0
with:
token: ${{ env.CODECOV_TOKEN }}
file: coverage.lcov
fail_ci_if_error: true

17
.github/workflows/ci-wasm.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: WASM
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
wasm-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
uses: swiftwasm/swiftwasm-action@v5.9
with:
shell-action: swift build --triple wasm32-unknown-wasi

39
.github/workflows/ci-windows.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Windows
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
windows-test-build-release:
runs-on: windows-latest
steps:
- name: Setup
uses: compnerd/gha-setup-swift@v0.2.3
with:
branch: swift-5.10-release
tag: 5.10-RELEASE
- name: Checkout
uses: actions/checkout@v4
- name: Test
run: swift test -c release --parallel --xunit-output .build/xUnit-output.xml
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4.4.3
with:
name: test-artifacts-windows-${{ github.run_id }}
path: |
.build/*.yaml
.build/*.xml
.build/*.json
.build/*.txt
.build/**/*.xctest
.build/**/*.json
.build/**/*.txt
if-no-files-found: warn
include-hidden-files: true

View File

@ -1,81 +0,0 @@
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
macOS:
runs-on: macOS-latest
strategy:
matrix:
xcode: ["11.7", "12"]
steps:
- name: Checkout
uses: actions/checkout@master
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Swift version
run: swift --version
- name: Test
run: swift test -v --skip-update --parallel --enable-test-discovery --enable-code-coverage
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
- name: Generate coverage report
run: xcrun llvm-cov export -format="lcov" .build/debug/*PackageTests.xctest/Contents/MacOS/*PackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov
- name: Upload code coverage report
uses: codecov/codecov-action@master
with:
token: ${{secrets.CODECOV_TOKEN}}
file: coverage.lcov
fail_ci_if_error: true
- name: Build Release
run: swift build -c release
env:
DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
linux:
runs-on: ubuntu-latest
strategy:
matrix:
swift: ["5.1", "latest"]
container:
image: swift:${{ matrix.swift }}
steps:
- name: Checkout
uses: actions/checkout@master
- name: "Update APT"
shell: bash
run: "apt update"
- name: "Install curl"
shell: bash
run: "apt-get install -y curl"
- name: Swift version
run: swift --version
- name: Test
run: swift test -v --skip-update --parallel --enable-test-discovery --enable-code-coverage
- name: Generate coverage report
run: llvm-cov export -format="lcov" .build/debug/*PackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > coverage.lcov
- name: Upload code coverage report
uses: codecov/codecov-action@master
with:
token: ${{secrets.CODECOV_TOKEN}}
file: coverage.lcov
fail_ci_if_error: true
- name: Build Release
run: swift build -c release
webAssembly:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@master
- name: Swift version
run: swift --version
- name: Build
uses: swiftwasm/swiftwasm-action@main
with:
shell-action: swift build --triple wasm32-unknown-wasi

View File

@ -18,8 +18,8 @@ jobs:
steps:
- uses: actions/checkout@master
- name: Generate documentation
uses: SwiftDocOrg/swift-doc@master
- name: Generate Swift Doc Documentation
uses: SwiftDocOrg/swift-doc@1.0.0-rc.1
with:
inputs: "Sources/FirebladeECS"
output: "Documentation"

View File

@ -1,25 +0,0 @@
image: swift:5.1.3
stages:
- test
- build_release
build_project:
stage: build_release
script:
- swift package reset
- swift build -c release
tags:
- docker
only:
- master
test_project:
stage: test
variables:
GIT_DEPTH: "50"
script:
- swift package reset
- swift test
tags:
- docker

View File

@ -1 +1 @@
5.1.3
5.8

13
.swiftformat Normal file
View File

@ -0,0 +1,13 @@
# file options
--exclude Sources/**/*.generated.swift
--exclude Sources/FirebladeECS/Entity+Component.swift # problems with self.get { }
--exclude Tests/**/*.swift
# format options
--extensionacl on-declarations
--stripunusedargs closure-only
--commas inline
--self remove
--selfrequired get
--disable preferKeyPath
--disable opaqueGenericParameters

View File

@ -9,23 +9,11 @@ identifier_name:
line_length: 220
number_separator:
minimum_length: 5
analyzer_rules:
- explicit_self
- unused_declaration
- unused_import
opt_in_rules:
#- explicit_acl
#- explicit_enum_raw_value
#- explicit_type_interface
#- extension_access_modifier
#- file_name
#- file_types_order
#- indentation_width
#- missing_docs
#- multiline_arguments_brackets
#- multiline_literal_brackets
#- multiline_parameters_brackets
#- no_grouping_extension
#- required_deinit
#- type_contents_order
#- unowned_variable_capture
- anyobject_protocol
- array_init
- attributes
- closure_body_length
@ -48,7 +36,6 @@ opt_in_rules:
- enum_case_associated_values_count
- expiring_todo
- explicit_init
- explicit_self
- explicit_top_level_acl
- fallthrough
- fatal_error_message
@ -109,8 +96,6 @@ opt_in_rules:
- unavailable_function
- unneeded_parentheses_in_closure_argument
- untyped_error_in_catch
- unused_declaration
- unused_import
- vertical_parameter_alignment_on_call
- vertical_whitespace_between_cases
- vertical_whitespace_closing_braces

View File

@ -0,0 +1,85 @@
//
// Base.swift
// FirebladeECSTests
//
// Created by Christian Treffs on 09.10.17.
//
import FirebladeECS
class EmptyComponent: Component {}
class Name: Component {
var name: String
init(name: String) {
self.name = name
}
}
class Position: Component {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
class Velocity: Component {
var a: Float
init(a: Float) {
self.a = a
}
}
class Party: Component {
var partying: Bool
init(partying: Bool) {
self.partying = partying
}
}
class Color: Component {
var r: UInt8 = 0
var g: UInt8 = 0
var b: UInt8 = 0
}
class ExampleSystem {
private let family: Family2<Position, Velocity>
init(nexus: Nexus) {
family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self)
}
func update(deltaT _: Double) {
for (position, velocity) in family {
position.x *= 2
velocity.a *= 2
}
}
}
final class SingleGameState: SingleComponent {
var shouldQuit: Bool = false
var playerHealth: Int = 67
}
func setUpNexus() -> Nexus {
let numEntities = 10000
let nexus = Nexus()
for i in 0 ..< numEntities {
nexus.createEntity().assign(Position(x: 1 + i, y: 2 + i),
Name(name: "myName\(i)"),
Velocity(a: 3.14),
EmptyComponent(),
Color())
}
precondition(nexus.numEntities == numEntities)
// precondition(nexus.numFamilies == 1)
precondition(nexus.numComponents == numEntities * 5)
return nexus
}

View File

@ -0,0 +1,198 @@
// swiftformat:disable preferForLoop
import Benchmark
import FirebladeECS
// derived from FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift in the parent project
let benchmarks = {
Benchmark("TraitMatching") { benchmark in
let nexus = setUpNexus()
let a = nexus.createEntity()
a.assign(Position(x: 1, y: 2))
a.assign(Name(name: "myName"))
a.assign(Velocity(a: 3.14))
a.assign(EmptyComponent())
let isMatch = nexus.family(requiresAll: Position.self, Velocity.self,
excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
isMatch.canBecomeMember(a)
)
}
}
Benchmark("TypedFamilyEntities") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requires: Position.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.entities
.forEach { (entity: Entity) in
_ = entity
}
)
}
}
Benchmark("TypedFamilyOneComponent") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requires: Position.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.forEach { (position: Position) in
_ = position
}
)
}
}
Benchmark("TypedFamilyEntityOneComponent") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requires: Position.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.entityAndComponents
.forEach { (entity: Entity, position: Position) in
_ = entity
_ = position
}
)
}
}
Benchmark("TypedFamilyTwoComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.forEach { (position: Position, velocity: Velocity) in
_ = position
_ = velocity
}
)
}
}
Benchmark("TypedFamilyEntityTwoComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.entityAndComponents
.forEach { (entity: Entity, position: Position, velocity: Velocity) in
_ = entity
_ = position
_ = velocity
}
)
}
}
Benchmark("TypedFamilyThreeComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.forEach { (position: Position, velocity: Velocity, name: Name) in
_ = position
_ = velocity
_ = name
}
)
}
}
Benchmark("TypedFamilyEntityThreeComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.entityAndComponents
.forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name) in
_ = entity
_ = position
_ = velocity
_ = name
}
)
}
}
Benchmark("TypedFamilyFourComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.forEach { (position: Position, velocity: Velocity, name: Name, color: Color) in
_ = position
_ = velocity
_ = name
_ = color
}
)
}
}
Benchmark("TypedFamilyEntityFourComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.entityAndComponents
.forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name, color: Color) in
_ = entity
_ = position
_ = velocity
_ = name
_ = color
}
)
}
}
Benchmark("TypedFamilyFiveComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, EmptyComponent.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(
family
.forEach { (position: Position, velocity: Velocity, name: Name, color: Color, empty: EmptyComponent) in
_ = position
_ = velocity
_ = name
_ = color
_ = empty
}
)
}
}
Benchmark("TypedFamilyEntityFiveComponents") { benchmark in
let nexus = setUpNexus()
let family = nexus.family(requiresAll: Position.self, Velocity.self, Name.self, Color.self, EmptyComponent.self, excludesAll: Party.self)
for _ in benchmark.scaledIterations {
blackHole(family
.entityAndComponents
.forEach { (entity: Entity, position: Position, velocity: Velocity, name: Name, color: Color, empty: EmptyComponent) in
_ = entity
_ = position
_ = velocity
_ = name
_ = color
_ = empty
}
)
}
}
}

28
Benchmarks/Package.swift Normal file
View File

@ -0,0 +1,28 @@
// swift-tools-version: 5.8
import PackageDescription
let package = Package(
name: "ECSBenchmarks",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
dependencies: [
.package(path: "../"),
.package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.27.3"))
],
targets: [
.executableTarget(
name: "ECSBenchmark",
dependencies: [
.product(name: "FirebladeECS", package: "ecs"),
.product(name: "Benchmark", package: "package-benchmark")
],
path: "Benchmarks/ECSBenchmark",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
]
)
]
)

22
Benchmarks/README.md Normal file
View File

@ -0,0 +1,22 @@
# Benchmarks for FirebladeECS
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).
To run all the available benchmarks:
swift package benchmark --format markdown
For more help on the package-benchmark SwiftPM plugin:
swift package benchmark help
Creating a local baseline:
swift package --allow-writing-to-package-directory benchmark baseline update dev
swift package benchmark baseline list
Comparing to a the baseline 'alpha'
swift package benchmark baseline compare dev
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).

12
CODEOWNERS Normal file
View File

@ -0,0 +1,12 @@
# This file is a list of the people responsible for ensuring that contributions
# to this projected are reviewed, either by themselves or by someone else.
# They are also the gatekeepers for their part of this project, with the final
# word on what goes in or not.
# The code owners file uses a .gitignore-like syntax to specify which parts of
# the codebase is associated with an owner. See
# <https://docs.github.com/github/creating-cloning-and-archiving-repositories/about-code-owners>
# for details.
# The following lines are used by GitHub to automatically recommend reviewers.
# Each line is a file pattern followed by one or more owners.
* @ctreffs

136
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,136 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement
e.g. via [content abuse report][ref-report-abuse].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][ref-homepage-cc],
version 2.0, available at
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
<https://www.contributor-covenant.org/faq>.
Translations are available at
<https://www.contributor-covenant.org/translations>.
<!-- REFERENCES -->
[ref-homepage-cc]: https://www.contributor-covenant.org
[ref-report-abuse]: https://docs.github.com/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam#reporting-an-issue-or-pull-request
[ref-gh-coc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project
[ref-gh-abuse]: https://docs.github.com/en/communities/moderating-comments-and-conversations/managing-how-contributors-report-abuse-in-your-organizations-repository
[ref-coc-guide]: https://opensource.guide/code-of-conduct/

View File

@ -8,18 +8,18 @@
The following is a set of **guidelines for contributing** to this project.
Use your best judgment and feel free to propose changes to this document in a pull request.
**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/series/how-to-contribute-to-an-open-source-project-on-github)
**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)
### 💡 Your contribution - the sky is the limit 🌈
This is an open source project and we love to receive contributions from our community — [**you**][ref-contributors]!
There are many ways to contribute, from writing __tutorials__ or __blog posts__, improving the [__documentation__][ref-documentation], submitting __bug reports__ and __feature requests__ or
__writing code__ which can be incorporated into the repository itself.
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
[__writing code__][ref-pull-request-new] which can be incorporated into the repository itself.
When contributing to this project, please feel free to discuss the change you wish to make via issue with the repository owners before making a change.
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.
<!--Note that we have a [code of conduct][ref-code-of-conduct], please follow it in all your interactions with the project.-->
Note that we have a [code of conduct][ref-code-of-conduct], please follow it in all your interactions with the project.
### 🐞 You want to report a bug or file an issue?
@ -55,11 +55,12 @@ If you want to start somewhere, this would be a good place to start.
That said, these aren't necessarily the easiest tickets.
For any new contributions please consider these guidelines:
1. Open a [new pull request (PR)][ref-pull-request-new] with a **clear and descriptive title**
2. Write a **detailed comment** with as much relevant information as possible including:
- What your feature is intended to do?
- How it can be used?
- What alternatives where considered?
- What alternatives where considered, if any?
- Has this feature impact on performance or stability of the project?
#### Your contribution responsibilities
@ -68,7 +69,7 @@ Don't be intimidated by these responsibilities, they are easy to meet if you tak
- [x] Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback.
- [x] Ensure (cross-)platform compatibility for every change that's accepted. An addition should not reduce the number of platforms that the project supports.
- [x] Ensure **coding conventions** are met. Lint your code with the project's default tools.
- [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.
- [x] Add tests for your feature that prove it's working as expected. Code coverage should not drop below its previous value.
- [x] Ensure none of the existing tests are failing after adding your changes.
- [x] Document your public API code and ensure to add code comments where necessary.
@ -80,7 +81,7 @@ Please consult the [README][ref-readme] for installation instructions.
<!-- REFERENCES -->
[ref-code-of-conduct]: t.b.d.
[ref-code-of-conduct]: https://github.com/fireblade-engine/ecs/blob/master/CODE_OF_CONDUCT.md
[ref-contributors]: https://github.com/fireblade-engine/ecs/graphs/contributors
[ref-documentation]: https://github.com/fireblade-engine/ecs/wiki
[ref-gh-actions]: https://github.com/fireblade-engine/ecs/actions
@ -91,3 +92,4 @@ Please consult the [README][ref-readme] for installation instructions.
[ref-pull-request-how-to]: https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls
[ref-pull-request-new]: https://github.com/fireblade-engine/ecs/compare
[ref-readme]: https://github.com/fireblade-engine/ecs/blob/master/README.md
[ref-makefile]: https://github.com/fireblade-engine/ecs/blob/master/Makefile

108
Makefile
View File

@ -1,79 +1,61 @@
# Version 1.0.0
UNAME_S := $(shell uname -s)
SWIFT_PACKAGE_VERSION := $(shell swift package tools-version)
# Lint
lint:
swiftlint autocorrect --format
swiftlint lint --quiet
# Lint fix and format code.
.PHONY: lint-fix
swiftlint:
mint run swiftlint lint --fix --config .swiftlint.yml --format --quiet
swiftformat:
mint run swiftformat . --swiftversion ${SWIFT_PACKAGE_VERSION}
lint-fix: swiftlint swiftformat
lintErrorOnly:
@swiftlint autocorrect --format --quiet
@swiftlint lint --quiet | grep error
# Generate code
.PHONY: generate-code
generate-code:
mint run sourcery --quiet --config ./.sourcery.yml
mint run sourcery --quiet --config ./.sourceryTests.yml
# Git
precommit: generateCode generateTestsCode lint genLinuxTests
# Run pre-push tasks
.PHONY: pre-push
pre-push: generate-code lint-fix
submodule:
git submodule init
git submodule update --recursive
.PHONY: precommit
precommit: pre-push
# Tests
genLinuxTests:
swift test --generate-linuxmain
swiftlint autocorrect --format --path Tests/
.PHONY: setup-brew
setup-brew:
@which -s brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
@brew update
test: genLinuxTests
swift test
.PHONY: install-dependencies-macOS
install-dependencies-macOS: setup-brew
brew install mint
mint bootstrap
# Package
latest:
swift package update
.PHONY: setupEnvironment
setupEnvironment: install-dependencies-macOS
resolve:
swift package resolve
# Build debug version
.PHONY: build-debug
build-debug:
swift build -c debug
# Xcode
genXcode:
swift package generate-xcodeproj --enable-code-coverage --skip-extra-files
genXcodeOpen: genXcode
open *.xcodeproj
# Clean
clean:
swift package reset
-rm -rdf .swiftpm/xcode
-rm -rdf .build/
-rm Package.resolved
-rm .DS_Store
cleanArtifacts:
swift package clean
# Build release version
.PHONY: build-release
build-release:
swift build -c release --skip-update
# Test links in README
# requires <https://github.com/tcort/markdown-link-check>
.PHONY: testReadme
testReadme:
markdown-link-check -p -v ./README.md
generateCode:
sourcery --config ./.sourcery.yml --verbose
generateTestsCode:
sourcery --config ./.sourceryTests.yml --verbose
# Delete package build artifacts.
.PHONY: clean
clean: clean-sourcery
swift package clean
brewInstallDeps: brewUpdate
brew install swiftenv
brew install swiftlint
brew install sourcery
brewSetup:
which -s brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
brewUpdate: brewSetup
brew update
setupEnvironment: brewInstallDeps
open Package.swift
# lines of code
loc: clean
find . -name "*.swift" -print0 | xargs -0 wc -l
# Clean sourcery cache
.PHONY: clean-sourcery
clean-sourcery:
rm -rdf ${HOME}/Library/Caches/Sourcery

3
Mintfile Normal file
View File

@ -0,0 +1,3 @@
realm/SwiftLint@0.57.0
nicklockwood/SwiftFormat@0.54.6
krzysztofzablocki/Sourcery@2.2.5

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.1
// swift-tools-version:5.8
import PackageDescription
let package = Package(
@ -8,9 +8,11 @@ let package = Package(
targets: ["FirebladeECS"])
],
targets: [
.target(name: "FirebladeECS"),
.target(name: "FirebladeECS",
exclude: ["Stencils/Family.stencil"]),
.testTarget(name: "FirebladeECSTests",
dependencies: ["FirebladeECS"]),
dependencies: ["FirebladeECS"],
exclude: ["Stencils/FamilyTests.stencil"]),
.testTarget(name: "FirebladeECSPerformanceTests",
dependencies: ["FirebladeECS"])
],

View File

@ -1,11 +1,13 @@
# Fireblade ECS (Entity-Component System)
[![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
[![github CI](https://github.com/fireblade-engine/ecs/workflows/CI/badge.svg)](https://github.com/fireblade-engine/ecs/actions?query=workflow%3ACI)
[![codecov](https://codecov.io/gh/fireblade-engine/ecs/branch/master/graph/badge.svg)](https://codecov.io/gh/fireblade-engine/ecs)
[![macOS](https://github.com/fireblade-engine/ecs/actions/workflows/ci-macos.yml/badge.svg)](https://github.com/fireblade-engine/ecs/actions/workflows/ci-macos.yml)
[![Linux](https://github.com/fireblade-engine/ecs/actions/workflows/ci-linux.yml/badge.svg)](https://github.com/fireblade-engine/ecs/actions/workflows/ci-linux.yml)
[![Windows](https://github.com/fireblade-engine/ecs/actions/workflows/ci-windows.yml/badge.svg)](https://github.com/fireblade-engine/ecs/actions/workflows/ci-windows.yml)
[![WASM](https://github.com/fireblade-engine/ecs/actions/workflows/ci-wasm.yml/badge.svg)](https://github.com/fireblade-engine/ecs/actions/workflows/ci-wasm.yml)
[![documentation](https://github.com/fireblade-engine/ecs/workflows/Documentation/badge.svg)](https://github.com/fireblade-engine/ecs/wiki)
[![codecov](https://codecov.io/gh/fireblade-engine/ecs/branch/master/graph/badge.svg)](https://codecov.io/gh/fireblade-engine/ecs)
[![spi-swift-versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffireblade-engine%2Fecs%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/fireblade-engine/ecs)
[![spi-swift-platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffireblade-engine%2Fecs%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/fireblade-engine/ecs)
[![platform-webassembly](https://img.shields.io/badge/Platform-WebAssembly-blue.svg)](https://github.com/swiftwasm/swift#swiftwasm)
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. It is developed and maintained as part of the [Fireblade Game Engine project](https://github.com/fireblade-engine).
@ -23,19 +25,19 @@ These instructions will get you a copy of the project up and running on your loc
### 💻 Installing
Fireblade ECS is available for all platforms that support [Swift 5.1](https://swift.org/) and higher and the [Swift Package Manager (SPM)](https://github.com/apple/swift-package-manager).
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).
Extend the following lines in your `Package.swift` file or use it to create a new project.
```swift
// swift-tools-version:5.1
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "YourPackageName",
dependencies: [
.package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.17.3")
.package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.17.5")
],
targets: [
.target(

View File

@ -13,6 +13,6 @@ public struct DynamicCodingKey: CodingKey {
public var intValue: Int?
public var stringValue: String
public init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
public init?(intValue: Int) { self.intValue = intValue; stringValue = "\(intValue)" }
public init?(stringValue: String) { self.stringValue = stringValue }
}

View File

@ -13,12 +13,12 @@ public struct ComponentIdentifier {
extension ComponentIdentifier {
@usableFromInline
init<C>(_ componentType: C.Type) where C: Component {
self.id = Self.makeRuntimeHash(componentType)
init(_ componentType: (some Component).Type) {
id = Self.makeRuntimeHash(componentType)
}
/// object identifier hash (only stable during runtime) - arbitrary hash is ok.
internal static func makeRuntimeHash<C>(_ componentType: C.Type) -> Identifier where C: Component {
static func makeRuntimeHash(_ componentType: (some Component).Type) -> Identifier {
ObjectIdentifier(componentType).hashValue
}
@ -34,5 +34,5 @@ extension ComponentIdentifier {
}
}
extension ComponentIdentifier: Equatable { }
extension ComponentIdentifier: Hashable { }
extension ComponentIdentifier: Equatable {}
extension ComponentIdentifier: Hashable {}

View File

@ -17,9 +17,9 @@ public struct Entity {
/// The unique entity identifier.
public private(set) var identifier: EntityIdentifier
internal init(nexus: Nexus, id: EntityIdentifier) {
init(nexus: Nexus, id: EntityIdentifier) {
self.nexus = nexus
self.identifier = id
identifier = id
}
/// Returns the number of components for this entity.
@ -38,13 +38,13 @@ public struct Entity {
}
@discardableResult
public func createEntity<C>(with components: C) -> Entity where C: Collection, C.Element == Component {
public func createEntity(with components: some Collection<Component>) -> Entity {
nexus.createEntity(with: components)
}
/// Checks if a component with given type is assigned to this entity.
/// - Parameter type: the component type.
public func has<C>(_ type: C.Type) -> Bool where C: Component {
public func has(_ type: (some Component).Type) -> Bool {
has(type.identifier)
}
@ -78,13 +78,13 @@ public struct Entity {
/// Add a typed component to this entity.
/// - Parameter component: the typed component.
@discardableResult
public func assign<C>(_ component: C) -> Entity where C: Component {
public func assign(_ component: some Component) -> Entity {
assign(component)
return self
}
@discardableResult
public func assign<C>(_ components: C) -> Entity where C: Collection, C.Element == Component {
public func assign(_ components: some Collection<Component>) -> Entity {
nexus.assign(components: components, to: self)
return self
}
@ -92,14 +92,14 @@ public struct Entity {
/// Remove a component from this entity.
/// - Parameter component: the component.
@discardableResult
public func remove<C>(_ component: C) -> Entity where C: Component {
public func remove(_ component: some Component) -> Entity {
remove(component.identifier)
}
/// Remove a component by type from this entity.
/// - Parameter compType: the component type.
@discardableResult
public func remove<C>(_ compType: C.Type) -> Entity where C: Component {
public func remove(_ compType: (some Component).Type) -> Entity {
remove(compType.identifier)
}
@ -130,7 +130,7 @@ public struct Entity {
extension Entity {
public struct ComponentsIterator: IteratorProtocol {
private var iterator: IndexingIterator<([Component])>?
private var iterator: IndexingIterator<[Component]>?
@usableFromInline
init(nexus: Nexus, entityIdentifier: EntityIdentifier) {
@ -144,8 +144,9 @@ extension Entity {
}
}
}
extension Entity.ComponentsIterator: LazySequenceProtocol { }
extension Entity.ComponentsIterator: Sequence { }
extension Entity.ComponentsIterator: LazySequenceProtocol {}
extension Entity.ComponentsIterator: Sequence {}
extension Entity: Equatable {
public static func == (lhs: Entity, rhs: Entity) -> Bool {

View File

@ -23,8 +23,8 @@ public struct EntityIdentifier {
}
}
extension EntityIdentifier: Equatable { }
extension EntityIdentifier: Hashable { }
extension EntityIdentifier: Equatable {}
extension EntityIdentifier: Hashable {}
extension EntityIdentifier: RawRepresentable {
/// The entity identifier represented as a raw value.
@ -33,7 +33,7 @@ extension EntityIdentifier: RawRepresentable {
@inlinable
public init(rawValue: Identifier) {
self.id = rawValue
id = rawValue
}
}

View File

@ -49,10 +49,10 @@ public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator {
@usableFromInline
init<EntityIds>(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier {
let initialInUse: [EntityIdentifier.Identifier] = initialEntityIds.map { $0.id }
let initialInUse: [EntityIdentifier.Identifier] = initialEntityIds.map(\.id)
let maxInUseValue = initialInUse.max() ?? 0
let inUseSet = Set(initialInUse) // a set of all eIds in use
let allSet = Set(0...maxInUseValue) // all eIds from 0 to including maxInUseValue
let allSet = Set(0 ... maxInUseValue) // all eIds from 0 to including maxInUseValue
let freeSet = allSet.subtracting(inUseSet) // all "holes" / unused / free eIds
let initialFree = Array(freeSet).sorted().reversed() // order them to provide them linear increasing after all initially used are provided.
stack = initialFree + initialInUse
@ -83,12 +83,12 @@ public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator {
@inlinable
public init<EntityIds>(startProviding initialEntityIds: EntityIds) where EntityIds: BidirectionalCollection, EntityIds.Element == EntityIdentifier {
self.storage = Storage(startProviding: initialEntityIds)
storage = Storage(startProviding: initialEntityIds)
}
@inlinable
public init() {
self.storage = Storage()
storage = Storage()
}
@inline(__always)

View File

@ -97,9 +97,7 @@ extension ComponentTypeProvider: ComponentProvider {
/// This component provider always returns the same instance of the component. The instance
/// is created when first required and is of the type passed in to the initializer.
public final class ComponentSingletonProvider {
private lazy var instance: Component = {
componentType.init()
}()
private lazy var instance: Component = componentType.init()
private var componentType: ComponentInitializable.Type
@ -171,7 +169,7 @@ extension DynamicComponentProvider: ComponentProvider {
/// Represents a state for an EntityStateMachine. The state contains any number of ComponentProviders which
/// are used to add components to the entity when this state is entered.
public class EntityState {
internal var providers = [ComponentIdentifier: ComponentProvider]()
var providers = [ComponentIdentifier: ComponentProvider]()
public init() {}
@ -253,7 +251,7 @@ extension EntityState {
/// - Returns: This EntityState, so more modifications can be applied.
@inline(__always)
@discardableResult
public func addProvider<C: ComponentInitializable>(type: C.Type, provider: ComponentProvider) -> Self {
public func addProvider(type: (some ComponentInitializable).Type, provider: ComponentProvider) -> Self {
addMapping(for: type).withProvider(provider)
return self
}
@ -272,7 +270,7 @@ public class StateComponentMapping {
/// by more specific mappings if other methods are called.
/// - Parameter creatingState: The EntityState that the mapping will belong to
/// - Parameter type: The component type for the mapping
internal init(creatingState: EntityState, type: ComponentInitializable.Type) {
init(creatingState: EntityState, type: ComponentInitializable.Type) {
self.creatingState = creatingState
componentType = type
provider = ComponentTypeProvider(type: type)
@ -318,7 +316,7 @@ public class StateComponentMapping {
/// - Parameter closure: The Closure instance to return the component instance
/// - Returns: This ComponentMapping, so more modifications can be applied
@discardableResult
public func withMethod<C: Component>(_ closure: DynamicComponentProvider<C>.Closure) -> Self {
public func withMethod(_ closure: DynamicComponentProvider<some Component>.Closure) -> Self {
setProvider(DynamicComponentProvider(closure: closure))
return self
}
@ -403,7 +401,7 @@ public class EntityStateMachine<StateIdentifier: Hashable> {
var toAdd: [ComponentIdentifier: ComponentProvider]
if let currentState = currentState {
if let currentState {
toAdd = .init()
for (identifier, provider) in newState.providers {
toAdd[identifier] = provider
@ -411,7 +409,8 @@ public class EntityStateMachine<StateIdentifier: Hashable> {
for (identifier, _) in currentState.providers {
if let other = toAdd[identifier], let current = currentState.providers[identifier],
current.identifier == other.identifier {
current.identifier == other.identifier
{
toAdd[identifier] = nil
} else {
entity.remove(identifier)

View File

@ -14,6 +14,7 @@ extension CodingUserInfoKey {
}
// MARK: - encoding
extension FamilyMemberContainer: Encodable where R: FamilyEncoding {
func encode(to encoder: Encoder) throws {
let strategy = encoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy()
@ -50,11 +51,12 @@ extension Family where R: FamilyEncoding {
}
// MARK: - decoding
extension FamilyMemberContainer: Decodable where R: FamilyDecoding {
init(from decoder: Decoder) throws {
var familyContainer = try decoder.unkeyedContainer()
let strategy = decoder.userInfo[.nexusCodingStrategy] as? CodingStrategy ?? DefaultCodingStrategy()
self.components = try R.decode(componentsIn: &familyContainer, using: strategy)
components = try R.decode(componentsIn: &familyContainer, using: strategy)
}
}

View File

@ -53,7 +53,7 @@ public struct Family<R> where R: FamilyRequirementsManaging {
/// - Returns: The newly created member entity.
@discardableResult
public func createMember(@FamilyMemberBuilder<R> using builder: () -> R.Components) -> Entity {
self.createMember(with: builder())
createMember(with: builder())
}
}
@ -70,16 +70,17 @@ extension Family: Sequence {
}
}
extension Family: LazySequenceProtocol { }
extension Family: LazySequenceProtocol {}
// MARK: - components iterator
extension Family {
public struct ComponentsIterator: IteratorProtocol {
@usableFromInline var memberIdsIterator: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Identifier>.ElementIterator
@usableFromInline unowned let nexus: Nexus
public init(family: Family<R>) {
self.nexus = family.nexus
nexus = family.nexus
memberIdsIterator = family.memberIds.makeIterator()
}
@ -93,10 +94,11 @@ extension Family {
}
}
extension Family.ComponentsIterator: LazySequenceProtocol { }
extension Family.ComponentsIterator: Sequence { }
extension Family.ComponentsIterator: LazySequenceProtocol {}
extension Family.ComponentsIterator: Sequence {}
// MARK: - entity iterator
extension Family {
@inlinable public var entities: EntityIterator {
EntityIterator(family: self)
@ -107,7 +109,7 @@ extension Family {
@usableFromInline unowned let nexus: Nexus
public init(family: Family<R>) {
self.nexus = family.nexus
nexus = family.nexus
memberIdsIterator = family.memberIds.makeIterator()
}
@ -120,10 +122,11 @@ extension Family {
}
}
extension Family.EntityIterator: LazySequenceProtocol { }
extension Family.EntityIterator: Sequence { }
extension Family.EntityIterator: LazySequenceProtocol {}
extension Family.EntityIterator: Sequence {}
// MARK: - entity component iterator
extension Family {
@inlinable public var entityAndComponents: EntityComponentIterator {
EntityComponentIterator(family: self)
@ -134,7 +137,7 @@ extension Family {
@usableFromInline unowned let nexus: Nexus
public init(family: Family<R>) {
self.nexus = family.nexus
nexus = family.nexus
memberIdsIterator = family.memberIds.makeIterator()
}
@ -147,10 +150,11 @@ extension Family {
}
}
extension Family.EntityComponentIterator: LazySequenceProtocol { }
extension Family.EntityComponentIterator: Sequence { }
extension Family.EntityComponentIterator: LazySequenceProtocol {}
extension Family.EntityComponentIterator: Sequence {}
// MARK: - member creation
extension Family {
/// Create a new entity with components required by this family.
///

View File

@ -5,6 +5,10 @@
// Created by Christian Treffs on 07.08.20.
//
@_functionBuilder
public enum FamilyMemberBuilderPreview<R> where R: FamilyRequirementsManaging { }
public typealias FamilyMemberBuilder<R> = FamilyMemberBuilderPreview<R> where R: FamilyRequirementsManaging
#if swift(<5.4)
@_functionBuilder
public enum FamilyMemberBuilder<R> where R: FamilyRequirementsManaging {}
#else
@resultBuilder
public enum FamilyMemberBuilder<R> where R: FamilyRequirementsManaging {}
#endif

View File

@ -19,7 +19,7 @@ public struct FamilyTraitSet {
self.requiresAll = requiresAll
self.excludesAll = excludesAll
self.setHash = FirebladeECS.hash(combine: [requiresAll, excludesAll])
setHash = FirebladeECS.hash(combine: [requiresAll, excludesAll])
}
@inlinable

View File

@ -6,8 +6,8 @@
//
#if canImport(Foundation)
import Foundation
import Foundation
extension JSONEncoder: TopLevelEncoder { }
extension JSONDecoder: TopLevelDecoder { }
extension JSONEncoder: TopLevelEncoder {}
extension JSONDecoder: TopLevelDecoder {}
#endif

View File

@ -1,6 +1,5 @@
// Generated using Sourcery 1.0.0 https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.5 https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable file_length
// swiftlint:disable function_parameter_count
// swiftlint:disable large_tuple

View File

@ -6,11 +6,11 @@
//
#if arch(x86_64) || arch(arm64) || arch(powerpc64) || arch(powerpc64le) || arch(s390x) // 64 bit
private let kFibA: UInt = 0x9e3779b97f4a7c15 // = 11400714819323198485 aka Fibonacci Hash a value for 2^64; calculate by: 2^64 / (golden ratio)
private let kFibA: UInt = 0x9E37_79B9_7F4A_7C15 // = 11400714819323198485 aka Fibonacci Hash a value for 2^64; calculate by: 2^64 / (golden ratio)
#elseif arch(i386) || arch(arm) || os(watchOS) || arch(wasm32) // 32 bit
private let kFibA: UInt = 0x9e3779b9 // = 2654435769 aka Fibonacci Hash a value for 2^32; calculate by: 2^32 / (golden ratio)
private let kFibA: UInt = 0x9E37_79B9 // = 2654435769 aka Fibonacci Hash a value for 2^32; calculate by: 2^32 / (golden ratio)
#else
#error("unsupported architecture")
#error("unsupported architecture")
#endif
/// entity id ^ component identifier hash
@ -20,6 +20,7 @@ public typealias EntityComponentHash = Int
public typealias ComponentTypeHash = Int
// MARK: - hash combine
/// Calculates the combined hash of two values. This implementation is based on boost::hash_combine.
/// Will always produce the same result for the same combination of seed and value during the single run of a program.
///
@ -55,27 +56,29 @@ public func hash<H: Sequence>(combine hashValues: H) -> Int where H.Element: Has
}
// MARK: - entity component hash
extension EntityComponentHash {
internal static func compose(entityId: EntityIdentifier, componentTypeHash: ComponentTypeHash) -> EntityComponentHash {
static func compose(entityId: EntityIdentifier, componentTypeHash: ComponentTypeHash) -> EntityComponentHash {
let entityIdSwapped = UInt(entityId.id).byteSwapped // needs to be 64 bit
let componentTypeHashUInt = UInt(bitPattern: componentTypeHash)
let hashUInt: UInt = componentTypeHashUInt ^ entityIdSwapped
return Int(bitPattern: hashUInt)
}
internal static func decompose(_ hash: EntityComponentHash, with entityId: EntityIdentifier) -> ComponentTypeHash {
static func decompose(_ hash: EntityComponentHash, with entityId: EntityIdentifier) -> ComponentTypeHash {
let entityIdSwapped = UInt(entityId.id).byteSwapped
let entityIdSwappedInt = Int(bitPattern: entityIdSwapped)
return hash ^ entityIdSwappedInt
}
internal static func decompose(_ hash: EntityComponentHash, with componentTypeHash: ComponentTypeHash) -> EntityIdentifier {
static func decompose(_ hash: EntityComponentHash, with componentTypeHash: ComponentTypeHash) -> EntityIdentifier {
let entityId: Int = (hash ^ componentTypeHash).byteSwapped
return EntityIdentifier(UInt32(truncatingIfNeeded: entityId))
}
}
// MARK: - string hashing
/// <https://stackoverflow.com/a/52440609>
public enum StringHashing {
/// *Waren Singer djb2*
@ -85,7 +88,7 @@ public enum StringHashing {
var hash: UInt64 = 5381
var iter = utf8String.unicodeScalars.makeIterator()
while let char = iter.next() {
hash = 127 * (hash & 0xFFFFFFFFFFFFFF) &+ UInt64(char.value)
hash = 127 * (hash & 0xFF_FFFF_FFFF_FFFF) &+ UInt64(char.value)
}
return hash
}

View File

@ -92,6 +92,7 @@ public struct ManagedContiguousArray<Element> {
}
// MARK: - Equatable
extension ManagedContiguousArray: Equatable where Element: Equatable {
public static func == (lhs: ManagedContiguousArray<Element>, rhs: ManagedContiguousArray<Element>) -> Bool {
lhs.store == rhs.store
@ -99,4 +100,5 @@ extension ManagedContiguousArray: Equatable where Element: Equatable {
}
// MARK: - Codable
extension ManagedContiguousArray: Codable where Element: Codable { }
extension ManagedContiguousArray: Codable where Element: Codable {}

View File

@ -28,12 +28,12 @@ extension Nexus {
}
@discardableResult
public final func assign<C>(component: C, to entity: Entity) -> Bool where C: Component {
public final func assign(component: some Component, to entity: Entity) -> Bool {
assign(component: component, to: entity)
}
@discardableResult
public final func assign<C>(components: C, to entity: Entity) -> Bool where C: Collection, C.Element == Component {
public final func assign(components: some Collection<Component>, to entity: Entity) -> Bool {
assign(components: components, to: entity.identifier)
}
@ -93,7 +93,7 @@ extension Nexus {
return false
}
var iter = allComponents.makeIterator()
var removedAll: Bool = true
var removedAll = true
while let component = iter.next() {
removedAll = removedAll && remove(component: component, from: entityId)
}

View File

@ -5,9 +5,13 @@
// Created by Christian Treffs on 30.07.20.
//
@_functionBuilder
public enum ComponentsBuilderPreview { }
public typealias ComponentsBuilder = ComponentsBuilderPreview
#if swift(<5.4)
@_functionBuilder
public enum ComponentsBuilder {}
#else
@resultBuilder
public enum ComponentsBuilder {}
#endif
extension ComponentsBuilder {
public static func buildBlock(_ components: Component...) -> [Component] {
@ -35,7 +39,7 @@ extension Nexus {
/// - Returns: The newly created entity with the provided component assigned.
@discardableResult
public func createEntity(@ComponentsBuilder using builder: () -> Component) -> Entity {
self.createEntity(with: builder())
createEntity(with: builder())
}
/// Create an entity assigning multiple components.
@ -51,7 +55,7 @@ extension Nexus {
/// - Returns: The newly created entity with the provided components assigned.
@discardableResult
public func createEntity(@ComponentsBuilder using builder: () -> [Component]) -> Entity {
self.createEntity(with: builder())
createEntity(with: builder())
}
/// Create multiple entities assigning one component each.
@ -68,7 +72,7 @@ extension Nexus {
/// - Returns: The newly created entities with the provided component assigned.
@discardableResult
public func createEntities(count: Int, @ComponentsBuilder using builder: (ComponentsBuilder.Context) -> Component) -> [Entity] {
(0..<count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) }
(0 ..< count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) }
}
/// Create multiple entities assigning multiple components each.
@ -86,6 +90,6 @@ extension Nexus {
/// - Returns: The newly created entities with the provided components assigned.
@discardableResult
public func createEntities(count: Int, @ComponentsBuilder using builder: (ComponentsBuilder.Context) -> [Component] = { _ in [] }) -> [Entity] {
(0..<count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) }
(0 ..< count).map { self.createEntity(with: builder(ComponentsBuilder.Context(index: $0))) }
}
}

View File

@ -22,8 +22,8 @@ extension Nexus {
}
@discardableResult
public func createEntity<C>(with components: C) -> Entity where C: Collection, C.Element == Component {
let entity = self.createEntity()
public func createEntity(with components: some Collection<Component>) -> Entity {
let entity = createEntity()
assign(components: components, to: entity.identifier)
return entity
}
@ -50,7 +50,7 @@ extension Nexus {
@discardableResult
public func destroy(entity: Entity) -> Bool {
self.destroy(entityId: entity.identifier)
destroy(entityId: entity.identifier)
}
@discardableResult
@ -76,6 +76,7 @@ extension Nexus {
}
// MARK: - entities iterator
extension Nexus {
public struct EntitiesIterator: IteratorProtocol {
private var iterator: AnyIterator<Entity>
@ -96,5 +97,6 @@ extension Nexus {
}
}
}
extension Nexus.EntitiesIterator: LazySequenceProtocol { }
extension Nexus.EntitiesIterator: Sequence { }
extension Nexus.EntitiesIterator: LazySequenceProtocol {}
extension Nexus.EntitiesIterator: Sequence {}

View File

@ -8,7 +8,7 @@
extension Nexus {
@usableFromInline
@discardableResult
func assign<C>(components: C, to entityId: EntityIdentifier) -> Bool where C: Collection, C.Element == Component {
func assign(components: some Collection<Component>, to entityId: EntityIdentifier) -> Bool {
var iter = components.makeIterator()
while let component = iter.next() {
let componentId = component.identifier
@ -103,8 +103,8 @@ extension Nexus {
return
}
let isMember: Bool = self.isMember(entity: entityId, inFamilyWithTraits: traits)
if !exists(entity: entityId) && isMember {
let isMember: Bool = isMember(entity: entityId, inFamilyWithTraits: traits)
if !exists(entity: entityId), isMember {
remove(entityWithId: entityId, fromFamilyWithTraits: traits)
return
}

View File

@ -47,13 +47,14 @@ public final class Nexus {
codingStrategy: DefaultCodingStrategy())
}
internal init(componentsByType: [ComponentIdentifier: ManagedContiguousArray<Component>],
componentsByEntity: [EntityIdentifier: Set<ComponentIdentifier>],
entityIdGenerator: EntityIdentifierGenerator,
familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Identifier>],
codingStrategy: CodingStrategy) {
init(componentsByType: [ComponentIdentifier: ManagedContiguousArray<Component>],
componentsByEntity: [EntityIdentifier: Set<ComponentIdentifier>],
entityIdGenerator: EntityIdentifierGenerator,
familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet<EntityIdentifier, EntityIdentifier.Identifier>],
codingStrategy: CodingStrategy)
{
self.componentsByType = componentsByType
self.componentIdsByEntity = componentsByEntity
componentIdsByEntity = componentsByEntity
self.familyMembersByTraits = familyMembersByTraits
self.entityIdGenerator = entityIdGenerator
self.codingStrategy = codingStrategy
@ -71,6 +72,7 @@ public final class Nexus {
}
// MARK: - CustomDebugStringConvertible
extension Nexus: CustomDebugStringConvertible {
public var debugDescription: String {
"<Nexus entities:\(numEntities) components:\(numComponents) families:\(numFamilies)>"
@ -78,8 +80,9 @@ extension Nexus: CustomDebugStringConvertible {
}
// MARK: - default coding strategy
public struct DefaultCodingStrategy: CodingStrategy {
public init() { }
public init() {}
public func codingKey<C>(for componentType: C.Type) -> DynamicCodingKey where C: Component {
DynamicCodingKey(stringValue: "\(C.self)").unsafelyUnwrapped

View File

@ -1,5 +1,5 @@
//
// NexusEvents.swift
// NexusEvent.swift
// FirebladeECS
//
// Created by Christian Treffs on 08.10.17.

View File

@ -32,13 +32,13 @@ extension Single where A: SingleComponent {
}
public var entity: Entity {
Entity(nexus: self.nexus, id: entityId)
Entity(nexus: nexus, id: entityId)
}
}
extension Nexus {
public func single<S>(_ component: S.Type) -> Single<S> where S: SingleComponent {
let family = self.family(requires: S.self)
let family = family(requires: S.self)
precondition(family.count <= 1, "Singleton count of \(S.self) must be 0 or 1: \(family.count)")
let entityId: EntityIdentifier
if family.isEmpty {

View File

@ -74,7 +74,7 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
guard let denseIndex = findIndex(at: key) else {
return nil
}
let entry = self.dense[denseIndex]
let entry = dense[denseIndex]
assert(entry.key == key, "entry.key and findIndex(at: key) must be equal!")
return entry.element
}
@ -99,7 +99,7 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
}
let removed = swapRemove(at: denseIndex)
if !dense.isEmpty && denseIndex < dense.count {
if !dense.isEmpty, denseIndex < dense.count {
let swappedElement = dense[denseIndex]
sparse[swappedElement.key] = denseIndex
}
@ -208,12 +208,14 @@ extension UnorderedSparseSet where Key == Int {
}
// MARK: - Sequence
extension UnorderedSparseSet: Sequence {
public func makeIterator() -> ElementIterator {
ElementIterator(self)
}
// MARK: - UnorderedSparseSetIterator
public struct ElementIterator: IteratorProtocol {
var iterator: IndexingIterator<ContiguousArray<Storage.Entry>>
@ -226,17 +228,20 @@ extension UnorderedSparseSet: Sequence {
}
}
}
extension UnorderedSparseSet.ElementIterator: LazySequenceProtocol { }
extension UnorderedSparseSet.ElementIterator: Sequence { }
extension UnorderedSparseSet.ElementIterator: LazySequenceProtocol {}
extension UnorderedSparseSet.ElementIterator: Sequence {}
// MARK: - Equatable
extension UnorderedSparseSet.Storage.Entry: Equatable where Element: Equatable { }
extension UnorderedSparseSet.Storage.Entry: Equatable where Element: Equatable {}
extension UnorderedSparseSet.Storage: Equatable where Element: Equatable {
@usableFromInline
static func == (lhs: UnorderedSparseSet<Element, Key>.Storage, rhs: UnorderedSparseSet<Element, Key>.Storage) -> Bool {
lhs.dense == rhs.dense && lhs.sparse == rhs.sparse
}
}
extension UnorderedSparseSet: Equatable where Element: Equatable {
public static func == (lhs: UnorderedSparseSet<Element, Key>, rhs: UnorderedSparseSet<Element, Key>) -> Bool {
lhs.storage == rhs.storage
@ -244,6 +249,7 @@ extension UnorderedSparseSet: Equatable where Element: Equatable {
}
// MARK: - Codable
extension UnorderedSparseSet.Storage.Entry: Codable where Element: Codable { }
extension UnorderedSparseSet.Storage: Codable where Element: Codable { }
extension UnorderedSparseSet: Codable where Element: Codable { }
extension UnorderedSparseSet.Storage.Entry: Codable where Element: Codable {}
extension UnorderedSparseSet.Storage: Codable where Element: Codable {}
extension UnorderedSparseSet: Codable where Element: Codable {}

View File

@ -5,6 +5,7 @@
// Created by Christian Treffs on 14.02.19.
//
#if os(macOS)
import FirebladeECS
import XCTest
@ -97,3 +98,6 @@ class HashingPerformanceTests: XCTestCase {
#endif
}
}
#else
#warning("Skipping HashingPerformanceTests")
#endif

View File

@ -5,6 +5,7 @@
// Created by Christian Treffs on 05.10.19.
//
#if os(macOS)
import FirebladeECS
import XCTest
@ -30,7 +31,7 @@ final class TypeIdentifierPerformanceTests: XCTestCase {
/// release: 1.034 sec
/// debug:
func testPerformanceHash() {
measure {
measure(options: .default) {
for _ in 0..<maxIterations {
_ = StringHashing.singer_djb2(String(describing: Color.self))
_ = StringHashing.singer_djb2(String(describing: EmptyComponent.self))
@ -91,3 +92,6 @@ final class TypeIdentifierPerformanceTests: XCTestCase {
}
}
}
#else
#warning("Skipping TypeIdentifierPerformanceTests tests")
#endif

View File

@ -1,70 +0,0 @@
#if !canImport(ObjectiveC)
import XCTest
extension ComponentIdentifierTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__ComponentIdentifierTests = [
("testMeasureComponentIdentifier", testMeasureComponentIdentifier),
("testMeasureStaticComponentIdentifier", testMeasureStaticComponentIdentifier),
]
}
extension HashingPerformanceTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__HashingPerformanceTests = [
("testMeasureBernsteinDjb2", testMeasureBernsteinDjb2),
("testMeasureCombineHash", testMeasureCombineHash),
("testMeasureSDBM", testMeasureSDBM),
("testMeasureSetOfSetHash", testMeasureSetOfSetHash),
("testMeasureSingerDjb2", testMeasureSingerDjb2),
("testMeasureSwiftHasher", testMeasureSwiftHasher),
]
}
extension TypeIdentifierPerformanceTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__TypeIdentifierPerformanceTests = [
("testPerformanceHash", testPerformanceHash),
("testPerformanceMirrorReflectingDescription", testPerformanceMirrorReflectingDescription),
("testPerformanceObjectIdentifier", testPerformanceObjectIdentifier),
("testPerformanceStringDescribing", testPerformanceStringDescribing),
("testPerformanceStringReflecting", testPerformanceStringReflecting),
]
}
extension TypedFamilyPerformanceTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__TypedFamilyPerformanceTests = [
("testMeasureTraitMatching", testMeasureTraitMatching),
("testPerformanceArray", testPerformanceArray),
("testPerformanceTypedFamilyEntities", testPerformanceTypedFamilyEntities),
("testPerformanceTypedFamilyEntityFiveComponents", testPerformanceTypedFamilyEntityFiveComponents),
("testPerformanceTypedFamilyEntityFourComponents", testPerformanceTypedFamilyEntityFourComponents),
("testPerformanceTypedFamilyEntityOneComponent", testPerformanceTypedFamilyEntityOneComponent),
("testPerformanceTypedFamilyEntityThreeComponents", testPerformanceTypedFamilyEntityThreeComponents),
("testPerformanceTypedFamilyEntityTwoComponents", testPerformanceTypedFamilyEntityTwoComponents),
("testPerformanceTypedFamilyFiveComponents", testPerformanceTypedFamilyFiveComponents),
("testPerformanceTypedFamilyFourComponents", testPerformanceTypedFamilyFourComponents),
("testPerformanceTypedFamilyOneComponent", testPerformanceTypedFamilyOneComponent),
("testPerformanceTypedFamilyThreeComponents", testPerformanceTypedFamilyThreeComponents),
("testPerformanceTypedFamilyTwoComponents", testPerformanceTypedFamilyTwoComponents),
]
}
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests),
testCase(HashingPerformanceTests.__allTests__HashingPerformanceTests),
testCase(TypeIdentifierPerformanceTests.__allTests__TypeIdentifierPerformanceTests),
testCase(TypedFamilyPerformanceTests.__allTests__TypedFamilyPerformanceTests),
]
}
#endif

View File

@ -1,6 +1,5 @@
// Generated using Sourcery 1.0.0 https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.2.5 https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
import FirebladeECS
import XCTest

View File

@ -1,10 +0,0 @@
import XCTest
import FirebladeECSPerformanceTests
import FirebladeECSTests
var tests = [XCTestCaseEntry]()
tests += FirebladeECSPerformanceTests.__allTests()
tests += FirebladeECSTests.__allTests()
XCTMain(tests)

13
renovate.json Normal file
View File

@ -0,0 +1,13 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"description": "Accumulate non-major updates into one pull request",
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": ">=1",
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch"
}
]
}