diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..5770440 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,28 @@ +image: swift:5.0 + +#before_script: + #- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" + +stages: + - test + - build_release + +build_project: + stage: build_release + script: + - swift package reset + - swift build -c release + tags: + - docker + only: + - master + +test_project: + stage: test + variables: + GIT_DEPTH: "50" + script: + - swift package reset + - swift test + tags: + - docker \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 223ef8f..5aa8fe1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,7 +1,6 @@ included: - Sources excluded: - - Tests - docs - build identifier_name: @@ -11,6 +10,17 @@ line_length: 220 number_separator: minimum_length: 5 opt_in_rules: + #- anyobject_protocol + #- explicit_acl + #- explicit_enum_raw_value + #- explicit_type_interface + #- extension_access_modifier + #- file_header + #- file_name + #- missing_docs + #- multiline_arguments_brackets + #- no_grouping_extension + #- multiline_literal_brackets - array_init - attributes - closure_body_length @@ -29,7 +39,7 @@ opt_in_rules: - empty_xctest_method - explicit_init - explicit_self - - extension_access_modifier + - explicit_top_level_acl - fallthrough - fatal_error_message - first_where @@ -47,7 +57,9 @@ opt_in_rules: - multiline_arguments - multiline_function_chains - multiline_parameters + - multiline_parameters_brackets - nimble_operator + - no_extension_access_modifier - number_separator - object_literal - operator_usage_whitespace diff --git a/.travis.yml b/.travis.yml index ac39e4b..c989ccd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,14 @@ 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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b868cea --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +lint: + swiftlint autocorrect --format + swiftlint lint --quiet + +genLinuxTests: + swift test --generate-linuxmain + swiftlint autocorrect --format --path Tests/ + +test: genLinuxTests + swift test + +clean: + swift package reset + rm -rdf .swiftpm/xcode + rm -rdf .build/ + rm Package.resolved + rm .DS_Store + +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 diff --git a/README.md b/README.md index 02591ff..53db762 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Fireblade ECS (Entity-Component-System) [![Build Status](https://travis-ci.com/fireblade-engine/ecs.svg?branch=master)](https://travis-ci.com/fireblade-engine/ecs) -[![version 0.9.1](https://img.shields.io/badge/version-0.9.1-brightgreen.svg)](releases/tag/v0.9.1) [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) -[![swift version](https://img.shields.io/badge/swift-5.0-brightgreen.svg)](#) -[![platforms](https://img.shields.io/badge/platforms-%20macOS%20|%20iOS%20|%20tvOS%20|%20watchOS%20|%20linux%20-brightgreen.svg)](#) +[![swift version](https://img.shields.io/badge/swift-5.0+-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)](#) This is a **dependency free**, **lightweight**, **fast** and **easy to use** [Entity-Component-System](https://en.wikipedia.org/wiki/Entity–component–system) implementation in Swift. It is developed and maintained as part of the [Fireblade Game Engine project](https://github.com/fireblade-engine). @@ -33,7 +33,7 @@ import PackageDescription let package = Package( name: "YourPackageName", dependencies: [ - .package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.9.1") + .package(url: "https://github.com/fireblade-engine/ecs.git", from: "0.10.0") ], targets: [ .target( @@ -184,6 +184,37 @@ class GameLogicSystem { ``` +### 👫 Relatives + +This ECS implementation provides an integrated way of creating a [directed acyclic graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph) hierarchy of entities by forming parent-child relationships. Entities can become children of a parent entity. In family terms they become **relatives**. Families provide iteration over these relationships. +The entity hierachy implementation does not use an additional component therefore keeping the hierarchy intact over different component-families. +This feature is especially useful for implenting a [scene graph](https://en.wikipedia.org/wiki/Scene_graph). + +```swift +// create entities with 0 to n components +let parent: Entity = nexus.createEntity(with: Position(x: 1, y: 1), SomeOtherComponent(...)) +let child: Entity = nexus.createEntity(with: Position(x: 2, y: 2)) +let child2: Entity = nexus.createEntity(with: Position(x: 3, y: 3), MySpecialComponent(...)) + +// create relationships between entities +parent.addChild(child) +child.addChild(child2) +// or remove them +// parent.removeChild(child) + +// iterate over component families descending the graph +nexus.family(requires: Position.self) + .descendRelatives(from: parent) // provide the start entity (aka root "node") + .forEach { (parent: Position, child: Position) in + // parent: the current parent component + // child: the current child component + + // update your components hierarchically + child.x += parent.x + child.y += parent.y + } +``` + ## 🧪 Demo See the [Fireblade ECS Demo App](https://github.com/fireblade-engine/ecs-demo) to get started. diff --git a/Sources/FirebladeECS/Component+Access.swift b/Sources/FirebladeECS/Component+Access.swift index acd650e..ebced3f 100644 --- a/Sources/FirebladeECS/Component+Access.swift +++ b/Sources/FirebladeECS/Component+Access.swift @@ -1,6 +1,6 @@ // // Component+Access.swift -// +// // // Created by Christian Treffs on 25.06.19. // @@ -14,7 +14,8 @@ public struct ReadableOnly where Comp: Component { self.component = component } - @inlinable public subscript(dynamicMember keyPath: KeyPath) -> C { + @inlinable + public subscript(dynamicMember keyPath: KeyPath) -> C { return component[keyPath: keyPath] } } @@ -27,7 +28,8 @@ public struct Writable where Comp: Component { self.component = component } - @inlinable public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> C { + @inlinable + public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> C { nonmutating get { return component[keyPath: keyPath] } nonmutating set { component[keyPath: keyPath] = newValue } } diff --git a/Sources/FirebladeECS/Component.swift b/Sources/FirebladeECS/Component.swift index b56345d..6764e89 100644 --- a/Sources/FirebladeECS/Component.swift +++ b/Sources/FirebladeECS/Component.swift @@ -14,7 +14,7 @@ public protocol Component: class { var identifier: ComponentIdentifier { get } } -public extension Component { - static var identifier: ComponentIdentifier { return ComponentIdentifier(Self.self) } - @inlinable var identifier: ComponentIdentifier { return Self.identifier } +extension Component { + public static var identifier: ComponentIdentifier { return ComponentIdentifier(Self.self) } + @inlinable public var identifier: ComponentIdentifier { return Self.identifier } } diff --git a/Sources/FirebladeECS/ComponentIdentifier.swift b/Sources/FirebladeECS/ComponentIdentifier.swift index 2c3b394..2ed9d57 100644 --- a/Sources/FirebladeECS/ComponentIdentifier.swift +++ b/Sources/FirebladeECS/ComponentIdentifier.swift @@ -1,6 +1,6 @@ // // ComponentIdentifier.swift -// +// // // Created by Christian Treffs on 20.08.19. // diff --git a/Sources/FirebladeECS/Entity+Component.swift b/Sources/FirebladeECS/Entity+Component.swift index fa8a70f..991a491 100644 --- a/Sources/FirebladeECS/Entity+Component.swift +++ b/Sources/FirebladeECS/Entity+Component.swift @@ -5,30 +5,30 @@ // Created by Christian Treffs on 22.10.17. // -public extension Entity { +extension Entity { @inlinable - func get() -> C? where C: Component { - return nexus.get(for: identifier) - } + public func get() -> C? where C: Component { + return nexus.get(for: identifier) + } @inlinable - func get(component compType: A.Type = A.self) -> A? where A: Component { - return nexus.get(for: identifier) - } + public func get(component compType: A.Type = A.self) -> A? where A: Component { + return nexus.get(for: identifier) + } @inlinable - func get(components _: A.Type, _: B.Type) -> (A?, B?) where A: Component, B: Component { - let compA: A? = get(component: A.self) - let compB: B? = get(component: B.self) - return (compA, compB) - } + public func get(components _: A.Type, _: B.Type) -> (A?, B?) where A: Component, B: Component { + let compA: A? = get(component: A.self) + let compB: B? = get(component: B.self) + return (compA, compB) + } // swiftlint:disable large_tuple - @inlinable - func get(components _: A.Type, _: B.Type, _: C.Type) -> (A?, B?, C?) where A: Component, B: Component, C: Component { - let compA: A? = get(component: A.self) - let compB: B? = get(component: B.self) - let compC: C? = get(component: C.self) - return (compA, compB, compC) - } + @inlinable + public func get(components _: A.Type, _: B.Type, _: C.Type) -> (A?, B?, C?) where A: Component, B: Component, C: Component { + let compA: A? = get(component: A.self) + let compB: B? = get(component: B.self) + let compC: C? = get(component: C.self) + return (compA, compB, compC) + } } diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index 0b45535..d7796ea 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -101,12 +101,36 @@ public struct Entity { public func destroy() { nexus.destroy(entity: self) } + + /// Add an entity as child. + /// - Parameter entity: The child entity. + @discardableResult + public func addChild(_ entity: Entity) -> Bool { + return 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) + } + + /// Removes all children from this entity. + public func removeAllChildren() { + return nexus.removeAllChildren(from: self) + } + + /// Returns the number of children for this entity. + public var numChildren: Int { + return 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.identifier == rhs.identifier } } diff --git a/Sources/FirebladeECS/EntityIdentifier.swift b/Sources/FirebladeECS/EntityIdentifier.swift index a12841d..6a1dc64 100644 --- a/Sources/FirebladeECS/EntityIdentifier.swift +++ b/Sources/FirebladeECS/EntityIdentifier.swift @@ -11,21 +11,17 @@ public struct EntityIdentifier { /// provides 4294967295 unique identifiers since it's constrained to UInt32 - invalid. public let index: Int - private init() { - self = .invalid - } - public init(_ uint32: UInt32) { self.index = Int(uint32) } } extension EntityIdentifier: Equatable { } - extension EntityIdentifier: Hashable { } - +extension EntityIdentifier: Codable { } extension EntityIdentifier: Comparable { - @inlinable public static func < (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool { + @inlinable + public static func < (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool { return lhs.index < rhs.index } } diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index cbfe10a..eded766 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -1,6 +1,6 @@ // // Family.swift -// +// // // Created by Christian Treffs on 21.08.19. // @@ -40,6 +40,14 @@ public struct Family where R: FamilyRequirementsManaging { } } +// MARK: - Equatable +extension Family: Equatable { + public static func == (lhs: Family, rhs: Family) -> Bool { + return lhs.nexus == rhs.nexus && + lhs.traits == rhs.traits + } +} + extension Family: Sequence { __consuming public func makeIterator() -> ComponentsIterator { return ComponentsIterator(family: self) @@ -123,10 +131,57 @@ extension Family { extension Family.EntityComponentIterator: LazySequenceProtocol { } -// MARK: - Equatable -extension Family: Equatable { - public static func == (lhs: Family, rhs: Family) -> Bool { - return lhs.nexus == rhs.nexus && - lhs.traits == rhs.traits +// MARK: - relatives iterator + +extension Family { + @inlinable + public func descendRelatives(from root: Entity) -> RelativesIterator { + return RelativesIterator(family: self, root: root) + } + + public struct RelativesIterator: IteratorProtocol { + @usableFromInline unowned let nexus: Nexus + @usableFromInline let familyTraits: FamilyTraitSet + + @usableFromInline var relatives: ContiguousArray<(EntityIdentifier, EntityIdentifier)> + + public init(family: Family, root: Entity) { + self.nexus = family.nexus + self.familyTraits = family.traits + + // FIXME: this is not the most efficient way to aggregate all parent child tuples + // Problems: + // - allocates new memory + // - needs to be build on every iteration + // - relies on isMember check + self.relatives = [] + self.relatives.reserveCapacity(family.memberIds.count) + aggregateRelativesBreathFirst(root.identifier) + relatives.reverse() + } + + mutating func aggregateRelativesBreathFirst(_ parent: EntityIdentifier) { + guard let children = nexus.parentChildrenMap[parent] else { + return + } + children + .compactMap { child in + guard nexus.isMember(child, in: familyTraits) else { + return nil + } + relatives.append((parent, child)) + return child + } + .forEach { aggregateRelativesBreathFirst($0) } + } + + public mutating func next() -> R.RelativesDescending? { + guard let (parentId, childId) = relatives.popLast() else { + return nil + } + return R.relativesDescending(nexus: nexus, parentId: parentId, childId: childId) + } } } + +extension Family.RelativesIterator: LazySequenceProtocol { } diff --git a/Sources/FirebladeECS/Family1.swift b/Sources/FirebladeECS/Family1.swift index d4bd37f..8e0f42c 100644 --- a/Sources/FirebladeECS/Family1.swift +++ b/Sources/FirebladeECS/Family1.swift @@ -1,6 +1,6 @@ // // Family1.swift -// +// // // Created by Christian Treffs on 21.08.19. // @@ -24,6 +24,12 @@ public struct Requires1: FamilyRequirementsManaging where A: Component { let compA: A = nexus.get(unsafeComponentFor: entityId) return (entity, compA) } + + public static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> (parent: A, child: A) { + let parentCompA: A = nexus.get(unsafeComponentFor: parentId) + let childCompA: A = nexus.get(unsafeComponentFor: childId) + return (parent: parentCompA, child: childCompA) + } } extension Nexus { @@ -32,7 +38,7 @@ extension Nexus { excludesAll excludedComponents: Component.Type... ) -> Family1 where A: Component { return Family1(nexus: self, - requiresAll: componentA, - excludesAll: excludedComponents) + requiresAll: componentA, + excludesAll: excludedComponents) } } diff --git a/Sources/FirebladeECS/Family2.swift b/Sources/FirebladeECS/Family2.swift index afb470e..8d33200 100644 --- a/Sources/FirebladeECS/Family2.swift +++ b/Sources/FirebladeECS/Family2.swift @@ -1,10 +1,12 @@ // // Family2.swift -// +// // // Created by Christian Treffs on 21.08.19. // +// swiftlint:disable large_tuple + public typealias Family2 = Family> public struct Requires2: FamilyRequirementsManaging where A: Component, B: Component { @@ -25,6 +27,14 @@ public struct Requires2: FamilyRequirementsManaging where A: Component, B: let compB: B = nexus.get(unsafeComponentFor: entityId) return (entity, compA, compB) } + + public static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> (parent: (A, B), child: (A, B)) { + let pcA: A = nexus.get(unsafeComponentFor: parentId) + let pcB: B = nexus.get(unsafeComponentFor: parentId) + let ccA: A = nexus.get(unsafeComponentFor: childId) + let ccB: B = nexus.get(unsafeComponentFor: childId) + return (parent: (pcA, pcB), child: (ccA, ccB)) + } } extension Nexus { diff --git a/Sources/FirebladeECS/Family3.swift b/Sources/FirebladeECS/Family3.swift index 272b936..74446e2 100644 --- a/Sources/FirebladeECS/Family3.swift +++ b/Sources/FirebladeECS/Family3.swift @@ -1,14 +1,17 @@ // // Family3.swift -// +// // // Created by Christian Treffs on 21.08.19. // +// swiftlint:disable large_tuple + public typealias Family3 = Family> public struct Requires3: FamilyRequirementsManaging where A: Component, B: Component, C: Component { public let componentTypes: [Component.Type] + public init(_ types: (A.Type, B.Type, C.Type)) { componentTypes = [A.self, B.self, C.self] } @@ -27,6 +30,16 @@ public struct Requires3: FamilyRequirementsManaging where A: Component, let compC: C = nexus.get(unsafeComponentFor: entityId) return (entity, compA, compB, compC) } + public static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> + (parent: (A, B, C), child: (A, B, C)) { + let pcA: A = nexus.get(unsafeComponentFor: parentId) + let pcB: B = nexus.get(unsafeComponentFor: parentId) + let pcC: C = nexus.get(unsafeComponentFor: parentId) + let ccA: A = nexus.get(unsafeComponentFor: childId) + let ccB: B = nexus.get(unsafeComponentFor: childId) + let ccC: C = nexus.get(unsafeComponentFor: childId) + return (parent: (pcA, pcB, pcC), child: (ccA, ccB, ccC)) + } } extension Nexus { diff --git a/Sources/FirebladeECS/Family4.swift b/Sources/FirebladeECS/Family4.swift index 68b1e5b..88050f7 100644 --- a/Sources/FirebladeECS/Family4.swift +++ b/Sources/FirebladeECS/Family4.swift @@ -1,14 +1,17 @@ // // Family4.swift -// +// // // Created by Christian Treffs on 21.08.19. // +// swiftlint:disable large_tuple + public typealias Family4 = Family> public struct Requires4: FamilyRequirementsManaging where A: Component, B: Component, C: Component, D: Component { public let componentTypes: [Component.Type] + public init(_ types: (A.Type, B.Type, C.Type, D.Type)) { componentTypes = [A.self, B.self, C.self, D.self] } @@ -29,6 +32,19 @@ public struct Requires4: FamilyRequirementsManaging where A: Compone let compD: D = nexus.get(unsafeComponentFor: entityId) return (entity, compA, compB, compC, compD) } + + public static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> + (parent: (A, B, C, D), child: (A, B, C, D)) { + let pcA: A = nexus.get(unsafeComponentFor: parentId) + let pcB: B = nexus.get(unsafeComponentFor: parentId) + let pcC: C = nexus.get(unsafeComponentFor: parentId) + let pcD: D = nexus.get(unsafeComponentFor: parentId) + let ccA: A = nexus.get(unsafeComponentFor: childId) + let ccB: B = nexus.get(unsafeComponentFor: childId) + let ccC: C = nexus.get(unsafeComponentFor: childId) + let ccD: D = nexus.get(unsafeComponentFor: childId) + return (parent: (pcA, pcB, pcC, pcD), child: (ccA, ccB, ccC, ccD)) + } } extension Nexus { @@ -38,7 +54,7 @@ extension Nexus { _ componentC: C.Type, _ componentD: D.Type, excludesAll excludedComponents: Component.Type... - ) -> Family4 where A: Component, B: Component, C: Component, D: Component { + ) -> Family4 where A: Component, B: Component, C: Component, D: Component { return Family4( nexus: self, requiresAll: (componentA, componentB, componentC, componentD), diff --git a/Sources/FirebladeECS/Family5.swift b/Sources/FirebladeECS/Family5.swift index 20186f8..0d76248 100644 --- a/Sources/FirebladeECS/Family5.swift +++ b/Sources/FirebladeECS/Family5.swift @@ -1,14 +1,17 @@ // // Family5.swift -// +// // // Created by Christian Treffs on 21.08.19. // +// swiftlint:disable large_tuple + public typealias Family5 = Family> public struct Requires5: FamilyRequirementsManaging where A: Component, B: Component, C: Component, D: Component, E: Component { public let componentTypes: [Component.Type] + public init(_ types: (A.Type, B.Type, C.Type, D.Type, E.Type)) { componentTypes = [A.self, B.self, C.self, D.self, E.self] } @@ -31,6 +34,22 @@ public struct Requires5: FamilyRequirementsManaging where A: Comp let compE: E = nexus.get(unsafeComponentFor: entityId) return (entity, compA, compB, compC, compD, compE) } + + public static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> + (parent: (A, B, C, D, E), child: (A, B, C, D, E)) { + let pcA: A = nexus.get(unsafeComponentFor: parentId) + let pcB: B = nexus.get(unsafeComponentFor: parentId) + let pcC: C = nexus.get(unsafeComponentFor: parentId) + let pcD: D = nexus.get(unsafeComponentFor: parentId) + let pcE: E = nexus.get(unsafeComponentFor: parentId) + let ccA: A = nexus.get(unsafeComponentFor: childId) + let ccB: B = nexus.get(unsafeComponentFor: childId) + let ccC: C = nexus.get(unsafeComponentFor: childId) + let ccD: D = nexus.get(unsafeComponentFor: childId) + let ccE: E = nexus.get(unsafeComponentFor: childId) + return (parent: (pcA, pcB, pcC, pcD, pcE), + child: (ccA, ccB, ccC, ccD, ccE)) + } } extension Nexus { diff --git a/Sources/FirebladeECS/FamilyRequirementsManaging.swift b/Sources/FirebladeECS/FamilyRequirementsManaging.swift index 02970d2..9774563 100644 --- a/Sources/FirebladeECS/FamilyRequirementsManaging.swift +++ b/Sources/FirebladeECS/FamilyRequirementsManaging.swift @@ -1,6 +1,6 @@ // // FamilyRequirementsManaging.swift -// +// // // Created by Christian Treffs on 21.08.19. // @@ -9,8 +9,13 @@ public protocol FamilyRequirementsManaging { associatedtype Components associatedtype ComponentTypes associatedtype EntityAndComponents + associatedtype RelativesDescending + init(_ types: ComponentTypes) + var componentTypes: [Component.Type] { get } + static func components(nexus: Nexus, entityId: EntityIdentifier) -> Components static func entityAndComponents(nexus: Nexus, entityId: EntityIdentifier) -> EntityAndComponents + static func relativesDescending(nexus: Nexus, parentId: EntityIdentifier, childId: EntityIdentifier) -> RelativesDescending } diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 55b3c7e..6195df4 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -5,23 +5,23 @@ // Created by Christian Treffs on 13.10.17. // -public extension Nexus { - final var numComponents: Int { +extension Nexus { + public final var numComponents: Int { return componentsByType.reduce(0) { $0 + $1.value.count } } - final func has(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> Bool { + public final func has(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> Bool { guard let uniforms = componentsByType[componentId] else { return false } return uniforms.contains(entityId.index) } - final func count(components entityId: EntityIdentifier) -> Int { + public final func count(components entityId: EntityIdentifier) -> Int { return componentIdsByEntity[entityId]?.count ?? 0 } - final func assign(component: Component, to entity: Entity) { + public final func assign(component: Component, to entity: Entity) { let componentId: ComponentIdentifier = component.identifier let entityId: EntityIdentifier = entity.identifier @@ -49,39 +49,44 @@ public extension Nexus { delegate?.nexusEvent(ComponentAdded(component: componentId, toEntity: entity.identifier)) } - final func assign(component: C, to entity: Entity) where C: Component { + public final func assign(component: C, to entity: Entity) where C: Component { assign(component: component, to: entity) } - @inlinable final func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { + @inlinable + public final func get(component componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component? { guard let uniformComponents = componentsByType[componentId] else { return nil } return uniformComponents.get(at: entityId.index) } - @inlinable final func get(unsafeComponent componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component { + @inlinable + public final func get(unsafeComponent componentId: ComponentIdentifier, for entityId: EntityIdentifier) -> Component { let uniformComponents = componentsByType[componentId].unsafelyUnwrapped return uniformComponents.get(unsafeAt: entityId.index) } - @inlinable final func get(for entityId: EntityIdentifier) -> C? where C: Component { + @inlinable + public final func get(for entityId: EntityIdentifier) -> C? where C: Component { let componentId: ComponentIdentifier = C.identifier return get(componentId: componentId, entityId: entityId) } - @inlinable final func get(unsafeComponentFor entityId: EntityIdentifier) -> C where C: Component { + @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 return unsafeDowncast(component, to: C.self) } - @inlinable final func get(components entityId: EntityIdentifier) -> Set? { + @inlinable + public final func get(components entityId: EntityIdentifier) -> Set? { return componentIdsByEntity[entityId] } @discardableResult - final func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool { + public final func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool { // delete component instance componentsByType[componentId]?.remove(at: entityId.index) // unasign component from entity @@ -94,7 +99,7 @@ public extension Nexus { } @discardableResult - final func removeAll(componentes entityId: EntityIdentifier) -> Bool { + public final func removeAll(componentes entityId: EntityIdentifier) -> Bool { guard let allComponents = get(components: entityId) else { delegate?.nexusNonFatalError("clearing components form entity \(entityId) with no components") return false @@ -106,10 +111,9 @@ public extension Nexus { } return removedAll } -} -extension Nexus { - @inlinable final func get(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> C? where C: Component { + @inlinable + public final func get(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> C? where C: Component { guard let uniformComponents = componentsByType[componentId] else { return nil } diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index fff53f7..d8ce86a 100644 --- a/Sources/FirebladeECS/Nexus+Entity.swift +++ b/Sources/FirebladeECS/Nexus+Entity.swift @@ -6,7 +6,8 @@ // extension Nexus { - @inlinable internal func nextEntityId() -> EntityIdentifier { + @inlinable + internal func nextEntityId() -> EntityIdentifier { guard let nextReused: EntityIdentifier = freeEntities.popLast() else { return EntityIdentifier(UInt32(entityStorage.count)) } @@ -55,6 +56,8 @@ extension Nexus { return false } + removeAllChildren(from: entity) + if removeAll(componentes: entityId) { update(familyMembership: entityId) } diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 6d01a0c..f7622a3 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -5,32 +5,32 @@ // Created by Christian Treffs on 13.10.17. // -public extension Nexus { - final var numFamilies: Int { +extension Nexus { + public final var numFamilies: Int { return familyMembersByTraits.keys.count } - func canBecomeMember(_ entity: Entity, in traits: FamilyTraitSet) -> Bool { - guard let componentIds = componentIdsByEntity[entity.identifier] else { - assertionFailure("no component set defined for entity: \(entity)") - return false - } - return traits.isMatch(components: componentIds) - } + public func canBecomeMember(_ entity: Entity, in traits: FamilyTraitSet) -> Bool { + guard let componentIds = componentIdsByEntity[entity.identifier] else { + assertionFailure("no component set defined for entity: \(entity)") + return false + } + return traits.isMatch(components: componentIds) + } - func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { - return familyMembersByTraits[traits] ?? UnorderedSparseSet() - } + public func members(withFamilyTraits traits: FamilyTraitSet) -> UnorderedSparseSet { + return familyMembersByTraits[traits] ?? UnorderedSparseSet() + } - func isMember(_ entity: Entity, in family: FamilyTraitSet) -> Bool { - return isMember(entity.identifier, in: family) - } + public func isMember(_ entity: Entity, in family: FamilyTraitSet) -> Bool { + return isMember(entity.identifier, in: family) + } - func isMember(_ entityId: EntityIdentifier, in family: FamilyTraitSet) -> Bool { + public func isMember(_ entityId: EntityIdentifier, in family: FamilyTraitSet) -> Bool { return isMember(entity: entityId, inFamilyWithTraits: family) } - func isMember(entity entityId: EntityIdentifier, inFamilyWithTraits traits: FamilyTraitSet) -> Bool { - return members(withFamilyTraits: traits).contains(entityId.index) - } + public func isMember(entity entityId: EntityIdentifier, inFamilyWithTraits traits: FamilyTraitSet) -> Bool { + return members(withFamilyTraits: traits).contains(entityId.index) + } } diff --git a/Sources/FirebladeECS/Nexus+SceneGraph.swift b/Sources/FirebladeECS/Nexus+SceneGraph.swift new file mode 100644 index 0000000..d8576a4 --- /dev/null +++ b/Sources/FirebladeECS/Nexus+SceneGraph.swift @@ -0,0 +1,45 @@ +// +// Nexus+SceneGraph.swift +// +// +// Created by Christian Treffs on 30.09.19. +// + +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] + inserted = true + } else { + let (isNewMember, _) = parentChildrenMap[parent.identifier]!.insert(child.identifier) + inserted = isNewMember + } + if inserted { + delegate?.nexusEvent(ChildAdded(parent: parent.identifier, child: child.identifier)) + } + return inserted + } + + public final func removeChild(_ child: Entity, from parent: Entity) -> Bool { + return 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 + if removed { + delegate?.nexusEvent(ChildRemoved(parent: parent, child: child)) + } + return removed + } + + public final func removeAllChildren(from parent: Entity) { + parentChildrenMap[parent.identifier]?.forEach { removeChild($0, from: parent.identifier) } + return parentChildrenMap[parent.identifier] = nil + } + + public final func numChildren(for entity: Entity) -> Int { + return parentChildrenMap[entity.identifier]?.count ?? 0 + } +} diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index fdb232e..ffe17d2 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -29,12 +29,17 @@ public final class Nexus { /// - 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 init() { entityStorage = UnorderedSparseSet() componentsByType = [:] componentIdsByEntity = [:] freeEntities = ContiguousArray() familyMembersByTraits = [:] + parentChildrenMap = [:] } public final func clear() { @@ -55,6 +60,7 @@ public final class Nexus { componentsByType.removeAll() componentIdsByEntity.removeAll() familyMembersByTraits.removeAll() + parentChildrenMap.removeAll() } deinit { @@ -64,12 +70,14 @@ public final class Nexus { // MARK: - Equatable extension Nexus: Equatable { - @inlinable public static func == (lhs: Nexus, rhs: Nexus) -> Bool { + @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.componentsByType.keys == rhs.componentsByType.keys && + lhs.parentChildrenMap == rhs.parentChildrenMap // NOTE: components are not equatable (yet) } } diff --git a/Sources/FirebladeECS/NexusEventDelegate.swift b/Sources/FirebladeECS/NexusEventDelegate.swift index c9ca461..72bf4c7 100644 --- a/Sources/FirebladeECS/NexusEventDelegate.swift +++ b/Sources/FirebladeECS/NexusEventDelegate.swift @@ -1,6 +1,6 @@ // // NexusEventDelegate.swift -// +// // // Created by Christian Treffs on 20.08.19. // diff --git a/Sources/FirebladeECS/NexusEvents.swift b/Sources/FirebladeECS/NexusEvents.swift index 08a136c..e686e6d 100644 --- a/Sources/FirebladeECS/NexusEvents.swift +++ b/Sources/FirebladeECS/NexusEvents.swift @@ -8,41 +8,51 @@ public protocol NexusEvent {} public struct EntityCreated: NexusEvent { - public let entityId: EntityIdentifier + public let entityId: EntityIdentifier } public struct EntityDestroyed: NexusEvent { - public let entityId: EntityIdentifier + public let entityId: EntityIdentifier } public struct ComponentAdded: NexusEvent { - public let component: ComponentIdentifier - public let toEntity: EntityIdentifier + public let component: ComponentIdentifier + public let toEntity: EntityIdentifier } public struct ComponentUpdated: NexusEvent { - public let atEnity: EntityIdentifier + public let atEnity: EntityIdentifier } public struct ComponentRemoved: NexusEvent { - public let component: ComponentIdentifier - public let from: EntityIdentifier + public let component: ComponentIdentifier + public let from: EntityIdentifier } public struct FamilyMemberAdded: NexusEvent { - public let member: EntityIdentifier - public let toFamily: FamilyTraitSet + public let member: EntityIdentifier + public let toFamily: FamilyTraitSet } public struct FamilyMemberRemoved: NexusEvent { - public let member: EntityIdentifier - public let from: FamilyTraitSet + public let member: EntityIdentifier + public let from: FamilyTraitSet } public struct FamilyCreated: NexusEvent { - public let family: FamilyTraitSet + public let family: FamilyTraitSet } public struct FamilyDestroyed: NexusEvent { - public let family: FamilyTraitSet + public let family: FamilyTraitSet +} + +public struct ChildAdded: NexusEvent { + public let parent: EntityIdentifier + public let child: EntityIdentifier +} + +public struct ChildRemoved: NexusEvent { + public let parent: EntityIdentifier + public let child: EntityIdentifier } diff --git a/Sources/FirebladeECS/Single.swift b/Sources/FirebladeECS/Single.swift index fbf42b8..bc0a50e 100644 --- a/Sources/FirebladeECS/Single.swift +++ b/Sources/FirebladeECS/Single.swift @@ -9,8 +9,8 @@ public protocol SingleComponent: Component { init() } -public extension Nexus { - func single(_ component: S.Type) -> Single where S: SingleComponent { +extension Nexus { + public func single(_ component: S.Type) -> Single where S: SingleComponent { let family = self.family(requires: S.self) precondition(family.count <= 1, "Singleton count of \(S.self) must be 0 or 1: \(family.count)") let entityId: EntityIdentifier @@ -23,21 +23,23 @@ public extension Nexus { } } -public struct Single: Equatable where A: SingleComponent { +public struct Single where A: SingleComponent { public let nexus: Nexus public let traits: FamilyTraitSet public let entityId: EntityIdentifier } -public extension Single where A: SingleComponent { - @inlinable var component: A { +extension Single: Equatable { } + +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. return nexus.get(unsafeComponentFor: entityId) } - var entity: Entity { + public var entity: Entity { return nexus.get(entity: entityId).unsafelyUnwrapped } } diff --git a/Tests/FirebladeECSPerformanceTests/Base.swift b/Tests/FirebladeECSPerformanceTests/Base.swift index 5f9d964..3b5834d 100644 --- a/Tests/FirebladeECSPerformanceTests/Base.swift +++ b/Tests/FirebladeECSPerformanceTests/Base.swift @@ -10,33 +10,33 @@ import FirebladeECS class EmptyComponent: Component { } class Name: Component { - var name: String - init(name: String) { - self.name = name - } + var name: String + init(name: String) { + self.name = name + } } class Position: Component { - var x: Int - var y: Int - init(x: Int, y: Int) { - self.x = x - self.y = y - } + var x: Int + var y: Int + init(x: Int, y: Int) { + self.x = x + self.y = y + } } class Velocity: Component { - var a: Float - init(a: Float) { - self.a = a - } + var a: Float + init(a: Float) { + self.a = a + } } class Party: Component { - var partying: Bool - init(partying: Bool) { - self.partying = partying - } + var partying: Bool + init(partying: Bool) { + self.partying = partying + } } class Color: Component { @@ -46,26 +46,21 @@ class Color: Component { } class ExampleSystem { - private let family: Family2 + private let family: Family2 - init(nexus: Nexus) { - family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) - } + init(nexus: Nexus) { + family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) + } - func update(deltaT: Double) { + func update(deltaT: Double) { family.forEach { (position: Position, velocity: Velocity) in - position.x *= 2 - velocity.a *= 2 - + position.x *= 2 + velocity.a *= 2 } - - } - + } } - final class SingleGameState: SingleComponent { var shouldQuit: Bool = false var playerHealth: Int = 67 - } diff --git a/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift b/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift index 6272077..14721d5 100644 --- a/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift +++ b/Tests/FirebladeECSPerformanceTests/ComponentPerformanceTests.swift @@ -9,7 +9,6 @@ import FirebladeECS import XCTest class ComponentTests: XCTestCase { - func testMeasureStaticComponentIdentifier() { let number: Int = 10_000 measure { @@ -19,7 +18,7 @@ class ComponentTests: XCTestCase { } } } - + func testMeasureComponentIdentifier() { let number: Int = 10_000 let pos = Position(x: 1, y: 2) diff --git a/Tests/FirebladeECSPerformanceTests/HashingPerformanceTests.swift b/Tests/FirebladeECSPerformanceTests/HashingPerformanceTests.swift index af535da..694b4e6 100644 --- a/Tests/FirebladeECSPerformanceTests/HashingPerformanceTests.swift +++ b/Tests/FirebladeECSPerformanceTests/HashingPerformanceTests.swift @@ -5,16 +5,15 @@ // Created by Christian Treffs on 14.02.19. // -import XCTest import FirebladeECS +import XCTest class HashingPerformanceTests: XCTestCase { - func testMeasureCombineHash() { 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]) let c: Set = Set([3_410_346_899_765, 90_000_002, 12_212_321, 71, 6_123_345_676_543]) - + let input: ContiguousArray = ContiguousArray(arrayLiteral: a.hashValue, b.hashValue, c.hashValue) measure { for _ in 0..<1_000_000 { @@ -23,12 +22,12 @@ class HashingPerformanceTests: XCTestCase { } } } - + 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]) let c: Set = Set([3_410_346_899_765, 90_000_002, 12_212_321, 71, 6_123_345_676_543]) - + let input = Set>(arrayLiteral: a, b, c) measure { for _ in 0..<1_000_000 { @@ -37,6 +36,4 @@ class HashingPerformanceTests: XCTestCase { } } } - - } diff --git a/Tests/FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift b/Tests/FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift index d575a1d..ce3a897 100644 --- a/Tests/FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift +++ b/Tests/FirebladeECSPerformanceTests/TypedFamilyPerformanceTests.swift @@ -9,39 +9,37 @@ import FirebladeECS import XCTest class TypedFamilyPerformanceTests: XCTestCase { - let numEntities: Int = 10_000 var nexus: Nexus! - + override func setUp() { super.setUp() nexus = Nexus() - + for i in 0.. [XCTestCaseEntry] { return [ testCase(ComponentTests.__allTests__ComponentTests), testCase(HashingPerformanceTests.__allTests__HashingPerformanceTests), - testCase(TypedFamilyPerformanceTests.__allTests__TypedFamilyPerformanceTests), + testCase(TypedFamilyPerformanceTests.__allTests__TypedFamilyPerformanceTests) ] } #endif diff --git a/Tests/FirebladeECSTests/AccessTests.swift b/Tests/FirebladeECSTests/AccessTests.swift index d5fbcd3..0c51137 100644 --- a/Tests/FirebladeECSTests/AccessTests.swift +++ b/Tests/FirebladeECSTests/AccessTests.swift @@ -1,45 +1,41 @@ // // 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 ada553d..eb9e589 100644 --- a/Tests/FirebladeECSTests/Base.swift +++ b/Tests/FirebladeECSTests/Base.swift @@ -45,42 +45,42 @@ class Color: Component { var b: UInt8 = 0 } +class Index: Component { + var index: Int + + init(index: Int) { + self.index = index + } +} final class SingleGameState: SingleComponent { var shouldQuit: Bool = false var playerHealth: Int = 67 - } - class ExampleSystem { private let family: Family2 - + init(nexus: Nexus) { family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: EmptyComponent.self) } - + func update(deltaT: Double) { family.forEach { (position: Position, velocity: Velocity) in position.x *= 2 velocity.a *= 2 - } - } - } - class ColorSystem { - let nexus: Nexus lazy var colors = nexus.family(requires: Color.self) - + init(nexus: Nexus) { self.nexus = nexus } - + func update() { colors .forEach { (color: Color) in @@ -93,30 +93,27 @@ class ColorSystem { class PositionSystem { let positions: Family1 - + var velocity: Double = 4.0 - + init(nexus: Nexus) { positions = nexus.family(requires: Position.self) } - + func randNorm() -> Double { return 4.0 } - + func update() { positions .forEach { [unowned self](pos: Position) in - let deltaX: Double = self.velocity * ((self.randNorm() * 2) - 1) let deltaY: Double = self.velocity * ((self.randNorm() * 2) - 1) let x = pos.x + Int(deltaX) let y = pos.y + Int(deltaY) - + pos.x = x pos.y = y } } - } - diff --git a/Tests/FirebladeECSTests/ComponentTests.swift b/Tests/FirebladeECSTests/ComponentTests.swift index 9ee80ee..0372b0c 100644 --- a/Tests/FirebladeECSTests/ComponentTests.swift +++ b/Tests/FirebladeECSTests/ComponentTests.swift @@ -9,19 +9,15 @@ import FirebladeECS import XCTest class ComponentTests: XCTestCase { - - func testComponentIdentifier() { - XCTAssertEqual(Position.identifier, Position.identifier) - XCTAssertEqual(Velocity.identifier, Velocity.identifier) + func testComponentIdentifier() { + XCTAssertEqual(Position.identifier, Position.identifier) + XCTAssertEqual(Velocity.identifier, Velocity.identifier) XCTAssertNotEqual(Velocity.identifier, Position.identifier) - + let p1 = Position(x: 1, y: 2) let v1 = Velocity(a: 3.14) XCTAssertEqual(p1.identifier, Position.identifier) XCTAssertEqual(v1.identifier, Velocity.identifier) XCTAssertNotEqual(v1.identifier, p1.identifier) - - } - - + } } diff --git a/Tests/FirebladeECSTests/EntityTests.swift b/Tests/FirebladeECSTests/EntityTests.swift index da6095d..d15452d 100644 --- a/Tests/FirebladeECSTests/EntityTests.swift +++ b/Tests/FirebladeECSTests/EntityTests.swift @@ -9,20 +9,21 @@ import FirebladeECS import XCTest class EntityTests: XCTestCase { - - func testEntityIdentifierAndIndex() { - - let min = EntityIdentifier(.min) - XCTAssertEqual(min.index, Int(UInt32.min)) + func testEntityIdentifierAndIndex() { + let min = EntityIdentifier(.min) + XCTAssertEqual(min.index, Int(UInt32.min)) let uRand = UInt32.random(in: UInt32.min...UInt32.max) - let rand = EntityIdentifier(uRand) - XCTAssertEqual(rand.index, Int(uRand)) + let rand = EntityIdentifier(uRand) + XCTAssertEqual(rand.index, Int(uRand)) - let max = EntityIdentifier(.max) - XCTAssertEqual(max, EntityIdentifier.invalid) + let max = EntityIdentifier(.max) + XCTAssertEqual(max, EntityIdentifier.invalid) XCTAssertEqual(max.index, Int(UInt32.max)) + } - } - + func testEntityIdentifierComparison() { + XCTAssertTrue(EntityIdentifier(1) < EntityIdentifier(2)) + XCTAssertTrue(EntityIdentifier(23) > EntityIdentifier(4)) + } } diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift index acdb36f..fb7fb50 100644 --- a/Tests/FirebladeECSTests/FamilyTests.swift +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -9,54 +9,51 @@ import XCTest class FamilyTests: XCTestCase { - var nexus: Nexus! - + override func setUp() { super.setUp() nexus = Nexus() } - + override func tearDown() { nexus = nil super.tearDown() } - + func createDefaultEntity() { let e = nexus.createEntity() e.assign(Position(x: 1, y: 2)) e.assign(Color()) } - + func testFamilyCreation() { - 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) XCTAssertEqual(nexus.numEntities, 0) - + let traits = FamilyTraitSet(requiresAll: [Position.self], excludesAll: [Name.self]) XCTAssertEqual(family.traits, traits) } - + func testFamilyReuse() { - let familyA = nexus.family(requires: Position.self, excludesAll: Name.self) - + let familyB = nexus.family(requires: Position.self, excludesAll: Name.self) - + XCTAssertEqual(nexus.numFamilies, 1) XCTAssertEqual(nexus.numComponents, 0) - + XCTAssertEqual(familyA, familyB) } - + func testFamilyAbandoned() { XCTAssertEqual(nexus.numFamilies, 0) XCTAssertEqual(nexus.numComponents, 0) @@ -83,9 +80,8 @@ class FamilyTests: XCTestCase { XCTAssertEqual(nexus.numFamilies, 1) XCTAssertEqual(nexus.numComponents, 0) XCTAssertEqual(nexus.numEntities, 0) - } - + func testFamilyLateMember() { let eEarly = nexus.createEntity(with: Position(x: 1, y: 2)) XCTAssertEqual(nexus.numFamilies, 0) @@ -102,95 +98,91 @@ class FamilyTests: XCTestCase { XCTAssertTrue(family.isMember(eEarly)) XCTAssertTrue(family.isMember(eLate)) } - + func testFamilyExchange() { let number: Int = 10 - + for i in 0.. Int { let upperBound: Int = 44 let range = UInt32.min...UInt32.max @@ -23,35 +22,32 @@ class HashingTests: XCTestCase { let cH = Int(bitPattern: rand) return cH } - + func testCollisionsInCritialRange() { - var hashSet: Set = Set() - + var range: CountableRange = 0 ..< 1_000_000 - + let maxComponents: Int = 1000 let components: [Int] = (0.. Entity { + let randComp = otherComponents.randomElement()! + let badChild = nexus.createEntity(with: randComp) + let child = nexus.createEntity(with: Index(index: index)) + parent.addChild(child) + parent.addChild(badChild) + return child + } + + let root = nexus.createEntity(with: Index(index: 0)) + + var parent: Entity = root + for i in 1..<10 { + parent = addChild(to: parent, index: i) + } + + XCTAssertEqual(nexus.numEntities, 19) + + var parentSum: Int = 0 + var childSum: Int = 0 + + var lastIndex: Int = -1 + + nexus + .family(requires: Index.self) + .descendRelatives(from: root) + .forEach { (parent: Index, child: Index) in + XCTAssertEqual(parent.index + 1, child.index) + XCTAssertGreaterThan(parent.index, lastIndex) + lastIndex = parent.index + parentSum += parent.index + childSum += child.index + } + + XCTAssertEqual(parentSum, 36) + XCTAssertEqual(childSum, 45) + } + +} diff --git a/Tests/FirebladeECSTests/SingleTests.swift b/Tests/FirebladeECSTests/SingleTests.swift index 49123b0..f0919c9 100644 --- a/Tests/FirebladeECSTests/SingleTests.swift +++ b/Tests/FirebladeECSTests/SingleTests.swift @@ -10,43 +10,42 @@ import XCTest class SingleTests: XCTestCase { var nexus: Nexus! - + override func setUp() { super.setUp() nexus = Nexus() } - + override func tearDown() { nexus = nil super.tearDown() } - + 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) - + XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1) XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1) - + let traits = FamilyTraitSet(requiresAll: [SingleGameState.self], excludesAll: []) XCTAssertEqual(single.traits, traits) } - + func testSingleReuse() { let singleA = nexus.single(SingleGameState.self) - + let singleB = nexus.single(SingleGameState.self) - + XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1) XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1) - + XCTAssertEqual(singleA, singleB) } - - + func testSingleEntityAndComponentCreation() { let single = nexus.single(SingleGameState.self) let gameState = SingleGameState() @@ -54,9 +53,8 @@ class SingleTests: XCTestCase { XCTAssertNotNil(single.component) XCTAssertEqual(single.component.shouldQuit, gameState.shouldQuit) XCTAssertEqual(single.component.playerHealth, gameState.playerHealth) - } - + func testSingleCreationOnExistingFamilyMember() { _ = nexus.createEntity(with: Position(x: 1, y: 2)) let singleGame = SingleGameState() diff --git a/Tests/FirebladeECSTests/SparseSetTests.swift b/Tests/FirebladeECSTests/SparseSetTests.swift index ff66583..7351f42 100644 --- a/Tests/FirebladeECSTests/SparseSetTests.swift +++ b/Tests/FirebladeECSTests/SparseSetTests.swift @@ -9,7 +9,6 @@ import XCTest class SparseSetTests: XCTestCase { - var set: UnorderedSparseSet! override func setUp() { @@ -23,7 +22,6 @@ class SparseSetTests: XCTestCase { } func testSparseSetAdd() { - let num: Int = 100 for idx in 0.. = [0, 30, 1, 21, 78, 56, 99, 3] for idx in indices { @@ -462,7 +451,6 @@ class SparseSetTests: XCTestCase { } func testSparseSetClear() { - let num: Int = 100 XCTAssertEqual(set.count, 0) @@ -506,27 +494,26 @@ class SparseSetTests: XCTestCase { // NOTE: this tests only dense insertion order, this is no guarantee for the real ordering. XCTAssertEqual(string, "Hello World") - } - + func testSubscript() { let characters = UnorderedSparseSet() - + characters[4] = "H" characters[13] = "e" characters[44] = "l" characters[123] = "l" characters[89] = "o" - + characters[66] = " " characters[77] = "W" characters[55] = "o" characters[90] = "r" characters[34] = "l" characters[140] = "d" - + XCTAssertEqual(characters.count, 11) - + XCTAssertEqual(characters[4], "H") XCTAssertEqual(characters[13], "e") XCTAssertEqual(characters[44], "l") @@ -539,17 +526,16 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(characters[34], "l") XCTAssertEqual(characters[140], "d") } - + func testStartEndIndex() { - let set = UnorderedSparseSet() - + set.insert("C", at: 33) set.insert("A", at: 11) set.insert("B", at: 22) - + let mapped = set.dense.map { $0.element } - + XCTAssertEqual(mapped, ["C", "A", "B"]) } } diff --git a/Tests/FirebladeECSTests/SystemsTests.swift b/Tests/FirebladeECSTests/SystemsTests.swift index 500f52e..ac815bd 100644 --- a/Tests/FirebladeECSTests/SystemsTests.swift +++ b/Tests/FirebladeECSTests/SystemsTests.swift @@ -9,116 +9,113 @@ import XCTest class SystemsTests: XCTestCase { - var nexus: Nexus! var colorSystem: ColorSystem! var positionSystem: PositionSystem! - + override func setUp() { super.setUp() nexus = Nexus() colorSystem = ColorSystem(nexus: nexus) positionSystem = PositionSystem(nexus: nexus) } - + override func tearDown() { colorSystem = nil positionSystem = nil nexus = nil super.tearDown() - } - + func testSystemsUpdate() { - let num: Int = 10_000 - + colorSystem.update() positionSystem.update() - + let posTraits = positionSystem.positions.traits - + XCTAssertEqual(nexus.numEntities, 0) XCTAssertEqual(colorSystem.colors.memberIds.count, 0) XCTAssertEqual(positionSystem.positions.memberIds.count, 0) XCTAssertEqual(nexus.freeEntities.count, 0) XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, 0) - + batchCreateEntities(count: num) - + XCTAssertEqual(nexus.numEntities, num) XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) XCTAssertEqual(colorSystem.colors.memberIds.count, num) XCTAssertEqual(positionSystem.positions.memberIds.count, num) XCTAssertEqual(nexus.freeEntities.count, 0) - + colorSystem.update() positionSystem.update() - + XCTAssertEqual(nexus.numEntities, num) XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) XCTAssertEqual(colorSystem.colors.memberIds.count, num) XCTAssertEqual(positionSystem.positions.memberIds.count, num) XCTAssertEqual(nexus.freeEntities.count, 0) - + batchCreateEntities(count: num) - + XCTAssertEqual(nexus.numEntities, num * 2) XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) XCTAssertEqual(nexus.freeEntities.count, 0) - + colorSystem.update() positionSystem.update() - + XCTAssertEqual(nexus.numEntities, num * 2) XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) XCTAssertEqual(nexus.freeEntities.count, 0) - + batchDestroyEntities(count: num) - + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) XCTAssertEqual(nexus.freeEntities.count, num) XCTAssertEqual(nexus.numEntities, num) XCTAssertEqual(colorSystem.colors.memberIds.count, num) XCTAssertEqual(positionSystem.positions.memberIds.count, num) - + colorSystem.update() positionSystem.update() - + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) XCTAssertEqual(nexus.numEntities, num) XCTAssertEqual(colorSystem.colors.memberIds.count, num) XCTAssertEqual(positionSystem.positions.memberIds.count, num) XCTAssertEqual(nexus.freeEntities.count, num) - + batchCreateEntities(count: num) - + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) XCTAssertEqual(nexus.numEntities, num * 2) XCTAssertEqual(colorSystem.colors.memberIds.count, num * 2) XCTAssertEqual(positionSystem.positions.memberIds.count, num * 2) XCTAssertEqual(nexus.freeEntities.count, 0) } - + func createDefaultEntity() { let e = nexus.createEntity() e.assign(Position(x: 1, y: 2)) e.assign(Color()) } - + func batchCreateEntities(count: Int) { for _ in 0.. [XCTestCaseEntry] { testCase(FamilyTraitsTests.__allTests__FamilyTraitsTests), testCase(HashingTests.__allTests__HashingTests), testCase(NexusTests.__allTests__NexusTests), + testCase(SceneGraphTests.__allTests__SceneGraphTests), testCase(SingleTests.__allTests__SingleTests), testCase(SparseSetTests.__allTests__SparseSetTests), - testCase(SystemsTests.__allTests__SystemsTests), + testCase(SystemsTests.__allTests__SystemsTests) ] } #endif