Refactor family management

This commit is contained in:
Christian Treffs 2018-05-09 18:46:43 +02:00
parent 7021e6eb30
commit 532741b126
6 changed files with 292 additions and 195 deletions

View File

@ -12,28 +12,25 @@ public final class Family: Equatable {
// members of this Family must conform to these traits
public let traits: FamilyTraitSet
// TODO: add family configuration feature
// TODO: implemenet
// a) define sort order of entities
// b) define read/write access
// c) set size and storage constraints
// d) conform to collection
// e) consider family to be a struct
// TODO: family unions
// a) iterate family A and family B in pairs - i.e. zip
// b) pair-wise comparison inside families or between families
// f) iterate family A and family B in pairs - i.e. zip
// g) pair-wise comparison inside families or between families
init(_ nexus: Nexus, traits: FamilyTraitSet) {
self.nexus = nexus
self.traits = traits
defer {
self.nexus?.onFamilyInit(family: self)
self.nexus?.onFamilyInit(traits: self.traits)
}
}
deinit {
let hash: FamilyTraitSetHash = traits.hashValue
nexus?.onFamilyDeinit(traitHash: hash)
nexus?.onFamilyDeinit(traits: traits)
}
public final var memberIds: UniformEntityIdentifiers {

View File

@ -20,10 +20,7 @@ public extension Nexus {
}
func family(with traits: FamilyTraitSet) -> Family {
guard let family: Family = get(family: traits) else {
return create(family: traits)
}
return family
return create(family: traits)
}
func canBecomeMember(_ entity: Entity, in family: Family) -> Bool {
@ -37,21 +34,24 @@ public extension Nexus {
}
func members(of family: Family) -> UniformEntityIdentifiers? {
let traitHash: FamilyTraitSetHash = family.traits.hashValue
return members(of: traitHash)
let traits: FamilyTraitSet = family.traits
return members(of: traits)
}
func members(of traitHash: FamilyTraitSetHash) -> UniformEntityIdentifiers? {
return familyMembersByTraitHash[traitHash]
func members(of traits: FamilyTraitSet) -> UniformEntityIdentifiers? {
return familyMembersByTraits[traits]
}
func isMember(_ entity: Entity, in family: Family) -> Bool {
return isMember(entity.identifier, in: family)
}
func isMember(_ entityId: EntityIdentifier, in family: Family) -> Bool {
let traitHash: FamilyTraitSetHash = family.traits.hashValue
guard let members: UniformEntityIdentifiers = members(of: traitHash) else {
func isMember(_ entityId: EntityIdentifier, in family: Family) -> Bool {
return isMember(entityId, in: family.traits)
}
func isMember(_ entityId: EntityIdentifier, in traits: FamilyTraitSet) -> Bool {
guard let members: UniformEntityIdentifiers = members(of: traits) else {
return false
}
return members.has(entityId.index)
@ -63,41 +63,41 @@ public extension Nexus {
extension Nexus {
/// will be called on family init defer
func onFamilyInit(family: Family) {
func onFamilyInit(traits: FamilyTraitSet) {
createTraitsIfNeccessary(traits: traits)
// FIXME: this is costly for many entities
for entity: Entity in entityStorage {
update(membership: family, for: entity.identifier)
update(membership: traits, for: entity.identifier)
}
}
func onFamilyDeinit(traitHash: FamilyTraitSetHash) {
guard let members: UniformEntityIdentifiers = members(of: traitHash) else {
func onFamilyDeinit(traits: FamilyTraitSet) {
guard let members: UniformEntityIdentifiers = members(of: traits) else {
return
}
for member: EntityIdentifier in members {
remove(from: traitHash, entityId: member, entityIdx: member.index)
remove(from: traits, entityId: member, entityIdx: member.index)
}
}
func update(familyMembership entityId: EntityIdentifier) {
// FIXME: iterating all families is costly for many families
for family: Family in familiesByTraitHash.values {
update(membership: family, for: entityId)
for (familyTraits, _) in familyMembersByTraits {
update(membership: familyTraits, for: entityId)
}
}
func update(membership family: Family, for entityId: EntityIdentifier) {
func update(membership traits: FamilyTraitSet, for entityId: EntityIdentifier) {
let entityIdx: EntityIndex = entityId.index
let traits: FamilyTraitSet = family.traits
let traitHash: FamilyTraitSetHash = traits.hashValue
guard let componentIds: SparseComponentIdentifierSet = componentIdsByEntity[entityIdx] else {
return
}
let isMember: Bool = self.isMember(entityId, in: family)
let isMember: Bool = self.isMember(entityId, in: traits)
if !has(entity: entityId) && isMember {
remove(from: traitHash, entityId: entityId, entityIdx: entityIdx)
remove(from: traits, entityId: entityId, entityIdx: entityIdx)
return
}
@ -105,10 +105,10 @@ extension Nexus {
let isMatch: Bool = traits.isMatch(components: componentsSet)
switch (isMatch, isMember) {
case (true, false):
add(to: traitHash, entityId: entityId, entityIdx: entityIdx)
add(to: traits, entityId: entityId, entityIdx: entityIdx)
notify(FamilyMemberAdded(member: entityId, toFamily: traits))
case (false, true):
remove(from: traitHash, entityId: entityId, entityIdx: entityIdx)
remove(from: traits, entityId: entityId, entityIdx: entityIdx)
notify(FamilyMemberRemoved(member: entityId, from: traits))
default:
break
@ -121,30 +121,31 @@ extension Nexus {
private extension Nexus {
func get(family traits: FamilyTraitSet) -> Family? {
let traitHash: FamilyTraitSetHash = traits.hashValue
return familiesByTraitHash[traitHash]
return create(family: traits)
}
func create(family traits: FamilyTraitSet) -> Family {
let traitHash: FamilyTraitSetHash = traits.hashValue
let family: Family = Family(self, traits: traits)
let replaced: Family? = familiesByTraitHash.updateValue(family, forKey: traitHash)
assert(replaced == nil, "Family with exact trait hash already exists: \(traitHash)")
notify(FamilyCreated(family: traits))
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 traitHash: FamilyTraitSetHash, entityId: EntityIdentifier, entityIdx: EntityIndex) {
if familyMembersByTraitHash[traitHash] == nil {
familyMembersByTraitHash[traitHash] = UniformEntityIdentifiers()
}
familyMembersByTraitHash[traitHash]?.add(entityId, at: entityIdx)
func add(to traits: FamilyTraitSet, entityId: EntityIdentifier, entityIdx: EntityIndex) {
createTraitsIfNeccessary(traits: traits)
familyMembersByTraits[traits]?.add(entityId, at: entityIdx)
}
func remove(from traitHash: FamilyTraitSetHash, entityId: EntityIdentifier, entityIdx: EntityIndex) {
familyMembersByTraitHash[traitHash]?.remove(at: entityIdx)
func remove(from traits: FamilyTraitSet, entityId: EntityIdentifier, entityIdx: EntityIndex) {
familyMembersByTraits[traits]?.remove(at: entityIdx)
}
}

View File

@ -44,17 +44,15 @@ public class Nexus: Equatable {
/// - Values: entity ids that are currently not used
var freeEntities: ContiguousArray<EntityIdentifier>
var familiesByTraitHash: [FamilyTraitSetHash: Family]
var familyMembersByTraitHash: [FamilyTraitSetHash: UniformEntityIdentifiers]
//var familiesByTraitHash: [FamilyTraitSetHash: Family]
var familyMembersByTraits: [FamilyTraitSet: UniformEntityIdentifiers]
public init() {
entityStorage = Entities()
componentsByType = [:]
componentIdsByEntity = [:]
freeEntities = ContiguousArray<EntityIdentifier>()
familiesByTraitHash = [:]
familyMembersByTraitHash = [:]
familyMembersByTraits = [:]
}
deinit {
@ -70,13 +68,11 @@ public class Nexus: Equatable {
assert(componentsByType.values.reduce(0) { $0 + $1.count } == 0)
assert(componentIdsByEntity.values.reduce(0) { $0 + $1.count } == 0)
assert(freeEntities.isEmpty)
assert(familiesByTraitHash.values.reduce(0) { $0 + $1.count } == 0)
assert(familyMembersByTraitHash.values.reduce(0) { $0 + $1.count } == 0)
assert(familyMembersByTraits.values.reduce(0) { $0 + $1.count } == 0)
componentsByType.removeAll()
componentIdsByEntity.removeAll()
familiesByTraitHash.removeAll()
familyMembersByTraitHash.removeAll()
familyMembersByTraits.removeAll()
}
// MARK: Equatable
@ -84,8 +80,7 @@ public class Nexus: Equatable {
return lhs.entityStorage == rhs.entityStorage &&
lhs.componentIdsByEntity == rhs.componentIdsByEntity &&
lhs.freeEntities == rhs.freeEntities &&
lhs.familiesByTraitHash == rhs.familiesByTraitHash &&
lhs.familyMembersByTraitHash == rhs.familyMembersByTraitHash
lhs.familyMembersByTraits == rhs.familyMembersByTraits
// TODO: components are not equatable yet
//lhs.componentsByType == rhs.componentsByType
}

View File

@ -0,0 +1,93 @@
//
// FamilyPerformanceTests.swift
// FirebladeECSTests
//
// Created by Christian Treffs on 09.05.18.
//
import XCTest
import FirebladeECS
class FamilyPerformanceTests: XCTestCase {
var nexus: Nexus!
override func setUp() {
super.setUp()
nexus = Nexus()
}
override func tearDown() {
nexus = nil
super.tearDown()
}
func testMeasureIterateMembers() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [Party.self],
needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssertEqual(family.count, number)
XCTAssertEqual(nexus.numEntities, number)
measure {
family.iterate { (entityId: EntityIdentifier) in
_ = entityId
}
}
}
func testMeasureFamilyIterationOne() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number)
XCTAssert(nexus.numEntities == number)
measure {
family.iterate { (velocity: Velocity!) in
_ = velocity
}
}
}
func testMeasureFamilyIterationThree() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number)
XCTAssert(nexus.numEntities == number)
measure {
family.iterate { (entity: Entity!, position: Position!, velocity: Velocity!, name: Name?) in
position.x += entity.identifier.index
_ = velocity
_ = name
}
}
}
}

View File

@ -30,8 +30,8 @@ class FamilyTests: XCTestCase {
XCTAssertEqual(family.nexus, self.nexus)
XCTAssertTrue(family.nexus === self.nexus)
XCTAssertEqual(nexus.familiesByTraitHash.count, 1)
XCTAssertEqual(nexus.familiesByTraitHash.values.first!, family)
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1)
let traits = FamilyTraitSet(requiresAll: [Position.self], excludesAll: [Name.self], needsAtLeastOne: [Velocity.self])
XCTAssertEqual(family.traits, traits)
@ -47,12 +47,55 @@ class FamilyTests: XCTestCase {
excludesAll: [Name.self],
needsAtLeastOne: [Velocity.self])
XCTAssertEqual(nexus.familiesByTraitHash.count, 1)
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
XCTAssertEqual(nexus.familyMembersByTraits.values.count, 1)
XCTAssertEqual(familyA, familyB)
XCTAssertTrue(familyA === familyB)
}
func testFamilyAbandoned() {
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 0)
nexus.family(requiresAll: [Position.self],
excludesAll: [])
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
let entity = nexus.create(entity: "eimer")
entity.assign(Position(x: 1, y: 1))
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
entity.remove(Position.self)
// FIXME: the family trait should vanish when no entity with revlevant component is present anymore
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
nexus.destroy(entity: entity)
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
}
func testFamilyLateMember() {
let eEarly = nexus.create(entity: "eary").assign(Position(x: 1, y: 2))
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 0)
let family = nexus.family(requiresAll: [Position.self],
excludesAll: [])
XCTAssertEqual(nexus.familyMembersByTraits.keys.count, 1)
let eLate = nexus.create(entity: "late").assign(Position(x: 1, y: 2))
XCTAssertTrue(family.isMember(eEarly))
XCTAssertTrue(family.isMember(eLate))
}
func testFamilyExchange() {
@ -68,162 +111,50 @@ class FamilyTests: XCTestCase {
let familyB = nexus.family(requiresAll: [Velocity.self],
excludesAll: [Position.self])
var countA: Int = 0
XCTAssertEqual(familyA.count, 10)
XCTAssertEqual(familyB.count, 0)
familyA.iterate { (entity: Entity!) in
entity.assign(Velocity(a: 3.14))
entity.remove(Position.self)
countA += 1
}
XCTAssert(countA == number)
var countB: Int = 0
XCTAssertEqual(familyA.count, 0)
XCTAssertEqual(familyB.count, 10)
familyB.iterate { (entity: Entity!, velocity: Velocity!) in
entity.assign(Position(x: 1, y: 2))
entity.remove(velocity)
countB += 1
}
XCTAssert(countB == number)
XCTAssertEqual(familyA.count, 10)
XCTAssertEqual(familyB.count, 0)
}
func testIterationSimple() {
func testFamilyMemberBasicIteration() {
for i in 0..<1000 {
nexus.create(entity: "\(i)").assign(Position(x: i + 1, y: i + 2))
nexus.create(entity: "\(i)").assign(Velocity(a: Float(i)))
}
let familyA = nexus.family(requiresAll: [Position.self], excludesAll: [Velocity.self])
_ = nexus.family(requiresAll: [Velocity.self], excludesAll: [Position.self])
let familyA = nexus.family(requiresAll: [Position.self],
excludesAll: [Velocity.self])
familyA.iterate { (pos: Position!, vel: Velocity!) in
_ = pos
_ = vel
let familyB = nexus.family(requiresAll: [Velocity.self],
excludesAll: [Position.self])
familyA.iterate { (pos: Position?, vel: Velocity?) in
XCTAssertNotNil(pos)
XCTAssertNil(vel)
}
}
// MARK: - family performance
func testMeasureIterateMembers() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [Party.self],
needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssertEqual(family.count, number)
XCTAssertEqual(nexus.numEntities, number)
measure {
family.iterate { (entityId: EntityIdentifier) in
_ = entityId
}
}
}
func testMeasureFamilyIterationOne() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number)
XCTAssert(nexus.numEntities == number)
measure {
family.iterate { (velocity: Velocity!) in
_ = velocity
}
}
}
func testMeasureFamilyIterationThree() {
let number: Int = 10_000
for i in 0..<number {
nexus.create(entity: "\(i)").assign(Position(x: 1 + i, y: 2 + i), Name(name: "myName\(i)"), Velocity(a: 3.14), EmptyComponent())
}
let family = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Party.self], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssert(family.count == number)
XCTAssert(nexus.numEntities == number)
measure {
family.iterate { (entity: Entity!, position: Position!, velocity: Velocity!, name: Name?) in
position.x += entity.identifier.index
_ = velocity
_ = name
}
}
}
// MARK: - family traits
func testTraitCommutativity() {
let t1 = FamilyTraitSet(requiresAll: [Position.self, Velocity.self], excludesAll: [Name.self], needsAtLeastOne: [])
let t2 = FamilyTraitSet(requiresAll: [Velocity.self, Position.self], excludesAll: [Name.self], needsAtLeastOne: [])
XCTAssertEqual(t1, t2)
XCTAssertEqual(t1.hashValue, t2.hashValue)
}
func testTraitMatching() {
let a = nexus.create(entity: "a")
a.assign(Position(x: 1, y: 2))
a.assign(Name(name: "myName"))
a.assign(Velocity(a: 3.14))
a.assign(EmptyComponent())
let noMatch = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [Name.self])
let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self], excludesAll: [], needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssertFalse(noMatch.canBecomeMember(a))
XCTAssertTrue(isMatch.canBecomeMember(a))
}
func testMeasureTraitMatching() {
let a = nexus.create(entity: "a")
a.assign(Position(x: 1, y: 2))
a.assign(Name(name: "myName"))
a.assign(Velocity(a: 3.14))
a.assign(EmptyComponent())
let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [Party.self],
needsAtLeastOne: [Name.self, EmptyComponent.self])
measure {
for _ in 0..<10_000 {
let success = isMatch.canBecomeMember(a)
XCTAssert(success)
}
familyB.iterate { (pos: Position?, vel: Velocity?) in
XCTAssertNil(pos)
XCTAssertNotNil(vel)
}
}

View File

@ -0,0 +1,80 @@
//
// FamilyTraitsTests.swift
// FirebladeECSTests
//
// Created by Christian Treffs on 09.05.18.
//
import XCTest
@testable import FirebladeECS
class FamilyTraitsTests: XCTestCase {
var nexus: Nexus!
override func setUp() {
super.setUp()
nexus = Nexus()
}
override func tearDown() {
nexus = nil
super.tearDown()
}
func testTraitCommutativity() {
let t1 = FamilyTraitSet(requiresAll: [Position.self, Velocity.self],
excludesAll: [Name.self],
needsAtLeastOne: [])
let t2 = FamilyTraitSet(requiresAll: [Velocity.self, Position.self],
excludesAll: [Name.self],
needsAtLeastOne: [])
XCTAssertEqual(t1, t2)
XCTAssertEqual(t1.hashValue, t2.hashValue)
}
func testTraitMatching() {
let a = nexus.create(entity: "a")
a.assign(Position(x: 1, y: 2))
a.assign(Name(name: "myName"))
a.assign(Velocity(a: 3.14))
a.assign(EmptyComponent())
let noMatch = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [Name.self])
let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [],
needsAtLeastOne: [Name.self, EmptyComponent.self])
XCTAssertFalse(noMatch.canBecomeMember(a))
XCTAssertTrue(isMatch.canBecomeMember(a))
}
func testMeasureTraitMatching() {
let a = nexus.create(entity: "a")
a.assign(Position(x: 1, y: 2))
a.assign(Name(name: "myName"))
a.assign(Velocity(a: 3.14))
a.assign(EmptyComponent())
let isMatch = nexus.family(requiresAll: [Position.self, Velocity.self],
excludesAll: [Party.self],
needsAtLeastOne: [Name.self, EmptyComponent.self])
measure {
for _ in 0..<10_000 {
let success = isMatch.canBecomeMember(a)
XCTAssert(success)
}
}
}
}