diff --git a/Package.swift b/Package.swift index e0f243e..2531b31 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "FirebladeECS", - targets: ["FirebladeECS"]), + targets: ["FirebladeECS"]) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -23,6 +23,6 @@ let package = Package( dependencies: []), .testTarget( name: "FirebladeECSTests", - dependencies: ["FirebladeECS"]), + dependencies: ["FirebladeECS"]) ] ) diff --git a/Sources/FirebladeECS/Component.swift b/Sources/FirebladeECS/Component.swift new file mode 100644 index 0000000..12c7346 --- /dev/null +++ b/Sources/FirebladeECS/Component.swift @@ -0,0 +1,21 @@ +// +// Component.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +public protocol Component: UniqueComponentIdentifiable {} + +// MARK: UCI +extension Component { + /// Uniquely identifies the component by its meta type + public static var uct: UCT { return UCT(Self.self) } + /// Uniquely identifies the component by its meta type + public var uct: UCT { return Self.uct } +} + +// MARK: Equatable +public func ==(lhs: A, rhs: B) -> Bool { + return A.uct == B.uct +} diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift new file mode 100644 index 0000000..a25a7e1 --- /dev/null +++ b/Sources/FirebladeECS/Entity.swift @@ -0,0 +1,209 @@ +// +// Entity.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +public final class Entity: UniqueEntityIdentifiable { + public let uei: UEI + + fileprivate var componentMap: [UCT:Component] + + private init(uei: UEI) { + self.uei = uei + componentMap = [UCT: Component]() + componentMap.reserveCapacity(2) + } + + convenience public init() { + let uei: UEI = UEI.next + self.init(uei: uei) + defer { + notify(init: unownedRef) + } + } + + deinit { + defer { + destroy() + } + } +} + +// MARK: - Equatable +public func ==(lhs: Entity, rhs: Entity) -> Bool { + return lhs.uei == rhs.uei +} + +// MARK: - number of components +public extension Entity { + public final var numComponents: Int { return componentMap.count } +} + +// MARK: - has component(s) +public extension Entity { + + public final func has(_ component: Component.Type) -> Bool { + return has(UCT(component)) + } + + public final func has(_ component: UCT) -> Bool { + fatalError() + } + + public final var hasComponents: Bool { return !componentMap.isEmpty } + +} + +// MARK: - push component(s) +public extension Entity { + + @discardableResult + public final func push(component: C) -> Entity { + let previousComponent: C? = componentMap.updateValue(component, forKey: component.uct) as? C + if let pComp: C = previousComponent { + notify(update: component, previous: pComp) + } else { + notify(add: component) + } + return self + } + + @discardableResult + public static func += (lhs: Entity, rhs: C) -> Entity { + return lhs.push(component: rhs) + } + + @discardableResult + public static func << (lhs: Entity, rhs: C) -> Entity { + return lhs.push(component: rhs) + } +} + +// MARK: - peek component +public extension Entity { + + public final func peekComponent() -> C? { + return componentMap[C.uct] as? C + } + + public final func peek(_ componentType: C.Type) -> C? { + return componentMap[componentType.uct] as? C + } + + public final func peek(_ uct: UCT) -> C? { + return componentMap[uct] as? C + } + + public final func peek(_ unwrapping: (C?) -> C) -> C { + return unwrapping(peekComponent()) + } + + @discardableResult + public final func peek(_ applying: (C?) -> Result) -> Result { + return applying(peekComponent()) + } + +} + +// MARK: - remove component(s) +public extension Entity { + + @discardableResult + public final func remove(_ component: Component) -> Entity { + return remove(component.uct) + } + + @discardableResult + public final func remove(_ componentType: C.Type) -> Entity { + let removedComponent: C? = componentMap.removeValue(forKey: C.uct) as? C + if let rComp: C = removedComponent { + notify(removed: rComp) + } + return self + } + + @discardableResult + public final func remove(_ uct: UCT) -> Entity { + let removedComponent = componentMap.removeValue(forKey: uct) + if let rComp = removedComponent { + assert(rComp.uct.type == uct.type) + notify(removed: rComp) + } + return self + } + + public final func removeAll() { + componentMap.forEach { remove($0.key) } + } + + @discardableResult + public static func -= (lhs: Entity, rhs: C) -> Entity { + return lhs.remove(rhs) + } + + @discardableResult + public static func -= (lhs: Entity, rhs: C.Type) -> Entity { + return lhs.remove(rhs) + } +} + +// MARK: - destroy/deinit entity +extension Entity { + final func destroy() { + removeAll() + UEI.free(uei) + notify(destroyed: unownedRef) + } +} + +extension Entity: EventSender { + + private unowned var unownedRef: Entity { + return self + } + + private func notify(init: Entity) { + dispatch(event: EntityCreated(entity: unownedRef)) + } + + private func notify(add component: C) { + dispatch(event: ComponentAdded(component: component, to: unownedRef)) + } + + private func notify(update newComponent: C, previous previousComponent: C) { + dispatch(event: ComponentUpdated(component: newComponent, previous: previousComponent, at: unownedRef)) + } + + private func notify(removed component: C) { + dispatch(event: ComponentRemoved(component: component, from: unownedRef)) + } + + private func notify(removed component: Component) { + dispatch(event: ComponentRemoved(component: component, from: unownedRef)) + } + + private func notify(destroyed: Entity) { + dispatch(event: EntityDestroyed(entity: unownedRef)) + } + +} + +// MARK: - debugging and string representation +/* +extension Entity: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { return "Entity\(stringifyLabel())[\(uid)]" } + public var debugDescription: String { + let comps: String = self.componentMap.map { (_: ComponentType, comp: Component) in + return comp.debugDescription + }.joined(separator: ",") + return "\(self.description){ \(comps) }" + } +} +extension Entity: CustomPlaygroundQuickLookable { + public var customPlaygroundQuickLook: PlaygroundQuickLook { + return .text(self.debugDescription) + } +} +*/ diff --git a/Sources/FirebladeECS/Event.swift b/Sources/FirebladeECS/Event.swift new file mode 100644 index 0000000..169a3dc --- /dev/null +++ b/Sources/FirebladeECS/Event.swift @@ -0,0 +1,170 @@ +// +// Event.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +protocol ECSEvent: UniqueEventIdentifiable { } +extension ECSEvent { + static var uet: UET { return UET(Self.self) } + var uet: UET { return Self.uet } +} + +// MARK: - event dispachter protocol + protocol EventHandler: class { + func subscribe(event eventHandler: @escaping (E) -> Void) + func unsubscribe(event eventHandler: @escaping (E) -> Void) + + unowned var listenerRef: EventHandler { get } +} + +extension EventHandler { + + /// Subscribe with an event handler closure to receive events of type T + /// + /// - Parameter eventHandler: event handler closure + func subscribe(event eventHandler: @escaping (E) -> Void) { + EventHub.shared.add(listener: listenerRef, handler: eventHandler) + } + + /// Unsubscribe from an event handler closure to stop receiving events of type T + /// + /// - Parameter eventHandler: event handler closure + func unsubscribe(event eventHandler: @escaping (E) -> Void) { + EventHub.shared.remove(listener: listenerRef, handler: eventHandler) + } + +} + +protocol EventSender: class { + func dispatch(event: E) +} + +extension EventSender { + /// Dispatch an event of type E + /// + /// - Parameter event: event of type E + func dispatch(event: E) { + EventHub.shared.dispatch(event) + } +} + +// MARK: - event hub central +fileprivate typealias EventListener = (weakRef: EventHandler, eventHandler: (ECSEvent) -> Void ) +final class EventHub { + + static let shared: EventHub = EventHub() + + private var listeners: [UET: ContiguousArray] = [:] + + private init() {} + +} + +extension ContiguousArray where Element == EventListener { + func index(is listenerRef: EventHandler) -> Int? { + return index { (eventListener: EventListener) -> Bool in + return eventListener.weakRef === listenerRef + } + } + +} + +extension EventHub { + + private static func relayEvent(opaqueEvent: ECSEvent, eventHandler: @escaping (E) -> Void ) { + guard let typedEvent: E = opaqueEvent as? E else { + fatalError() // TODO: meaningful message + } + eventHandler(typedEvent) + } + + final func add(listener listenerRef: EventHandler, handler: @escaping (E) -> Void) { + let eventListener: EventListener = (weakRef: listenerRef, + eventHandler: { EventHub.relayEvent(opaqueEvent: $0, eventHandler: handler) }) + + push(listener: eventListener, for: E.uet) + } + + private func push(listener newListener: EventListener, `for` uet: UET) { + if listeners[uet] == nil { + listeners[uet] = [] + listeners.reserveCapacity(1) + } + listeners[uet]?.append(newListener) + } + +} + +extension EventHub { + + final func remove(listener listenerRef: EventHandler, handler: @escaping (E) -> Void) { + let uet: UET = E.uet + + if let removeIdx: Int = listeners[uet]?.index(is: listenerRef) { + let removed = listeners[uet]?.remove(at: removeIdx) + assert(removed != nil) + } + + } + +} + +extension EventHub { + + final func dispatch(_ event: E) { + let uet: UET = E.uet + listeners[uet]?.forEach { + $0.eventHandler(event) + } + } + +} + +/* +extension EventHandler { + func subscribe(event eventHandler: @escaping (T) -> Void, syncOnQueue queue: WorkQueue) + func subscribe(event eventHandler: @escaping (T) -> Void, asyncOnQueue queue: WorkQueue) +} + +extension EventSender { + + /// Subscribe with a synchronous dispatched event handler closure to receive events of type T + /// + /// - Parameters: + /// - eventHandler: event handler closure + /// - queue: queue to handle events + func subscribe(event eventHandler: @escaping (T) -> Void, syncOnQueue queue: WorkQueue) { + EventHub.shared.addSyncListener(owner: type(of: self), syncOnQueue: queue, handler: eventHandler) + } + + /// Subscribe with an asynchronous dispatched event handler closure to receive events of type T + /// + /// - Parameters: + /// - eventHandler: event handler closure + /// - queue: queue to handle events on + func subscribe(event eventHandler: @escaping (T) -> Void, asyncOnQueue queue: WorkQueue) { + EventHub.shared.addAsyncListener(owner: type(of: self), asyncOnQueue: queue, handler: eventHandler) + } +} + +extension EventHub { + + final func addSyncListener(owner: AnyClass, syncOnQueue queue: WorkQueue, handler: @escaping (T) -> Void) { + let syncListener: Listener = (owner: owner, handler: { event in + guard let tEvent: T = event as? T else { fatalError("can not cast event to required type") } + queue.syncExec { handler(tEvent) } + }) + addToList(eventType: T.eventType, listener: syncListener) + } + + final func addAsyncListener(owner: AnyClass, asyncOnQueue queue: WorkQueue, handler: @escaping (T) -> Void) { + let asyncListener: Listener = (owner: owner, handler: { event in + guard let tEvent: T = event as? T else { fatalError("can not cast event to required type") } + queue.asyncExec { handler(tEvent) } + }) + addToList(eventType: T.eventType, listener: asyncListener) + } +} +*/ diff --git a/Sources/FirebladeECS/Events.swift b/Sources/FirebladeECS/Events.swift new file mode 100644 index 0000000..e03d482 --- /dev/null +++ b/Sources/FirebladeECS/Events.swift @@ -0,0 +1,44 @@ +// +// Events.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +struct EntityCreated: ECSEvent { + let entity: Entity +} + +struct EntityDestroyed: ECSEvent { + let entity: Entity +} + +struct ComponentAdded: ECSEvent { + let component: Component + let to: Entity +} + +struct ComponentUpdated: ECSEvent { + let component: Component + let previous: Component + let at: Entity +} + +struct ComponentRemoved: ECSEvent { + let component: Component + let from: Entity +} + +/* +public enum ECSEvent { + + case entityCreated(Entity) + case entityDestroyed(Entity) + + case componentAdded(Component, to: Entity) + case componentUpdated(Component, previous: Component, at: Entity) + case componentRemoved(Component, from: Entity) + + +} +*/ diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift new file mode 100644 index 0000000..1032ff1 --- /dev/null +++ b/Sources/FirebladeECS/Family.swift @@ -0,0 +1,119 @@ +// +// Family.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// +/* +// TODO: is this needed? +struct FamilyMemberAdded: Event { + let entity: Entity + let family: Family +} +// TODO: is this needed? +struct FamilyMemberRemoved: Event { + let entity: Entity + let family: Family +} + +struct FamilyCreated: Event { + let family: Family +} + +struct FamilyDestroyed: Event { + //TODO: family +} + +public final class Family: EventSender, EventHandler { + + // members of this Family must conform to: + let required: Set + let excluded: Set + + public private(set) var members: ContiguousArray + + public convenience init(requiresAll required: ComponentType...) { + self.init(requiresAll: required, excludesAll: []) + } + + public init(requiresAll required: [ComponentType], excludesAll excluded: [ComponentType]) { + self.required = Set(required) + self.excluded = Set(excluded) + + self.members = [] + + subscribe(event: handleComponentAddedToEntity) + subscribe(event: handleComponentRemovedFromEntity) + + dispatch(event: FamilyCreated(family: self)) + } + + deinit { + + //TODO: optimize for large sets + //TODO: dispatch entity removed event + self.members.removeAll() + + unsubscribe(event: handleComponentAddedToEntity) + unsubscribe(event: handleComponentRemovedFromEntity) + + dispatch(event: FamilyDestroyed()) + } + + final func handleComponentAddedToEntity(event: ComponentAdded) { + //TODO: optimize by more specific comparison + self.update(familyMembership: event.toEntity) + } + final func handleComponentRemovedFromEntity(event: ComponentRemoved) { + //TODO: optimize by more specific comparison + self.update(familyMembership: event.fromEntity) + } + + final func matches(familyRequirements entity: Entity) -> Bool { + return entity.contains(all: required) && entity.contains(none: excluded) + } + + final func contains(entity: Entity) -> Bool { + return self.members.contains(where: { $0.uid == entity.uid }) + } + final func indexOf(entity: Entity) -> Int? { + return self.members.index(where: { $0.uid == entity.uid }) + } + + final func update(familyMemberships entities: ContiguousArray) { + //TODO: optimize for large sets + entities.forEach { self.update(familyMembership:$0) } + } + + private final func update(familyMembership entity: Entity) { + + let NEW: Int = -1 + let isMatch: Bool = matches(familyRequirements: entity) + let index: Int = indexOf(entity: entity) ?? NEW + let isNew: Bool = index == NEW + + switch (isMatch, isNew) { + case (true, true): // isMatch && new -> add + add(toFamily: entity) + return + + case (false, false): // noMatch && isPart -> remove + remove(entityAtIndex: index) + return + + default: + return + } + } + + private final func add(toFamily entity: Entity) { + self.members.append(entity) + dispatch(event: FamilyMemberAdded(entity: entity, family: self)) + } + + private final func remove(entityAtIndex index: Int) { + let removedEntity: Entity = self.members.remove(at: index) + dispatch(event: FamilyMemberRemoved(entity: removedEntity, family: self)) + } +} +*/ diff --git a/Sources/FirebladeECS/FirebladeECS.swift b/Sources/FirebladeECS/FirebladeECS.swift deleted file mode 100644 index be3396f..0000000 --- a/Sources/FirebladeECS/FirebladeECS.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct FirebladeECS { - var text = "Hello, World!" -} diff --git a/Sources/FirebladeECS/Logging.swift b/Sources/FirebladeECS/Logging.swift new file mode 100644 index 0000000..c8e4fdd --- /dev/null +++ b/Sources/FirebladeECS/Logging.swift @@ -0,0 +1,49 @@ +// +// Logging.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +struct Log { + + private init() { } + + enum Level { + case info, debug, warn, error + + var toString: String { + switch self { + case .info: return "INFO" + case .debug: return "DEBUG" + case .warn: return "WARN" + case .error: return "ERROR" + } + } + } + + static func info(_ args: Any?...) { Log.log(level: .info, args: args) } + static func debug(_ args: Any?...) { Log.log(level: .debug, args: args) } + static func warn(_ args: Any?...) { Log.log(level: .warn, args: args) } + static func error(_ args: Any?...) { Log.log(level: .error, args: args) } + + private static func log(level: Log.Level, args: [Any?]) { + let entry: String = Log.serialize(level: level, args: args) + Swift.print(entry) + } + + private static func serialize(level: Log.Level, args: [Any?]) -> String { + let argStrings: [String] = args.flatMap { arg in + if let arg = arg { + return String(describing: arg) + } + return nil + } + + let argString: String = argStrings.joined(separator: " ") + let levelString: String = level.toString + + return ["[", levelString, "]\t", argString].joined() + } + +} diff --git a/Sources/FirebladeECS/System.swift b/Sources/FirebladeECS/System.swift new file mode 100644 index 0000000..a778b09 --- /dev/null +++ b/Sources/FirebladeECS/System.swift @@ -0,0 +1,20 @@ +// +// System.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +public enum SystemState { + case running, paused, inactive +} + +public protocol System: class { + //var state: SystemState { set get } + func startup() + func shutdown() +} + +public protocol EntitySystem: System { + //TODO: var systemFamily: Family { get } +} diff --git a/Sources/FirebladeECS/UCT.swift b/Sources/FirebladeECS/UCT.swift new file mode 100644 index 0000000..40933d1 --- /dev/null +++ b/Sources/FirebladeECS/UCT.swift @@ -0,0 +1,41 @@ +// +// UCT.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +// MARK: Unique Component Type +/// Unique Component Type +public struct UCT { + let objectIdentifier: ObjectIdentifier + let type: Component.Type + + init(_ componentType: Component.Type) { + objectIdentifier = ObjectIdentifier(componentType) + type = componentType + } + + init(_ component: Component) { + let componentType: Component.Type = component.uct.type + self.init(componentType) + } +} + +extension UCT: Equatable { + public static func ==(lhs: UCT, rhs: UCT) -> Bool { + return lhs.objectIdentifier == rhs.objectIdentifier + } +} + +extension UCT: Hashable { + public var hashValue: Int { + return objectIdentifier.hashValue + } +} + +// MARK: Unique Component Identifiable +public protocol UniqueComponentIdentifiable { + static var uct: UCT { get } + var uct: UCT { get } +} diff --git a/Sources/FirebladeECS/UEI.swift b/Sources/FirebladeECS/UEI.swift new file mode 100644 index 0000000..c343023 --- /dev/null +++ b/Sources/FirebladeECS/UEI.swift @@ -0,0 +1,36 @@ +// +// UEI.swift +// FirebladeECS +// +// Created by Christian Treffs on 08.10.17. +// + +// MARK: Unique Entity Index + +public typealias UEI = UInt32 // provides 4294967295 unique identifiers + +public extension UEI { + + private static var currentUEI: UEI = UInt32.min // starts at 0 + + /// Provides the next (higher/free) unique entity identifer. + /// Minimum: 1, maximum: 4294967295. + /// - Returns: next higher unique entity identifer. + public static var next: UEI { + currentUEI += 1 + return currentUEI + } + + internal static func free(_ uei: UEI) { + // TODO: free used index + } +} + +// MARK: Unique Entity Identifiable +public protocol UniqueEntityIdentifiable: Hashable { + var uei: UEI { get } +} + +public extension UniqueEntityIdentifiable { + public var hashValue: Int { return uei.hashValue } +} diff --git a/Sources/FirebladeECS/UET.swift b/Sources/FirebladeECS/UET.swift new file mode 100644 index 0000000..81fe884 --- /dev/null +++ b/Sources/FirebladeECS/UET.swift @@ -0,0 +1,41 @@ +// +// UET.swift +// FirebladeECS +// +// Created by Christian Treffs on 09.10.17. +// + +// MARK: Unique Event Type +/// Unique Event Type +public struct UET { + let objectIdentifier: ObjectIdentifier + let type: ECSEvent.Type + + init(_ eventType: ECSEvent.Type) { + objectIdentifier = ObjectIdentifier(eventType) + type = eventType + } + + init(_ event: ECSEvent) { + let eventType: ECSEvent.Type = event.uet.type + self.init(eventType) + } +} + +extension UET: Equatable { + public static func ==(lhs: UET, rhs: UET) -> Bool { + return lhs.objectIdentifier == rhs.objectIdentifier + } +} + +extension UET: Hashable { + public var hashValue: Int { + return objectIdentifier.hashValue + } +} + +// MARK: Unique Event Identifiable +public protocol UniqueEventIdentifiable { + static var uet: UET { get } + var uet: UET { get } +} diff --git a/Tests/FirebladeECSTests/FirebladeECSTests.swift b/Tests/FirebladeECSTests/FirebladeECSTests.swift index a24dd85..5c0c3da 100644 --- a/Tests/FirebladeECSTests/FirebladeECSTests.swift +++ b/Tests/FirebladeECSTests/FirebladeECSTests.swift @@ -2,15 +2,11 @@ import XCTest @testable import FirebladeECS class FirebladeECSTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(FirebladeECS().text, "Hello, World!") - } - static var allTests = [ - ("testExample", testExample), - ] + func testCreateEntity() { + let newEntity = Entity() + XCTAssert(newEntity.hasComponents == false) + XCTAssert(newEntity.uei == 1) + } } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 66c8f57..bf6795e 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,5 +2,5 @@ import XCTest @testable import FirebladeECSTests XCTMain([ - testCase(FirebladeECSTests.allTests), + testCase(FirebladeECSTests.allTests) ])