diff --git a/.gitignore b/.gitignore index 3572c28..58f4b72 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ fastlane/test_output Gemfile* Icon Network Trash Folder -Package.resolved Packages playground.xcworkspace Temporary Items diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5770440..863f3c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: swift:5.0 +image: swift:5.2.2 #before_script: #- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" diff --git a/.swift-version b/.swift-version index 50e2274..ce7f2b4 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.0.3 +5.2.2 diff --git a/.swiftlint.yml b/.swiftlint.yml index 5aa8fe1..f7c8e45 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -57,7 +57,7 @@ opt_in_rules: - multiline_arguments - multiline_function_chains - multiline_parameters - - multiline_parameters_brackets + #- multiline_parameters_brackets - nimble_operator - no_extension_access_modifier - number_separator diff --git a/.travis.yml b/.travis.yml index c989ccd..d17894a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,14 +11,4 @@ script: - swift package reset - swift build - swift test -env: - - BADGE=linux - - BADGE=osx -# hack to get some OS-specific badges -# see: https://github.com/travis-ci/travis-ci/issues/9579 -matrix: - exclude: - - os: linux - env: BADGE=osx - - os: osx - env: BADGE=linux + \ No newline at end of file diff --git a/Makefile b/Makefile index 58164fa..aaa0d8b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,23 @@ +# Version 1.0.0 +UNAME_S := $(shell uname -s) + +# Lint lint: swiftlint autocorrect --format swiftlint lint --quiet +lintErrorOnly: + @swiftlint autocorrect --format --quiet + @swiftlint lint --quiet | grep error + +# Git +precommit: lint genLinuxTests + +submodule: + git submodule init + git submodule update --recursive + +# Tests genLinuxTests: swift test --generate-linuxmain swiftlint autocorrect --format --path Tests/ @@ -9,6 +25,21 @@ genLinuxTests: test: genLinuxTests swift test +# Package +latest: + swift package update + +resolve: + swift package resolve + +# 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 @@ -19,19 +50,7 @@ clean: cleanArtifacts: swift package clean -genXcode: - swift package generate-xcodeproj --enable-code-coverage --skip-extra-files - -latest: - swift package update - -resolve: - swift package resolve - -genXcodeOpen: genXcode - open *.xcodeproj - -precommit: lint genLinuxTests - +# Test links in README +# requires testReadme: - markdown-link-check -p -v ./README.md + markdown-link-check -p -v ./README.md \ No newline at end of file diff --git a/README.md b/README.md index d3eb348..584e821 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Fireblade ECS (Entity-Component System) [![Build Status](https://travis-ci.com/fireblade-engine/ecs.svg?branch=master)](https://travis-ci.com/fireblade-engine/ecs) [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) -[![swift version](https://img.shields.io/badge/swift-5.0+-brightgreen.svg)](https://swift.org/download) +[![swift version](https://img.shields.io/badge/swift-5+-brightgreen.svg)](https://swift.org/download) [![platforms](https://img.shields.io/badge/platforms-%20macOS%20|%20iOS%20|%20tvOS%20|%20watchOS-brightgreen.svg)](#) [![platforms](https://img.shields.io/badge/platforms-linux-brightgreen.svg)](#) @@ -21,7 +21,7 @@ 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.0](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](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. diff --git a/Sources/FirebladeECS/Component+Access.swift b/Sources/FirebladeECS/Component+Access.swift deleted file mode 100644 index ebced3f..0000000 --- a/Sources/FirebladeECS/Component+Access.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Component+Access.swift -// -// -// Created by Christian Treffs on 25.06.19. -// - -#if swift(>=5.1) -@dynamicMemberLookup -public struct ReadableOnly where Comp: Component { - @usableFromInline let component: Comp - - public init(_ component: Comp) { - self.component = component - } - - @inlinable - public subscript(dynamicMember keyPath: KeyPath) -> C { - return component[keyPath: keyPath] - } -} - -@dynamicMemberLookup -public struct Writable where Comp: Component { - @usableFromInline let component: Comp - - public init(_ component: Comp) { - self.component = component - } - - @inlinable - public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> C { - nonmutating get { return component[keyPath: keyPath] } - nonmutating set { component[keyPath: keyPath] = newValue } - } -} -#endif diff --git a/Sources/FirebladeECS/Component.swift b/Sources/FirebladeECS/Component.swift index 6764e89..23adf94 100644 --- a/Sources/FirebladeECS/Component.swift +++ b/Sources/FirebladeECS/Component.swift @@ -7,14 +7,17 @@ /// **Component** /// -/// A component represents the raw data for one aspect of the object, -/// and how it interacts with the world. -public protocol Component: class { +/// A component represents the raw data for one aspect of an entity. +public protocol Component: AnyObject { + /// Unique, immutable identifier of this component type. static var identifier: ComponentIdentifier { get } + + /// Unique, immutable identifier of this component type. var identifier: ComponentIdentifier { get } } extension Component { - public static var identifier: ComponentIdentifier { return ComponentIdentifier(Self.self) } - @inlinable public var identifier: ComponentIdentifier { return Self.identifier } + public static var identifier: ComponentIdentifier { ComponentIdentifier(Self.self) } + @inline(__always) + public var identifier: ComponentIdentifier { Self.identifier } } diff --git a/Sources/FirebladeECS/ComponentIdentifier.swift b/Sources/FirebladeECS/ComponentIdentifier.swift index d3ae5d7..b3ef562 100644 --- a/Sources/FirebladeECS/ComponentIdentifier.swift +++ b/Sources/FirebladeECS/ComponentIdentifier.swift @@ -6,11 +6,19 @@ // /// Identifies a component by it's meta type -public struct ComponentIdentifier: Identifiable { - public let id: ObjectIdentifier +public struct ComponentIdentifier { + @usableFromInline + typealias Hash = Int + @usableFromInline + typealias StableId = UInt - init(_ type: T.Type) where T: Component { - self.id = ObjectIdentifier(type) + @usableFromInline let hash: Hash +} + +extension ComponentIdentifier { + @usableFromInline + init(_ componentType: C.Type) where C: Component { + self.hash = Nexus.makeOrGetComponentId(componentType) } } diff --git a/Sources/FirebladeECS/Entity+Component.swift b/Sources/FirebladeECS/Entity+Component.swift index 991a491..900f785 100644 --- a/Sources/FirebladeECS/Entity+Component.swift +++ b/Sources/FirebladeECS/Entity+Component.swift @@ -8,12 +8,12 @@ extension Entity { @inlinable public func get() -> C? where C: Component { - return nexus.get(for: identifier) + nexus.get(for: identifier) } @inlinable public func get(component compType: A.Type = A.self) -> A? where A: Component { - return nexus.get(for: identifier) + nexus.get(for: identifier) } @inlinable diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index d7796ea..29bc66f 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -24,24 +24,24 @@ public struct Entity { /// Returns the number of components for this entity. public var numComponents: Int { - return nexus.count(components: identifier) + nexus.count(components: identifier) } /// Checks if a component with given type is assigned to this entity. /// - Parameter type: the component type. public func has(_ type: C.Type) -> Bool where C: Component { - return has(type.identifier) + has(type.identifier) } /// Checks if a component with a given component identifier is assigned to this entity. /// - Parameter compId: the component identifier. public func has(_ compId: ComponentIdentifier) -> Bool { - return nexus.has(componentId: compId, entityId: identifier) + nexus.has(componentId: compId, entityId: identifier) } /// Checks if this entity has any components. public var hasComponents: Bool { - return nexus.count(components: identifier) > 0 + nexus.count(components: identifier) > 0 } /// Add one or more components to this entity. @@ -74,14 +74,14 @@ public struct Entity { /// - Parameter component: the component. @discardableResult public func remove(_ component: C) -> Entity where C: Component { - return remove(component.identifier) + remove(component.identifier) } /// Remove a component by type from this entity. /// - Parameter compType: the component type. @discardableResult public func remove(_ compType: C.Type) -> Entity where C: Component { - return remove(compType.identifier) + remove(compType.identifier) } /// Remove a component by id from this entity. @@ -106,31 +106,41 @@ public struct Entity { /// - Parameter entity: The child entity. @discardableResult public func addChild(_ entity: Entity) -> Bool { - return nexus.addChild(entity, to: self) + nexus.addChild(entity, to: self) } /// Remove entity as child. /// - Parameter entity: The child entity. @discardableResult public func removeChild(_ entity: Entity) -> Bool { - return nexus.removeChild(entity, from: self) + nexus.removeChild(entity, from: self) } /// Removes all children from this entity. public func removeAllChildren() { - return nexus.removeAllChildren(from: self) + nexus.removeAllChildren(from: self) } /// Returns the number of children for this entity. public var numChildren: Int { - return nexus.numChildren(for: self) + nexus.numChildren(for: self) } } -// MARK: - Equatable extension Entity: Equatable { public static func == (lhs: Entity, rhs: Entity) -> Bool { - return lhs.nexus == rhs.nexus && - lhs.identifier == rhs.identifier + lhs.nexus === rhs.nexus && lhs.identifier == rhs.identifier + } +} + +extension Entity: CustomStringConvertible { + public var description: String { + "" + } +} + +extension Entity: CustomDebugStringConvertible { + public var debugDescription: String { + "" } } diff --git a/Sources/FirebladeECS/EntityIdentifier.swift b/Sources/FirebladeECS/EntityIdentifier.swift index 3bf35d1..0ccbade 100644 --- a/Sources/FirebladeECS/EntityIdentifier.swift +++ b/Sources/FirebladeECS/EntityIdentifier.swift @@ -5,24 +5,18 @@ // Created by Christian Treffs on 08.10.17. // -public struct EntityIdentifier: Identifiable { - /// provides 4294967295 unique identifiers since it's constrained to UInt32 - invalid. - public let id: Int +public struct EntityIdentifier { + static let invalid = EntityIdentifier(.max) - public init(_ uint32: UInt32) { + /// provides 4294967295 unique identifiers since it's constrained to UInt32 - invalid. + @usableFromInline let id: Int + + @usableFromInline + init(_ uint32: UInt32) { self.id = Int(uint32) } } -extension EntityIdentifier { - public static let invalid = EntityIdentifier(.max) -} extension EntityIdentifier: Equatable { } extension EntityIdentifier: Hashable { } extension EntityIdentifier: Codable { } -extension EntityIdentifier: Comparable { - @inlinable - public static func < (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool { - return lhs.id < rhs.id - } -} diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index eded766..dca646f 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -18,39 +18,38 @@ public struct Family where R: FamilyRequirementsManaging { } @inlinable public var memberIds: UnorderedSparseSet { - return nexus.members(withFamilyTraits: traits) + nexus.members(withFamilyTraits: traits) } @inlinable public var count: Int { - return memberIds.count + memberIds.count } @inlinable public var isEmpty: Bool { - return memberIds.isEmpty + memberIds.isEmpty } @inlinable public func canBecomeMember(_ entity: Entity) -> Bool { - return nexus.canBecomeMember(entity, in: traits) + nexus.canBecomeMember(entity, in: traits) } @inlinable public func isMember(_ entity: Entity) -> Bool { - return nexus.isMember(entity, in: traits) + nexus.isMember(entity, in: traits) } } -// MARK: - Equatable extension Family: Equatable { public static func == (lhs: Family, rhs: Family) -> Bool { - return lhs.nexus == rhs.nexus && + lhs.nexus === rhs.nexus && lhs.traits == rhs.traits } } extension Family: Sequence { __consuming public func makeIterator() -> ComponentsIterator { - return ComponentsIterator(family: self) + ComponentsIterator(family: self) } } @@ -59,7 +58,7 @@ extension Family: LazySequenceProtocol { } // MARK: - components iterator extension Family { public struct ComponentsIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSetIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { @@ -82,11 +81,11 @@ extension Family.ComponentsIterator: LazySequenceProtocol { } // MARK: - entity iterator extension Family { @inlinable public var entities: EntityIterator { - return EntityIterator(family: self) + EntityIterator(family: self) } public struct EntityIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSetIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { @@ -108,11 +107,11 @@ extension Family.EntityIterator: LazySequenceProtocol { } // MARK: - entity component iterator extension Family { @inlinable public var entityAndComponents: EntityComponentIterator { - return EntityComponentIterator(family: self) + EntityComponentIterator(family: self) } public struct EntityComponentIterator: IteratorProtocol { - @usableFromInline var memberIdsIterator: UnorderedSparseSetIterator + @usableFromInline var memberIdsIterator: UnorderedSparseSet.ElementIterator @usableFromInline unowned let nexus: Nexus public init(family: Family) { @@ -136,7 +135,7 @@ extension Family.EntityComponentIterator: LazySequenceProtocol { } extension Family { @inlinable public func descendRelatives(from root: Entity) -> RelativesIterator { - return RelativesIterator(family: self, root: root) + RelativesIterator(family: self, root: root) } public struct RelativesIterator: IteratorProtocol { @@ -161,7 +160,7 @@ extension Family { } mutating func aggregateRelativesBreathFirst(_ parent: EntityIdentifier) { - guard let children = nexus.parentChildrenMap[parent] else { + guard let children = nexus.childrenByParentEntity[parent] else { return } children diff --git a/Sources/FirebladeECS/Family1.swift b/Sources/FirebladeECS/Family1.swift index 8e0f42c..b32409d 100644 --- a/Sources/FirebladeECS/Family1.swift +++ b/Sources/FirebladeECS/Family1.swift @@ -37,8 +37,8 @@ extension Nexus { requires componentA: A.Type, excludesAll excludedComponents: Component.Type... ) -> Family1 where A: Component { - return Family1(nexus: self, - requiresAll: componentA, - excludesAll: excludedComponents) + Family1(nexus: self, + requiresAll: componentA, + excludesAll: excludedComponents) } } diff --git a/Sources/FirebladeECS/Family2.swift b/Sources/FirebladeECS/Family2.swift index 8d33200..388002f 100644 --- a/Sources/FirebladeECS/Family2.swift +++ b/Sources/FirebladeECS/Family2.swift @@ -43,7 +43,7 @@ extension Nexus { _ componentB: B.Type, excludesAll excludedComponents: Component.Type... ) -> Family2 where A: Component, B: Component { - return Family2( + Family2( nexus: self, requiresAll: (componentA, componentB), excludesAll: excludedComponents diff --git a/Sources/FirebladeECS/Family3.swift b/Sources/FirebladeECS/Family3.swift index 74446e2..bc3b941 100644 --- a/Sources/FirebladeECS/Family3.swift +++ b/Sources/FirebladeECS/Family3.swift @@ -49,7 +49,7 @@ extension Nexus { _ componentC: C.Type, excludesAll excludedComponents: Component.Type... ) -> Family3 where A: Component, B: Component, C: Component { - return Family3( + Family3( nexus: self, requiresAll: (componentA, componentB, componentC), excludesAll: excludedComponents diff --git a/Sources/FirebladeECS/Family4.swift b/Sources/FirebladeECS/Family4.swift index 88050f7..f6ab1a6 100644 --- a/Sources/FirebladeECS/Family4.swift +++ b/Sources/FirebladeECS/Family4.swift @@ -55,7 +55,7 @@ extension Nexus { _ componentD: D.Type, excludesAll excludedComponents: Component.Type... ) -> Family4 where A: Component, B: Component, C: Component, D: Component { - return Family4( + Family4( nexus: self, requiresAll: (componentA, componentB, componentC, componentD), excludesAll: excludedComponents diff --git a/Sources/FirebladeECS/Family5.swift b/Sources/FirebladeECS/Family5.swift index 0d76248..02ee245 100644 --- a/Sources/FirebladeECS/Family5.swift +++ b/Sources/FirebladeECS/Family5.swift @@ -62,7 +62,7 @@ extension Nexus { _ componentE: E.Type, excludesAll excludedComponents: Component.Type... ) -> Family5 where A: Component, B: Component, C: Component, D: Component, E: Component { - return Family5( + Family5( nexus: self, requiresAll: (componentA, componentB, componentC, componentD, componentE), excludesAll: excludedComponents diff --git a/Sources/FirebladeECS/FamilyTraitSet.swift b/Sources/FirebladeECS/FamilyTraitSet.swift index bf1220c..be195aa 100644 --- a/Sources/FirebladeECS/FamilyTraitSet.swift +++ b/Sources/FirebladeECS/FamilyTraitSet.swift @@ -23,50 +23,48 @@ public struct FamilyTraitSet { self.setHash = FirebladeECS.hash(combine: [requiresAll, excludesAll]) } - // MARK: - match @inlinable public func isMatch(components: Set) -> Bool { - return hasAll(components) && hasNone(components) + hasAll(components) && hasNone(components) } @inlinable public func hasAll(_ components: Set) -> Bool { - return requiresAll.isSubset(of: components) + requiresAll.isSubset(of: components) } @inlinable public func hasNone(_ components: Set) -> Bool { - return excludesAll.isDisjoint(with: components) + excludesAll.isDisjoint(with: components) } - // MARK: - valid @inlinable public static func isValid(requiresAll: Set, excludesAll: Set) -> Bool { - return !requiresAll.isEmpty && + !requiresAll.isEmpty && requiresAll.isDisjoint(with: excludesAll) } } -// MARK: - Equatable extension FamilyTraitSet: Equatable { public static func == (lhs: FamilyTraitSet, rhs: FamilyTraitSet) -> Bool { - return lhs.setHash == rhs.setHash + lhs.setHash == rhs.setHash } } -// MARK: - Hashable extension FamilyTraitSet: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(setHash) } } -extension FamilyTraitSet: CustomStringConvertible, CustomDebugStringConvertible { +extension FamilyTraitSet: CustomStringConvertible { @inlinable public var description: String { - return "" - } - - @inlinable public var debugDescription: String { - return "" + "" + } +} + +extension FamilyTraitSet: CustomDebugStringConvertible { + @inlinable public var debugDescription: String { + "" } } diff --git a/Sources/FirebladeECS/Hashing.swift b/Sources/FirebladeECS/Hashing.swift index 104e530..2bcbe00 100644 --- a/Sources/FirebladeECS/Hashing.swift +++ b/Sources/FirebladeECS/Hashing.swift @@ -83,3 +83,52 @@ extension EntityComponentHash { return EntityIdentifier(UInt32(truncatingIfNeeded: entityId)) } } + +// MARK: - string hashing +/// https://stackoverflow.com/a/52440609 +public enum StringHashing { + /// *Waren Singer djb2* + /// + /// + public static func singer_djb2(_ utf8String: String) -> UInt { + var hash = UInt(5381) + var iter = utf8String.unicodeScalars.makeIterator() + while let char = iter.next() { + hash = 127 * (hash & 0x00ffffffffffffff) + UInt(char.value) + } + return hash + } + + /// *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]; + /// The magic of number 33 (why it works better than many other constants, prime or not) has never been adequately explained. + /// + /// + public static func bernstein_djb2(_ string: String) -> UInt { + var hash: UInt = 5381 + var iter = string.unicodeScalars.makeIterator() + while let char = iter.next() { + hash = (hash << 5) &+ hash &+ UInt(char.value) + //hash = ((hash << 5) + hash) + UInt(c.value) + } + return hash + } + + /// *sdbm* + /// + /// This algorithm was created for sdbm (a public-domain reimplementation of ndbm) database library. + /// It was found to do well in scrambling bits, causing better distribution of the keys and fewer splits. + /// It also happens to be a good general hashing function with good distribution. + /// + /// + public static func sdbm(_ string: String) -> UInt { + var hash: UInt = 0 + var iter = string.unicodeScalars.makeIterator() + while let char = iter.next() { + hash = (UInt(char.value) &+ (hash << 6) &+ (hash << 16)) + } + return hash + } +} diff --git a/Sources/FirebladeECS/Identifiable.swift b/Sources/FirebladeECS/Identifiable.swift deleted file mode 100644 index bd0f924..0000000 --- a/Sources/FirebladeECS/Identifiable.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Identifiable.swift -// -// -// Created by Christian Treffs on 05.10.19. -// - -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2019 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -#if swift(<5.1) -/// A class of types whose instances hold the value of an entity with stable identity. -public protocol Identifiable { - /// A type representing the stable identity of the entity associated with `self`. - associatedtype ID: Hashable - - /// The stable identity of the entity associated with `self`. - var id: ID { get } -} - -extension Identifiable where Self: AnyObject { - public var id: ObjectIdentifier { - return ObjectIdentifier(self) - } -} -#endif diff --git a/Sources/FirebladeECS/ManagedContiguousArray.swift b/Sources/FirebladeECS/ManagedContiguousArray.swift deleted file mode 100644 index b6fbdb7..0000000 --- a/Sources/FirebladeECS/ManagedContiguousArray.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ManagedContiguousArray.swift -// FirebladeECS -// -// Created by Christian Treffs on 28.10.17. -// - -public class ManagedContiguousArray { - public typealias Index = Int - private let chunkSize: Int - private var size: Int = 0 - private var store: ContiguousArray = [] - - public init(minCount: Int = 4096) { - chunkSize = minCount - store = ContiguousArray(repeating: nil, count: minCount) - } - - deinit { - clear() - } - - public var count: Int { - return size - } - - @discardableResult - public func insert(_ element: Element, at index: Int) -> Bool { - if needsToGrow(index) { - grow(to: index) - } - if store[index] == nil { - size += 1 - } - store[index] = element - return true - } - public func contains(_ index: Index) -> Bool { - if store.count <= index { - return false - } - return store[index] != nil - } - - public func get(at index: Index) -> Element? { - return store[index] - } - - public func get(unsafeAt index: Index) -> Element { - return store[index].unsafelyUnwrapped - } - - @discardableResult - public func remove(at index: Index) -> Bool { - if store[index] != nil { - size -= 1 - } - store[index] = nil - if size == 0 { - clear() - } - return true - } - - public func clear(keepingCapacity: Bool = false) { - size = 0 - store.removeAll(keepingCapacity: keepingCapacity) - } - - private func needsToGrow(_ index: Index) -> Bool { - return index > store.count - 1 - } - - private func grow(to index: Index) { - let newCapacity: Int = calculateCapacity(to: index) - let newCount: Int = newCapacity - store.count - store += ContiguousArray(repeating: nil, count: newCount) - } - - private func calculateCapacity(to index: Index) -> Int { - let delta = Float(index) / Float(chunkSize) - let multiplier = Int(delta.rounded(.up)) + 1 - return multiplier * chunkSize - } -} - -// MARK: - Equatable -extension ManagedContiguousArray: Equatable where Element: Equatable { - public static func == (lhs: ManagedContiguousArray, rhs: ManagedContiguousArray) -> Bool { - return lhs.store == rhs.store - } -} diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 4a0ec73..664f6bd 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -7,7 +7,7 @@ extension Nexus { public final var numComponents: Int { - return componentsByType.reduce(0) { $0 + $1.value.count } + componentsByType.reduce(0) { $0 + $1.value.count } } public final func has(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> Bool { @@ -18,14 +18,14 @@ extension Nexus { } public final func count(components entityId: EntityIdentifier) -> Int { - return componentIdsByEntity[entityId]?.count ?? 0 + componentIdsByEntity[entityId]?.count ?? 0 } public final func assign(component: Component, to entity: Entity) { let componentId: ComponentIdentifier = component.identifier let entityId: EntityIdentifier = entity.identifier - /// test if component is already assigned + // test if component is already assigned guard !has(componentId: componentId, entityId: entityId) else { delegate?.nexusNonFatalError("ComponentAdd collision: \(entityId) already has a component \(component)") assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)") @@ -34,7 +34,7 @@ extension Nexus { // add component instances to uniform component stores if componentsByType[componentId] == nil { - componentsByType[componentId] = ManagedContiguousArray() + componentsByType[componentId] = UnorderedSparseSet() } componentsByType[componentId]?.insert(component, at: entityId.id) @@ -76,13 +76,13 @@ extension Nexus { @inlinable public final func get(unsafeComponentFor entityId: EntityIdentifier) -> C where C: Component { let component: Component = get(unsafeComponent: C.identifier, for: entityId) - /// components are guaranteed to be reference tyes so unsafeDowncast is applicable here + // components are guaranteed to be reference tyes so unsafeDowncast is applicable here return unsafeDowncast(component, to: C.self) } @inlinable public final func get(components entityId: EntityIdentifier) -> Set? { - return componentIdsByEntity[entityId] + componentIdsByEntity[entityId] } @discardableResult diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index b40308b..6422b13 100644 --- a/Sources/FirebladeECS/Nexus+Entity.swift +++ b/Sources/FirebladeECS/Nexus+Entity.swift @@ -17,10 +17,9 @@ extension Nexus { @discardableResult public func createEntity() -> Entity { let newEntityIdentifier: EntityIdentifier = nextEntityId() - let newEntity = Entity(nexus: self, id: newEntityIdentifier) - entityStorage.insert(newEntity, at: newEntityIdentifier.id) + entityStorage.insert(newEntityIdentifier, at: newEntityIdentifier.id) delegate?.nexusEvent(EntityCreated(entityId: newEntityIdentifier)) - return newEntity + return Entity(nexus: self, id: newEntityIdentifier) } @discardableResult @@ -32,31 +31,37 @@ extension Nexus { /// Number of entities in nexus. public var numEntities: Int { - return entityStorage.count + entityStorage.count } public func exists(entity entityId: EntityIdentifier) -> Bool { - return entityStorage.contains(entityId.id) + entityStorage.contains(entityId.id) } public func get(entity entityId: EntityIdentifier) -> Entity? { - return entityStorage.get(at: entityId.id) + guard let id = entityStorage.get(at: entityId.id) else { + return nil + } + return Entity(nexus: self, id: id) } public func get(unsafeEntity entityId: EntityIdentifier) -> Entity { - return entityStorage.get(unsafeAt: entityId.id) + Entity(nexus: self, id: entityStorage.get(unsafeAt: entityId.id)) } @discardableResult public func destroy(entity: Entity) -> Bool { - let entityId: EntityIdentifier = entity.identifier + self.destroy(entityId: entity.identifier) + } + @discardableResult + public func destroy(entityId: EntityIdentifier) -> Bool { guard entityStorage.remove(at: entityId.id) != nil else { delegate?.nexusNonFatalError("EntityRemove failure: no entity \(entityId) to remove") return false } - removeAllChildren(from: entity) + removeAllChildren(from: entityId) if removeAll(componentes: entityId) { update(familyMembership: entityId) diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 3895b4d..5b5f9a4 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -7,7 +7,7 @@ extension Nexus { public final var numFamilies: Int { - return familyMembersByTraits.keys.count + familyMembersByTraits.keys.count } public func canBecomeMember(_ entity: Entity, in traits: FamilyTraitSet) -> Bool { @@ -19,18 +19,18 @@ extension Nexus { } public func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { - return familyMembersByTraits[traits] ?? UnorderedSparseSet() + familyMembersByTraits[traits] ?? UnorderedSparseSet() } public func isMember(_ entity: Entity, in family: FamilyTraitSet) -> Bool { - return isMember(entity.identifier, in: family) + isMember(entity.identifier, in: family) } public func isMember(_ entityId: EntityIdentifier, in family: FamilyTraitSet) -> Bool { - return isMember(entity: entityId, inFamilyWithTraits: family) + isMember(entity: entityId, inFamilyWithTraits: family) } public func isMember(entity entityId: EntityIdentifier, inFamilyWithTraits traits: FamilyTraitSet) -> Bool { - return members(withFamilyTraits: traits).contains(entityId.id) + members(withFamilyTraits: traits).contains(entityId.id) } } diff --git a/Sources/FirebladeECS/Nexus+FamilyUpdate.swift b/Sources/FirebladeECS/Nexus+FamilyUpdate.swift index 2c1ab51..7424b27 100644 --- a/Sources/FirebladeECS/Nexus+FamilyUpdate.swift +++ b/Sources/FirebladeECS/Nexus+FamilyUpdate.swift @@ -13,14 +13,15 @@ extension Nexus { } familyMembersByTraits[traits] = UnorderedSparseSet() + defer { delegate?.nexusEvent(FamilyCreated(family: traits)) } update(familyMembership: traits) } final func update(familyMembership traits: FamilyTraitSet) { // FIXME: iterating all entities is costly for many entities var iter = entityStorage.makeIterator() - while let entity = iter.next() { - update(membership: traits, for: entity.identifier) + while let entityId = iter.next() { + update(membership: traits, for: entityId) } } @@ -50,25 +51,26 @@ extension Nexus { case (true, false): add(entityWithId: entityId, toFamilyWithTraits: traits) delegate?.nexusEvent(FamilyMemberAdded(member: entityId, toFamily: traits)) - return case (false, true): remove(entityWithId: entityId, fromFamilyWithTraits: traits) delegate?.nexusEvent(FamilyMemberRemoved(member: entityId, from: traits)) - return default: - return + break } } final func add(entityWithId entityId: EntityIdentifier, toFamilyWithTraits traits: FamilyTraitSet) { - precondition(familyMembersByTraits[traits] != nil) - familyMembersByTraits[traits].unsafelyUnwrapped.insert(entityId, at: entityId.id) + familyMembersByTraits[traits]!.insert(entityId, at: entityId.id) } final func remove(entityWithId entityId: EntityIdentifier, fromFamilyWithTraits traits: FamilyTraitSet) { - precondition(familyMembersByTraits[traits] != nil) - familyMembersByTraits[traits].unsafelyUnwrapped.remove(at: entityId.id) + familyMembersByTraits[traits]!.remove(at: entityId.id) + if familyMembersByTraits[traits]!.isEmpty { + // delete family if no more entities are present + familyMembersByTraits[traits] = nil + delegate?.nexusEvent(FamilyDestroyed(family: traits)) + } } } diff --git a/Sources/FirebladeECS/Nexus+SceneGraph.swift b/Sources/FirebladeECS/Nexus+SceneGraph.swift index d8576a4..d806b18 100644 --- a/Sources/FirebladeECS/Nexus+SceneGraph.swift +++ b/Sources/FirebladeECS/Nexus+SceneGraph.swift @@ -8,11 +8,11 @@ extension Nexus { public final func addChild(_ child: Entity, to parent: Entity) -> Bool { let inserted: Bool - if parentChildrenMap[parent.identifier] == nil { - parentChildrenMap[parent.identifier] = [child.identifier] + if childrenByParentEntity[parent.identifier] == nil { + childrenByParentEntity[parent.identifier] = [child.identifier] inserted = true } else { - let (isNewMember, _) = parentChildrenMap[parent.identifier]!.insert(child.identifier) + let (isNewMember, _) = childrenByParentEntity[parent.identifier]!.insert(child.identifier) inserted = isNewMember } if inserted { @@ -22,12 +22,12 @@ extension Nexus { } public final func removeChild(_ child: Entity, from parent: Entity) -> Bool { - return removeChild(child.identifier, from: parent.identifier) + removeChild(child.identifier, from: parent.identifier) } @discardableResult public final func removeChild(_ child: EntityIdentifier, from parent: EntityIdentifier) -> Bool { - let removed: Bool = parentChildrenMap[parent]?.remove(child) != nil + let removed: Bool = childrenByParentEntity[parent]?.remove(child) != nil if removed { delegate?.nexusEvent(ChildRemoved(parent: parent, child: child)) } @@ -35,11 +35,15 @@ extension Nexus { } public final func removeAllChildren(from parent: Entity) { - parentChildrenMap[parent.identifier]?.forEach { removeChild($0, from: parent.identifier) } - return parentChildrenMap[parent.identifier] = nil + self.removeAllChildren(from: parent.identifier) + } + + public final func removeAllChildren(from parentId: EntityIdentifier) { + childrenByParentEntity[parentId]?.forEach { removeChild($0, from: parentId) } + return childrenByParentEntity[parentId] = nil } public final func numChildren(for entity: Entity) -> Int { - return parentChildrenMap[entity.identifier]?.count ?? 0 + childrenByParentEntity[entity.identifier]?.count ?? 0 } } diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index ffe17d2..4559e30 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -6,85 +6,95 @@ // public final class Nexus { - public final weak var delegate: NexusEventDelegate? - /// Main entity storage. /// Entities are tightly packed by EntityIdentifier. - @usableFromInline final var entityStorage: UnorderedSparseSet + @usableFromInline final var entityStorage: UnorderedSparseSet + + /// Entity ids that are currently not used. + @usableFromInline final var freeEntities: [EntityIdentifier] /// - Key: ComponentIdentifier aka component type. /// - Value: Array of component instances of same type (uniform). /// New component instances are appended. - @usableFromInline final var componentsByType: [ComponentIdentifier: ManagedContiguousArray] + @usableFromInline final var componentsByType: [ComponentIdentifier: UnorderedSparseSet] /// - Key: EntityIdentifier aka entity index /// - Value: Set of unique component types (ComponentIdentifier). /// Each element is a component identifier associated with this entity. @usableFromInline final var componentIdsByEntity: [EntityIdentifier: Set] - /// Entity ids that are currently not used. - @usableFromInline final var freeEntities: ContiguousArray + /// - Key: A parent entity id. + /// - Value: Adjacency Set of all associated children. + @usableFromInline final var childrenByParentEntity: [EntityIdentifier: Set] /// - Key: FamilyTraitSet aka component types that make up one distinct family. /// - Value: Tightly packed EntityIdentifiers that represent the association of an entity to the family. @usableFromInline final var familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet] - /// - Key: A parent entity id. - /// - Value: Adjacency Set of all associated children. - @usableFromInline final var parentChildrenMap: [EntityIdentifier: Set] + public final weak var delegate: NexusEventDelegate? - public init() { - entityStorage = UnorderedSparseSet() - componentsByType = [:] - componentIdsByEntity = [:] - freeEntities = ContiguousArray() - familyMembersByTraits = [:] - parentChildrenMap = [:] + public convenience init() { + self.init(entityStorage: UnorderedSparseSet(), + componentsByType: [:], + componentsByEntity: [:], + freeEntities: [], + familyMembersByTraits: [:], + childrenByParentEntity: [:]) } - public final func clear() { - var iter = entityStorage.makeIterator() - while let entity = iter.next() { - destroy(entity: entity) - } - - entityStorage.removeAll() - freeEntities.removeAll() - - assert(entityStorage.isEmpty) - assert(componentsByType.values.reduce(0) { $0 + $1.count } == 0) - assert(componentIdsByEntity.values.reduce(0) { $0 + $1.count } == 0) - assert(freeEntities.isEmpty) - assert(familyMembersByTraits.values.reduce(0) { $0 + $1.count } == 0) - - componentsByType.removeAll() - componentIdsByEntity.removeAll() - familyMembersByTraits.removeAll() - parentChildrenMap.removeAll() + internal init(entityStorage: UnorderedSparseSet, + componentsByType: [ComponentIdentifier: UnorderedSparseSet], + componentsByEntity: [EntityIdentifier: Set], + freeEntities: [EntityIdentifier], + familyMembersByTraits: [FamilyTraitSet: UnorderedSparseSet], + childrenByParentEntity: [EntityIdentifier: Set]) { + self.entityStorage = entityStorage + self.componentsByType = componentsByType + self.componentIdsByEntity = componentsByEntity + self.freeEntities = freeEntities + self.familyMembersByTraits = familyMembersByTraits + self.childrenByParentEntity = childrenByParentEntity } deinit { clear() } + + public final func clear() { + entityStorage.forEach { destroy(entityId: $0) } + entityStorage.removeAll() + freeEntities.removeAll() + componentsByType.removeAll() + componentIdsByEntity.removeAll() + familyMembersByTraits.removeAll() + childrenByParentEntity.removeAll() + } + + public static var knownUniqueComponentTypes: Set { + Set(stableComponentIdentifierMap.keys.map { ComponentIdentifier(hash: $0) }) + } } -// MARK: - Equatable -extension Nexus: Equatable { - @inlinable - public static func == (lhs: Nexus, rhs: Nexus) -> Bool { - return lhs.entityStorage == rhs.entityStorage && - lhs.componentIdsByEntity == rhs.componentIdsByEntity && - lhs.freeEntities == rhs.freeEntities && - lhs.familyMembersByTraits == rhs.familyMembersByTraits && - lhs.componentsByType.keys == rhs.componentsByType.keys && - lhs.parentChildrenMap == rhs.parentChildrenMap - // NOTE: components are not equatable (yet) +// MARK: - centralized component identifier mapping +extension Nexus { + internal static var stableComponentIdentifierMap: [ComponentIdentifier.Hash: ComponentIdentifier.StableId] = [:] + + internal static func makeOrGetComponentId(_ componentType: C.Type) -> ComponentIdentifier.Hash where C: Component { + /// object identifier hash (only stable during runtime) - arbitrary hash is ok. + let objIdHash = ObjectIdentifier(componentType).hashValue + // if we do not know this component type yet - we register a stable identifier generator for it. + if stableComponentIdentifierMap[objIdHash] == nil { + let string = String(describing: C.self) + let stableHash = StringHashing.singer_djb2(string) + stableComponentIdentifierMap[objIdHash] = stableHash + } + return objIdHash } } // MARK: - CustomDebugStringConvertible extension Nexus: CustomDebugStringConvertible { public var debugDescription: String { - return "" + "" } } diff --git a/Sources/FirebladeECS/NexusEvents.swift b/Sources/FirebladeECS/NexusEvents.swift index e686e6d..d82d1e1 100644 --- a/Sources/FirebladeECS/NexusEvents.swift +++ b/Sources/FirebladeECS/NexusEvents.swift @@ -20,10 +20,6 @@ public struct ComponentAdded: NexusEvent { public let toEntity: EntityIdentifier } -public struct ComponentUpdated: NexusEvent { - public let atEnity: EntityIdentifier -} - public struct ComponentRemoved: NexusEvent { public let component: ComponentIdentifier public let from: EntityIdentifier diff --git a/Sources/FirebladeECS/Single.swift b/Sources/FirebladeECS/Single.swift index bc0a50e..014a99a 100644 --- a/Sources/FirebladeECS/Single.swift +++ b/Sources/FirebladeECS/Single.swift @@ -29,17 +29,23 @@ public struct Single where A: SingleComponent { public let entityId: EntityIdentifier } -extension Single: Equatable { } +extension Single: Equatable { + public static func == (lhs: Single, rhs: Single) -> Bool { + lhs.traits == rhs.traits && + lhs.entityId == rhs.entityId && + lhs.nexus === rhs.nexus + } +} extension Single where A: SingleComponent { @inlinable public var component: A { - /// Since we guarantee that the component will always be present by managing the complete lifecycle of the entity - /// and component assignment we may unsafelyUnwrap here. - /// Since components will allways be of reference type (class) we may use unsafeDowncast here for performance reasons. + // Since we guarantee that the component will always be present by managing the complete lifecycle of the entity + // and component assignment we may unsafelyUnwrap here. + // Since components will allways be of reference type (class) we may use unsafeDowncast here for performance reasons. return nexus.get(unsafeComponentFor: entityId) } public var entity: Entity { - return nexus.get(entity: entityId).unsafelyUnwrapped + nexus.get(entity: entityId).unsafelyUnwrapped } } diff --git a/Sources/FirebladeECS/UnorderedSparseSet.swift b/Sources/FirebladeECS/UnorderedSparseSet.swift index 8d07370..ad13dde 100644 --- a/Sources/FirebladeECS/UnorderedSparseSet.swift +++ b/Sources/FirebladeECS/UnorderedSparseSet.swift @@ -5,7 +5,7 @@ // Created by Christian Treffs on 30.10.17. // -open class UnorderedSparseSet { +public struct UnorderedSparseSet { public typealias Index = Int public typealias Key = Int @@ -18,20 +18,20 @@ open class UnorderedSparseSet { @usableFromInline var sparse: [Index: Key] public init() { - sparse = [Index: Key]() - dense = ContiguousArray() + self.init(sparse: [:], dense: []) } - deinit { - removeAll() + init(sparse: [Index: Key], dense: ContiguousArray) { + self.sparse = sparse + self.dense = dense } - public var count: Int { return dense.count } - public var isEmpty: Bool { return dense.isEmpty } + public var count: Int { dense.count } + public var isEmpty: Bool { dense.isEmpty } @inlinable public func contains(_ key: Key) -> Bool { - return find(at: key) != nil + find(at: key) != nil } /// Inset an element for a given key into the set in O(1). @@ -42,7 +42,7 @@ open class UnorderedSparseSet { /// - key: the key /// - Returns: true if new, false if replaced. @discardableResult - public func insert(_ element: Element, at key: Key) -> Bool { + public mutating func insert(_ element: Element, at key: Key) -> Bool { if let (denseIndex, _) = find(at: key) { dense[denseIndex] = Entry(key: key, element: element) return false @@ -69,7 +69,7 @@ open class UnorderedSparseSet { @inlinable public func get(unsafeAt key: Key) -> Element { - return find(at: key).unsafelyUnwrapped.1 + find(at: key).unsafelyUnwrapped.1 } /// Removes the element entry for given key in O(1). @@ -77,7 +77,7 @@ open class UnorderedSparseSet { /// - Parameter key: the key /// - Returns: removed value or nil if key not found. @discardableResult - public func remove(at key: Key) -> Entry? { + public mutating func remove(at key: Key) -> Entry? { guard let (denseIndex, _) = find(at: key) else { return nil } @@ -92,22 +92,17 @@ open class UnorderedSparseSet { } @inlinable - public func removeAll(keepingCapacity: Bool = false) { + public mutating func removeAll(keepingCapacity: Bool = false) { sparse.removeAll(keepingCapacity: keepingCapacity) dense.removeAll(keepingCapacity: keepingCapacity) } - @inlinable - public func makeIterator() -> UnorderedSparseSetIterator { - return UnorderedSparseSetIterator(self) - } - /// Removes an element from the set and retuns it in O(1). /// The removed element is replaced with the last element of the set. /// /// - Parameter denseIndex: the dense index /// - Returns: the element entry - private func swapRemove(at denseIndex: Int) -> Entry { + private mutating func swapRemove(at denseIndex: Int) -> Entry { dense.swapAt(denseIndex, dense.count - 1) return dense.removeLast() } @@ -128,7 +123,7 @@ open class UnorderedSparseSet { @inlinable public subscript(position: Index) -> Element { get { - return get(unsafeAt: position) + get(unsafeAt: position) } set(newValue) { @@ -137,30 +132,42 @@ open class UnorderedSparseSet { } @inlinable public var first: Element? { - return dense.first?.element + dense.first?.element } @inlinable public var last: Element? { - return dense.last?.element + dense.last?.element } } +// MARK: - Sequence +extension UnorderedSparseSet: Sequence { + public __consuming func makeIterator() -> ElementIterator { + ElementIterator(self) + } + + // MARK: - UnorderedSparseSetIterator + public struct ElementIterator: IteratorProtocol { + public private(set) var iterator: IndexingIterator.Entry>> + + public init(_ sparseSet: UnorderedSparseSet) { + iterator = sparseSet.dense.makeIterator() + } + + public mutating func next() -> Element? { + iterator.next()?.element + } + } +} + +// MARK: - Equatable extension UnorderedSparseSet.Entry: Equatable where Element: Equatable { } extension UnorderedSparseSet: Equatable where Element: Equatable { public static func == (lhs: UnorderedSparseSet, rhs: UnorderedSparseSet) -> Bool { - return lhs.dense == rhs.dense && lhs.sparse == rhs.sparse + lhs.dense == rhs.dense && lhs.sparse == rhs.sparse } } -// MARK: - UnorderedSparseSetIterator -public struct UnorderedSparseSetIterator: IteratorProtocol { - public private(set) var iterator: IndexingIterator.Entry>> - - public init(_ sparseSet: UnorderedSparseSet) { - iterator = sparseSet.dense.makeIterator() - } - - public mutating func next() -> Element? { - return iterator.next()?.element - } -} +// MARK: - Codable +extension UnorderedSparseSet.Entry: Codable where Element: Codable { } +extension UnorderedSparseSet: Codable where Element: Codable { } diff --git a/Tests/FirebladeECSPerformanceTests/Base.swift b/Tests/FirebladeECSPerformanceTests/Base.swift index 3b5834d..b50f07b 100644 --- a/Tests/FirebladeECSPerformanceTests/Base.swift +++ b/Tests/FirebladeECSPerformanceTests/Base.swift @@ -7,9 +7,12 @@ import FirebladeECS -class EmptyComponent: Component { } +class EmptyComponent: Component { + +} class Name: Component { + var name: String init(name: String) { self.name = name @@ -17,6 +20,7 @@ class Name: Component { } class Position: Component { + var x: Int var y: Int init(x: Int, y: Int) { @@ -26,6 +30,7 @@ class Position: Component { } class Velocity: Component { + var a: Float init(a: Float) { self.a = a @@ -33,6 +38,7 @@ class Velocity: Component { } class Party: Component { + var partying: Bool init(partying: Bool) { self.partying = partying @@ -40,6 +46,7 @@ class Party: Component { } class Color: Component { + var r: UInt8 = 0 var g: UInt8 = 0 var b: UInt8 = 0 diff --git a/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift b/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift index 14721d5..8dfebc8 100644 --- a/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift +++ b/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift @@ -1,5 +1,5 @@ // -// ComponentPerformanceTests.swift +// ComponentIdentifierTests.swift // FirebladeECSPerformanceTests // // Created by Christian Treffs on 14.02.19. @@ -8,9 +8,11 @@ import FirebladeECS import XCTest -class ComponentTests: XCTestCase { +class ComponentIdentifierTests: XCTestCase { + + /// debug: 0.456 sec func testMeasureStaticComponentIdentifier() { - let number: Int = 10_000 + let number: Int = 1_000_000 measure { for _ in 0.. = Set([14_561_291, 26_451_562, 34_562_182, 488_972_556, 5_128_426_962, 68_211_812]) let b: Set = Set([1_083_838, 912_312, 83_333, 71_234_555, 4_343_234]) @@ -23,6 +32,8 @@ class HashingPerformanceTests: XCTestCase { } } + /// release: 0.494 sec + /// debug: 1.026 sec func testMeasureSetOfSetHash() { let a: Set = Set([14_561_291, 26_451_562, 34_562_182, 488_972_556, 5_128_426_962, 68_211_812]) let b: Set = Set([1_083_838, 912_312, 83_333, 71_234_555, 4_343_234]) @@ -36,4 +47,58 @@ class HashingPerformanceTests: XCTestCase { } } } + + /// release: 0.098 sec + /// debug: 16.702 sec + func testMeasureBernsteinDjb2() throws { + try XCTSkipIf(isDebug) + let string = "The quick brown fox jumps over the lazy dog" + measure { + for _ in 0..<1_000_000 { + let hash = StringHashing.bernstein_djb2(string) + _ = hash + } + } + } + + /// release: 0.087 sec + /// debug: 2.613 sec + func testMeasureSingerDjb2() throws { + let string = "The quick brown fox jumps over the lazy dog" + measure { + for _ in 0..<1_000_000 { + let hash = StringHashing.singer_djb2(string) + _ = hash + } + } + } + + /// release: 0.088 sec + /// debug: 30.766 sec + func testMeasureSDBM() throws { + try XCTSkipIf(isDebug) + let string = "The quick brown fox jumps over the lazy dog" + measure { + for _ in 0..<1_000_000 { + let hash = StringHashing.sdbm(string) + _ = hash + } + } + } + + /// release: 0.036 sec + /// debug: 0.546 sec + func testMeasureSwiftHasher() throws { + try XCTSkipIf(isDebug) + let string = "The quick brown fox jumps over the lazy dog" + measure { + for _ in 0..<1_000_000 { + var hasher = Hasher() + hasher.combine(string) + let hash = hasher.finalize() + _ = hash + } + } + } + } diff --git a/Tests/FirebladeECSPerformanceTests/TypeIdentifierPerformanceTests.swift b/Tests/FirebladeECSPerformanceTests/TypeIdentifierPerformanceTests.swift new file mode 100644 index 0000000..49c895f --- /dev/null +++ b/Tests/FirebladeECSPerformanceTests/TypeIdentifierPerformanceTests.swift @@ -0,0 +1,94 @@ +// +// TypeIdentifierPerformanceTests.swift +// +// +// Created by Christian Treffs on 05.10.19. +// + +import FirebladeECS +import XCTest + +final class TypeIdentifierPerformanceTests: XCTestCase { + let maxIterations: Int = 100_000 + + // release: 0.000 sec + // debug: 0.051 sec + func testPerformanceObjectIdentifier() { + measure { + for _ in 0.. [XCTestCaseEntry] { return [ - testCase(ComponentTests.__allTests__ComponentTests), + testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests), testCase(HashingPerformanceTests.__allTests__HashingPerformanceTests), + testCase(TypeIdentifierPerformanceTests.__allTests__TypeIdentifierPerformanceTests), testCase(TypedFamilyPerformanceTests.__allTests__TypedFamilyPerformanceTests) ] } diff --git a/Tests/FirebladeECSTests/AccessTests.swift b/Tests/FirebladeECSTests/AccessTests.swift deleted file mode 100644 index 0c51137..0000000 --- a/Tests/FirebladeECSTests/AccessTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// AccessTests.swift -// -// -// Created by Christian Treffs on 25.06.19. -// - -import FirebladeECS -import XCTest - -#if swift(>=5.1) -class AccessTests: XCTestCase { - func testReadOnly() { - let pos = Position(x: 1, y: 2) - - let readable = ReadableOnly(pos) - - XCTAssertEqual(readable.x, 1) - XCTAssertEqual(readable.y, 2) - - // readable.x = 3 // does not work and that's correct! - } - - func testWrite() { - let pos = Position(x: 1, y: 2) - - let writable = Writable(pos) - - XCTAssertEqual(writable.x, 1) - XCTAssertEqual(writable.y, 2) - - writable.x = 3 - - XCTAssertEqual(writable.x, 3) - XCTAssertEqual(pos.x, 3) - - XCTAssertEqual(writable.y, 2) - XCTAssertEqual(pos.y, 2) - } -} -#endif diff --git a/Tests/FirebladeECSTests/Base.swift b/Tests/FirebladeECSTests/Base.swift index eb9e589..ded9b86 100644 --- a/Tests/FirebladeECSTests/Base.swift +++ b/Tests/FirebladeECSTests/Base.swift @@ -7,7 +7,9 @@ import FirebladeECS -class EmptyComponent: Component { } +class EmptyComponent: Component { + +} class Name: Component { var name: String diff --git a/Tests/FirebladeECSTests/ComponentIdentifierTests.swift b/Tests/FirebladeECSTests/ComponentIdentifierTests.swift new file mode 100644 index 0000000..6cc5e61 --- /dev/null +++ b/Tests/FirebladeECSTests/ComponentIdentifierTests.swift @@ -0,0 +1,22 @@ +// +// ComponentIdentifierTests.swift +// +// +// Created by Christian Treffs on 05.10.19. +// +import XCTest + +final class ComponentIdentifierTests: XCTestCase { + + func testMirrorAsStableIdentifier() { + let m = String(reflecting: Position.self) + let identifier: String = m + XCTAssertEqual(identifier, "FirebladeECSTests.Position") + } + + func testStringDescribingAsStableIdentifier() { + let s = String(describing: Position.self) + let identifier: String = s + XCTAssertEqual(identifier, "Position") + } +} diff --git a/Tests/FirebladeECSTests/EntityTests.swift b/Tests/FirebladeECSTests/EntityTests.swift index 9a6803d..ab2ae46 100644 --- a/Tests/FirebladeECSTests/EntityTests.swift +++ b/Tests/FirebladeECSTests/EntityTests.swift @@ -5,7 +5,8 @@ // Created by Christian Treffs on 22.10.17. // -import FirebladeECS +#if DEBUG +@testable import FirebladeECS import XCTest class EntityTests: XCTestCase { @@ -21,9 +22,5 @@ class EntityTests: XCTestCase { XCTAssertEqual(max, EntityIdentifier.invalid) XCTAssertEqual(max.id, Int(UInt32.max)) } - - func testEntityIdentifierComparison() { - XCTAssertTrue(EntityIdentifier(1) < EntityIdentifier(2)) - XCTAssertTrue(EntityIdentifier(23) > EntityIdentifier(4)) - } } +#endif diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift index fb7fb50..e80cd7e 100644 --- a/Tests/FirebladeECSTests/FamilyTests.swift +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -5,6 +5,7 @@ // Created by Christian Treffs on 09.10.17. // +#if DEBUG @testable import FirebladeECS import XCTest @@ -31,7 +32,6 @@ class FamilyTests: XCTestCase { let family = nexus.family(requires: Position.self, excludesAll: Name.self) - XCTAssertEqual(family.nexus, self.nexus) XCTAssertTrue(family.nexus === self.nexus) XCTAssertEqual(nexus.numFamilies, 1) XCTAssertEqual(nexus.numComponents, 0) @@ -73,11 +73,11 @@ class FamilyTests: XCTestCase { XCTAssertEqual(nexus.numComponents, 1) XCTAssertEqual(nexus.numEntities, 1) entity.remove(Position.self) - XCTAssertEqual(nexus.numFamilies, 1) + XCTAssertEqual(nexus.numFamilies, 0) XCTAssertEqual(nexus.numComponents, 0) XCTAssertEqual(nexus.numEntities, 1) nexus.destroy(entity: entity) - XCTAssertEqual(nexus.numFamilies, 1) + XCTAssertEqual(nexus.numFamilies, 0) XCTAssertEqual(nexus.numComponents, 0) XCTAssertEqual(nexus.numEntities, 0) } @@ -132,7 +132,7 @@ class FamilyTests: XCTestCase { entity.remove(velocity) } - XCTAssertEqual(familyA.count, 10) + XCTAssertEqual(familyA.count, 0) XCTAssertEqual(familyB.count, 0) } @@ -186,3 +186,4 @@ class FamilyTests: XCTestCase { XCTAssertEqual(family.memberIds.count, count + (count / 2)) } } +#endif diff --git a/Tests/FirebladeECSTests/HashingTests.swift b/Tests/FirebladeECSTests/HashingTests.swift index 66ce2b3..4aad5f0 100644 --- a/Tests/FirebladeECSTests/HashingTests.swift +++ b/Tests/FirebladeECSTests/HashingTests.swift @@ -5,6 +5,7 @@ // Created by Christian Treffs on 16.10.17. // +#if DEBUG @testable import FirebladeECS import XCTest @@ -50,4 +51,18 @@ class HashingTests: XCTestCase { XCTAssert(EntityComponentHash.decompose(h, with: entityId) == cH) } } + + func testStringHashes() throws { + let string = "EiMersaufEn1" + + XCTAssertEqual(StringHashing.bernstein_djb2(string), 13447802024599246090) + XCTAssertEqual(StringHashing.singer_djb2(string), 5428736256651916664) + XCTAssertEqual(StringHashing.sdbm(string), 15559770072020577201) + + XCTAssertEqual(StringHashing.bernstein_djb2("gamedev"), 229466792000542) + XCTAssertEqual(StringHashing.singer_djb2("gamedev"), 2867840411746895486) + XCTAssertEqual(StringHashing.sdbm("gamedev"), 2761443862055442870) + + } } +#endif diff --git a/Tests/FirebladeECSTests/NexusTests.swift b/Tests/FirebladeECSTests/NexusTests.swift index f334879..733d20d 100644 --- a/Tests/FirebladeECSTests/NexusTests.swift +++ b/Tests/FirebladeECSTests/NexusTests.swift @@ -5,7 +5,8 @@ // Created by Christian Treffs on 09.10.17. // -import FirebladeECS +#if DEBUG +@testable import FirebladeECS import XCTest class NexusTests: XCTestCase { @@ -33,9 +34,6 @@ class NexusTests: XCTestCase { XCTAssert(e1.identifier.id == 1) XCTAssert(nexus.numEntities == 2) - - //FIXME: XCTAssertNil(e0.name) - //FIXME: XCTAssertEqual(e1.name, "Entity 1") } func testEntityDestroy() { @@ -158,3 +156,4 @@ class NexusTests: XCTestCase { XCTAssert(pB.y != pA.y) } } +#endif diff --git a/Tests/FirebladeECSTests/SingleTests.swift b/Tests/FirebladeECSTests/SingleTests.swift index f0919c9..c485228 100644 --- a/Tests/FirebladeECSTests/SingleTests.swift +++ b/Tests/FirebladeECSTests/SingleTests.swift @@ -5,6 +5,7 @@ // Created by Christian Treffs on 13.02.19. // +#if DEBUG @testable import FirebladeECS import XCTest @@ -23,7 +24,6 @@ class SingleTests: XCTestCase { func testSingleCreation() { let single = nexus.single(SingleGameState.self) - XCTAssertEqual(single.nexus, self.nexus) XCTAssertTrue(single.nexus === self.nexus) XCTAssertEqual(single.traits.requiresAll.count, 1) XCTAssertEqual(single.traits.excludesAll.count, 0) @@ -63,3 +63,4 @@ class SingleTests: XCTestCase { XCTAssertTrue(singleGame === single.component) } } +#endif diff --git a/Tests/FirebladeECSTests/SparseSetTests.swift b/Tests/FirebladeECSTests/SparseSetTests.swift index 7351f42..033695e 100644 --- a/Tests/FirebladeECSTests/SparseSetTests.swift +++ b/Tests/FirebladeECSTests/SparseSetTests.swift @@ -5,6 +5,7 @@ // Created by Christian Treffs on 31.10.17. // +#if DEBUG @testable import FirebladeECS import XCTest @@ -387,7 +388,7 @@ class SparseSetTests: XCTestCase { func testSparseSetDoubleRemove() { class AClass { } - let set = UnorderedSparseSet() + var set = UnorderedSparseSet() let a = AClass() let b = AClass() set.insert(a, at: 0) @@ -471,7 +472,7 @@ class SparseSetTests: XCTestCase { } func testSparseSetReduce() { - let characters = UnorderedSparseSet() + var characters = UnorderedSparseSet() characters.insert("H", at: 4) characters.insert("e", at: 13) @@ -497,7 +498,7 @@ class SparseSetTests: XCTestCase { } func testSubscript() { - let characters = UnorderedSparseSet() + var characters = UnorderedSparseSet() characters[4] = "H" characters[13] = "e" @@ -528,7 +529,7 @@ class SparseSetTests: XCTestCase { } func testStartEndIndex() { - let set = UnorderedSparseSet() + var set = UnorderedSparseSet() set.insert("C", at: 33) set.insert("A", at: 11) @@ -539,3 +540,4 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(mapped, ["C", "A", "B"]) } } +#endif diff --git a/Tests/FirebladeECSTests/SystemsTests.swift b/Tests/FirebladeECSTests/SystemsTests.swift index ac815bd..4c762ca 100644 --- a/Tests/FirebladeECSTests/SystemsTests.swift +++ b/Tests/FirebladeECSTests/SystemsTests.swift @@ -5,6 +5,7 @@ // Created by Christian Treffs on 10.05.18. // +#if DEBUG @testable import FirebladeECS import XCTest @@ -124,3 +125,4 @@ class SystemsTests: XCTestCase { } } } +#endif diff --git a/Tests/FirebladeECSTests/XCTestManifests.swift b/Tests/FirebladeECSTests/XCTestManifests.swift index 2ae99ac..cf77a10 100644 --- a/Tests/FirebladeECSTests/XCTestManifests.swift +++ b/Tests/FirebladeECSTests/XCTestManifests.swift @@ -1,6 +1,16 @@ #if !canImport(ObjectiveC) import XCTest +extension ComponentIdentifierTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ComponentIdentifierTests = [ + ("testMirrorAsStableIdentifier", testMirrorAsStableIdentifier), + ("testStringDescribingAsStableIdentifier", testStringDescribingAsStableIdentifier) + ] +} + extension ComponentTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -15,8 +25,7 @@ extension EntityTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__EntityTests = [ - ("testEntityIdentifierAndIndex", testEntityIdentifierAndIndex), - ("testEntityIdentifierComparison", testEntityIdentifierComparison) + ("testEntityIdentifierAndIndex", testEntityIdentifierAndIndex) ] } @@ -50,7 +59,8 @@ extension HashingTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__HashingTests = [ - ("testCollisionsInCritialRange", testCollisionsInCritialRange) + ("testCollisionsInCritialRange", testCollisionsInCritialRange), + ("testStringHashes", testStringHashes) ] } @@ -124,6 +134,7 @@ extension SystemsTests { public func __allTests() -> [XCTestCaseEntry] { return [ + testCase(ComponentIdentifierTests.__allTests__ComponentIdentifierTests), testCase(ComponentTests.__allTests__ComponentTests), testCase(EntityTests.__allTests__EntityTests), testCase(FamilyTests.__allTests__FamilyTests),