diff --git a/Sources/FirebladeECS/FamilyTraitSet.swift b/Sources/FirebladeECS/FamilyTraitSet.swift index df0c7b2..d22ee5e 100644 --- a/Sources/FirebladeECS/FamilyTraitSet.swift +++ b/Sources/FirebladeECS/FamilyTraitSet.swift @@ -5,13 +5,14 @@ // Created by Christian Treffs on 09.10.17. // -public struct FamilyTraitSet { +public struct FamilyTraitSet: CustomStringConvertible, CustomDebugStringConvertible { public let requiresAll: ComponentSet public let excludesAll: ComponentSet public let needsAtLeastOne: ComponentSet private let setHash: Int private let isEmptyAny: Bool + private let stringRespresentation: String public init(requiresAll: [Component.Type], excludesAll: [Component.Type], needsAtLeastOne: [Component.Type] = []) { @@ -29,6 +30,12 @@ public struct FamilyTraitSet { self.requiresAll = all self.needsAtLeastOne = one self.excludesAll = none + + let allString: String = requiresAll.map { "\($0)" }.joined(separator: ",") + let excludedString: String = excludesAll.map { "\($0)" }.joined(separator: ",") + let oneString: String = needsAtLeastOne.map { "\($0)" }.joined(separator: ",") + + stringRespresentation = "[all:\(allString) excluded:\(excludedString) one:\(oneString)]" } // MARK: - match @@ -63,6 +70,14 @@ public struct FamilyTraitSet { return !requiresAll.isEmpty || !atLeastOne.isEmpty } + public var description: String { + return stringRespresentation + } + + public var debugDescription: String { + return stringRespresentation + } + } // MARK: - Equatable diff --git a/Sources/FirebladeECS/Nexus+Component.swift b/Sources/FirebladeECS/Nexus+Component.swift index 50eefb9..7d26565 100644 --- a/Sources/FirebladeECS/Nexus+Component.swift +++ b/Sources/FirebladeECS/Nexus+Component.swift @@ -83,8 +83,8 @@ extension Nexus { @discardableResult public func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool { + let entityIdx: EntityIndex = entityId.index - //let hash: EntityComponentHash = componentId.hashValue(using: entityIdx) // delete component instance componentsByType[componentId]?.remove(at: entityIdx) @@ -99,6 +99,7 @@ extension Nexus { @discardableResult public func clear(componentes entityId: EntityIdentifier) -> Bool { + guard let allComponents: SparseComponentIdentifierSet = get(components: entityId) else { report("clearing components form entity \(entityId) with no components") return false diff --git a/Sources/FirebladeECS/Nexus+Family.swift b/Sources/FirebladeECS/Nexus+Family.swift index 6f3d1f3..c5a1f25 100644 --- a/Sources/FirebladeECS/Nexus+Family.swift +++ b/Sources/FirebladeECS/Nexus+Family.swift @@ -57,26 +57,6 @@ public extension Nexus { // MARK: - internal extensions extension Nexus { - /// will be called on family init defer - func onFamilyInit(traits: FamilyTraitSet) { - createTraitsIfNeccessary(traits: traits) - - // FIXME: this is costly for many entities - for entity: Entity in entityStorage { - update(membership: traits, for: entity.identifier) - } - } - - func onFamilyDeinit(traits: FamilyTraitSet) { - guard let members: UniformEntityIdentifiers = members(of: traits) else { - return - } - - for member: EntityIdentifier in members { - remove(from: traits, entityId: member, entityIdx: member.index) - } - } - func update(familyMembership entityId: EntityIdentifier) { // FIXME: iterating all families is costly for many families for (familyTraits, _) in familyMembersByTraits { @@ -84,16 +64,25 @@ extension Nexus { } } - func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) { + enum UpdateState { + case noComponents(id: EntityIdentifier, traits: FamilyTraitSet) + case added(id: EntityIdentifier, traits: FamilyTraitSet) + case removedDeleted(id: EntityIdentifier, traits: FamilyTraitSet) + case removed(id: EntityIdentifier, traits: FamilyTraitSet) + case unchanged(id: EntityIdentifier, traits: FamilyTraitSet) + } + + @discardableResult + func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) -> UpdateState { let entityIdx: EntityIndex = entityId.index guard let componentIds: SparseComponentIdentifierSet = componentIdsByEntity[entityIdx] else { - return + return .noComponents(id: entityId, traits: traits) } let isMember: Bool = self.isMember(entityId, in: traits) if !has(entity: entityId) && isMember { remove(from: traits, entityId: entityId, entityIdx: entityIdx) - return + return .removedDeleted(id: entityId, traits: traits) } let componentsSet: ComponentSet = ComponentSet(componentIds) @@ -102,14 +91,33 @@ extension Nexus { case (true, false): add(to: traits, entityId: entityId, entityIdx: entityIdx) notify(FamilyMemberAdded(member: entityId, toFamily: traits)) + return .added(id: entityId, traits: traits) case (false, true): remove(from: traits, entityId: entityId, entityIdx: entityIdx) notify(FamilyMemberRemoved(member: entityId, from: traits)) + return .removed(id: entityId, traits: traits) default: - break + return .unchanged(id: entityId, traits: traits) } } + /// will be called on family init defer + func onFamilyInit(traits: FamilyTraitSet) { + + if familyMembersByTraits[traits] == nil { + familyMembersByTraits[traits] = UniformEntityIdentifiers() + } + + // FIXME: this is costly for many entities + for entity: Entity in entityStorage { + update(membership: traits, for: entity.identifier) + } + } + + func onFamilyDeinit(traits: FamilyTraitSet) { + // nothing todo here + } + } // MARK: - fileprivate extensions @@ -124,19 +132,15 @@ private extension Nexus { return family } - func createTraitsIfNeccessary(traits: FamilyTraitSet) { - guard familyMembersByTraits[traits] == nil else { - return - } - familyMembersByTraits[traits] = UniformEntityIdentifiers() - } - func calculateTraitEntityIdHash(traitHash: FamilyTraitSetHash, entityIdx: EntityIndex) -> TraitEntityIdHash { return hash(combine: traitHash, entityIdx) } func add(to traits: FamilyTraitSet, entityId: EntityIdentifier, entityIdx: EntityIndex) { - createTraitsIfNeccessary(traits: traits) + if familyMembersByTraits[traits] == nil { + familyMembersByTraits[traits] = UniformEntityIdentifiers() + } + familyMembersByTraits[traits]?.insert(entityId, at: entityIdx) } diff --git a/Sources/FirebladeECS/UnorderedSparseSet.swift b/Sources/FirebladeECS/UnorderedSparseSet.swift index 7768332..24d3852 100644 --- a/Sources/FirebladeECS/UnorderedSparseSet.swift +++ b/Sources/FirebladeECS/UnorderedSparseSet.swift @@ -77,7 +77,6 @@ public class UnorderedSparseSet: Sequence { @discardableResult public func remove(at key: Key) -> Entry? { guard let (denseIndex, _) = find(at: key) else { - return nil } diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift index dc10c69..958e023 100644 --- a/Tests/FirebladeECSTests/FamilyTests.swift +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -57,8 +57,7 @@ class FamilyTests: XCTestCase { XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 0) - nexus.family(requiresAll: [Position.self], - excludesAll: []) + _ = nexus.family(requiresAll: [Position.self], excludesAll: []) XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1) diff --git a/Tests/FirebladeECSTests/SparseSetTests.swift b/Tests/FirebladeECSTests/SparseSetTests.swift index 70bf593..bf9fa1d 100644 --- a/Tests/FirebladeECSTests/SparseSetTests.swift +++ b/Tests/FirebladeECSTests/SparseSetTests.swift @@ -102,6 +102,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, 6) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertTrue(set.contains(0)) + XCTAssertTrue(set.contains(1)) + XCTAssertTrue(set.contains(2)) + XCTAssertTrue(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertTrue(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], 0) XCTAssertEqual(set.sparse[1], 1) XCTAssertEqual(set.sparse[2], 2) @@ -129,6 +138,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, 6) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertTrue(set.contains(0)) + XCTAssertTrue(set.contains(1)) + XCTAssertTrue(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertTrue(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], 0) XCTAssertEqual(set.sparse[1], 1) XCTAssertEqual(set.sparse[2], 2) @@ -157,6 +175,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, 6) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertTrue(set.contains(0)) + XCTAssertTrue(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertTrue(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], 0) XCTAssertEqual(set.sparse[1], 1) XCTAssertEqual(set.sparse[2], nil) @@ -185,6 +212,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, 6) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertFalse(set.contains(0)) + XCTAssertTrue(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertTrue(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], nil) XCTAssertEqual(set.sparse[1], 1) XCTAssertEqual(set.sparse[2], nil) @@ -212,6 +248,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, 6) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertFalse(set.contains(0)) + XCTAssertFalse(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertTrue(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], nil) XCTAssertEqual(set.sparse[1], nil) XCTAssertEqual(set.sparse[2], nil) @@ -237,6 +282,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, nil) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertFalse(set.contains(0)) + XCTAssertFalse(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertTrue(set.contains(5)) + XCTAssertFalse(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], nil) XCTAssertEqual(set.sparse[1], nil) XCTAssertEqual(set.sparse[2], nil) @@ -262,6 +316,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, nil) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertFalse(set.contains(0)) + XCTAssertFalse(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertTrue(set.contains(4)) + XCTAssertFalse(set.contains(5)) + XCTAssertFalse(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], nil) XCTAssertEqual(set.sparse[1], nil) XCTAssertEqual(set.sparse[2], nil) @@ -288,6 +351,15 @@ class SparseSetTests: XCTestCase { XCTAssertEqual(set.get(at: 6)?.x, nil) XCTAssertEqual(set.get(at: 7)?.x, nil) + XCTAssertFalse(set.contains(0)) + XCTAssertFalse(set.contains(1)) + XCTAssertFalse(set.contains(2)) + XCTAssertFalse(set.contains(3)) + XCTAssertFalse(set.contains(4)) + XCTAssertFalse(set.contains(5)) + XCTAssertFalse(set.contains(6)) + XCTAssertFalse(set.contains(7)) + XCTAssertEqual(set.sparse[0], nil) XCTAssertEqual(set.sparse[1], nil) XCTAssertEqual(set.sparse[2], nil) diff --git a/Tests/FirebladeECSTests/SystemsTests.swift b/Tests/FirebladeECSTests/SystemsTests.swift index 629a6aa..5274cfd 100644 --- a/Tests/FirebladeECSTests/SystemsTests.swift +++ b/Tests/FirebladeECSTests/SystemsTests.swift @@ -6,7 +6,7 @@ // import XCTest -import FirebladeECS +@testable import FirebladeECS class SystemsTests: XCTestCase { @@ -31,57 +31,78 @@ class SystemsTests: XCTestCase { func testSystemsUpdate() { + let num: Int = 10_000 + colorSystem.update() positionSystem.update() + let posTraits = positionSystem.family.traits + XCTAssertEqual(nexus.numEntities, 0) XCTAssertEqual(colorSystem.family.memberIds.count, 0) XCTAssertEqual(positionSystem.family.memberIds.count, 0) + XCTAssertEqual(nexus.freeEntities.count, 0) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, 0) - batchCreateEntities(count: 10_000) + batchCreateEntities(count: num) - XCTAssertEqual(nexus.numEntities, 10_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 10_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 10_000) + XCTAssertEqual(nexus.numEntities, num) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) + XCTAssertEqual(colorSystem.family.memberIds.count, num) + XCTAssertEqual(positionSystem.family.memberIds.count, num) + XCTAssertEqual(nexus.freeEntities.count, 0) colorSystem.update() positionSystem.update() - XCTAssertEqual(nexus.numEntities, 10_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 10_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 10_000) + XCTAssertEqual(nexus.numEntities, num) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) + XCTAssertEqual(colorSystem.family.memberIds.count, num) + XCTAssertEqual(positionSystem.family.memberIds.count, num) + XCTAssertEqual(nexus.freeEntities.count, 0) - batchCreateEntities(count: 10_000) + batchCreateEntities(count: num) - XCTAssertEqual(nexus.numEntities, 20_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 20_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 20_000) + XCTAssertEqual(nexus.numEntities, num * 2) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) + XCTAssertEqual(colorSystem.family.memberIds.count, num * 2) + XCTAssertEqual(positionSystem.family.memberIds.count, num * 2) + XCTAssertEqual(nexus.freeEntities.count, 0) colorSystem.update() positionSystem.update() - XCTAssertEqual(nexus.numEntities, 20_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 20_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 20_000) + XCTAssertEqual(nexus.numEntities, num * 2) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) + XCTAssertEqual(colorSystem.family.memberIds.count, num * 2) + XCTAssertEqual(positionSystem.family.memberIds.count, num * 2) + XCTAssertEqual(nexus.freeEntities.count, 0) - batchDestroyEntities(count: 10_000) + batchDestroyEntities(count: num) + + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) + XCTAssertEqual(nexus.freeEntities.count, num) + XCTAssertEqual(nexus.numEntities, num) + XCTAssertEqual(colorSystem.family.memberIds.count, num) + XCTAssertEqual(positionSystem.family.memberIds.count, num) - XCTAssertEqual(nexus.numEntities, 10_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 10_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 10_000) colorSystem.update() positionSystem.update() - XCTAssertEqual(nexus.numEntities, 10_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 10_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 10_000) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num) + XCTAssertEqual(nexus.numEntities, num) + XCTAssertEqual(colorSystem.family.memberIds.count, num) + XCTAssertEqual(positionSystem.family.memberIds.count, num) + XCTAssertEqual(nexus.freeEntities.count, num) - batchCreateEntities(count: 10_000) + batchCreateEntities(count: num) - XCTAssertEqual(nexus.numEntities, 20_000) - XCTAssertEqual(colorSystem.family.memberIds.count, 20_000) - XCTAssertEqual(positionSystem.family.memberIds.count, 20_000) + XCTAssertEqual(nexus.familyMembersByTraits[posTraits]?.count, num * 2) + XCTAssertEqual(nexus.numEntities, num * 2) + XCTAssertEqual(colorSystem.family.memberIds.count, num * 2) + XCTAssertEqual(positionSystem.family.memberIds.count, num * 2) + XCTAssertEqual(nexus.freeEntities.count, 0) } func createDefaultEntity(name: String?) { @@ -99,6 +120,7 @@ class SystemsTests: XCTestCase { func batchDestroyEntities(count: Int) { let family = nexus.family(requiresAll: [Position.self], excludesAll: []) var currentCount = count + family.iterate { (entity: Entity!) in if currentCount > 0 { entity.destroy() @@ -150,8 +172,8 @@ class PositionSystem { let deltaX: Double = self.velocity*((self.randNorm() * 2) - 1) let deltaY: Double = self.velocity*((self.randNorm() * 2) - 1) - var x = pos.x + Int(deltaX) - var y = pos.y + Int(deltaY) + let x = pos.x + Int(deltaX) + let y = pos.y + Int(deltaY) pos.x = x pos.y = y