From e6cb5770e0e879630bac242b406bb5986817b9da Mon Sep 17 00:00:00 2001 From: Christian Treffs Date: Mon, 9 Oct 2017 19:43:05 +0200 Subject: [PATCH] Add Family basics --- Sources/FirebladeECS/Entity.swift | 10 +- Sources/FirebladeECS/EntityHub.swift | 56 +++-- Sources/FirebladeECS/Events.swift | 42 ++-- Sources/FirebladeECS/Family.swift | 209 +++++++++++-------- Sources/FirebladeECS/FamilyTraits.swift | 103 +++++++++ Tests/FirebladeECSTests/EntityHubTests.swift | 10 +- Tests/FirebladeECSTests/FamilyTests.swift | 27 +++ 7 files changed, 332 insertions(+), 125 deletions(-) create mode 100644 Sources/FirebladeECS/FamilyTraits.swift create mode 100644 Tests/FirebladeECSTests/FamilyTests.swift diff --git a/Sources/FirebladeECS/Entity.swift b/Sources/FirebladeECS/Entity.swift index c97d858..99fe8a1 100644 --- a/Sources/FirebladeECS/Entity.swift +++ b/Sources/FirebladeECS/Entity.swift @@ -47,7 +47,7 @@ public extension Entity { } public final func has(_ component: UCT) -> Bool { - fatalError() + return componentMap[component] != nil } public final var hasComponents: Bool { return !componentMap.isEmpty } @@ -188,25 +188,25 @@ extension Entity: EventDispatcher { private func notify(add component: C) { unowned { - $0.dispatch(ComponentAdded(component: component, to: $0)) + $0.dispatch(ComponentAdded(to: $0)) } } private func notify(update newComponent: C, previous previousComponent: C) { unowned { - $0.dispatch(ComponentUpdated(component: newComponent, previous: previousComponent, at: $0)) + $0.dispatch(ComponentUpdated(at: $0)) } } private func notify(removed component: C) { unowned { - $0.dispatch(ComponentRemoved(component: component, from: $0)) + $0.dispatch(ComponentRemoved(from: $0)) } } private func notify(removed component: Component) { //unowned { /* this keeps a reference since we need it */ - dispatch(ComponentRemoved(component: component, from: self)) + dispatch(ComponentRemoved(from: self)) //} } diff --git a/Sources/FirebladeECS/EntityHub.swift b/Sources/FirebladeECS/EntityHub.swift index bdf3835..ec354e5 100644 --- a/Sources/FirebladeECS/EntityHub.swift +++ b/Sources/FirebladeECS/EntityHub.swift @@ -5,41 +5,65 @@ // Created by Christian Treffs on 09.10.17. // -class EntityHub: EventHandler { - weak var delegate: EventHub? - lazy var eventCenter: DefaultEventHub = { return DefaultEventHub() }() +public class EntityHub: EventHandler { + public weak var delegate: EventHub? - private(set) var entites: [UEI:Entity] = [:] + public lazy var eventHub: DefaultEventHub = { return DefaultEventHub() }() - init() { - self.delegate = eventCenter + private(set) var entites: Set + //private(set) var entites: [UEI:Entity] = [:] + private(set) var families: Set + + public init() { + entites = Set() + entites.reserveCapacity(512) + + families = Set() + families.reserveCapacity(64) + + self.delegate = eventHub subscribe(event: handleEntityCreated) + subscribe(event: handleFamilyCreated) subscribe(event: handleComponentAdded) + subscribe(event: handleFamilyMemberAdded) } deinit { unsubscribe(event: handleEntityCreated) + unsubscribe(event: handleFamilyCreated) unsubscribe(event: handleComponentAdded) + unsubscribe(event: handleFamilyMemberAdded) } - func createEntity() -> Entity { - let newEntity = Entity(uei: UEI.next, dispatcher: eventCenter) +} + +// MARK: - creators +extension EntityHub { + public func createEntity() -> Entity { + let newEntity = Entity(uei: UEI.next, dispatcher: eventHub) // ^ dispatches entity creation event here ^ - let prevEntity: Entity? = entites.updateValue(newEntity, forKey: newEntity.uei) - assert(prevEntity == nil) + let (success, _) = entites.insert(newEntity) + assert(success == true, "Entity with the exact identifier already exists") return newEntity } + public func createFamily(with traits: FamilyTraits) -> Family { + let newFamily = Family(traits: traits, eventHub: eventHub) + // ^ dispatches family creation event here ^ + let (success, _) = families.insert(newFamily) + assert(success == true, "Family with the exact traits already exists") + + return newFamily + } + } // MARK: - event handler extension EntityHub { - func handleEntityCreated(_ ec: EntityCreated) { - print(ec) - } + func handleEntityCreated(_ e: EntityCreated) { print(e) } + func handleFamilyCreated(_ e: FamilyCreated) { print(e) } - func handleComponentAdded(_ ca: ComponentAdded) { - print(ca) - } + func handleComponentAdded(_ e: ComponentAdded) { print(e) } + func handleFamilyMemberAdded(_ e: FamilyMemberAdded) { print(e) } } diff --git a/Sources/FirebladeECS/Events.swift b/Sources/FirebladeECS/Events.swift index 4fa841b..a253541 100644 --- a/Sources/FirebladeECS/Events.swift +++ b/Sources/FirebladeECS/Events.swift @@ -14,31 +14,41 @@ public struct EntityDestroyed: Event { } public struct ComponentAdded: Event { - let component: Component + //let component: Component let to: Entity } public struct ComponentUpdated: Event { - let component: Component - let previous: Component + //let component: Component + //let previous: Component let at: Entity } public struct ComponentRemoved: Event { - let component: Component + //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) - - +struct FamilyMemberAdded: Event { + let member: Entity + let to: Family +} + +struct FamilyMemberUpdated: Event { + let newMember: Entity + let oldMember: Entity + let `in`: Family +} + +struct FamilyMemberRemoved: Event { + let member: Entity + let from: Family +} + +struct FamilyCreated: Event { + let family: Family +} + +struct FamilyDestroyed: Event { + let family: Family } -*/ diff --git a/Sources/FirebladeECS/Family.swift b/Sources/FirebladeECS/Family.swift index 1032ff1..98b4f9b 100644 --- a/Sources/FirebladeECS/Family.swift +++ b/Sources/FirebladeECS/Family.swift @@ -4,116 +4,159 @@ // // 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 -} +// MARK: - family +public final class Family { -struct FamilyDestroyed: Event { - //TODO: family -} + public var delegate: EventHub? + fileprivate var dispatcher: EventDispatcher -public final class Family: EventSender, EventHandler { + // members of this Family must conform to these traits + public let traits: FamilyTraits - // members of this Family must conform to: - let required: Set - let excluded: Set + public private(set) var members: Set - public private(set) var members: ContiguousArray + public init(traits: FamilyTraits, eventHub: EventHub & EventDispatcher) { - public convenience init(requiresAll required: ComponentType...) { - self.init(requiresAll: required, excludesAll: []) - } + members = Set() - public init(requiresAll required: [ComponentType], excludesAll excluded: [ComponentType]) { - self.required = Set(required) - self.excluded = Set(excluded) + self.traits = traits - self.members = [] + delegate = eventHub + dispatcher = eventHub subscribe(event: handleComponentAddedToEntity) subscribe(event: handleComponentRemovedFromEntity) - dispatch(event: FamilyCreated(family: self)) + defer { + notifyCreated() + } + } deinit { - //TODO: optimize for large sets - //TODO: dispatch entity removed event - self.members.removeAll() + members.removeAll() unsubscribe(event: handleComponentAddedToEntity) unsubscribe(event: handleComponentRemovedFromEntity) - dispatch(event: FamilyDestroyed()) + defer { + notifyDestroyed() + } + } +} - 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) - } +// MARK: update family membership +extension Family { - 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 + fileprivate func update(membership entity: Entity) { + let isMatch: Bool = traits.isMatch(entity) + switch isMatch { + case true: + push(entity) + case false: + remove(entity) } } - private final func add(toFamily entity: Entity) { - self.members.append(entity) - dispatch(event: FamilyMemberAdded(entity: entity, family: self)) + fileprivate func push(_ entity: Entity) { + let (added, member) = members.insert(entity) + switch added { + case true: + notify(added: member) + case false: + notify(update: entity, previous: member) + } } - private final func remove(entityAtIndex index: Int) { - let removedEntity: Entity = self.members.remove(at: index) - dispatch(event: FamilyMemberRemoved(entity: removedEntity, family: self)) + fileprivate func remove(_ entity: Entity) { + let removed: Entity? = members.remove(entity) + assert(removed != nil) + if let removedEntity: Entity = removed { + notify(removed: removedEntity) + } } } -*/ + +// MARK: - Equatable +extension Family: Equatable { + public static func ==(lhs: Family, rhs: Family) -> Bool { + return lhs.traits == rhs.traits + } +} + +// MARK: - Hashable +extension Family: Hashable { + public var hashValue: Int { + return traits.hashValue + } +} + +// MARK: - event dispatcher +extension Family: EventDispatcher { + public func dispatch(_ event: E) where E : Event { + dispatcher.dispatch(event) + } + + fileprivate func unowned(closure: @escaping (Family) -> Void) { + let unownedClosure = { [unowned self] in + closure(self) + } + unownedClosure() + } + + fileprivate func notifyCreated() { + unowned { + $0.dispatch(FamilyCreated(family: $0)) + } + } + + fileprivate func notify(added newEntity: Entity) { + unowned { [unowned newEntity] in + $0.dispatch(FamilyMemberAdded(member: newEntity, to: $0)) + } + } + + fileprivate func notify(update newEntity: Entity, previous oldEntity: Entity) { + unowned { [unowned newEntity, unowned oldEntity] in + $0.dispatch(FamilyMemberUpdated(newMember: newEntity, oldMember: oldEntity, in: $0) ) + } + } + + fileprivate func notify(removed removedEntity: Entity) { + unowned { [unowned removedEntity] in + $0.dispatch(FamilyMemberRemoved(member: removedEntity, from: $0)) + } + } + + fileprivate func notifyDestroyed() { + //dispatch(event: FamilyDestroyed()) + // dispatch(FamilyDestroyed(family: self)) + } +} + +// MARK: - event handler +extension Family: EventHandler { + + fileprivate final func handleComponentAddedToEntity(event: ComponentAdded) { + //let newComponent: Component = event.component + let entity: Entity = event.to + update(membership: entity) + } + + fileprivate final func handleComponentUpdatedAtEntity(event: ComponentUpdated) { + //let newComponent: Component = event.component + //let oldComponent: Component = event.previous + let entity: Entity = event.at + update(membership: entity) + } + + fileprivate final func handleComponentRemovedFromEntity(event: ComponentRemoved) { + //let removedComponent: Component = event.component + let entity: Entity = event.from + update(membership: entity) + } + +} diff --git a/Sources/FirebladeECS/FamilyTraits.swift b/Sources/FirebladeECS/FamilyTraits.swift new file mode 100644 index 0000000..36a281c --- /dev/null +++ b/Sources/FirebladeECS/FamilyTraits.swift @@ -0,0 +1,103 @@ +// +// FamilyPredicate.swift +// FirebladeECS +// +// Created by Christian Treffs on 09.10.17. +// + +// trait/predicate/characteristic +public struct FamilyTraits { + let hasAll: Set + let hasAny: Set + let hasNone: Set + + public init(hasAll: Set, hasAny: Set, hasNone: Set) { + self.hasAll = hasAll + self.hasAny = hasAny + self.hasNone = hasNone + assert(isValid) + } + + fileprivate var iteratorAll: SetIterator { return hasAll.makeIterator() } + fileprivate var iteratorAny: SetIterator { return hasAny.makeIterator() } + fileprivate var iteratorNone: SetIterator { return hasNone.makeIterator() } +} +extension FamilyTraits { + var isValid: Bool { + return (!hasAll.isEmpty || !hasAny.isEmpty) && + hasAll.isDisjoint(with: hasAny) && + hasAll.isDisjoint(with: hasNone) && + hasAny.isDisjoint(with: hasNone) + } +} + +extension FamilyTraits { + + fileprivate func matches(all entity: Entity) -> Bool { + var all = iteratorAll + while let uct: UCT = all.next() { + guard entity.has(uct) else { return false } + } + return true + } + + fileprivate func matches(none entity: Entity) -> Bool { + var none = iteratorNone + while let uct: UCT = none.next() { + guard !entity.has(uct) else { return false } + } + return true + } + + fileprivate func matches(any entity: Entity) -> Bool { + guard !hasAny.isEmpty else { return true } + var any = iteratorAny + while let uct: UCT = any.next() { + if entity.has(uct) { + return true + } + } + return false + } + + func isMatch(_ entity: Entity) -> Bool { + guard matches(all: entity) else { return false } + guard matches(none: entity) else { return false } + guard matches(any: entity) else { return false } + + return true + } +} + +// MARK: - Equatable +extension FamilyTraits: Equatable { + + fileprivate var xorHash: Int { + return hasAll.hashValue ^ hasNone.hashValue ^ hasAny.hashValue + } + + public static func ==(lhs: FamilyTraits, rhs: FamilyTraits) -> Bool { + return lhs.xorHash == rhs.xorHash + } +} + +// MARK: - Hashable +extension FamilyTraits: Hashable { + public var hashValue: Int { + return xorHash + } +} + +// MARK: - description +extension FamilyTraits: CustomStringConvertible { + + public var description: String { + let all: String = hasAll.map { "\($0.type)" }.joined(separator: " AND ") + let any: String = hasAny.map { "\($0.type)" }.joined(separator: " OR ") + let none: String = hasNone.map { "!\($0.type)"}.joined(separator: " NOT ") + let out: String = ["\(all)", "\(any)", "\(none)"].joined(separator: " AND ") + //TODO: nicer + return "FamilyTraits(\(out))" + } + +} diff --git a/Tests/FirebladeECSTests/EntityHubTests.swift b/Tests/FirebladeECSTests/EntityHubTests.swift index 7f5361b..4d427e9 100644 --- a/Tests/FirebladeECSTests/EntityHubTests.swift +++ b/Tests/FirebladeECSTests/EntityHubTests.swift @@ -14,15 +14,15 @@ class EntityHubTests: XCTestCase { override func setUp() { super.setUp() - entityHub.eventCenter.sniffer = self + entityHub.eventHub.sniffer = self } func testCreateEntity() { let newEntity: Entity = entityHub.createEntity() XCTAssert(newEntity.hasComponents == false) - XCTAssert(entityHub.entites[newEntity.uei] == newEntity) - XCTAssert(entityHub.entites[newEntity.uei] === newEntity) + //TODO: XCTAssert(entityHub.entites[newEntity.uei] == newEntity) + //TODO: XCTAssert(entityHub.entites[newEntity.uei] === newEntity) } func testCreateEntityAndAddComponent() { @@ -35,8 +35,8 @@ class EntityHubTests: XCTestCase { XCTAssert(newEntity.hasComponents) XCTAssert(newEntity.numComponents == 1) - XCTAssert(entityHub.entites[newEntity.uei] == newEntity) - XCTAssert(entityHub.entites[newEntity.uei] === newEntity) + //TODO: XCTAssert(entityHub.entites[newEntity.uei] == newEntity) + //TODO: XCTAssert(entityHub.entites[newEntity.uei] === newEntity) } diff --git a/Tests/FirebladeECSTests/FamilyTests.swift b/Tests/FirebladeECSTests/FamilyTests.swift new file mode 100644 index 0000000..c1a832c --- /dev/null +++ b/Tests/FirebladeECSTests/FamilyTests.swift @@ -0,0 +1,27 @@ +// +// FamilyTests.swift +// FirebladeECS +// +// Created by Christian Treffs on 09.10.17. +// + +import XCTest +/*@testable */import FirebladeECS + +class FamilyTests: XCTestCase { + + let entityHub: EntityHub = EntityHub() + + func testFamily() { + + let traits = FamilyTraits(hasAll: [EmptyComponent.uct], hasAny: [], hasNone: []) + + let simpleFamily = entityHub.createFamily(with: traits) + + let e = entityHub.createEntity() + e += EmptyComponent() + + e.remove(EmptyComponent.self) + + } +}