From fdf08687d630ad74c5420d2b3e567b65d862f09e Mon Sep 17 00:00:00 2001 From: Joseph Heck Date: Thu, 31 Oct 2024 22:52:41 -0700 Subject: [PATCH] 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 --- .spi.yml | 4 + Makefile | 34 +++ Package.swift | 3 + README.md | 3 +- .../Documentation.docc/Documentation.md | 112 ++++++++++ .../GettingStartedWithFirebladeECS.md | 210 ++++++++++++++++++ Sources/FirebladeECS/Hashing.swift | 24 +- .../FirebladeECS/ManagedContiguousArray.swift | 28 +++ Sources/FirebladeECS/UnorderedSparseSet.swift | 25 ++- 9 files changed, 427 insertions(+), 16 deletions(-) create mode 100644 .spi.yml create mode 100644 Sources/FirebladeECS/Documentation.docc/Documentation.md create mode 100644 Sources/FirebladeECS/Documentation.docc/GettingStartedWithFirebladeECS.md diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..c223ee8 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [FirebladeECS] diff --git a/Makefile b/Makefile index 7518a32..b2994d4 100644 --- a/Makefile +++ b/Makefile @@ -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' \ No newline at end of file diff --git a/Package.swift b/Package.swift index 424738d..0c39cc6 100644 --- a/Package.swift +++ b/Package.swift @@ -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"]), diff --git a/README.md b/README.md index d554942..2ae9419 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Sources/FirebladeECS/Documentation.docc/Documentation.md b/Sources/FirebladeECS/Documentation.docc/Documentation.md new file mode 100644 index 0000000..fe1204a --- /dev/null +++ b/Sources/FirebladeECS/Documentation.docc/Documentation.md @@ -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 + +- +- ``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`` diff --git a/Sources/FirebladeECS/Documentation.docc/GettingStartedWithFirebladeECS.md b/Sources/FirebladeECS/Documentation.docc/GettingStartedWithFirebladeECS.md new file mode 100644 index 0000000..00be102 --- /dev/null +++ b/Sources/FirebladeECS/Documentation.docc/GettingStartedWithFirebladeECS.md @@ -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 + + 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) + +``` + diff --git a/Sources/FirebladeECS/Hashing.swift b/Sources/FirebladeECS/Hashing.swift index 033b946..9a45a73 100644 --- a/Sources/FirebladeECS/Hashing.swift +++ b/Sources/FirebladeECS/Hashing.swift @@ -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(combine hashValues: H) -> Int where H.Element: Hashable { @@ -79,11 +83,13 @@ extension EntityComponentHash { // MARK: - string hashing -/// +/// 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* /// - /// + /// Implementation from 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. /// /// diff --git a/Sources/FirebladeECS/ManagedContiguousArray.swift b/Sources/FirebladeECS/ManagedContiguousArray.swift index 9f00950..e418e92 100644 --- a/Sources/FirebladeECS/ManagedContiguousArray.swift +++ b/Sources/FirebladeECS/ManagedContiguousArray.swift @@ -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 { public typealias Index = Int @@ -11,16 +12,24 @@ public struct ManagedContiguousArray { @usableFromInline var size: Int = 0 @usableFromInline var store: ContiguousArray = [] + /// Creates a new array. + /// - Parameter minCount: The minimum number of elements, which defaults to `4096`. public init(minCount: Int = 4096) { chunkSize = minCount store = ContiguousArray(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 { 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 { 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 { 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 { store += ContiguousArray(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) diff --git a/Sources/FirebladeECS/UnorderedSparseSet.swift b/Sources/FirebladeECS/UnorderedSparseSet.swift index 7999b5a..3c416ec 100644 --- a/Sources/FirebladeECS/UnorderedSparseSet.swift +++ b/Sources/FirebladeECS/UnorderedSparseSet.swift @@ -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 { } } + /// Creates a new sparse set. public init() { self.init(storage: Storage()) } @@ -141,21 +142,26 @@ public struct UnorderedSparseSet { @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 { /// 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 { 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 {