DocC documentation generation (#77)

* initial stub files to support DocC generation

* add dependency on docc-plugin to support local previewing of documentation

* adding preview documentation commands to view documentation

* adding documentation generation commands

* cleanup with precommit

* update to reference expected location, or how to preview

* cleaning up documentation for global hashing algorithms

* adding documentation for ManagedContiguousArray

* adding documentation for UnorderedSparseSet

* directly porting README getting started into an Essentials article
This commit is contained in:
Joseph Heck 2024-10-31 22:52:41 -07:00 committed by GitHub
parent 9708eba6cb
commit fdf08687d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 427 additions and 16 deletions

4
.spi.yml Normal file
View File

@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [FirebladeECS]

View File

@ -59,3 +59,37 @@ clean: clean-sourcery
.PHONY: clean-sourcery
clean-sourcery:
rm -rdf ${HOME}/Library/Caches/Sourcery
# Preview DocC documentation
.PHONY: preview-docs
preview-docs:
swift package --disable-sandbox preview-documentation --target FirebladeECS
# Preview DocC documentation with analysis/warnings and overview of coverage
.PHONY: preview-analysis-docs
preview-analysis-docs:
swift package --disable-sandbox preview-documentation --target FirebladeECS --analyze --experimental-documentation-coverage --level brief
# Generates a plain DocC archive in the .build directory
.PHONY: generate-docs
generate-docs:
DOCC_JSON_PRETTYPRINT=YES \
swift package \
generate-documentation \
--fallback-bundle-identifier com.github.fireblade-engine.FirebladeECS \
--target FirebladeECS \
# Generates documentation pages suitable to push/host on github pages (or another static site)
# Expected location, if set up, would be:
# https://fireblade-engine.github.io/FirebladeECS/documentation/FirebladeECS/
.PHONY: generate-docs-githubpages
generate-docs-githubpages:
DOCC_JSON_PRETTYPRINT=YES \
swift package \
--allow-writing-to-directory ./docs \
generate-documentation \
--fallback-bundle-identifier com.github.fireblade-engine.FirebladeECS \
--target FirebladeECS \
--output-path ./docs \
--transform-for-static-hosting \
--hosting-base-path 'FirebladeECS'

View File

@ -7,6 +7,9 @@ let package = Package(
.library(name: "FirebladeECS",
targets: ["FirebladeECS"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
],
targets: [
.target(name: "FirebladeECS",
exclude: ["Stencils/Family.stencil"]),

View File

@ -233,8 +233,9 @@ See the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo) t
## 📖 Documentation
Consult the [wiki](https://github.com/fireblade-engine/ecs/wiki) for in-depth [documentation](https://github.com/fireblade-engine/ecs/wiki).
Consult the [online documentation](https://swiftpackageindex.com/fireblade-engine/ecs/documentation/FirebladeECS), or preview it locally:
- `make preview-docs`
## 💁 How to contribute

View File

@ -0,0 +1,112 @@
# ``FirebladeECS``
Seamlessly, consistently, and asynchronously replicate data.
## Overview
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.
An ECS comprises entities composed from components of data, with systems which operate on the components.
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).
It is developed and maintained as part of the [Fireblade Game Engine project](https://github.com/fireblade-engine).
For a more detailed example of FirebladeECS in action, see the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo).
## Topics
### Essentials
- <doc:GettingStartedWithFirebladeECS>
- ``Nexus``
- ``NexusEvent``
- ``NexusEventDelegate``
### Entities
- ``Entity``
- ``EntityState``
- ``EntityStateMachine``
- ``EntityCreated``
- ``EntityDestroyed``
- ``EntityComponentHash``
- ``EntityIdentifier``
- ``EntityIdentifierGenerator``
- ``DefaultEntityIdGenerator``
- ``LinearIncrementingEntityIdGenerator``
### Components
- ``Component``
- ``ComponentAdded``
- ``ComponentRemoved``
- ``ComponentProvider``
- ``ComponentsBuilder-4co42``
- ``ComponentsBuilder``
- ``ComponentInstanceProvider``
- ``ComponentIdentifier``
- ``ComponentInitializable``
- ``ComponentTypeHash``
- ``ComponentTypeProvider``
- ``ComponentSingletonProvider``
- ``SingleComponent``
- ``EntityComponentHash``
- ``StateComponentMapping``
- ``DynamicComponentProvider``
- ``RequiringComponents1``
- ``RequiringComponents2``
- ``RequiringComponents3``
- ``RequiringComponents4``
- ``RequiringComponents5``
- ``RequiringComponents6``
- ``RequiringComponents7``
- ``RequiringComponents8``
- ``DefaultInitializable``
- ``SingleComponent``
### Systems
- ``Family``
- ``FamilyEncoding``
- ``FamilyDecoding``
- ``FamilyMemberAdded``
- ``FamilyMemberRemoved``
- ``FamilyMemberBuilder-3f2i6``
- ``FamilyMemberBuilder``
- ``FamilyTraitSet``
- ``Requires1``
- ``Requires2``
- ``Requires3``
- ``Requires4``
- ``Requires5``
- ``Requires6``
- ``Requires7``
- ``Requires8``
- ``Single``
- ``Family1``
- ``Family2``
- ``Family3``
- ``Family4``
- ``Family5``
- ``Family6``
- ``Family7``
- ``Family8``
- ``FamilyRequirementsManaging``
### Coding Strategies
- ``CodingStrategy``
- ``DefaultCodingStrategy``
- ``TopLevelDecoder``
- ``TopLevelEncoder``
- ``DynamicCodingKey``
### Supporting Types
- ``ManagedContiguousArray``
- ``UnorderedSparseSet``
### Hash Functions
- ``hash(combine:)``
- ``hash(combine:_:)``
- ``StringHashing``

View File

@ -0,0 +1,210 @@
# Getting started with Fireblade ECS
Learn the API and key types Fireblade provides to compose your game or app logic.
## Overview
Fireblade ECS is a dependency free, Swift language implementation of an Entity-Component-System ([ECS](https://en.wikipedia.org/wiki/Entity_component_system)).
An ECS comprises entities composed from components of data, with systems which operate on the components.
Extend the following lines in your `Package.swift` file or use it to create a new project.
```swift
// 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.5")
],
targets: [
.target(
name: "YourTargetName",
dependencies: ["FirebladeECS"])
]
)
```
This article introduces you to the key concepts of Fireblade ECS's API.
For a more detailed example, see the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo).
### 🏛️ Nexus
The core element in the Fireblade-ECS is the [Nexus](https://en.wiktionary.org/wiki/nexus#Noun).
It acts as a centralized way to store, access and manage entities and their components.
A single `Nexus` may (theoretically) hold up to 4294967295 `Entities` at a time.
You may use more than one `Nexus` at a time.
Initialize a `Nexus` with
```swift
let nexus = Nexus()
```
### 👤 Entities
then create entities by letting the `Nexus` generate them.
```swift
// an entity without components
let newEntity = nexus.createEntity()
```
To define components, conform your class to the `Component` protocol
```swift
final class Position: Component {
var x: Int = 0
var y: Int = 0
}
```
and assign instances of it to an `Entity` with
```swift
let position = Position(x: 1, y: 2)
entity.assign(position)
```
You can be more efficient by assigning components while creating an entity.
```swift
// an entity with two components assigned.
nexus.createEntity {
Position(x: 1, y: 2)
Color(.red)
}
// bulk create entities with multiple components assigned.
nexus.createEntities(count: 100) { _ in
Position()
Color()
}
```
### 👪 Families
This ECS uses a grouping approach for entities with the same component types to optimize cache locality and ease up access to them.
Entities with the __same component types__ may belong to one `Family`.
A `Family` has entities as members and component types as family traits.
Create a family by calling `.family` with a set of traits on the nexus.
A family that contains only entities with a `Movement` and `PlayerInput` component, but no `Texture` component is created by
```swift
let family = nexus.family(requiresAll: Movement.self, PlayerInput.self,
excludesAll: Texture.self)
```
These entities are cached in the nexus for efficient access and iteration.
Families conform to the [Sequence](https://developer.apple.com/documentation/swift/sequence) protocol so that members (components)
may be iterated and accessed like any other sequence in Swift.
Access a family's components directly on the family instance. To get family entities and access components at the same time call `family.entityAndComponents`.
If you are only interested in a family's entities call `family.entities`.
```swift
class PlayerMovementSystem {
let family = nexus.family(requiresAll: Movement.self, PlayerInput.self,
excludesAll: Texture.self)
func update() {
family
.forEach { (mov: Movement, input: PlayerInput) in
// position & velocity component for the current entity
// get properties
_ = mov.position
_ = mov.velocity
// set properties
mov.position.x = mov.position.x + 3.0
...
// current input command for the given entity
_ = input.command
...
}
}
func update2() {
family
.entityAndComponents
.forEach { (entity: Entity, mov: Movement, input: PlayerInput) in
// the current entity instance
_ = entity
// position & velocity component for the current entity
// get properties
_ = mov.position
_ = mov.velocity
}
}
func update3() {
family
.entities
.forEach { (entity: Entity) in
// the current entity instance
_ = entity
}
}
}
```
### 🧑 Singles
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.
```swift
final class GameState: SingleComponent {
var quitGame: Bool = false
}
class GameLogicSystem {
let gameState: Single<GameState>
init(nexus: Nexus) {
gameState = nexus.single(GameState.self)
}
func update() {
// update your game sate here
gameState.component.quitGame = true
// entity access is provided as well
_ = gameState.entity
}
}
```
### 🔗 Serialization
To serialize/deserialize entities you must conform their assigned components to the `Codable` protocol.
Conforming components can then be serialized per family like this:
```swift
// MyComponent and YourComponent both conform to Component and Codable protocols.
let nexus = Nexus()
let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self)
// JSON encode entities from given family.
var jsonEncoder = JSONEncoder()
let encodedData = try family.encodeMembers(using: &jsonEncoder)
// Decode entities into given family from JSON.
// The decoded entities will be added to the nexus.
var jsonDecoder = JSONDecoder()
let newEntities = try family.decodeMembers(from: jsonData, using: &jsonDecoder)
```

View File

@ -21,8 +21,10 @@ 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.
/// Calculates the combined hash of two values.
///
/// This implementation is based on boost::hash_combine.
/// It produces the same result for the same combination of seed and value during the single run of a program.
///
/// - Parameters:
/// - seed: seed hash.
@ -46,8 +48,10 @@ public func hash(combine seed: Int, _ value: Int) -> Int {
return Int(bitPattern: uSeed)
}
/// Calculates the combined hash value of the elements. This implementation is based on boost::hash_range.
/// Is sensitive to the order of the elements.
/// Calculates the combined hash value of the elements.
///
/// This implementation is based on boost::hash_range.
/// The hash value this method computes is sensitive to the order of the elements.
/// - Parameter hashValues: sequence of hash values to combine.
/// - Returns: combined hash value.
public func hash<H: Sequence>(combine hashValues: H) -> Int where H.Element: Hashable {
@ -79,11 +83,13 @@ extension EntityComponentHash {
// MARK: - string hashing
/// <https://stackoverflow.com/a/52440609>
/// A type that provides stable hash values for String.
///
/// The details are based on [StackOverflow Q&A on String hashing in Swift](https://stackoverflow.com/a/52440609)
public enum StringHashing {
/// *Waren Singer djb2*
/// *Warren Stringer djb2*
///
/// <https://stackoverflow.com/a/43149500>
/// Implementation from <https://stackoverflow.com/a/43149500>
public static func singer_djb2(_ utf8String: String) -> UInt64 {
var hash: UInt64 = 5381
var iter = utf8String.unicodeScalars.makeIterator()
@ -95,8 +101,8 @@ public enum StringHashing {
/// *Dan Bernstein djb2*
///
/// This algorithm (k=33) was first reported by dan bernstein many years ago in comp.lang.c.
/// Another version of this algorithm (now favored by bernstein) uses xor: hash(i) = hash(i - 1) * 33 ^ str[i];
/// This algorithm (k=33) was first reported by Dan Bernstein many years ago in `comp.lang.c`.
/// Another version of this algorithm (now favored by Bernstein) uses xor: `hash(i) = hash(i - 1) * 33 ^ str[i];`
/// The magic of number 33 (why it works better than many other constants, prime or not) has never been adequately explained.
///
/// <http://www.cse.yorku.ca/~oz/hash.html>

View File

@ -4,6 +4,7 @@
//
// Created by Christian Treffs on 28.10.17.
//
/// A type that provides a managed contiguous array of elements that you provide.
public struct ManagedContiguousArray<Element> {
public typealias Index = Int
@ -11,16 +12,24 @@ public struct ManagedContiguousArray<Element> {
@usableFromInline var size: Int = 0
@usableFromInline var store: ContiguousArray<Element?> = []
/// Creates a new array.
/// - Parameter minCount: The minimum number of elements, which defaults to `4096`.
public init(minCount: Int = 4096) {
chunkSize = minCount
store = ContiguousArray<Element?>(repeating: nil, count: minCount)
}
/// The number of elements in the array.
@inline(__always)
public var count: Int {
size
}
/// Inserts an element into the managed array.
/// - Parameters:
/// - element: The element to insert
/// - index: The location at which to insert the element.
/// - Returns: `true` to indicate the element was inserted.
@discardableResult
@inlinable
public mutating func insert(_ element: Element, at index: Int) -> Bool {
@ -34,6 +43,8 @@ public struct ManagedContiguousArray<Element> {
return true
}
/// Returns a Boolean value that indicates whether the index location holds an element.
/// - Parameter index: The index location in the contiguous array to inspect.
@inlinable
public func contains(_ index: Index) -> Bool {
if store.count <= index {
@ -42,16 +53,25 @@ public struct ManagedContiguousArray<Element> {
return store[index] != nil
}
/// Retrieves the value at the index location you provide.
/// - Parameter index: The index location.
/// - Returns: The element at the index location, or `nil`.
@inline(__always)
public func get(at index: Index) -> Element? {
store[index]
}
/// Unsafely retrieves the value at the index location you provide.
/// - Parameter index: The index location.
/// - Returns: The element at the index location.
@inline(__always)
public func get(unsafeAt index: Index) -> Element {
store[index].unsafelyUnwrapped
}
/// Removes the object at the index location you provide.
/// - Parameter index: The index location.
/// - Returns: `true` to indicate the element was removed.
@discardableResult
@inlinable
public mutating func remove(at index: Index) -> Bool {
@ -65,17 +85,23 @@ public struct ManagedContiguousArray<Element> {
return true
}
/// Clears the array of all elements.
/// - Parameter keepingCapacity: A Boolean value that indicates whether to keep the capacity of the array.
@inlinable
public mutating func clear(keepingCapacity: Bool = false) {
size = 0
store.removeAll(keepingCapacity: keepingCapacity)
}
/// Returns a Boolean value that indicates if the array needs to grow to insert another item.
/// - Parameter index: The index location to check.
@inlinable
func needsToGrow(_ index: Index) -> Bool {
index > store.count - 1
}
/// Expands the contiguous array to encompass the index location you provide.
/// - Parameter index: The index location.
@inlinable
mutating func grow(to index: Index) {
let newCapacity: Int = calculateCapacity(to: index)
@ -83,6 +109,8 @@ public struct ManagedContiguousArray<Element> {
store += ContiguousArray<Element?>(repeating: nil, count: newCount)
}
/// Returns the capacity of the array to the index location you provide.
/// - Parameter index: The index location
@inlinable
func calculateCapacity(to index: Index) -> Int {
let delta = Float(index) / Float(chunkSize)

View File

@ -5,7 +5,7 @@
// Created by Christian Treffs on 30.10.17.
//
/// An (unordered) sparse set.
/// An unordered sparse set.
///
/// - `Element`: the element (instance) to store.
/// - `Key`: the unique, hashable datastructure to use as a key to retrieve
@ -130,6 +130,7 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
}
}
/// Creates a new sparse set.
public init() {
self.init(storage: Storage())
}
@ -141,21 +142,26 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
@usableFromInline let storage: Storage
/// The size of the set.
public var count: Int { storage.count }
/// A Boolean value that indicates whether the set is empty.
public var isEmpty: Bool { storage.isEmpty }
/// Returns a Boolean value that indicates whether the key is included in the set.
/// - Parameter key: The key to inspect.
@inlinable
public func contains(_ key: Key) -> Bool {
storage.findIndex(at: key) != nil
}
/// Inset an element for a given key into the set in O(1).
///
/// Elements at previously set keys will be replaced.
///
/// - Parameters:
/// - element: the element
/// - key: the key
/// - Returns: true if new, false if replaced.
/// - element: The element.
/// - key: The key.
/// - Returns: `true` if new, `false` if replaced.
@discardableResult
public func insert(_ element: Element, at key: Key) -> Bool {
storage.insert(element, at: key)
@ -163,13 +169,16 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
/// Get the element for the given key in O(1).
///
/// - Parameter key: the key
/// - Returns: the element or nil of key not found.
/// - Parameter key: The key.
/// - Returns: the element or `nil` if the key wasn't found.
@inlinable
public func get(at key: Key) -> Element? {
storage.findElement(at: key)
}
/// Unsafely gets the element for the given key,
/// - Parameter key: The key.
/// - Returns: The element.
@inlinable
public func get(unsafeAt key: Key) -> Element {
storage.findElement(at: key).unsafelyUnwrapped
@ -184,17 +193,21 @@ public struct UnorderedSparseSet<Element, Key: Hashable & Codable> {
storage.remove(at: key)?.element
}
/// Removes all keys and elements from the set.
/// - Parameter keepingCapacity: A Boolean value that indicates whether the set should maintain it's capacity.
@inlinable
public func removeAll(keepingCapacity: Bool = false) {
storage.removeAll(keepingCapacity: keepingCapacity)
}
/// The first element of the set.
@inlinable public var first: Element? {
storage.first
}
}
extension UnorderedSparseSet where Key == Int {
/// Retrieve or set an element using the key.
@inlinable
public subscript(key: Key) -> Element {
get {