Refactor EntityIdentifier

This commit is contained in:
Christian Treffs 2019-08-20 16:36:45 +02:00
parent 89f0a92557
commit b62cf7464b
12 changed files with 98 additions and 101 deletions

View File

@ -11,7 +11,7 @@
/// It only consists of a unique id (EntityIdentifier).
/// Components can be assigned to an entity to give it behavior or functionality.
/// An entity creates the relationship between all it's assigned components.
public struct Entity: UniqueEntityIdentifiable {
public struct Entity {
@usableFromInline unowned let nexus: Nexus
/// The unique entity identifier.
@ -36,7 +36,7 @@ public struct Entity: UniqueEntityIdentifiable {
/// Checks if a component with a given component identifier is assigned to this entity.
/// - Parameter compId: the component identifier.
public func has(_ compId: ComponentIdentifier) -> Bool {
return nexus.has(componentId: compId, entityIdx: identifier.index)
return nexus.has(componentId: compId, entityId: identifier)
}
/// Checks if this entity has any components.

View File

@ -5,26 +5,27 @@
// Created by Christian Treffs on 08.10.17.
//
public typealias EntityIdentifier = UInt32 // provides 4294967295 unique identifiers
public typealias EntityIndex = Int
public struct EntityIdentifier {
public static let invalid = EntityIdentifier(.max)
public extension EntityIdentifier {
static let invalid = EntityIdentifier.max
/// 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)
}
}
public extension EntityIdentifier {
var index: EntityIndex {
return EntityIndex(self)
}
}
extension EntityIdentifier: Equatable { }
public extension EntityIndex {
var identifier: EntityIdentifier {
return EntityIdentifier(truncatingIfNeeded: self)
}
}
extension EntityIdentifier: Hashable { }
// MARK: Unique Entity Identifiable
public protocol UniqueEntityIdentifiable {
var identifier: EntityIdentifier { get }
extension EntityIdentifier: Comparable {
@inlinable public static func < (lhs: EntityIdentifier, rhs: EntityIdentifier) -> Bool {
return lhs.index < rhs.index
}
}

View File

@ -60,20 +60,20 @@ public func hash<H: Sequence>(combine hashables: H) -> Int where H.Element: Hash
// MARK: - entity component hash
extension EntityComponentHash {
internal static func compose(entityId: EntityIdentifier, componentTypeHash: ComponentTypeHash) -> EntityComponentHash {
let entityIdSwapped = UInt(entityId).byteSwapped // needs to be 64 bit
let entityIdSwapped = UInt(entityId.index).byteSwapped // needs to be 64 bit
let componentTypeHashUInt = UInt(bitPattern: componentTypeHash)
let hashUInt: UInt = componentTypeHashUInt ^ entityIdSwapped
return Int(bitPattern: hashUInt)
}
internal static func decompose(_ hash: EntityComponentHash, with entityId: EntityIdentifier) -> ComponentTypeHash {
let entityIdSwapped = UInt(entityId).byteSwapped
let entityIdSwapped = UInt(entityId.index).byteSwapped
let entityIdSwappedInt = Int(bitPattern: entityIdSwapped)
return hash ^ entityIdSwappedInt
}
internal static func decompose(_ hash: EntityComponentHash, with componentTypeHash: ComponentTypeHash) -> EntityIdentifier {
let entityId: Int = (hash ^ componentTypeHash).byteSwapped
return EntityIdentifier(truncatingIfNeeded: entityId)
return EntityIdentifier(UInt32(truncatingIfNeeded: entityId))
}
}

View File

@ -10,26 +10,25 @@ public extension Nexus {
return componentsByType.reduce(0) { $0 + $1.value.count }
}
final func has(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> Bool {
final func has(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> Bool {
guard let uniforms: UniformComponents = componentsByType[componentId] else {
return false
}
return uniforms.contains(entityIdx)
return uniforms.contains(entityId.index)
}
final func count(components entityId: EntityIdentifier) -> Int {
return componentIdsByEntity[entityId.index]?.count ?? 0
return componentIdsByEntity[entityId]?.count ?? 0
}
final func assign(component: Component, to entity: Entity) {
let componentId: ComponentIdentifier = component.identifier
let entityIdx: EntityIndex = entity.identifier.index
let entityId: EntityIdentifier = entity.identifier
/// test if component is already assigned
guard !has(componentId: componentId, entityIdx: entityIdx) else {
report("ComponentAdd collision: \(entityIdx) already has a component \(component)")
assertionFailure("ComponentAdd collision: \(entityIdx) already has a component \(component)")
guard !has(componentId: componentId, entityId: entityId) else {
report("ComponentAdd collision: \(entityId) already has a component \(component)")
assertionFailure("ComponentAdd collision: \(entityId) already has a component \(component)")
return
}
@ -37,13 +36,13 @@ public extension Nexus {
if componentsByType[componentId] == nil {
componentsByType[componentId] = UniformComponents()
}
componentsByType[componentId]?.insert(component, at: entityIdx)
componentsByType[componentId]?.insert(component, at: entityId.index)
// assigns the component id to the entity id
if componentIdsByEntity[entityIdx] == nil {
componentIdsByEntity[entityIdx] = ComponentSet()
if componentIdsByEntity[entityId] == nil {
componentIdsByEntity[entityId] = ComponentSet()
}
componentIdsByEntity[entityIdx]?.insert(componentId) //, at: componentId.hashValue)
componentIdsByEntity[entityId]?.insert(componentId) //, at: componentId.hashValue)
update(familyMembership: entityId)
@ -68,7 +67,7 @@ public extension Nexus {
final func get<C>(for entityId: EntityIdentifier) -> C? where C: Component {
let componentId: ComponentIdentifier = C.identifier
return get(componentId: componentId, entityIdx: entityId.index)
return get(componentId: componentId, entityId: entityId)
}
final func get<C>(unsafeComponentFor entityId: EntityIdentifier) -> C where C: Component {
@ -78,17 +77,15 @@ public extension Nexus {
}
final func get(components entityId: EntityIdentifier) -> ComponentSet? {
return componentIdsByEntity[entityId.index]
return componentIdsByEntity[entityId]
}
@discardableResult
final func remove(component componentId: ComponentIdentifier, from entityId: EntityIdentifier) -> Bool {
let entityIdx: EntityIndex = entityId.index
// delete component instance
componentsByType[componentId]?.remove(at: entityIdx)
componentsByType[componentId]?.remove(at: entityId.index)
// unasign component from entity
componentIdsByEntity[entityIdx]?.remove(componentId) //.hashValue)
componentIdsByEntity[entityId]?.remove(componentId)
update(familyMembership: entityId)
@ -112,10 +109,10 @@ public extension Nexus {
}
private extension Nexus {
final func get<C>(componentId: ComponentIdentifier, entityIdx: EntityIndex) -> C? where C: Component {
final func get<C>(componentId: ComponentIdentifier, entityId: EntityIdentifier) -> C? where C: Component {
guard let uniformComponents: UniformComponents = componentsByType[componentId] else {
return nil
}
return uniformComponents.get(at: entityIdx) as? C
return uniformComponents.get(at: entityId.index) as? C
}
}

View File

@ -6,57 +6,54 @@
//
extension Nexus {
private func nextEntityIdx() -> EntityIndex {
guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
return entityStorage.count
}
return nextReused.index
}
@inlinable internal func nextEntityId() -> EntityIdentifier {
guard let nextReused: EntityIdentifier = freeEntities.popLast() else {
return EntityIdentifier(UInt32(entityStorage.count))
}
return nextReused
}
@discardableResult
public func createEntity() -> Entity {
let newEntityIndex: EntityIndex = nextEntityIdx()
let newEntityIdentifier: EntityIdentifier = newEntityIndex.identifier
let newEntityIdentifier: EntityIdentifier = nextEntityId()
let newEntity = Entity(nexus: self, id: newEntityIdentifier)
entityStorage.insert(newEntity, at: newEntityIndex)
entityStorage.insert(newEntity, at: newEntityIdentifier.index)
notify(EntityCreated(entityId: newEntityIdentifier))
return newEntity
}
@discardableResult
@discardableResult
public func createEntity(with components: Component...) -> Entity {
let newEntity = createEntity()
let newEntity = createEntity()
components.forEach { newEntity.assign($0) }
return newEntity
}
// swiftlint:enable function_default_parameter_at_end
/// Number of entities in nexus.
public var numEntities: Int {
return entityStorage.count
}
/// Number of entities in nexus.
public var numEntities: Int {
return entityStorage.count
}
public func exists(entity entityId: EntityIdentifier) -> Bool {
return entityStorage.contains(entityId.index)
}
public func exists(entity entityId: EntityIdentifier) -> Bool {
return entityStorage.contains(entityId.index)
}
public func get(entity entityId: EntityIdentifier) -> Entity? {
return entityStorage.get(at: entityId.index)
}
public func get(entity entityId: EntityIdentifier) -> Entity? {
return entityStorage.get(at: entityId.index)
}
public func get(unsafeEntity entityId: EntityIdentifier) -> Entity {
return entityStorage.get(unsafeAt: entityId.index)
}
@discardableResult
public func destroy(entity: Entity) -> Bool {
let entityId: EntityIdentifier = entity.identifier
let entityIdx: EntityIndex = entityId.index
@discardableResult
public func destroy(entity: Entity) -> Bool {
let entityId: EntityIdentifier = entity.identifier
guard entityStorage.remove(at: entityIdx) != nil else {
report("EntityRemove failure: no entity \(entityId) to remove")
return false
}
guard entityStorage.remove(at: entityId.index) != nil else {
report("EntityRemove failure: no entity \(entityId) to remove")
return false
}
if removeAll(componentes: entityId) {
update(familyMembership: entityId)
@ -64,7 +61,7 @@ extension Nexus {
freeEntities.append(entityId)
notify(EntityDestroyed(entityId: entityId))
return true
}
notify(EntityDestroyed(entityId: entityId))
return true
}
}

View File

@ -11,8 +11,8 @@ public extension Nexus {
}
func canBecomeMember(_ entity: Entity, in traits: FamilyTraitSet) -> Bool {
let entityIdx: EntityIndex = entity.identifier.index
guard let componentIds = componentIdsByEntity[entityIdx] else {
let entityId = entity.identifier
guard let componentIds = componentIdsByEntity[entityId] else {
assertionFailure("no component set defined for entity: \(entity)")
return false
}

View File

@ -32,15 +32,14 @@ extension Nexus {
}
final func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) {
let entityIdx: EntityIndex = entityId.index
guard let componentIds = componentIdsByEntity[entityIdx] else {
guard let componentIds = componentIdsByEntity[entityId] else {
// no components - so skip
return
}
let isMember: Bool = self.isMember(entity: entityId, inFamilyWithTraits: traits)
if !exists(entity: entityId) && isMember {
remove(entityWithId: entityId, andIndex: entityIdx, fromFamilyWithTraits: traits)
remove(entityWithId: entityId, fromFamilyWithTraits: traits)
return
}
@ -48,12 +47,12 @@ extension Nexus {
switch (isMatch, isMember) {
case (true, false):
add(entityWithId: entityId, andIndex: entityIdx, toFamilyWithTraits: traits)
add(entityWithId: entityId, toFamilyWithTraits: traits)
notify(FamilyMemberAdded(member: entityId, toFamily: traits))
return
case (false, true):
remove(entityWithId: entityId, andIndex: entityIdx, fromFamilyWithTraits: traits)
remove(entityWithId: entityId, fromFamilyWithTraits: traits)
notify(FamilyMemberRemoved(member: entityId, from: traits))
return
@ -62,13 +61,13 @@ extension Nexus {
}
}
final func add(entityWithId entityId: EntityIdentifier, andIndex entityIdx: EntityIndex, toFamilyWithTraits traits: FamilyTraitSet) {
final func add(entityWithId entityId: EntityIdentifier, toFamilyWithTraits traits: FamilyTraitSet) {
precondition(familyMembersByTraits[traits] != nil)
familyMembersByTraits[traits].unsafelyUnwrapped.insert(entityId, at: entityIdx)
familyMembersByTraits[traits].unsafelyUnwrapped.insert(entityId, at: entityId.index)
}
final func remove(entityWithId entityId: EntityIdentifier, andIndex entityIdx: EntityIndex, fromFamilyWithTraits traits: FamilyTraitSet) {
final func remove(entityWithId entityId: EntityIdentifier, fromFamilyWithTraits traits: FamilyTraitSet) {
precondition(familyMembersByTraits[traits] != nil)
familyMembersByTraits[traits].unsafelyUnwrapped.remove(at: entityIdx)
familyMembersByTraits[traits].unsafelyUnwrapped.remove(at: entityId.index)
}
}

View File

@ -10,21 +10,21 @@ public class Nexus {
/// - Index: index value matching entity identifier shifted to Int
/// - Value: each element is a entity instance
internal var entityStorage: Entities
@usableFromInline internal var entityStorage: Entities
/// - Key: component type identifier
/// - Value: each element is a component instance of the same type (uniform). New component instances are appended.
internal var componentsByType: [ComponentIdentifier: UniformComponents]
@usableFromInline internal var componentsByType: [ComponentIdentifier: UniformComponents]
/// - Key: entity id as index
/// - Value: each element is a component identifier associated with this entity
internal var componentIdsByEntity: [EntityIndex: ComponentSet]
@usableFromInline internal var componentIdsByEntity: [EntityIdentifier: ComponentSet]
/// - Values: entity ids that are currently not used
internal var freeEntities: ContiguousArray<EntityIdentifier>
@usableFromInline internal var freeEntities: ContiguousArray<EntityIdentifier>
//var familiesByTraitHash: [FamilyTraitSetHash: Family]
internal var familyMembersByTraits: [FamilyTraitSet: UniformEntityIdentifiers]
@usableFromInline internal var familyMembersByTraits: [FamilyTraitSet: UniformEntityIdentifiers]
public init() {
entityStorage = Entities()

View File

@ -12,14 +12,16 @@ class EntityTests: XCTestCase {
func testEntityIdentifierAndIndex() {
let min: EntityIndex = EntityIdentifier(EntityIdentifier.min).index
XCTAssert(EntityIndex(min).identifier == min)
let min = EntityIdentifier(.min)
XCTAssertEqual(min.index, Int(UInt32.min))
let rand: EntityIndex = EntityIdentifier(EntityIdentifier(arc4random())).index
XCTAssert(EntityIndex(rand).identifier == rand)
let uRand = UInt32.random(in: UInt32.min...UInt32.max)
let rand = EntityIdentifier(uRand)
XCTAssertEqual(rand.index, Int(uRand))
let max: EntityIndex = EntityIdentifier(EntityIdentifier.max).index
XCTAssert(EntityIndex(max).identifier == max)
let max = EntityIdentifier(.max)
XCTAssertEqual(max, EntityIdentifier.invalid)
XCTAssertEqual(max.index, Int(UInt32.max))
}

View File

@ -27,13 +27,14 @@ class HashingTests: XCTestCase {
var hashSet: Set<Int> = Set<Int>()
var range: CountableRange<EntityIdentifier> = 0 ..< 1_000_000
var range: CountableRange<UInt32> = 0 ..< 1_000_000
let maxComponents: Int = 1000
let components: [Int] = (0..<maxComponents).map { _ in makeComponent() }
var index: Int = 0
while let eId: EntityIdentifier = range.popLast() {
while let idx: UInt32 = range.popLast() {
let eId = EntityIdentifier(idx)
let entityId: EntityIdentifier = eId
let c = (index % maxComponents)

View File

@ -43,7 +43,7 @@ class NexusTests: XCTestCase {
testEntityCreate()
XCTAssertEqual(nexus.numEntities, 2)
let e1: Entity = nexus.get(entity: 1)!
let e1: Entity = nexus.get(entity: EntityIdentifier(1))!
XCTAssertEqual(e1.identifier.index, 1)
XCTAssertTrue(nexus.destroy(entity: e1))
@ -51,7 +51,7 @@ class NexusTests: XCTestCase {
XCTAssertEqual(nexus.numEntities, 1)
let e1Again: Entity? = nexus.get(entity: 1)
let e1Again: Entity? = nexus.get(entity: EntityIdentifier(1))
XCTAssertNil(e1Again)
XCTAssertEqual(nexus.numEntities, 1)

View File

@ -1,4 +1,4 @@
@testable import FirebladeECSTests
import FirebladeECSTests
import XCTest
XCTMain([