From c3d84b4f12b2066c52e10bfc5e79514785c5878a Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Sat, 5 Oct 2019 22:38:28 +0200 Subject: [PATCH] Refactor component and entity identifier --- Sources/FirebladeECS/Component.swift | 1 - .../FirebladeECS/ComponentIdentifier.swift | 14 +++- Sources/FirebladeECS/Nexus+Entity.swift | 19 ++++-- Sources/FirebladeECS/Nexus+FamilyUpdate.swift | 4 +- Sources/FirebladeECS/Nexus+SceneGraph.swift | 8 ++- Sources/FirebladeECS/Nexus.swift | 68 +++++++++++++------ Tests/FirebladeECSPerformanceTests/Base.swift | 16 ++++- Tests/FirebladeECSTests/Base.swift | 11 ++- 8 files changed, 104 insertions(+), 37 deletions(-) diff --git a/Sources/FirebladeECS/Component.swift b/Sources/FirebladeECS/Component.swift index c87e7a5..a6ea2df 100644 --- a/Sources/FirebladeECS/Component.swift +++ b/Sources/FirebladeECS/Component.swift @@ -14,6 +14,5 @@ public protocol Component: class, Codable { } 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 d3ae5d7..33e564a 100644 --- a/Sources/FirebladeECS/ComponentIdentifier.swift +++ b/Sources/FirebladeECS/ComponentIdentifier.swift @@ -7,12 +7,20 @@ /// Identifies a component by it's meta type public struct ComponentIdentifier: Identifiable { - public let id: ObjectIdentifier + public let id: String - init(_ type: T.Type) where T: Component { - self.id = ObjectIdentifier(type) + public init(_ componentType: T.Type) where T: Component { + defer { Nexus.register(component: T.self, using: self) } + + self.id = String(reflecting: componentType) } } extension ComponentIdentifier: Equatable { } extension ComponentIdentifier: Hashable { } +extension ComponentIdentifier: Codable { } +extension ComponentIdentifier: Comparable { + public static func < (lhs: ComponentIdentifier, rhs: ComponentIdentifier) -> Bool { + return lhs.id < rhs.id + } +} diff --git a/Sources/FirebladeECS/Nexus+Entity.swift b/Sources/FirebladeECS/Nexus+Entity.swift index b40308b..3578bfe 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 @@ -40,23 +39,29 @@ extension Nexus { } 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) + return Entity(nexus: self, id: entityStorage.get(unsafeAt: entityId.id)) } @discardableResult public func destroy(entity: Entity) -> Bool { - let entityId: EntityIdentifier = entity.identifier + return 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+FamilyUpdate.swift b/Sources/FirebladeECS/Nexus+FamilyUpdate.swift index 9b92e72..cd7d78d 100644 --- a/Sources/FirebladeECS/Nexus+FamilyUpdate.swift +++ b/Sources/FirebladeECS/Nexus+FamilyUpdate.swift @@ -19,8 +19,8 @@ extension Nexus { 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) } } diff --git a/Sources/FirebladeECS/Nexus+SceneGraph.swift b/Sources/FirebladeECS/Nexus+SceneGraph.swift index 24e8497..4203f4e 100644 --- a/Sources/FirebladeECS/Nexus+SceneGraph.swift +++ b/Sources/FirebladeECS/Nexus+SceneGraph.swift @@ -35,8 +35,12 @@ extension Nexus { } public final func removeAllChildren(from parent: Entity) { - childrenByParentEntity[parent.identifier]?.forEach { removeChild($0, from: parent.identifier) } - return childrenByParentEntity[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 { diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 5fdf2fb..bbffe12 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -6,9 +6,12 @@ // public final class Nexus { + /// Static version string. + public static let version: String = "1.0.0" + /// 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] @@ -33,13 +36,27 @@ public final class Nexus { public final weak var delegate: NexusEventDelegate? - public init() { - entityStorage = UnorderedSparseSet() - componentsByType = [:] - componentIdsByEntity = [:] - freeEntities = [] - familyMembersByTraits = [:] - childrenByParentEntity = [:] + public convenience init() { + self.init(entityStorage: UnorderedSparseSet(), + componentsByType: [:], + componentsByEntity: [:], + freeEntities: [], + familyMembersByTraits: [:], + childrenByParentEntity: [:]) + } + + 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 { @@ -47,25 +64,36 @@ public final class Nexus { } public final func clear() { - var iter = entityStorage.makeIterator() - while let entity = iter.next() { - destroy(entity: entity) - } - + entityStorage.forEach { destroy(entityId: $0) } 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() childrenByParentEntity.removeAll() } + + public static var knownUniqueComponentTypes: Set { + return Set(componentDecoderMap.keys) + } + + internal static var componentDecoderMap: [ComponentIdentifier: (Decoder) throws -> Component] = [:] + + /// Register a component type uniquely with the Nexus implementation. + /// - Parameters: + /// - componentType: The component meta type. + /// - identifier: The unique identifier. + internal static func register(component componentType: C.Type, using identifier: ComponentIdentifier) where C: Component { + precondition(componentDecoderMap[identifier] == nil, "Component type collision: \(identifier) already in use.") + componentDecoderMap[identifier] = { try C(from: $0) } + } +} + +// MARK: - Errors +extension Nexus { + public enum Error: Swift.Error { + case versionMismatch(required: String, provided: String) + } } // MARK: - Equatable diff --git a/Tests/FirebladeECSPerformanceTests/Base.swift b/Tests/FirebladeECSPerformanceTests/Base.swift index 3b5834d..c9bbc33 100644 --- a/Tests/FirebladeECSPerformanceTests/Base.swift +++ b/Tests/FirebladeECSPerformanceTests/Base.swift @@ -7,9 +7,13 @@ import FirebladeECS -class EmptyComponent: Component { } +class EmptyComponent: Component { + static let identifier: ComponentIdentifier = .init(EmptyComponent.self) +} class Name: Component { + static let identifier: ComponentIdentifier = .init(Name.self) + var name: String init(name: String) { self.name = name @@ -17,6 +21,8 @@ class Name: Component { } class Position: Component { + static var identifier: ComponentIdentifier = .init(Position.self) + var x: Int var y: Int init(x: Int, y: Int) { @@ -26,6 +32,8 @@ class Position: Component { } class Velocity: Component { + static var identifier: ComponentIdentifier = .init(Velocity.self) + var a: Float init(a: Float) { self.a = a @@ -33,6 +41,8 @@ class Velocity: Component { } class Party: Component { + static var identifier: ComponentIdentifier = .init(Party.self) + var partying: Bool init(partying: Bool) { self.partying = partying @@ -40,6 +50,8 @@ class Party: Component { } class Color: Component { + static var identifier: ComponentIdentifier = .init(Color.self) + var r: UInt8 = 0 var g: UInt8 = 0 var b: UInt8 = 0 @@ -61,6 +73,8 @@ class ExampleSystem { } final class SingleGameState: SingleComponent { + static var identifier: ComponentIdentifier = .init(SingleGameState.self) + var shouldQuit: Bool = false var playerHealth: Int = 67 } diff --git a/Tests/FirebladeECSTests/Base.swift b/Tests/FirebladeECSTests/Base.swift index eb9e589..f7bc120 100644 --- a/Tests/FirebladeECSTests/Base.swift +++ b/Tests/FirebladeECSTests/Base.swift @@ -7,9 +7,12 @@ import FirebladeECS -class EmptyComponent: Component { } +class EmptyComponent: Component { + static let identifier: ComponentIdentifier = .init(EmptyComponent.self) +} class Name: Component { + static let identifier: ComponentIdentifier = .init(Name.self) var name: String init(name: String) { self.name = name @@ -17,6 +20,7 @@ class Name: Component { } class Position: Component { + static let identifier: ComponentIdentifier = .init(Position.self) var x: Int var y: Int init(x: Int, y: Int) { @@ -26,6 +30,7 @@ class Position: Component { } class Velocity: Component { + static let identifier: ComponentIdentifier = .init(Velocity.self) var a: Float init(a: Float) { self.a = a @@ -33,6 +38,7 @@ class Velocity: Component { } class Party: Component { + static let identifier: ComponentIdentifier = .init(Party.self) var partying: Bool init(partying: Bool) { self.partying = partying @@ -40,12 +46,14 @@ class Party: Component { } class Color: Component { + static let identifier: ComponentIdentifier = .init(Color.self) var r: UInt8 = 0 var g: UInt8 = 0 var b: UInt8 = 0 } class Index: Component { + static let identifier: ComponentIdentifier = .init(Index.self) var index: Int init(index: Int) { @@ -54,6 +62,7 @@ class Index: Component { } final class SingleGameState: SingleComponent { + static let identifier: ComponentIdentifier = .init(SingleGameState.self) var shouldQuit: Bool = false var playerHealth: Int = 67 }