├── .travis.yml ├── Entitas-Swift └── Logging │ ├── EntitasLogger+Query.swift │ ├── EntitasLogger.swift │ ├── EntitasLoggerQueryTests.swift │ ├── EventDataTests.swift │ ├── EventType.swift │ ├── LoggerMatcher.swift │ └── RelevantEntityEventsTests.swift ├── EntitasKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── EntitasKit-ios.xcscheme │ │ └── EntitasKit-macos.xcscheme └── xcuserdata │ └── mzaks.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE.txt ├── README.md ├── Sources ├── Collector.swift ├── Component.swift ├── Context.swift ├── Entitas-Swift.h ├── EntitasLoggerTests.swift ├── Entity.swift ├── Group.swift ├── Index.swift ├── Info.plist ├── Loop.swift ├── Matcher.swift ├── Observer.swift ├── ReactiveLoop.swift └── System.swift ├── Tests ├── CollectorTests.swift ├── ComponentTests.swift ├── ContextTests.swift ├── EntityObserverTests.swift ├── EntityTests.swift ├── GroupTests.swift ├── IndexTests.swift ├── Info.plist ├── LoopTests.swift ├── MatcherTests.swift ├── ObserverTests.swift ├── PerformanceTests.swift ├── ReactiveLoopTests.swift ├── Stubs.swift └── TestComponents.swift └── docs └── screenshot001.png /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9 3 | script: 4 | - xcodebuild test -project EntitasKit.xcodeproj -scheme EntitasKit-macos ONLY_ACTIVE_ARCH=NO 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) 7 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/EntitasLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntitasLogger.swift 3 | // EntitasKitTests 4 | // 5 | // Created by Maxim Zaks on 04.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class EntitasLogger { 12 | var tick: Tick = 0 13 | var eventTypes = [EventType]() 14 | var ticks = [Tick]() 15 | var timestamps = [Timestamp]() 16 | var contextIds = [ContextId]() 17 | var entityIds = [EntityId]() 18 | var compNameIds = [CompNameId]() 19 | var systemNameIds = [SystemNameId]() 20 | var infoIds = [InfoId]() 21 | var infos = [String]() 22 | 23 | let loggingStartTime: CFAbsoluteTime 24 | 25 | var contextMap = [ObjectIdentifier: ContextId]() 26 | var contextNamesMap = [String: ContextId]() 27 | var contextNames = [String]() 28 | var entityContextMap = [ObjectIdentifier: ContextId]() 29 | 30 | var systemNameMap = [String: SystemNameId]() 31 | var systemNames = [String]() 32 | var firstExecSystemNameId = SystemNameId.max 33 | private func systemId(_ name: String) -> SystemNameId { 34 | if let systemId = systemNameMap[name] { 35 | return systemId 36 | } 37 | let id = SystemNameId(systemNameMap.count) 38 | systemNameMap[name] = id 39 | systemNames.append(name) 40 | return id 41 | } 42 | 43 | var componentCidMap = [CID: CompNameId]() 44 | var componentNameMap = [String: CompNameId]() 45 | var compNames = [String]() 46 | private func compId(_ component: Component) -> CompNameId { 47 | if let compId = componentCidMap[component.cid] { 48 | return compId 49 | } 50 | let id = UInt16(componentCidMap.count) 51 | componentCidMap[component.cid] = id 52 | componentNameMap["\(type(of:component))"] = id 53 | compNames.append("\(type(of:component))") 54 | return id 55 | } 56 | 57 | var sysCallStack = [(Timestamp, SystemNameId, EventType)]() 58 | var sysCallStackMaterialised = 0 59 | 60 | public init(contexts: [(Context, String)]) { 61 | loggingStartTime = CFAbsoluteTimeGetCurrent() 62 | for (ctx, name) in contexts { 63 | contextMap[ObjectIdentifier(ctx)] = ContextId(contextMap.count) 64 | contextNamesMap[name] = ContextId(contextNames.count) 65 | contextNames.append(name) 66 | ctx.observer(add: self) 67 | } 68 | } 69 | 70 | public func addInfo(_ text: String) { 71 | sysCallStackMaterialise() 72 | let infoId = InfoId(infos.count) 73 | infos.append(text) 74 | addEvent( 75 | type: .info, 76 | timestamp: currentTimestamp, 77 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max, 78 | infoId: infoId 79 | ) 80 | } 81 | 82 | public func addError(_ text: String) { 83 | sysCallStackMaterialise() 84 | let infoId = InfoId(infos.count) 85 | infos.append(text) 86 | addEvent( 87 | type: .error, 88 | timestamp: currentTimestamp, 89 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max, 90 | infoId: infoId 91 | ) 92 | } 93 | 94 | private func addEvent( 95 | type: EventType, 96 | timestamp: Timestamp, 97 | contextId: ContextId = .max, 98 | entityId: EntityId = .max, 99 | compNameId: CompNameId = .max, 100 | systemNameId: SystemNameId = .max, 101 | infoId: InfoId = .max 102 | ) { 103 | eventTypes.append(type) 104 | ticks.append(tick) 105 | timestamps.append(timestamp) 106 | contextIds.append(contextId) 107 | entityIds.append(entityId) 108 | compNameIds.append(compNameId) 109 | systemNameIds.append(systemNameId) 110 | infoIds.append(infoId) 111 | } 112 | 113 | private var currentTimestamp: Timestamp { 114 | // WE can record only 710 minutes 115 | return Timestamp((CFAbsoluteTimeGetCurrent() - loggingStartTime) * 100_000) 116 | } 117 | } 118 | 119 | extension EntitasLogger: SystemExecuteLogger { 120 | 121 | private func pushSysCall(event: EventType, sysName: String) { 122 | let sysId = systemId(sysName) 123 | if event == .willExec { 124 | if firstExecSystemNameId == .max { 125 | firstExecSystemNameId = sysId 126 | tick += 1 127 | } else if firstExecSystemNameId == sysId { 128 | tick += 1 129 | } 130 | } 131 | sysCallStack.append((currentTimestamp, sysId, event)) 132 | } 133 | 134 | private func popSysCal(event: EventType, name: String) { 135 | let timestamp = currentTimestamp 136 | if sysCallStackMaterialised != sysCallStack.count { 137 | if let sysCall = sysCallStack.last, timestamp - sysCall.0 > 500 { 138 | sysCallStackMaterialise() 139 | addEvent(type: event, timestamp: timestamp, systemNameId: systemId(name)) 140 | sysCallStack.removeLast() 141 | sysCallStackMaterialised = sysCallStack.count 142 | } else { 143 | sysCallStack.removeLast() 144 | } 145 | } else { 146 | addEvent(type: event, timestamp: timestamp, systemNameId: systemId(name)) 147 | sysCallStack.removeLast() 148 | sysCallStackMaterialised = sysCallStack.count 149 | } 150 | } 151 | 152 | private func sysCallStackMaterialise() { 153 | guard sysCallStack.isEmpty == false 154 | && sysCallStackMaterialised < sysCallStack.count else { return } 155 | for sysCal in sysCallStack[sysCallStackMaterialised...] { 156 | addEvent(type: sysCal.2, timestamp: sysCal.0, systemNameId: sysCal.1) 157 | } 158 | sysCallStackMaterialised = sysCallStack.count 159 | } 160 | 161 | public func willExecute(_ name: String) { 162 | pushSysCall(event: .willExec, sysName: name) 163 | } 164 | 165 | public func didExecute(_ name: String) { 166 | popSysCal(event: .didExec, name: name) 167 | } 168 | 169 | public func willInit(_ name: String) { 170 | pushSysCall(event: .willInit, sysName: name) 171 | } 172 | 173 | public func didInit(_ name: String) { 174 | popSysCal(event: .didInit, name: name) 175 | } 176 | 177 | public func willCleanup(_ name: String) { 178 | pushSysCall(event: .willCleanup, sysName: name) 179 | } 180 | 181 | public func didCleanup(_ name: String) { 182 | popSysCal(event: .didCleanup, name: name) 183 | } 184 | 185 | public func willTeardown(_ name: String) { 186 | pushSysCall(event: .willTeardown, sysName: name) 187 | } 188 | 189 | public func didTeardown(_ name: String) { 190 | popSysCal(event: .didTeardown, name: name) 191 | } 192 | } 193 | 194 | extension EntitasLogger: ContextObserver { 195 | public func created(entity: Entity, in context: Context) { 196 | entity.observer(add: self) 197 | sysCallStackMaterialise() 198 | let entityId = EntityId(entity.creationIndex) 199 | let contextId = contextMap[ObjectIdentifier(context)] ?? ContextId.max 200 | addEvent( 201 | type: .created, 202 | timestamp: currentTimestamp, 203 | contextId: contextId, 204 | entityId: entityId, 205 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max 206 | ) 207 | entityContextMap[ObjectIdentifier(entity)] = contextId 208 | } 209 | 210 | public func created(group: Group, withMatcher matcher: Matcher, in context: Context) {} 211 | 212 | public func created(index: Index, in context: Context) {} 213 | } 214 | 215 | extension EntitasLogger: EntityObserver { 216 | public func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity) { 217 | sysCallStackMaterialise() 218 | let entityId = EntityId(entity.creationIndex) 219 | let entityObjectId = ObjectIdentifier(entity) 220 | let contextId = entityContextMap[entityObjectId] ?? ContextId.max 221 | let compNameId = compId(newComponent) 222 | let infoId: InfoId 223 | if let compInfo = (newComponent as? ComponentInfo)?.info { 224 | infoId = InfoId(infos.count) 225 | infos.append(compInfo) 226 | } else { 227 | infoId = .max 228 | } 229 | addEvent( 230 | type: oldComponent == nil ? .added : .replaced, 231 | timestamp: currentTimestamp, 232 | contextId: contextId, 233 | entityId: entityId, 234 | compNameId: compNameId, 235 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max, 236 | infoId: infoId 237 | ) 238 | } 239 | 240 | public func removed(component: Component, from entity: Entity) { 241 | sysCallStackMaterialise() 242 | let entityId = EntityId(entity.creationIndex) 243 | let entityObjectId = ObjectIdentifier(entity) 244 | let contextId = entityContextMap[entityObjectId] ?? ContextId.max 245 | let compNameId = compId(component) 246 | addEvent( 247 | type: .removed, 248 | timestamp: currentTimestamp, 249 | contextId: contextId, 250 | entityId: entityId, 251 | compNameId: compNameId, 252 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max 253 | ) 254 | } 255 | 256 | public func destroyed(entity: Entity) { 257 | entity.observer(add: self) 258 | sysCallStackMaterialise() 259 | let entityId = EntityId(entity.creationIndex) 260 | let entityObjectId = ObjectIdentifier(entity) 261 | let contextId = entityContextMap[entityObjectId] ?? ContextId.max 262 | addEvent( 263 | type: .destroyed, 264 | timestamp: currentTimestamp, 265 | contextId: contextId, 266 | entityId: entityId, 267 | systemNameId: sysCallStack.last?.1 ?? SystemNameId.max 268 | ) 269 | entityContextMap[entityObjectId] = nil 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/EntitasLoggerQueryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntitasLoggerQueryTests.swift 3 | // EntitasKit 4 | // 5 | // Created by Maxim Zaks on 07.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class EntitasLoggerQueryTests: XCTestCase { 13 | 14 | var entitasLogger: EntitasLogger! 15 | var ctx : Context! 16 | var ctx2 : Context! 17 | 18 | override func setUp() { 19 | ctx = Context() 20 | ctx2 = Context() 21 | entitasLogger = EntitasLogger(contexts: [(ctx, "mainCtx"), (ctx2, "secondaryCtx")]) 22 | entitasLogger.willExecute("S1") 23 | entitasLogger.willExecute("S2") 24 | _ = ctx.createEntity() 25 | entitasLogger.didExecute("S2") 26 | entitasLogger.didExecute("S1") 27 | entitasLogger.willExecute("S3") 28 | entitasLogger.didExecute("S3") 29 | } 30 | 31 | func testSystemsWithoutMatcher() { 32 | let result = entitasLogger.systems(matcher: nil) 33 | XCTAssertEqual(result, ["S1", "S2", "S3"]) 34 | } 35 | 36 | func testSystemsWithWillExecEventType() { 37 | let matcher = EventTypeMatcher(logger: entitasLogger, type: .willExec) 38 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher], any: [], none: [])) 39 | XCTAssertEqual(result, ["S1", "S2"]) 40 | } 41 | 42 | func testSystemsWithWillExecEventTypeAndNotS2() { 43 | let matcher = EventTypeMatcher(logger: entitasLogger, type: .willExec) 44 | let matcher2 = SystemNameIdMatcher(logger: entitasLogger, sysId: 1) 45 | do { 46 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher, matcher2], any: [], none: [])) 47 | XCTAssertEqual(result, ["S2"]) 48 | } 49 | do { 50 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher], any: [matcher2], none: [])) 51 | XCTAssertEqual(result, ["S2"]) 52 | } 53 | do { 54 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [], any: [matcher, matcher2], none: [])) 55 | XCTAssertEqual(result, ["S1", "S2"]) 56 | } 57 | do { 58 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher], any: [], none: [matcher2])) 59 | XCTAssertEqual(result, ["S1"]) 60 | } 61 | } 62 | 63 | func testSystemsWithEntityId() { 64 | entitasLogger.willExecute("S3") 65 | _ = ctx.createEntity() 66 | entitasLogger.didExecute("S3") 67 | do { 68 | let matcher = EntityIdMatcher(logger: entitasLogger, entityId: 1) 69 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher], any: [], none: [])) 70 | XCTAssertEqual(result, ["S2"]) 71 | } 72 | do { 73 | let matcher = EntityIdMatcher(logger: entitasLogger, entityId: .max) 74 | let result = entitasLogger.systems(matcher: GroupMatcher(all: [matcher], any: [], none: [])) 75 | XCTAssertEqual(result, ["S1", "S2", "S3"]) 76 | } 77 | } 78 | 79 | func testComponents() { 80 | let e = ctx.createEntity() 81 | e += God() 82 | XCTAssertEqual(entitasLogger.components(matcher: nil), ["God"]) 83 | } 84 | 85 | func testComponentsWithMatcher() { 86 | let e = ctx.createEntity() 87 | e += God() 88 | e.destroy() 89 | let matcher = EntityIdMatcher(logger: entitasLogger, entityId: .max) 90 | 91 | XCTAssertEqual(entitasLogger.components(matcher: GroupMatcher(all: [matcher], any: [], none: [])), []) 92 | 93 | XCTAssertEqual(entitasLogger.components(matcher: GroupMatcher(all: [], any: [], none: [matcher])), ["God"]) 94 | } 95 | 96 | func testEvents() { 97 | let e = ctx.createEntity() 98 | e += God() 99 | e.destroy() 100 | let matcher = EntityIdMatcher(logger: entitasLogger, entityId: .max) 101 | 102 | XCTAssertEqual(entitasLogger.events(matcher: GroupMatcher(all: [matcher], any: [], none: [])), [0, 1, 3, 4]) 103 | 104 | XCTAssertEqual(entitasLogger.events(matcher: GroupMatcher(all: [], any: [], none: [matcher])), [2, 5, 6, 7, 8]) 105 | } 106 | 107 | func testSimpleSystemQuery() throws { 108 | let result = try entitasLogger.query(" systems ") 109 | XCTAssertEqual(result, QueryResult.names(["S1", "S2", "S3"])) 110 | } 111 | 112 | func testSystemQuery() throws { 113 | XCTAssertEqual( 114 | try entitasLogger.query("systems where all(system:S1)"), 115 | QueryResult.names(["S1"]) 116 | ) 117 | XCTAssertEqual( 118 | try entitasLogger.query("systems where all(system:S2)"), 119 | QueryResult.names(["S2"]) 120 | ) 121 | XCTAssertEqual( 122 | try entitasLogger.query("systems where all(system:S1 system:S2)"), 123 | QueryResult.names([]) 124 | ) 125 | XCTAssertEqual( 126 | try entitasLogger.query("systems where any(system:S1 system:S2)"), 127 | QueryResult.names(["S1", "S2"]) 128 | ) 129 | XCTAssertEqual( 130 | try entitasLogger.query("systems where any(system:S1 system:S2) none(system:S1)"), 131 | QueryResult.names(["S2"]) 132 | ) 133 | XCTAssertEqual( 134 | try entitasLogger.query("systems where all(component:-)"), 135 | QueryResult.names(["S1", "S2"]) 136 | ) 137 | XCTAssertThrowsError(try entitasLogger.query("systems where all(system:S4)")) 138 | } 139 | 140 | func testSystemEventType() { 141 | entitasLogger.willInit("S1") 142 | let e1 = ctx.createEntity() 143 | entitasLogger.didInit("S1") 144 | entitasLogger.willCleanup("S2") 145 | let e2 = ctx2.createEntity() 146 | entitasLogger.didCleanup("S2") 147 | entitasLogger.willTeardown("S5") 148 | e1.destroy() 149 | e2.destroy() 150 | entitasLogger.addInfo("Done") 151 | entitasLogger.didTeardown("S5") 152 | entitasLogger.addError("Boom") 153 | 154 | XCTAssertEqual( 155 | try entitasLogger.query("systems where all(willExec)"), 156 | QueryResult.names(["S1", "S2"]) 157 | ) 158 | XCTAssertEqual( 159 | try entitasLogger.query("systems where all(didExec)"), 160 | QueryResult.names(["S1", "S2"]) 161 | ) 162 | XCTAssertEqual( 163 | try entitasLogger.query("systems where all(willInit)"), 164 | QueryResult.names(["S1"]) 165 | ) 166 | XCTAssertEqual( 167 | try entitasLogger.query("systems where all(didInit)"), 168 | QueryResult.names(["S1"]) 169 | ) 170 | XCTAssertEqual( 171 | try entitasLogger.query("systems where all(willCleanup)"), 172 | QueryResult.names(["S2"]) 173 | ) 174 | XCTAssertEqual( 175 | try entitasLogger.query("systems where all(didCleanup)"), 176 | QueryResult.names(["S2"]) 177 | ) 178 | XCTAssertEqual( 179 | try entitasLogger.query("systems where all(willTeardown)"), 180 | QueryResult.names(["S5"]) 181 | ) 182 | XCTAssertEqual( 183 | try entitasLogger.query("systems where all(didTeardown)"), 184 | QueryResult.names(["S5"]) 185 | ) 186 | XCTAssertEqual( 187 | try entitasLogger.query("systems where all(destroyed)"), 188 | QueryResult.names(["S5"]) 189 | ) 190 | XCTAssertEqual( 191 | try entitasLogger.query("systems where all(created)"), 192 | QueryResult.names(["S1", "S2"]) 193 | ) 194 | XCTAssertEqual( 195 | try entitasLogger.query("systems where all(infoLog)"), 196 | QueryResult.names(["S5"]) 197 | ) 198 | } 199 | 200 | func testSimpleComponentsQuery() throws { 201 | let e = ctx.createEntity() 202 | e += God() 203 | e += Position(x: 2, y: 4) 204 | e.destroy() 205 | let result = try entitasLogger.query(" components ") 206 | XCTAssertEqual(result, QueryResult.names(["God", "Position"])) 207 | } 208 | 209 | func testComponentQuery() throws { 210 | let e = ctx.createEntity() 211 | e += God() 212 | entitasLogger.willExecute("S1") 213 | e += Position(x: 2, y: 4) 214 | e += Position(x: 2, y: 2) 215 | entitasLogger.didExecute("S1") 216 | entitasLogger.willExecute("S2") 217 | e.destroy() 218 | entitasLogger.didExecute("S2") 219 | XCTAssertEqual( 220 | try entitasLogger.query("components where all(system:S1)"), 221 | QueryResult.names(["Position"]) 222 | ) 223 | XCTAssertEqual( 224 | try entitasLogger.query("components where all(system:-)"), 225 | QueryResult.names(["God"]) 226 | ) 227 | XCTAssertEqual( 228 | try entitasLogger.query("components where all(component:God)"), 229 | QueryResult.names(["God"]) 230 | ) 231 | XCTAssertEqual( 232 | try entitasLogger.query("components where none(system:S1 system:S2)"), 233 | QueryResult.names(["God"]) 234 | ) 235 | XCTAssertEqual( 236 | try entitasLogger.query("components where all(component:-)"), 237 | QueryResult.names([]) 238 | ) 239 | XCTAssertEqual( 240 | try entitasLogger.query("components where all(context:mainCtx)"), 241 | QueryResult.names(["God", "Position"]) 242 | ) 243 | XCTAssertEqual( 244 | try entitasLogger.query("components where all(context:secondaryCtx)"), 245 | QueryResult.names([]) 246 | ) 247 | XCTAssertEqual( 248 | try entitasLogger.query("components where all(removed)"), 249 | QueryResult.names(["God", "Position"]) 250 | ) 251 | XCTAssertEqual( 252 | try entitasLogger.query("components where all(added)"), 253 | QueryResult.names(["God", "Position"]) 254 | ) 255 | XCTAssertEqual( 256 | try entitasLogger.query("components where all(replaced)"), 257 | QueryResult.names(["Position"]) 258 | ) 259 | XCTAssertEqual( 260 | try entitasLogger.query("components where all(info: \"x:2\" )"), 261 | QueryResult.names(["Position"]) 262 | ) 263 | XCTAssertEqual( 264 | try entitasLogger.query("components where all(info: \"x:3\" )"), 265 | QueryResult.names([]) 266 | ) 267 | } 268 | 269 | func testEntityMatcher() { 270 | entitasLogger.willExecute("S1") 271 | _ = ctx.createEntity() 272 | entitasLogger.didExecute("S1") 273 | entitasLogger.willExecute("S2") 274 | _ = ctx.createEntity() 275 | entitasLogger.didExecute("S2") 276 | entitasLogger.willExecute("S3") 277 | _ = ctx2.createEntity() 278 | entitasLogger.didExecute("S3") 279 | 280 | XCTAssertEqual( 281 | try entitasLogger.query("systems where all(entity:1)"), 282 | QueryResult.names(["S2", "S3"]) 283 | ) 284 | XCTAssertEqual( 285 | try entitasLogger.query("systems where all(entity:1 context:mainCtx)"), 286 | QueryResult.names(["S2"]) 287 | ) 288 | XCTAssertEqual( 289 | try entitasLogger.query("systems where all(entity:1 context:secondaryCtx)"), 290 | QueryResult.names(["S3"]) 291 | ) 292 | XCTAssertEqual( 293 | try entitasLogger.query("systems where all(entity:2)"), 294 | QueryResult.names(["S1"]) 295 | ) 296 | XCTAssertEqual( 297 | try entitasLogger.query("systems where all(entity:3)"), 298 | QueryResult.names(["S2"]) 299 | ) 300 | XCTAssertEqual( 301 | try entitasLogger.query("systems where all(entity:4)"), 302 | QueryResult.names([]) 303 | ) 304 | } 305 | 306 | func testEventIndexMatcher() { 307 | XCTAssertEqual( 308 | try entitasLogger.query("systems where all(willExec event:1)"), 309 | QueryResult.names(["S2"]) 310 | ) 311 | XCTAssertEqual( 312 | try entitasLogger.query("systems where all(willExec event:1..)"), 313 | QueryResult.names(["S2"]) 314 | ) 315 | XCTAssertEqual( 316 | try entitasLogger.query("systems where all(event:1.. willExec)"), 317 | QueryResult.names(["S2"]) 318 | ) 319 | XCTAssertEqual( 320 | try entitasLogger.query("systems where all(willExec event:..25)"), 321 | QueryResult.names(["S1", "S2"]) 322 | ) 323 | XCTAssertEqual( 324 | try entitasLogger.query("systems where all(didExec event:3..25)"), 325 | QueryResult.names(["S1", "S2"]) 326 | ) 327 | } 328 | 329 | func testTickMatcher() { 330 | entitasLogger.willExecute("S1") 331 | _ = ctx.createEntity() 332 | entitasLogger.didExecute("S1") 333 | 334 | XCTAssertEqual( 335 | try entitasLogger.query("systems where all(willExec tick:1)"), 336 | QueryResult.names(["S1", "S2"]) 337 | ) 338 | XCTAssertEqual( 339 | try entitasLogger.query("systems where all(willExec tick:..10)"), 340 | QueryResult.names(["S1", "S2"]) 341 | ) 342 | XCTAssertEqual( 343 | try entitasLogger.query("systems where all(willExec tick:0)"), 344 | QueryResult.names([]) 345 | ) 346 | XCTAssertEqual( 347 | try entitasLogger.query("systems where all(willExec tick:0..)"), 348 | QueryResult.names(["S1", "S2"]) 349 | ) 350 | XCTAssertEqual( 351 | try entitasLogger.query("systems where all(willExec tick:2)"), 352 | QueryResult.names(["S1"]) 353 | ) 354 | XCTAssertEqual( 355 | try entitasLogger.query("systems where all(willExec tick:3..12)"), 356 | QueryResult.names([]) 357 | ) 358 | } 359 | 360 | func testEventsQuery() { 361 | XCTAssertEqual( 362 | try entitasLogger.query("events"), 363 | QueryResult.eventIds([0, 1, 2, 3, 4]) 364 | ) 365 | XCTAssertEqual( 366 | try entitasLogger.query("events where all(willExec)"), 367 | QueryResult.eventIds([0, 1]) 368 | ) 369 | XCTAssertEqual( 370 | try entitasLogger.query("events where any(willExec didExec)"), 371 | QueryResult.eventIds([0, 1, 3, 4]) 372 | ) 373 | XCTAssertEqual( 374 | try entitasLogger.query("events where any(system:S1)"), 375 | QueryResult.eventIds([0, 4]) 376 | ) 377 | XCTAssertEqual( 378 | try entitasLogger.query("events where any(system:S2)"), 379 | QueryResult.eventIds([1, 2, 3]) 380 | ) 381 | XCTAssertEqual( 382 | try entitasLogger.query("events where any(system:-)"), 383 | QueryResult.eventIds([]) 384 | ) 385 | } 386 | 387 | func testDurations() throws { 388 | do { 389 | let result = try entitasLogger.query("durations") 390 | switch result { 391 | case let .durations(durations): 392 | XCTAssertEqual(durations.count, 2) 393 | let willEvents = durations.map{$0.1} 394 | let didEvents = durations.map{$0.2} 395 | XCTAssertEqual(willEvents,[EventId(1), EventId(0)]) 396 | XCTAssertEqual(didEvents, [EventId(3), EventId(4)]) 397 | default: 398 | XCTFail("Expected a duration") 399 | } 400 | } 401 | do { 402 | let result = try entitasLogger.query("durations where all(system:S1)") 403 | switch result { 404 | case let .durations(durations): 405 | XCTAssertEqual(durations.count, 1) 406 | let willEvents = durations.map{$0.1} 407 | let didEvents = durations.map{$0.2} 408 | XCTAssertEqual(willEvents,[EventId(0)]) 409 | XCTAssertEqual(didEvents, [EventId(4)]) 410 | default: 411 | XCTFail("Expected a duration") 412 | } 413 | } 414 | } 415 | 416 | func testEntitiesQuery() throws { 417 | let e1 = ctx.createEntity() 418 | let e2 = ctx2.createEntity() 419 | e1.destroy() 420 | e2 += God() 421 | 422 | switch try entitasLogger.query("entities") { 423 | case .entities(let results): 424 | XCTAssertEqual(results.count, 3) 425 | XCTAssertEqual(results.map{$0.entityId}, [EntityId(1), 2, 1]) 426 | XCTAssertEqual(results.map{$0.contextId}, [ContextId(0), 0, 1]) 427 | XCTAssertEqual(results.map{$0.eventsCount}, [1, 2, 2]) 428 | XCTAssertEqual(results.map{$0.lifeSpan.0}, [0.25, 0.625, 0.75]) 429 | XCTAssertEqual(results.map{$0.lifeSpan.1}, [1.0, 0.875, 1.0]) 430 | default: 431 | XCTFail() 432 | } 433 | 434 | switch try entitasLogger.query("entities where all(system:S1)") { 435 | case .entities(let results): 436 | XCTAssertEqual(results.count, 0) 437 | default: 438 | XCTFail() 439 | } 440 | 441 | switch try entitasLogger.query("entities where all(system:S2)") { 442 | case .entities(let results): 443 | XCTAssertEqual(results.count, 1) 444 | XCTAssertEqual(results.map{$0.entityId}, [EntityId(1)]) 445 | XCTAssertEqual(results.map{$0.contextId}, [ContextId(0)]) 446 | XCTAssertEqual(results.map{$0.eventsCount}, [1]) 447 | XCTAssertEqual(results.map{$0.lifeSpan.0}, [0.25]) 448 | XCTAssertEqual(results.map{$0.lifeSpan.1}, [1.0]) 449 | default: 450 | XCTFail() 451 | } 452 | 453 | switch try entitasLogger.query("entities where all(destroyed)") { 454 | case .entities(let results): 455 | XCTAssertEqual(results.count, 1) 456 | XCTAssertEqual(results.map{$0.entityId}, [EntityId(2)]) 457 | XCTAssertEqual(results.map{$0.contextId}, [ContextId(0)]) 458 | XCTAssertEqual(results.map{$0.eventsCount}, [1]) 459 | XCTAssertEqual(results.map{$0.lifeSpan.0}, [0.625]) 460 | XCTAssertEqual(results.map{$0.lifeSpan.1}, [0.875]) 461 | default: 462 | XCTFail() 463 | } 464 | 465 | switch try entitasLogger.query("entities where all(added)") { 466 | case .entities(let results): 467 | XCTAssertEqual(results.count, 1) 468 | XCTAssertEqual(results.map{$0.entityId}, [EntityId(1)]) 469 | XCTAssertEqual(results.map{$0.contextId}, [ContextId(1)]) 470 | XCTAssertEqual(results.map{$0.eventsCount}, [1]) 471 | XCTAssertEqual(results.map{$0.lifeSpan.0}, [0.75]) 472 | XCTAssertEqual(results.map{$0.lifeSpan.1}, [1.0]) 473 | default: 474 | XCTFail() 475 | } 476 | 477 | switch try entitasLogger.query("entities where all(component:God)") { 478 | case .entities(let results): 479 | XCTAssertEqual(results.count, 1) 480 | XCTAssertEqual(results.map{$0.entityId}, [EntityId(1)]) 481 | XCTAssertEqual(results.map{$0.contextId}, [ContextId(1)]) 482 | XCTAssertEqual(results.map{$0.eventsCount}, [1]) 483 | XCTAssertEqual(results.map{$0.lifeSpan.0}, [0.75]) 484 | XCTAssertEqual(results.map{$0.lifeSpan.1}, [1.0]) 485 | default: 486 | XCTFail() 487 | } 488 | 489 | // XCTAssertEqual( 490 | // try entitasLogger.query("entities"), 491 | // QueryResult.entities([_e0, _e1, _e2]) 492 | // ) 493 | // XCTAssertEqual( 494 | // try entitasLogger.query("entities where all(system:S1)"), 495 | // QueryResult.entities([ 496 | // ]) 497 | // ) 498 | // XCTAssertEqual( 499 | // try entitasLogger.query("entities where all(system:S2)"), 500 | // QueryResult.entities([(EntityId(1), ContextId(0))]) 501 | // ) 502 | // 503 | // XCTAssertEqual( 504 | // try entitasLogger.query("entities where all(destroyed)"), 505 | // QueryResult.entities([_e1]) 506 | // ) 507 | // XCTAssertEqual( 508 | // try entitasLogger.query("entities where all(added)"), 509 | // QueryResult.entities([_e2]) 510 | // ) 511 | // XCTAssertEqual( 512 | // try entitasLogger.query("entities where all(component:God)"), 513 | // QueryResult.entities([_e2]) 514 | // ) 515 | } 516 | 517 | func testInfoAndErrorEvents() throws { 518 | entitasLogger.addInfo("Bla") 519 | entitasLogger.addError("Bla") 520 | XCTAssertEqual( 521 | try entitasLogger.query("events"), 522 | QueryResult.eventIds([0, 1, 2, 3, 4, 5, 6]) 523 | ) 524 | XCTAssertEqual( 525 | try entitasLogger.query("events where all(infoLog)"), 526 | QueryResult.eventIds([5]) 527 | ) 528 | XCTAssertEqual( 529 | try entitasLogger.query("events where all(errorLog)"), 530 | QueryResult.eventIds([6]) 531 | ) 532 | 533 | XCTAssertEqual( 534 | try entitasLogger.query("events where any(infoLog errorLog)"), 535 | QueryResult.eventIds([5, 6]) 536 | ) 537 | } 538 | 539 | func testContextName() { 540 | XCTAssertEqual(entitasLogger.contextName(id: 0), "mainCtx") 541 | XCTAssertEqual(entitasLogger.contextName(id: 1), "secondaryCtx") 542 | } 543 | 544 | func testBadQuery() throws { 545 | XCTAssertThrowsError(try entitasLogger.query(" compo ")) 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/EventDataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventDataTests.swift 3 | // EntitasKitTests 4 | // 5 | // Created by Maxim Zaks on 11.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class EventDataTests: XCTestCase { 13 | 14 | var entitasLogger: EntitasLogger! 15 | var ctx : Context! 16 | var ctx2 : Context! 17 | 18 | override func setUp() { 19 | ctx = Context() 20 | ctx2 = Context() 21 | entitasLogger = EntitasLogger(contexts: [(ctx, "mainCtx"), (ctx2, "secondaryCtx")]) 22 | } 23 | 24 | func testEntityAndComponentEventsInSystem() { 25 | entitasLogger.willExecute("S1") 26 | let e = ctx.createEntity() 27 | entitasLogger.didExecute("S1") 28 | entitasLogger.willExecute("S1") 29 | e += God() 30 | e += Position(x: 1, y: 2) 31 | entitasLogger.didExecute("S1") 32 | entitasLogger.willCleanup("S2") 33 | entitasLogger.didCleanup("S2") 34 | entitasLogger.willExecute("S1") 35 | e -= God.cid 36 | e += Position(x: 3, y: 3) 37 | entitasLogger.didExecute("S1") 38 | entitasLogger.willCleanup("S2") 39 | e.destroy() 40 | entitasLogger.didCleanup("S2") 41 | 42 | do { 43 | switch entitasLogger.eventData(eventId: 0)! { 44 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 45 | XCTAssertEqual(eventTtype, .willExec) 46 | XCTAssertEqual(tick, 1) 47 | XCTAssert(timestampInMs > 0) 48 | XCTAssertEqual(systemName, "S1") 49 | default: 50 | XCTFail("expected system event") 51 | } 52 | } 53 | 54 | do { 55 | switch entitasLogger.eventData(eventId: 1)! { 56 | case let .entityEvent(eventTtype, tick, timestampInMs, entityId, contextName, systemName): 57 | XCTAssertEqual(eventTtype, .created) 58 | XCTAssertEqual(tick, 1) 59 | XCTAssert(timestampInMs > 0) 60 | XCTAssertEqual(entityId, 1) 61 | XCTAssertEqual(contextName, "mainCtx") 62 | XCTAssertEqual(systemName, "S1") 63 | default: 64 | XCTFail("expected system event") 65 | } 66 | } 67 | 68 | do { 69 | switch entitasLogger.eventData(eventId: 2)! { 70 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 71 | XCTAssertEqual(eventTtype, .didExec) 72 | XCTAssertEqual(tick, 1) 73 | XCTAssert(timestampInMs > 0) 74 | XCTAssertEqual(systemName, "S1") 75 | default: 76 | XCTFail("expected system event") 77 | } 78 | } 79 | 80 | do { 81 | switch entitasLogger.eventData(eventId: 3)! { 82 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 83 | XCTAssertEqual(eventTtype, .willExec) 84 | XCTAssertEqual(tick, 2) 85 | XCTAssert(timestampInMs > 0) 86 | XCTAssertEqual(systemName, "S1") 87 | default: 88 | XCTFail("expected system event") 89 | } 90 | } 91 | 92 | do { 93 | switch entitasLogger.eventData(eventId: 4)! { 94 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 95 | XCTAssertEqual(eventTtype, .added) 96 | XCTAssertEqual(tick, 2) 97 | XCTAssert(timestampInMs > 0) 98 | XCTAssertEqual(entityId, 1) 99 | XCTAssertEqual(contextName, "mainCtx") 100 | XCTAssertEqual(componentName, "God") 101 | XCTAssertNil(info) 102 | XCTAssertEqual(systemName, "S1") 103 | default: 104 | XCTFail("expected system event") 105 | } 106 | } 107 | 108 | do { 109 | switch entitasLogger.eventData(eventId: 5)! { 110 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 111 | XCTAssertEqual(eventTtype, .added) 112 | XCTAssertEqual(tick, 2) 113 | XCTAssert(timestampInMs > 0) 114 | XCTAssertEqual(entityId, 1) 115 | XCTAssertEqual(contextName, "mainCtx") 116 | XCTAssertEqual(componentName, "Position") 117 | XCTAssertEqual(info, "x:1, y:2") 118 | XCTAssertEqual(systemName, "S1") 119 | default: 120 | XCTFail("expected system event") 121 | } 122 | } 123 | 124 | do { 125 | switch entitasLogger.eventData(eventId: 6)! { 126 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 127 | XCTAssertEqual(eventTtype, .didExec) 128 | XCTAssertEqual(tick, 2) 129 | XCTAssert(timestampInMs > 0) 130 | XCTAssertEqual(systemName, "S1") 131 | default: 132 | XCTFail("expected system event") 133 | } 134 | } 135 | 136 | do { 137 | switch entitasLogger.eventData(eventId: 7)! { 138 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 139 | XCTAssertEqual(eventTtype, .willExec) 140 | XCTAssertEqual(tick, 3) 141 | XCTAssert(timestampInMs > 0) 142 | XCTAssertEqual(systemName, "S1") 143 | default: 144 | XCTFail("expected system event") 145 | } 146 | } 147 | 148 | do { 149 | switch entitasLogger.eventData(eventId: 8)! { 150 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 151 | XCTAssertEqual(eventTtype, .removed) 152 | XCTAssertEqual(tick, 3) 153 | XCTAssert(timestampInMs > 0) 154 | XCTAssertEqual(entityId, 1) 155 | XCTAssertEqual(contextName, "mainCtx") 156 | XCTAssertEqual(componentName, "God") 157 | XCTAssertNil(info) 158 | XCTAssertEqual(systemName, "S1") 159 | default: 160 | XCTFail("expected system event") 161 | } 162 | } 163 | 164 | do { 165 | switch entitasLogger.eventData(eventId: 9)! { 166 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 167 | XCTAssertEqual(eventTtype, .replaced) 168 | XCTAssertEqual(tick, 3) 169 | XCTAssert(timestampInMs > 0) 170 | XCTAssertEqual(entityId, 1) 171 | XCTAssertEqual(contextName, "mainCtx") 172 | XCTAssertEqual(componentName, "Position") 173 | XCTAssertEqual(info, "x:3, y:3") 174 | XCTAssertEqual(systemName, "S1") 175 | default: 176 | XCTFail("expected system event") 177 | } 178 | } 179 | 180 | do { 181 | switch entitasLogger.eventData(eventId: 10)! { 182 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 183 | XCTAssertEqual(eventTtype, .didExec) 184 | XCTAssertEqual(tick, 3) 185 | XCTAssert(timestampInMs > 0) 186 | XCTAssertEqual(systemName, "S1") 187 | default: 188 | XCTFail("expected system event") 189 | } 190 | } 191 | 192 | do { 193 | switch entitasLogger.eventData(eventId: 11)! { 194 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 195 | XCTAssertEqual(eventTtype, .willCleanup) 196 | XCTAssertEqual(tick, 3) 197 | XCTAssert(timestampInMs > 0) 198 | XCTAssertEqual(systemName, "S2") 199 | default: 200 | XCTFail("expected system event") 201 | } 202 | } 203 | 204 | do { 205 | switch entitasLogger.eventData(eventId: 12)! { 206 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 207 | XCTAssertEqual(eventTtype, .removed) 208 | XCTAssertEqual(tick, 3) 209 | XCTAssert(timestampInMs > 0) 210 | XCTAssertEqual(entityId, 1) 211 | XCTAssertEqual(contextName, "mainCtx") 212 | XCTAssertEqual(componentName, "Position") 213 | XCTAssertNil(info) 214 | XCTAssertEqual(systemName, "S2") 215 | default: 216 | XCTFail("expected system event") 217 | } 218 | } 219 | 220 | do { 221 | switch entitasLogger.eventData(eventId: 13)! { 222 | case let .entityEvent(eventTtype, tick, timestampInMs, entityId, contextName, systemName): 223 | XCTAssertEqual(eventTtype, .destroyed) 224 | XCTAssertEqual(tick, 3) 225 | XCTAssert(timestampInMs > 0) 226 | XCTAssertEqual(entityId, 1) 227 | XCTAssertEqual(contextName, "mainCtx") 228 | XCTAssertEqual(systemName, "S2") 229 | default: 230 | XCTFail("expected system event") 231 | } 232 | } 233 | 234 | do { 235 | switch entitasLogger.eventData(eventId: 14)! { 236 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 237 | XCTAssertEqual(eventTtype, .didCleanup) 238 | XCTAssertEqual(tick, 3) 239 | XCTAssert(timestampInMs > 0) 240 | XCTAssertEqual(systemName, "S2") 241 | default: 242 | XCTFail("expected system event") 243 | } 244 | } 245 | } 246 | 247 | func testInfoInSystemAndOutside() { 248 | entitasLogger.willInit("S1") 249 | entitasLogger.willInit("S2") 250 | entitasLogger.addInfo("Bla") 251 | entitasLogger.didInit("S2") 252 | entitasLogger.addError("Foo") 253 | entitasLogger.didInit("S1") 254 | entitasLogger.addInfo("Bla 22") 255 | entitasLogger.addError("Foo 22") 256 | 257 | do { 258 | switch entitasLogger.eventData(eventId: 0)! { 259 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 260 | XCTAssertEqual(eventTtype, .willInit) 261 | XCTAssertEqual(tick, 0) 262 | XCTAssert(timestampInMs > 0) 263 | XCTAssertEqual(systemName, "S1") 264 | default: 265 | XCTFail("expected system event") 266 | } 267 | } 268 | 269 | do { 270 | switch entitasLogger.eventData(eventId: 1)! { 271 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 272 | XCTAssertEqual(eventTtype, .willInit) 273 | XCTAssertEqual(tick, 0) 274 | XCTAssert(timestampInMs > 0) 275 | XCTAssertEqual(systemName, "S2") 276 | default: 277 | XCTFail("expected system event") 278 | } 279 | } 280 | 281 | do { 282 | switch entitasLogger.eventData(eventId: 2)! { 283 | case let .infoEvent(eventTtype, tick, timestampInMs, systemName, info): 284 | XCTAssertEqual(eventTtype, .info) 285 | XCTAssertEqual(tick, 0) 286 | XCTAssert(timestampInMs > 0) 287 | XCTAssertEqual(systemName, "S2") 288 | XCTAssertEqual(info, "Bla") 289 | default: 290 | XCTFail("expected system event") 291 | } 292 | } 293 | 294 | do { 295 | switch entitasLogger.eventData(eventId: 3)! { 296 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 297 | XCTAssertEqual(eventTtype, .didInit) 298 | XCTAssertEqual(tick, 0) 299 | XCTAssert(timestampInMs > 0) 300 | XCTAssertEqual(systemName, "S2") 301 | default: 302 | XCTFail("expected system event") 303 | } 304 | } 305 | 306 | do { 307 | switch entitasLogger.eventData(eventId: 4)! { 308 | case let .infoEvent(eventTtype, tick, timestampInMs, systemName, info): 309 | XCTAssertEqual(eventTtype, .error) 310 | XCTAssertEqual(tick, 0) 311 | XCTAssert(timestampInMs > 0) 312 | XCTAssertEqual(systemName, "S1") 313 | XCTAssertEqual(info, "Foo") 314 | default: 315 | XCTFail("expected system event") 316 | } 317 | } 318 | 319 | do { 320 | switch entitasLogger.eventData(eventId: 5)! { 321 | case let .systemEvent(eventTtype, tick, timestampInMs, systemName): 322 | XCTAssertEqual(eventTtype, .didInit) 323 | XCTAssertEqual(tick, 0) 324 | XCTAssert(timestampInMs > 0) 325 | XCTAssertEqual(systemName, "S1") 326 | default: 327 | XCTFail("expected system event") 328 | } 329 | } 330 | 331 | do { 332 | switch entitasLogger.eventData(eventId: 6)! { 333 | case let .infoEvent(eventTtype, tick, timestampInMs, systemName, info): 334 | XCTAssertEqual(eventTtype, .info) 335 | XCTAssertEqual(tick, 0) 336 | XCTAssert(timestampInMs > 0) 337 | XCTAssertEqual(systemName, nil) 338 | XCTAssertEqual(info, "Bla 22") 339 | default: 340 | XCTFail("expected system event") 341 | } 342 | } 343 | 344 | do { 345 | switch entitasLogger.eventData(eventId: 7)! { 346 | case let .infoEvent(eventTtype, tick, timestampInMs, systemName, info): 347 | XCTAssertEqual(eventTtype, .error) 348 | XCTAssertEqual(tick, 0) 349 | XCTAssert(timestampInMs > 0) 350 | XCTAssertEqual(systemName, nil) 351 | XCTAssertEqual(info, "Foo 22") 352 | default: 353 | XCTFail("expected system event") 354 | } 355 | } 356 | } 357 | 358 | func testEntitAndComponentEventsNotInSystem() { 359 | let e = ctx.createEntity() 360 | e += God() 361 | e.destroy() 362 | let e1 = ctx2.createEntity() 363 | e1 += Position(x: 1, y: 1) 364 | e1 += Position(x: 2, y: 2) 365 | 366 | do { 367 | switch entitasLogger.eventData(eventId: 0)! { 368 | case let .entityEvent(eventTtype, tick, timestampInMs, entityId, contextName, systemName): 369 | XCTAssertEqual(eventTtype, .created) 370 | XCTAssertEqual(tick, 0) 371 | XCTAssert(timestampInMs > 0) 372 | XCTAssertEqual(entityId, 1) 373 | XCTAssertEqual(contextName, "mainCtx") 374 | XCTAssertEqual(systemName, nil) 375 | default: 376 | XCTFail("expected system event") 377 | } 378 | } 379 | 380 | do { 381 | switch entitasLogger.eventData(eventId: 1)! { 382 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 383 | XCTAssertEqual(eventTtype, .added) 384 | XCTAssertEqual(tick, 0) 385 | XCTAssert(timestampInMs > 0) 386 | XCTAssertEqual(entityId, 1) 387 | XCTAssertEqual(contextName, "mainCtx") 388 | XCTAssertEqual(componentName, "God") 389 | XCTAssertNil(info) 390 | XCTAssertEqual(systemName, nil) 391 | default: 392 | XCTFail("expected system event") 393 | } 394 | } 395 | 396 | do { 397 | switch entitasLogger.eventData(eventId: 2)! { 398 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 399 | XCTAssertEqual(eventTtype, .removed) 400 | XCTAssertEqual(tick, 0) 401 | XCTAssert(timestampInMs > 0) 402 | XCTAssertEqual(entityId, 1) 403 | XCTAssertEqual(contextName, "mainCtx") 404 | XCTAssertEqual(componentName, "God") 405 | XCTAssertNil(info) 406 | XCTAssertEqual(systemName, nil) 407 | default: 408 | XCTFail("expected system event") 409 | } 410 | } 411 | 412 | do { 413 | switch entitasLogger.eventData(eventId: 3)! { 414 | case let .entityEvent(eventTtype, tick, timestampInMs, entityId, contextName, systemName): 415 | XCTAssertEqual(eventTtype, .destroyed) 416 | XCTAssertEqual(tick, 0) 417 | XCTAssert(timestampInMs > 0) 418 | XCTAssertEqual(entityId, 1) 419 | XCTAssertEqual(contextName, "mainCtx") 420 | XCTAssertEqual(systemName, nil) 421 | default: 422 | XCTFail("expected system event") 423 | } 424 | } 425 | 426 | do { 427 | switch entitasLogger.eventData(eventId: 4)! { 428 | case let .entityEvent(eventTtype, tick, timestampInMs, entityId, contextName, systemName): 429 | XCTAssertEqual(eventTtype, .created) 430 | XCTAssertEqual(tick, 0) 431 | XCTAssert(timestampInMs > 0) 432 | XCTAssertEqual(entityId, 1) 433 | XCTAssertEqual(contextName, "secondaryCtx") 434 | XCTAssertEqual(systemName, nil) 435 | default: 436 | XCTFail("expected system event") 437 | } 438 | } 439 | 440 | do { 441 | switch entitasLogger.eventData(eventId: 5)! { 442 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 443 | XCTAssertEqual(eventTtype, .added) 444 | XCTAssertEqual(tick, 0) 445 | XCTAssert(timestampInMs > 0) 446 | XCTAssertEqual(entityId, 1) 447 | XCTAssertEqual(contextName, "secondaryCtx") 448 | XCTAssertEqual(componentName, "Position") 449 | XCTAssertEqual(info, "x:1, y:1") 450 | XCTAssertEqual(systemName, nil) 451 | default: 452 | XCTFail("expected system event") 453 | } 454 | } 455 | 456 | do { 457 | switch entitasLogger.eventData(eventId: 6)! { 458 | case let .componentEvent(eventTtype, tick, timestampInMs, entityId, contextName, componentName, info, systemName): 459 | XCTAssertEqual(eventTtype, .replaced) 460 | XCTAssertEqual(tick, 0) 461 | XCTAssert(timestampInMs > 0) 462 | XCTAssertEqual(entityId, 1) 463 | XCTAssertEqual(contextName, "secondaryCtx") 464 | XCTAssertEqual(componentName, "Position") 465 | XCTAssertEqual(info, "x:2, y:2") 466 | XCTAssertEqual(systemName, nil) 467 | default: 468 | XCTFail("expected system event") 469 | } 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/EventType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventType.swift 3 | // EntitasKit 4 | // 5 | // Created by Maxim Zaks on 06.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum EventType { 12 | case created 13 | case destroyed 14 | case added 15 | case removed 16 | case replaced 17 | case willInit 18 | case didInit 19 | case willExec 20 | case didExec 21 | case willCleanup 22 | case didCleanup 23 | case willTeardown 24 | case didTeardown 25 | case info 26 | case error 27 | } 28 | 29 | public typealias Tick = UInt16 30 | public typealias EventId = UInt32 31 | public typealias Timestamp = UInt32 32 | public typealias ContextId = UInt8 33 | public typealias EntityId = UInt32 34 | public typealias CompNameId = UInt16 35 | public typealias SystemNameId = UInt16 36 | public typealias InfoId = UInt32 37 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/LoggerMatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerMatcher.swift 3 | // EntitasKit 4 | // 5 | // Created by Maxim Zaks on 06.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LoggerMatcher { 12 | func match(index: Int) -> Bool 13 | } 14 | 15 | struct TickMatcher: LoggerMatcher { 16 | let logger: EntitasLogger 17 | let min: Tick 18 | let max: Tick 19 | 20 | func match(index: Int) -> Bool { 21 | let tick = logger.ticks[index] 22 | return tick >= min && tick <= max 23 | } 24 | } 25 | 26 | struct EventTypeMatcher: LoggerMatcher { 27 | let logger: EntitasLogger 28 | let type: EventType 29 | 30 | func match(index: Int) -> Bool { 31 | return logger.eventTypes[index] == type 32 | } 33 | } 34 | 35 | struct EventIndexMatcher: LoggerMatcher { 36 | let logger: EntitasLogger 37 | let min: Int 38 | let max: Int 39 | 40 | func match(index: Int) -> Bool { 41 | return index <= max && index >= min 42 | } 43 | } 44 | 45 | struct ContextNameMatcher: LoggerMatcher { 46 | let logger: EntitasLogger 47 | let contextId: ContextId 48 | 49 | func match(index: Int) -> Bool { 50 | return logger.contextIds[index] == contextId 51 | } 52 | } 53 | 54 | struct EntityIdMatcher: LoggerMatcher { 55 | let logger: EntitasLogger 56 | let entityId: EntityId 57 | 58 | func match(index: Int) -> Bool { 59 | return logger.entityIds[index] == entityId 60 | } 61 | } 62 | 63 | struct ComponentNameIdMatcher: LoggerMatcher { 64 | let logger: EntitasLogger 65 | let compId: CompNameId 66 | 67 | func match(index: Int) -> Bool { 68 | return logger.compNameIds[index] == compId 69 | } 70 | } 71 | 72 | struct SystemNameIdMatcher: LoggerMatcher { 73 | let logger: EntitasLogger 74 | let sysId: SystemNameId 75 | 76 | func match(index: Int) -> Bool { 77 | return logger.systemNameIds[index] == sysId 78 | } 79 | } 80 | 81 | struct InfoMatcher: LoggerMatcher { 82 | let logger: EntitasLogger 83 | let info: String 84 | 85 | func match(index: Int) -> Bool { 86 | let id = Int(logger.infoIds[index]) 87 | guard logger.infos.count > id else { return false } 88 | return logger.infos[id].contains(info) 89 | } 90 | } 91 | 92 | struct GroupMatcher: LoggerMatcher { 93 | let all: [LoggerMatcher] 94 | let any: [LoggerMatcher] 95 | let none: [LoggerMatcher] 96 | 97 | func match(index: Int) -> Bool { 98 | for matcher in all { 99 | if matcher.match(index: index) == false { 100 | return false 101 | } 102 | } 103 | for matcher in none { 104 | if matcher.match(index: index) { 105 | return false 106 | } 107 | } 108 | if any.isEmpty { 109 | return true 110 | } 111 | 112 | for matcher in any { 113 | if matcher.match(index: index) { 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Entitas-Swift/Logging/RelevantEntityEventsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelevantEntityEventsTests.swift 3 | // EntitasKitTests 4 | // 5 | // Created by Maxim Zaks on 16.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class RelevantEntityEventsTests: XCTestCase { 13 | 14 | var entitasLogger: EntitasLogger! 15 | var ctx : Context! 16 | var ctx2 : Context! 17 | 18 | override func setUp() { 19 | ctx = Context() 20 | ctx2 = Context() 21 | entitasLogger = EntitasLogger(contexts: [(ctx, "mainCtx"), (ctx2, "secondaryCtx")]) 22 | } 23 | 24 | func testCreateEntityAndAddComponents() { 25 | let e = ctx.createEntity() 26 | e += Position(x: 1, y: 2) 27 | e += God() 28 | 29 | XCTAssertEqual(entitasLogger.relevantEntityEvents(entityId: 1, contextId: 0), [0, 1, 2]) 30 | } 31 | 32 | func testCreateEntityAndAddComponentsAndDestroyEntity() { 33 | let e = ctx.createEntity() 34 | e += Position(x: 1, y: 2) 35 | e += God() 36 | e.destroy() 37 | 38 | XCTAssertEqual(entitasLogger.relevantEntityEvents(entityId: 1, contextId: 0), [0, 5]) 39 | } 40 | 41 | func testCreateEntityAddComponentsAndReplace() { 42 | let e = ctx.createEntity() 43 | e += Position(x: 1, y: 2) 44 | e += God() 45 | e += Position(x: 2, y: 2) 46 | 47 | XCTAssertEqual(entitasLogger.relevantEntityEvents(entityId: 1, contextId: 0), [0, 2, 3]) 48 | } 49 | 50 | func testCreateEntityAddComponentsReplaceAndRemove() { 51 | let e = ctx.createEntity() 52 | e += Position(x: 1, y: 2) 53 | e += God() 54 | e += Position(x: 2, y: 2) 55 | e -= God.cid 56 | 57 | XCTAssertEqual(entitasLogger.relevantEntityEvents(entityId: 1, contextId: 0), [0, 3]) 58 | } 59 | 60 | func testCreateEntityAddComponentsAndIgnore() { 61 | let e = ctx.createEntity() 62 | e += Position(x: 1, y: 2) 63 | entitasLogger.willExecute("S1") 64 | e += God() 65 | entitasLogger.didExecute("S1") 66 | 67 | 68 | XCTAssertEqual(entitasLogger.relevantEntityEvents(entityId: 1, contextId: 0), [0, 1, 3]) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /EntitasKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EntitasKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EntitasKit.xcodeproj/xcshareddata/xcschemes/EntitasKit-ios.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /EntitasKit.xcodeproj/xcshareddata/xcschemes/EntitasKit-macos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /EntitasKit.xcodeproj/xcuserdata/mzaks.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | EntitasKit-ios.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | EntitasKit-macos.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 0B1BF9B01EE47F92005F654F 21 | 22 | primary 23 | 24 | 25 | 0B1BF9B91EE47F92005F654F 26 | 27 | primary 28 | 29 | 30 | 0B50D77E1EF95F0C003B3B23 31 | 32 | primary 33 | 34 | 35 | 0B50D7951EF96061003B3B23 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Maxim Zaks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EntitasKit 2 | 3 | [![Build Status](https://travis-ci.org/mzaks/EntitasKit.svg?branch=master)](https://travis-ci.org/mzaks/EntitasKit) 4 | [![codecov](https://codecov.io/gh/mzaks/EntitasKit/branch/master/graph/badge.svg)](https://codecov.io/gh/mzaks/EntitasKit) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | ![swift](https://img.shields.io/badge/swift-3-blue.svg) 7 | ![ios](https://img.shields.io/badge/ios-9.0-blue.svg) 8 | ![macos](https://img.shields.io/badge/macos-10.10-blue.svg) 9 | 10 | EntitasKit is a member of Entitas family. Entitas is a framework, which helps developers follow [Entity Component System architecture](https://en.wikipedia.org/wiki/Entity–component–system) (or ECS for short). Even though ECS finds it roots in game development. Entitas proved to be helpful for App development. 11 | 12 | It can be used as: 13 | - [service registry](https://en.wikipedia.org/wiki/Service_locator_pattern) 14 | - strategy for [event driven programming](https://en.wikipedia.org/wiki/Event-driven_programming) 15 | - highly flexible data model, based on generic [product type](https://en.wikipedia.org/wiki/Product_type) 16 | 17 | Applications designed with ECS, are proved to be easy to unit test and simple to change. 18 | 19 | ## How does it work 20 | 21 | Let's imagine we have an app which has a NetworkingService. This networking service is implemented as a class or a struct, we really don't care 😉. We need to be able to use this networking service from multiple places in our app. This is a typical use case for service locator pattern. 22 | 23 | ### How can we make it work with EntitasKit? 24 | First we need to import EntitasKit and create a context. 25 | ```swift 26 | import EntitasKit 27 | 28 | let appContext = Context() 29 | ``` 30 | Now we need to define a _component_ which will hold reference to our networking service: 31 | ```swift 32 | struct NetworkingServiceComponent: UniqueComponent { 33 | let ref: NetworkingService 34 | } 35 | ``` 36 | 37 | The component is defined as _unique component_, this means that a context can hold only one instance of such component. And as a matter of fact, we would like to have only one networking service present in our application. 38 | Now lets setup our context. 39 | 40 | ```swift 41 | func setupAppContext() { 42 | appContext.setUniqueComponent(NetworkingServiceComponent(ref: NetworkingService())) 43 | } 44 | ``` 45 | 46 | In the setup function, we instantiate networking service component with an instance of networking service and put the component into our app context. 47 | 48 | Now, if we want to call `sendMessage` method on our networking service, we can do it as following: 49 | 50 | ```swift 51 | appContext.uniqueComponent(NetworkingServiceComponent.self)?.ref.sendMessage() 52 | ``` 53 | 54 | I mentioned before that EntitasKit makes it easy to test your app. This is due to the fact, that even though the networking service is unique in your app it can be easily replaced/mocked in your test. Just call this in your test setup method. 55 | 56 | ```swift 57 | appContext.setUniqueComponent(NetworkingServiceComponent(ref: NetworkingServiceMock())) 58 | ``` 59 | 60 | And call `setupAppContext()` during tear down. 61 | 62 | --- 63 | 64 | Let me show you another typical problem which can be solved with service registry and EntitasKit. 65 | 66 | Imagine you have an app and there is an unfortunate feature requirement. You need to show a modal view controller on top of another modal view controller. And than, there needs to be a way, not only to discard the current modal view controller, but all modal view controllers. 67 | 68 | For this use case we will create another _component_: 69 | 70 | ```swift 71 | struct ModalViewControllerComponent: Component { 72 | weak var ref: UIViewController? 73 | } 74 | ``` 75 | 76 | This time our _component_ is not unique and the field of the component is a _weak_ reference to `UIViewController`. The reference should be weak because we don't want to retain a view controller in our context. It should be retained only through our view controller hierarchy. 77 | 78 | In the `viewDidLoad` method of the modal view controller, we can register our view by adding following line: 79 | 80 | ```swift 81 | appContext.createEntity().set(ModalViewControllerComponent(ref: self)) 82 | ``` 83 | 84 | As you can see, this time we create an entity and set modal view controller component on it, referencing `self`. Entity is a very generic product type, which can hold any instance which implements `Component` or `UniqueComponent` protocol. The only caveat is that it cannot hold multiple instances of the same type. 85 | 86 | Let's assume, we have a situation where we showed multiple modal view controller and want to dismiss them all. How can we do it? 87 | 88 | ```swift 89 | for entity in appContext.group(ModalViewControllerComponent.matcher) { 90 | entity.get(ModalViewControllerComponent.self)?.ref?.dismiss(animated: false, completion: nil) 91 | entity.destroy() 92 | } 93 | ``` 94 | 95 | In the code above, we see that we can ask context for group of entities which have `ModalViewControllerComponent`. `Group` implements `Sequence` protocol, this is why we can iterate through entities in it. Access the modal view controller component and call `dismiss` method on it's reference. We than destroy the entity, because we don't need to hold the reference to the modal view controller any more. 96 | 97 | --- 98 | 99 | ## Strategy for event driven programming 100 | 101 | An event in Entitas is also just a component 102 | 103 | ```swift 104 | let eventContext = Context() 105 | 106 | struct MyEventComponent: Component {} 107 | ``` 108 | 109 | As you can see, we defined a context for events and a component `MyEvent`. The component does not have properties in this case, but it is ok to have an event which transports data. It is just not needed in our example. 110 | 111 | Now we need to describe the observer 112 | 113 | ```swift 114 | protocol EventObserver: class { 115 | func eventDidOccur() 116 | } 117 | 118 | struct EventObserverComponent: Component { 119 | weak var ref: EventObserver? 120 | } 121 | ``` 122 | 123 | We defined a protocol which classes has to implement in order to become an event observer. I decided to restrain the implementors to classes because, mostly the event observers are UIKit classes and this way, we can define the reference in the `EventObserverComponent` as `weak`. However if you wan to decouple event observing from UIKit, you can rethink this restriction. 124 | 125 | The component we defined are very similar to what we did in the service registry part. However now we will introduce something new. 126 | 127 | ```swift 128 | class MyEventNotifyObserverSystem: ReactiveSystem { 129 | let collector: Collector = Collector(group: eventContext.group(MyEventComponent.matcher), type: .added) 130 | let name = "Notify event observer" 131 | let observers = eventContext.group(EventObserverComponent.matcher) 132 | 133 | func execute(entities: Set) { 134 | for observer in observers { 135 | observer.get(EventObserverComponent.self)?.ref?.eventDidOccur() 136 | } 137 | } 138 | } 139 | ``` 140 | 141 | We introduced a reactive system. A reactive system is something like a reducer, if you are familiar with [Redux](http://redux.js.org/docs/Glossary.html#reducer) or [ReSwift](http://reswift.github.io/ReSwift/master/getting-started-guide.html). However, reactive systems are triggered by state change not an explicit action. 142 | 143 | When we implement a `ReactiveSystem` protocol we need to provide two properties: 144 | - name, this is just a string used for logging 145 | - collector, an instance of a class which will monitor a group and collect entities on a defined collection change type 146 | 147 | In our example we define, that we want to collect from a group of `MyEventComponent` entities. And we would like to collect them when they enter the group. Meaning that when we create a new entity and set `MyEventComponent` for the first time, this entity gets collected. 148 | 149 | So if we have something like this in a view controller: 150 | 151 | ```swift 152 | @IBAction func buttonPushed(sender: Any) { 153 | eventContext.createEntity().set(MyEventComponent()) 154 | } 155 | ``` 156 | 157 | Our button pushed "event" will be processed by `MyEventNotifyObserverSystem`. 158 | 159 | To define the "processing" logic we have to implement the `execute(entities:)` method. As we can see in the listing above, to make it all work, we just walk through observers and call `eventDidOccur`. 160 | 161 | If a view controller would register itself as observer 162 | 163 | ```swift 164 | override func viewDidLoad() { 165 | super.viewDidLoad() 166 | eventContext.createEntity().set(EventObserverComponent(ref: self)) 167 | ... 168 | } 169 | ``` 170 | 171 | We will get something people call unidirectional data flow. 172 | 173 | A reactive system by itself is not doing anything, it is just a definition of behavior. In order for it to run, we have to put it in a reactive loop 174 | 175 | ```swift 176 | let eventLoop = ReactiveLoop(ctx: eventContext, systems: [MyEventNotifyObserverSystem()]) 177 | ``` 178 | 179 | Now if we push the button, event loop will trigger the reactive system, which will call all registered observers. 180 | 181 | --- 182 | 183 | As you can see the `buttonPushed` `IBAction` is creating new entities every time. This means that we are polluting memory with all this previous events. Now don't get me wrong, specifically if the event would carry some data, it might be a desirable effect, but in our case we would like to clean up previous events. For this we can implement another reactive system 184 | 185 | ```swift 186 | class MyEventCleanupSystem: ReactiveSystem { 187 | let collector: Collector = Collector(group: eventContext.group(MyEventComponent.matcher), type: .added) 188 | let name = "Event cleanup system" 189 | 190 | func execute(entities: Set) { 191 | for e in entities { 192 | e.destroy() 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | and add it to the event loop 199 | 200 | ```swift 201 | let eventLoop = ReactiveLoop(ctx: eventContext, systems: [MyEventNotifyObserverSystem(), MyEventCleanupSystem()]) 202 | ``` 203 | 204 | As the execution order of systems is granted by `systems` array, we can be certain that a new event will be first processed by `MyEventNotifyObserverSystem` and than destroyed by `MyEventCleanupSystem`. 205 | 206 | You might think, wouldn't it be easier to destroy the event entity directly in `MyEventNotifyObserverSystem` and skip definition of cleanup system? This might be true for a simple examples, but in complex applications it is better to stick to [single responsibility principal](https://en.wikipedia.org/wiki/Single_responsibility_principle) and therefor split up the notification and cleanup logic in to separate systems. 207 | 208 | --- 209 | 210 | In previous section I mentioned that execution order of the systems is granted. Is the execution order of the observers granted as well? 211 | 212 | In current implementation it is not and there are two reasons for it: 213 | 1. `Group` is backed by a `Set`, this means that when we iterate other a group we can't predict the order. 214 | 2. It is also hard to predict the order, when the observers will register. This call `eventContext.createEntity().set(EventObserverComponent(ref: self))` will be executed in different scenarios and decoupled from each other. 215 | 216 | But no worries if the execution order is important in your use case it is a simple problem to fix. 217 | 218 | We can introduce another component 219 | 220 | ```swift 221 | struct ExecutionIndexComponent: Component { 222 | let value: Int 223 | } 224 | ``` 225 | 226 | And let listeners/observers define there execution order 227 | 228 | ```swift 229 | class Listener: EventObserver { 230 | let index: Int 231 | init(_ index: Int) { 232 | self.index = index 233 | eventContext.createEntity().set(EventObserverComponent(ref: self)).set(ExecutionIndexComponent(value: index)) 234 | } 235 | func eventDidOccur() { 236 | print("!!!!! \(index)") 237 | } 238 | } 239 | 240 | let listeners = (1...10).map{Listener($0)} 241 | ``` 242 | 243 | Now we can write a new reactive system which will execute the observers in defined order. 244 | 245 | ```swift 246 | class MyEventNotifyObserverInOrderSystem: ReactiveSystem { 247 | let collector: Collector = Collector(group: eventContext.group(MyEventComponent.matcher), type: .added) 248 | let name = "Notify event observer" 249 | let observers = eventContext.group(Matcher(all: [EventObserverComponent.cid, ExecutionIndexComponent.cid])) 250 | 251 | func execute(entities: Set) { 252 | let sortedObservers = observers.sorted(forObject: ObjectIdentifier(self)) { (e1, e2) -> Bool in 253 | return (e1.get( ExecutionIndexComponent.self)?.value ?? Int.max) < (e2.get( ExecutionIndexComponent.self)?.value ?? Int.max) 254 | } 255 | for observer in sortedObservers { 256 | observer.get(EventObserverComponent.self)?.ref?.eventDidOccur() 257 | } 258 | } 259 | } 260 | ``` 261 | 262 | I want to point out two details in `MyEventNotifyObserverInOrderSystem`: 263 | 264 | 1. The `observers` field is now a more complex group. We say that it is a group of entities which have `EventObserverComponent` and `ExecutionIndex` components 265 | 2. We compute `sortedObservers` in the `execute` method. It recompute the sorted observers each time because than it is always up to date. This way we support removing and adding observers at runtime. Also as you can see we are using a special `sorted` method. It caches the result of the sorting, if group did not change. 266 | 267 | Now as we have a reactive system which is responsible for notifying observers with execution order, let's change our `MyEventNotifyObserverSystem` to not handle observers with execution order. This can be done by exchanging the `observers` property definition 268 | 269 | ```swift 270 | let observers = eventContext.group(Matcher(all: [EventObserverComponent.cid], none: [ExecutionIndexComponent.cid])) 271 | ``` 272 | 273 | Here we say that we need a group of entities which have `EventObserverComponent` but don't have `ExecutionIndexComponent` 274 | 275 | Last, but not least. Lets update our event loop 276 | 277 | ```swift 278 | let eventLoop = ReactiveLoop(ctx: eventContext, systems: [MyEventNotifyObserverSystem(), MyEventNotifyObserverInOrderSystem(), MyEventCleanupSystem()]) 279 | ``` 280 | 281 | And we are done. 282 | 283 | If you are interested to see all of the presented thing in action, please take following gist and put it into a PlayGround together with EntitasKit 284 | 285 | https://gist.github.com/mzaks/f0fd31b2dc0f5d45b5ad077881de649d 286 | 287 | ## Highly flexible data model 288 | 289 | In this section we will use all the knowledge we accumulated from the previous section and learn a bit more. As a use case, we will build an options menu. Imagine you have an app and you would like users be able to turn on and off some features. 290 | 291 | A picture tells more than 1000 words so here is a screen shot of what the result will look like: 292 | 293 | ![](docs/screenshot001.png) 294 | 295 | First things first let's define an options context: 296 | 297 | ```swift 298 | import EntitasKit 299 | 300 | let optionsCtx = Context() 301 | 302 | struct OptionsBoolValueComponent: Component { 303 | let value: Bool 304 | } 305 | 306 | struct TitleComponent: Component { 307 | let value: String 308 | } 309 | 310 | struct DescriptionComponent: Component { 311 | let value: String 312 | } 313 | 314 | struct IndexComponent: Component { 315 | let value: Int 316 | } 317 | 318 | enum OptionsKey: String { 319 | case OptionA, OptionB 320 | } 321 | 322 | struct OptionsKeyComponent: Component { 323 | let value: OptionsKey 324 | } 325 | ``` 326 | 327 | I previous sections we defined components to hold references, now we define components to hold data. An options entity should hold following components: 328 | - boolean value component which hold the information if the option is enabled or disabled 329 | - title string, which defines the title of the options the table view 330 | - description string, which defines the description in the options table view 331 | - index of the entry in the table 332 | - and an options key, as you can see we define that we can have two options _A_ and _B_ 333 | 334 | As you can see we slice our components very thin (every component has only one property). This way it is much more robust for change and implementation of reactive behavior. 335 | 336 | In order to setup options context we need to configure the options. Here is an example how you can do it in code: 337 | 338 | ```swift 339 | struct OptionsConfigData { 340 | let key: OptionsKey 341 | let index: Int 342 | let title: String 343 | let description: String 344 | let defaultValue: Bool 345 | } 346 | 347 | let optionsConfig = [ 348 | OptionsConfigData(key: .OptionA, 349 | index: 0, 350 | title: "Enable A", 351 | description: "By enabling A we will get ...", 352 | defaultValue: true 353 | ), 354 | OptionsConfigData(key: .OptionB, 355 | index: 1, 356 | title: "Enable B", 357 | description: "By enabling B we will get ...", 358 | defaultValue: false 359 | ) 360 | ] 361 | ``` 362 | 363 | But you could also do it with JSON, plist or what ever comes to mind. Important is that we can declare what kind of options we would like to have in our app. 364 | 365 | Here is the options context setup logic: 366 | 367 | ```swift 368 | func setupOptionsContext() { 369 | for option in optionsConfig { 370 | let e = optionsCtx.createEntity() 371 | .set(OptionsKeyComponent(value: option.key)) 372 | .set(IndexComponent(value: option.index)) 373 | .set(TitleComponent(value: option.title)) 374 | .set(DescriptionComponent(value: option.description)) 375 | 376 | let value = (UserDefaults.standard.object(forKey: option.key.rawValue) as? Bool) ?? option.defaultValue 377 | 378 | e.set(OptionsBoolValueComponent(value: value)) 379 | } 380 | } 381 | ``` 382 | 383 | As you can see it is quite simple. We iterate over every option and create a corresponding entity. We also decide to persist the options in user defaults. If it is not there yet, we will just get a default value from our config. Keeping our options in CoreData, Realm or even at remote BackEnd would be also simple, but will not be covered in this tutorial. 384 | 385 | Now let's have a look at our view controller: 386 | 387 | ```swift 388 | class ViewController: UITableViewController { 389 | 390 | var items = optionsCtx.group(Matcher(all:[OptionsKeyComponent.cid, IndexComponent.cid])) 391 | 392 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 393 | return items.count 394 | } 395 | 396 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 397 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "BoolOptionCell") as? BoolOptionCell else { 398 | return UITableViewCell() 399 | } 400 | 401 | cell.setEntity(e: getItem(indexPath: indexPath)) 402 | return cell 403 | } 404 | 405 | private func getItem(indexPath: IndexPath) -> Entity{ 406 | return items.sorted(forObject: ObjectIdentifier(self), by: { (e1, e2) -> (Bool) in 407 | let i1 = e1.get(IndexComponent.self)?.value ?? 0 408 | let i2 = e2.get(IndexComponent.self)?.value ?? 0 409 | return i1 < i2 410 | })[indexPath.row] 411 | } 412 | } 413 | ``` 414 | 415 | It is a simple table view controller. The items are backed by a group of entities which have options key component and index component. The row count is directly determined b the group. The cell is a special type which has a `setEntity` method. We get the entity by sorting the items by index component value. 416 | 417 | Here is the implementation of `BoolOptionCell`: 418 | 419 | ```swift 420 | class BoolOptionCell: UITableViewCell { 421 | 422 | @IBOutlet weak var titleLabel: UILabel! 423 | @IBOutlet weak var descriptionLabel: UILabel! 424 | @IBOutlet weak var optionsSwitch: UISwitch! 425 | 426 | weak var entity: Entity? 427 | 428 | @IBAction func optionSwitched(_ sender: UISwitch) { 429 | entity?.set(OptionsBoolValueComponent(value: sender.isOn)) 430 | } 431 | 432 | func setEntity(e : Entity){ 433 | self.entity = e 434 | titleLabel.text = e.get(TitleComponent.self)?.value 435 | descriptionLabel.text = e.get(DescriptionComponent.self)?.value 436 | if let boolComponent: OptionsBoolValueComponent = e.get() { 437 | optionsSwitch.setOn(boolComponent.value, animated: true) 438 | } 439 | } 440 | } 441 | ``` 442 | 443 | It has `IBOutlet` to title/description label and also to the options switch. Those will be updated when `setEntity` method is called. We also hold a weak reference to the backing entity. This way we can set the boolean value on the entity in the `IBAction`. 444 | 445 | Last but not least, we would like to persist, the value of the changed option entry. This we can do we a reactive system. 446 | 447 | ```swift 448 | class PersistOptionsSystem: ReactiveSystem { 449 | let collector: Collector = Collector( 450 | group: optionsCtx.group( 451 | Matcher( 452 | all: [OptionsBoolValueComponent.cid, OptionsKeyComponent.cid] 453 | ) 454 | ), 455 | type: .updated 456 | ) 457 | let name = "Options persisting" 458 | 459 | func execute(entities: Set) { 460 | for e in entities { 461 | guard let boolComponent: OptionsBoolValueComponent = e.get(), 462 | let keyComponent: OptionsKeyComponent = e.get() else { 463 | continue 464 | } 465 | UserDefaults.standard.set(boolComponent.value, forKey: keyComponent.value.rawValue) 466 | } 467 | } 468 | } 469 | ``` 470 | 471 | We collect updated entities inside of the group over OptionsBoolValueComponent and OptionsKeyComponent. Meaning that entities which have both components are in the group and if we replace one of the components than this entity will be collected and passed to `execute(entities:)` method. 472 | 473 | In the `execute(entities:)` unpack bool and key components and set the value for key in user defaults. 474 | 475 | To tight it all together lets update our app delegate to setup options context and hold a reference to reactive loop: 476 | 477 | ```swift 478 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 479 | setupOptionsContext() 480 | 481 | optionsLoop = ReactiveLoop(ctx: optionsCtx, systems: [ 482 | PersistOptionsSystem() 483 | ]) 484 | 485 | return true 486 | } 487 | ``` 488 | 489 | Now we have a great way of declaring storing persisting and displaying the options. But what about querying the options in other places of our app. We could use a group similar to the one we used in our table view controller and than filter it for option type we are interested in, but there is a better way in EntitasKit. We can define an index over component values: 490 | 491 | ```swift 492 | let optionsKeyIndex = optionsCtx.index { (comp: OptionsKeyComponent) -> OptionsKey in 493 | return comp.value 494 | } 495 | ``` 496 | 497 | This way we can query for options as following `optionsKeyIndex[.OptionA1].first`. More on it in the next section. 498 | 499 | --- 500 | 501 | Let's spice it up a bit let's say we want to add two more options _A1_ and _A2_ 502 | 503 | ```swift 504 | enum OptionsKey: String { 505 | case OptionA, OptionB, OptionA1, OptionA2 506 | } 507 | 508 | let optionsConfig = [ 509 | OptionsConfigData(key: .OptionA, 510 | index: 0, 511 | title: "Enable A", 512 | description: "By enabling A we will get ...", 513 | defaultValue: true 514 | ), 515 | OptionsConfigData(key: .OptionB, 516 | index: 3, 517 | title: "Enable B", 518 | description: "By enbaling B we will get ...", 519 | defaultValue: false 520 | ), 521 | OptionsConfigData(key: .OptionA1, 522 | index: 1, 523 | title: "Enable A1", 524 | description: "By enabling A1 we will get ...", 525 | defaultValue: true 526 | ), 527 | OptionsConfigData(key: .OptionA2, 528 | index: 2, 529 | title: "Enable A2", 530 | description: "By enbaling A2 we will get ...", 531 | defaultValue: true 532 | ) 533 | ] 534 | ``` 535 | 536 | And we have a nasty feature requirement. If we turn of option A, options A1 and A2 should also be turned off. If we turn on A1 or A2 option A should be turned on as well. This is btw. an actual real world use case I had to implement a few months ago. 537 | 538 | This is a reactive system which will implement the logical part of the turn of A1 and A2 if A got turned of: 539 | 540 | ```swift 541 | class SwitchChildrenOfOptionASystem: ReactiveSystem { 542 | let collector: Collector = Collector( 543 | group: optionsCtx.group( 544 | Matcher( 545 | all: [OptionsBoolValueComponent.cid, OptionsKeyComponent.cid] 546 | ) 547 | ), 548 | type: .updated 549 | ) 550 | let name = "Switching off A1 and A2 when A got switched off" 551 | 552 | func execute(entities: Set) { 553 | guard 554 | let optionA = entities.first(where: { (e) -> Bool in 555 | return e.get(OptionsKeyComponent.self)?.value == .OptionA 556 | }), 557 | let aIsOn = optionA.get(OptionsBoolValueComponent.self)?.value, 558 | let optionA1 = optionsKeyIndex[.OptionA1].first, 559 | let optionA2 = optionsKeyIndex[.OptionA2].first 560 | else { 561 | return 562 | } 563 | 564 | if aIsOn == false { 565 | if optionA1.get(OptionsBoolValueComponent.self)?.value != false { 566 | optionA1.set(OptionsBoolValueComponent(value: false)) 567 | } 568 | if optionA2.get(OptionsBoolValueComponent.self)?.value != false { 569 | optionA2.set(OptionsBoolValueComponent(value: false)) 570 | } 571 | } 572 | } 573 | } 574 | ``` 575 | 576 | I called it the logical part because here we only change the boolean value of options A1 and A2 if they are not set to false yet. We don't manipulate the UI. In order to update the table view we need to introduce another component: 577 | 578 | ```swift 579 | struct OptionsDisplayComponent: UniqueComponent { 580 | weak var ref: ViewController? 581 | } 582 | ``` 583 | 584 | And override the `viewDidLoad` method on the view controller and introduce a new method `updateCell`: 585 | 586 | ```swift 587 | override func viewDidLoad() { 588 | super.viewDidLoad() 589 | optionsCtx.setUniqueComponent(OptionsDisplayComponent(ref: self)) 590 | } 591 | 592 | func updateCell(at: Int) { 593 | let indexPath = IndexPath(row: at, section: 0) 594 | guard let cell = tableView.cellForRow(at: indexPath) as? BoolOptionCell else { 595 | return 596 | } 597 | cell.setEntity(e: getItem(indexPath: indexPath)) 598 | } 599 | ``` 600 | 601 | Now let's implement the reactive system which will handle the updates of the ui: 602 | 603 | ```swift 604 | class UpdateOptionsDisplaySystem: ReactiveSystem { 605 | let collector: Collector = Collector( 606 | group: optionsCtx.group( 607 | Matcher( 608 | all: [OptionsBoolValueComponent.cid, IndexComponent.cid] 609 | ) 610 | ), 611 | type: .updated 612 | ) 613 | let name = "Update options cells on value change" 614 | 615 | func execute(entities: Set) { 616 | for e in entities { 617 | guard let index = e.get(IndexComponent.self)?.value else { 618 | continue 619 | } 620 | optionsCtx.uniqueComponent(OptionsDisplayComponent.self)?.ref?.updateCell(at: index) 621 | } 622 | } 623 | } 624 | ``` 625 | 626 | Let's introduce a system which will implement the second part of our requirement - enable option A if A1 or A2 get enabled: 627 | 628 | ```swift 629 | class SwitchParentOfOptionASystem: ReactiveSystem { 630 | let collector: Collector = Collector( 631 | group: optionsCtx.group( 632 | Matcher( 633 | all: [OptionsBoolValueComponent.cid, OptionsKeyComponent.cid] 634 | ) 635 | ), 636 | type: .updated 637 | ) 638 | let name = "Switching on A when A1 or A2 got switched on" 639 | 640 | func execute(entities: Set) { 641 | for e in entities { 642 | guard e.get(OptionsKeyComponent.self)?.value == .OptionA1 || e.get(OptionsKeyComponent.self)?.value == .OptionA2 else { 643 | continue 644 | } 645 | guard let isON = e.get(OptionsBoolValueComponent.self)?.value, 646 | let optionA = optionsKeyIndex[.OptionA].first 647 | else { 648 | continue 649 | } 650 | if isON && optionA.get(OptionsBoolValueComponent.self)?.value != true { 651 | optionA.set(OptionsBoolValueComponent(value: true)) 652 | } 653 | } 654 | } 655 | } 656 | ``` 657 | 658 | Last thing we need to do is to update our AppDelegate 659 | 660 | ```swift 661 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 662 | 663 | setupOptionsContext() 664 | 665 | optionsLoop = ReactiveLoop(ctx: optionsCtx, systems: [ 666 | PersistOptionsSystem(), 667 | SwitchChildrenOfOptionASystem(), 668 | UpdateOptionsDisplaySystem(), 669 | SwitchParentOfOptionASystem() 670 | ]) 671 | 672 | return true 673 | } 674 | ``` 675 | 676 | All code for this example can be found here: 677 | https://gist.github.com/mzaks/c1428efcbbbb0c0c430537715afea8c6 678 | 679 | ## Entitas family 680 | - [C#](https://github.com/sschmid/Entitas-CSharp) 681 | - [C++](https://github.com/JuDelCo/Entitas-Cpp) 682 | - [Objective-C](https://github.com/wooga/entitas) 683 | - [Java](https://github.com/Rubentxu/entitas-java) 684 | - [Python](https://github.com/Aenyhm/entitas-python) 685 | - [Scala](https://github.com/darkoverlordofdata/entitas-scala) 686 | - [Go](https://github.com/wooga/go-entitas) 687 | - [F#](https://github.com/darkoverlordofdata/entitas-fsharp) 688 | - [TypeScript](https://github.com/darkoverlordofdata/entitas-ts) 689 | - [Kotlin](https://github.com/darkoverlordofdata/entitas-kotlin) 690 | - [Haskell](https://github.com/mhaemmerle/entitas-haskell) 691 | - [Erlang](https://github.com/mhaemmerle/entitas_erl) 692 | - [Clojure](https://github.com/mhaemmerle/entitas-clj) 693 | -------------------------------------------------------------------------------- /Sources/Collector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collector.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 18.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Collector { 12 | public struct ChangeOptions : OptionSet { 13 | public let rawValue: UInt8 14 | public static let added = ChangeOptions(rawValue:1 << 0) 15 | public static let updated = ChangeOptions(rawValue:1 << 1) 16 | public static let removed = ChangeOptions(rawValue:1 << 2) 17 | public static let addedOrUpdated : ChangeOptions = [.added, .updated] 18 | public static let addedUpdatedOrRemoved : ChangeOptions = [.added, .updated, .removed] 19 | 20 | public init(rawValue : UInt8) { 21 | self.rawValue = rawValue 22 | } 23 | } 24 | 25 | fileprivate var entities: Set = [] 26 | public let matcher: Matcher 27 | fileprivate let type: ChangeOptions 28 | 29 | public var isPaused: Bool 30 | 31 | public init(group: Group, type: ChangeOptions, paused: Bool = false) { 32 | self.type = type 33 | self.matcher = group.matcher 34 | self.isPaused = paused 35 | group.observer(add: self) 36 | } 37 | 38 | public var collected : Set { 39 | let result = entities 40 | entities = [] 41 | return result 42 | } 43 | 44 | public var collectedAndMatching : [Entity] { 45 | let result = entities 46 | entities = [] 47 | let matcher = self.matcher 48 | return result.filter { 49 | return matcher.matches($0) 50 | } 51 | } 52 | 53 | public var first : Entity? { 54 | return entities.popFirst() 55 | } 56 | 57 | @discardableResult 58 | public func drainAndPause() -> Set { 59 | isPaused = true 60 | return collected 61 | } 62 | 63 | public var isEmpty: Bool { 64 | return entities.isEmpty 65 | } 66 | } 67 | 68 | extension Collector: GroupObserver { 69 | public func added(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 70 | guard self.type.contains(.added) else { 71 | return 72 | } 73 | guard isPaused == false else { 74 | return 75 | } 76 | entities.insert(entity) 77 | } 78 | public func updated(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 79 | guard self.type.contains(.updated) else { 80 | return 81 | } 82 | guard isPaused == false else { 83 | return 84 | } 85 | entities.insert(entity) 86 | } 87 | public func removed(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 88 | guard self.type.contains(.removed) else { 89 | return 90 | } 91 | guard isPaused == false else { 92 | return 93 | } 94 | entities.insert(entity) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Component.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CID : Hashable { 12 | let oi: ObjectIdentifier 13 | init(_ c : Component.Type) { 14 | oi = ObjectIdentifier(c) 15 | } 16 | public var hashValue: Int { 17 | return oi.hashValue 18 | } 19 | public static func ==(a: CID, b: CID) -> Bool { 20 | return a.oi == b.oi 21 | } 22 | } 23 | 24 | public protocol Component {} 25 | public protocol UniqueComponent: Component {} 26 | 27 | extension Component { 28 | public static var cid : CID { 29 | return CID(Self.self) 30 | } 31 | public var cid : CID { 32 | return CID(Self.self) 33 | } 34 | } 35 | 36 | public protocol ComponentInfo { 37 | var info: String {get} 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 05.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Context { 12 | public private(set) var entities: Set = [] 13 | private var entityIndex: Int = 0 14 | lazy private var mainObserver: MainObserver = { 15 | return MainObserver(ctx: self) 16 | }() 17 | private var groups: [Matcher: Group] = [:] 18 | private var groupsByCID: [CID: Set] = [:] 19 | private var observers : Set = [] 20 | 21 | public init() {} 22 | 23 | deinit { 24 | for e in entities { 25 | e.destroy() 26 | } 27 | entities.removeAll() 28 | groups.removeAll() 29 | groupsByCID.removeAll() 30 | observers.removeAll() 31 | } 32 | 33 | public func createEntity() -> Entity { 34 | entityIndex += 1 35 | let e = Entity(index: entityIndex, mainObserver: mainObserver) 36 | entities.insert(e) 37 | for o in observers where o.ref is ContextObserver { 38 | (o.ref as! ContextObserver).created(entity: e, in: self) 39 | } 40 | return e 41 | } 42 | 43 | public func group(_ matcher: Matcher) -> Group { 44 | if let group = groups[matcher] { 45 | return group 46 | } 47 | 48 | let group = Group(matcher: matcher) 49 | for o in observers where o.ref is ContextObserver { 50 | (o.ref as! ContextObserver).created(group: group, withMatcher: matcher, in: self) 51 | } 52 | for e in entities { 53 | if matcher.matches(e) { 54 | group.add(e) 55 | } 56 | } 57 | 58 | groups[matcher] = group 59 | for cid in matcher.allOf { 60 | var set = groupsByCID[cid] ?? [] 61 | set.insert(group) 62 | groupsByCID[cid] = set 63 | } 64 | for cid in matcher.anyOf { 65 | var set = groupsByCID[cid] ?? [] 66 | set.insert(group) 67 | groupsByCID[cid] = set 68 | } 69 | for cid in matcher.noneOf { 70 | var set = groupsByCID[cid] ?? [] 71 | set.insert(group) 72 | groupsByCID[cid] = set 73 | } 74 | return group 75 | } 76 | 77 | public func uniqueEntity(_ matcher: Matcher) -> Entity? { 78 | let g = group(matcher) 79 | assert(g.count <= 1, "\(g.count) entites found for matcher \(matcher)") 80 | return g.first(where: {_ in return true}) 81 | } 82 | 83 | public func uniqueEntity(_ type: T.Type) -> Entity? { 84 | return uniqueEntity(Matcher(all: [type.cid])) 85 | } 86 | 87 | public func uniqueComponent(_ type: T.Type) -> T? { 88 | let g = group(Matcher(all: [T.cid])) 89 | assert(g.count <= 1, "\(g.count) entites found") 90 | return g.first(where: {_ in return true})?.get() } 91 | 92 | public func uniqueComponent() -> T? { 93 | return uniqueComponent(T.self) 94 | } 95 | 96 | public func setUniqueComponent(_ component: T) { 97 | if let e = uniqueEntity(Matcher(all: [component.cid])) { 98 | e.set(component) 99 | } else { 100 | createEntity().set(component) 101 | } 102 | } 103 | 104 | public func hasUniqueComponent(_ type: T.Type) -> Bool { 105 | return uniqueEntity(Matcher(all: [type.cid])) != nil 106 | } 107 | 108 | public func index(paused: Bool = false, keyBuilder: @escaping (C) -> T) -> Index { 109 | let index = Index(ctx: self, paused: paused, keyBuilder: keyBuilder) 110 | for o in observers where o.ref is ContextObserver { 111 | (o.ref as! ContextObserver).created(index: index, in: self) 112 | } 113 | return index 114 | } 115 | 116 | public func observer(add o: ContextObserver){ 117 | observers.update(with: ObserverBox(o)) 118 | } 119 | 120 | public func observer(remove o: ContextObserver){ 121 | observers.remove(ObserverBox(o)) 122 | } 123 | 124 | fileprivate func destroyed(entity: Entity) { 125 | entities.remove(entity) 126 | } 127 | 128 | fileprivate func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity) { 129 | if newComponent is UniqueComponent, 130 | let uniqueEntity = uniqueEntity(Matcher(all: [newComponent.cid])), 131 | uniqueEntity != entity { 132 | uniqueEntity.remove(newComponent.cid) 133 | if uniqueEntity.isEmpty { 134 | uniqueEntity.destroy() 135 | } 136 | } 137 | for group in groupsByCID[newComponent.cid] ?? [] { 138 | group.checkOnUpdate(oldComponent: oldComponent, newComponent: newComponent, entity: entity) 139 | } 140 | } 141 | 142 | fileprivate func removed(component: Component, from entity: Entity) { 143 | for group in groupsByCID[component.cid] ?? [] { 144 | group.checkOnRemove(oldComponent: component, entity: entity) 145 | } 146 | } 147 | } 148 | 149 | public protocol ContextObserver : Observer { 150 | func created(entity: Entity, in context: Context) 151 | func created(group: Group, withMatcher matcher: Matcher, in context: Context) 152 | func created(index: Index, in context: Context) 153 | } 154 | 155 | private final class MainObserver: EntityObserver { 156 | weak var ctx: Context? 157 | init(ctx: Context) { 158 | self.ctx = ctx 159 | } 160 | public func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity) { 161 | ctx?.updated(component: oldComponent, with: newComponent, in: entity) 162 | } 163 | public func removed(component: Component, from entity: Entity) { 164 | ctx?.removed(component: component, from: entity) 165 | } 166 | public func destroyed(entity: Entity) { 167 | ctx?.destroyed(entity: entity) 168 | } 169 | } 170 | 171 | extension Context { 172 | public func collector(for matcher: Matcher, type: Collector.ChangeOptions = .added, paused: Bool = false) -> Collector { 173 | return Collector(group: self.group(matcher), type: type, paused: paused) 174 | } 175 | public func all(_ cids: Set, any: Set = [], none: Set = []) -> Group { 176 | return self.group(Matcher(all: cids, any: any, none: none)) 177 | } 178 | public func any(_ cids: Set, none: Set = []) -> Group { 179 | return self.group(Matcher(any: cids, none: none)) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/Entitas-Swift.h: -------------------------------------------------------------------------------- 1 | // 2 | // Entitas-Swift.h 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Entitas-Swift. 12 | FOUNDATION_EXPORT double Entitas_SwiftVersionNumber; 13 | 14 | //! Project version string for Entitas-Swift. 15 | FOUNDATION_EXPORT const unsigned char Entitas_SwiftVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/EntitasLoggerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntitasLoggerTests.swift 3 | // EntitasKitTests 4 | // 5 | // Created by Maxim Zaks on 04.03.18. 6 | // Copyright © 2018 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class EntitasLoggerTests: XCTestCase { 13 | 14 | var entitasLogger: EntitasLogger! 15 | var ctx : Context! 16 | var ctx2 : Context! 17 | 18 | override func setUp() { 19 | ctx = Context() 20 | ctx2 = Context() 21 | entitasLogger = EntitasLogger(contexts: [(ctx, "mainCtx"), (ctx2, "secondaryCtx")]) 22 | } 23 | 24 | func testPushAndPopSystemsCalls() { 25 | entitasLogger.willInit("S1") 26 | entitasLogger.didInit("S1") 27 | 28 | XCTAssertEqual(entitasLogger.systemNames, ["S1"]) 29 | 30 | entitasLogger.willInit("S2") 31 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 32 | entitasLogger.didInit("S2") 33 | 34 | XCTAssertEqual(entitasLogger.systemNames, ["S1", "S2"]) 35 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willInit, EventType.didInit]) 36 | XCTAssertEqual(entitasLogger.systemNameIds, [1, 1]) 37 | } 38 | 39 | func testHierarchicalSystemCalls() { 40 | entitasLogger.willInit("S1") 41 | entitasLogger.willInit("S2") 42 | entitasLogger.didInit("S2") 43 | entitasLogger.willInit("S3") 44 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 45 | entitasLogger.didInit("S3") 46 | entitasLogger.willInit("S4") 47 | entitasLogger.didInit("S4") 48 | entitasLogger.willInit("S5") 49 | entitasLogger.willInit("S6") 50 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 51 | entitasLogger.didInit("S6") 52 | entitasLogger.didInit("S5") 53 | entitasLogger.didInit("S1") 54 | 55 | XCTAssertEqual(entitasLogger.systemNames, ["S1", "S2", "S3", "S4", "S5", "S6"]) 56 | XCTAssertEqual( 57 | entitasLogger.eventTypes, 58 | [ 59 | EventType.willInit, // S1 60 | EventType.willInit, EventType.didInit, // S3 61 | EventType.willInit, // S5 62 | EventType.willInit, EventType.didInit, // S6 63 | EventType.didInit, // S5 64 | EventType.didInit, // S1 65 | ] 66 | ) 67 | XCTAssertEqual( 68 | entitasLogger.systemNameIds, 69 | [0, 2, 2, 4, 5, 5, 4, 0] 70 | ) 71 | } 72 | 73 | func testExecuteSystem() { 74 | entitasLogger.willExecute("S1") 75 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 76 | entitasLogger.didExecute("S1") 77 | 78 | XCTAssertEqual(entitasLogger.systemNames, ["S1"]) 79 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willExec, EventType.didExec]) 80 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 0]) 81 | } 82 | 83 | func testCleanupSystem() { 84 | entitasLogger.willCleanup("S1") 85 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 86 | entitasLogger.didCleanup("S1") 87 | 88 | XCTAssertEqual(entitasLogger.systemNames, ["S1"]) 89 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willCleanup, EventType.didCleanup]) 90 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 0]) 91 | } 92 | 93 | func testTeardownSystem() { 94 | entitasLogger.willTeardown("S1") 95 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 96 | entitasLogger.didTeardown("S1") 97 | 98 | XCTAssertEqual(entitasLogger.systemNames, ["S1"]) 99 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willTeardown, EventType.didTeardown]) 100 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 0]) 101 | } 102 | 103 | func testEntityCreateAndDestroy() { 104 | let e1 = ctx.createEntity() 105 | e1.destroy() 106 | let e2 = ctx.createEntity() 107 | let e3 = ctx.createEntity() 108 | e3.destroy() 109 | e2.destroy() 110 | 111 | XCTAssertEqual(entitasLogger.systemNames, []) 112 | XCTAssertEqual( 113 | entitasLogger.eventTypes, 114 | [ 115 | EventType.created, EventType.destroyed, 116 | EventType.created, 117 | EventType.created, 118 | EventType.destroyed, 119 | EventType.destroyed 120 | ] 121 | ) 122 | XCTAssertEqual( 123 | entitasLogger.systemNameIds, 124 | [ 125 | SystemNameId.max, SystemNameId.max, 126 | SystemNameId.max, SystemNameId.max, 127 | SystemNameId.max, SystemNameId.max 128 | ] 129 | ) 130 | XCTAssertEqual(entitasLogger.entityIds, [1, 1, 2, 3, 3, 2]) 131 | } 132 | 133 | func testEntityCreateAndDestroyMultiplCtx() { 134 | let e1 = ctx.createEntity() 135 | let e2 = ctx2.createEntity() 136 | e2.destroy() 137 | e1.destroy() 138 | 139 | XCTAssertEqual(entitasLogger.entityIds, [1, 1, 1, 1]) 140 | XCTAssertEqual(entitasLogger.contextIds, [0, 1, 1, 0]) 141 | } 142 | 143 | func testEntityCreateAndDestroyInsideSystems() { 144 | entitasLogger.willExecute("S1") 145 | entitasLogger.willExecute("S2") 146 | let e1 = ctx.createEntity() 147 | entitasLogger.didExecute("S2") 148 | entitasLogger.willExecute("S3") 149 | entitasLogger.didExecute("S3") 150 | let e2 = ctx2.createEntity() 151 | e2.destroy() 152 | entitasLogger.didExecute("S1") 153 | e1.destroy() 154 | 155 | 156 | XCTAssertEqual( 157 | entitasLogger.eventTypes, 158 | [ 159 | EventType.willExec, 160 | .willExec, 161 | .created, 162 | .didExec, 163 | .created, 164 | .destroyed, 165 | .didExec, 166 | .destroyed 167 | ] 168 | ) 169 | 170 | XCTAssertEqual(entitasLogger.entityIds, [EntityId.max, .max, 1, .max, 1, 1, .max, 1]) 171 | XCTAssertEqual(entitasLogger.contextIds, [ContextId.max, .max, 0, .max, 1, 1, .max, 0]) 172 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 1, 1, 1, 0, 0, 0, .max]) 173 | } 174 | 175 | func testAddUpdateRemoveComponent() { 176 | let e = ctx.createEntity() 177 | e += Position(x: 1, y: 2) 178 | e += Position(x: 2, y: 2) 179 | e -= Position.cid 180 | 181 | XCTAssertEqual( 182 | entitasLogger.eventTypes, 183 | [ 184 | .created, 185 | .added, 186 | .replaced, 187 | .removed 188 | ] 189 | ) 190 | XCTAssertEqual(entitasLogger.entityIds, [1, 1, 1, 1]) 191 | XCTAssertEqual(entitasLogger.contextIds, [0, 0, 0, 0]) 192 | XCTAssertEqual(entitasLogger.compNames, ["Position"]) 193 | XCTAssertEqual(entitasLogger.compNameIds, [CompNameId.max, 0, 0, 0]) 194 | 195 | } 196 | 197 | func testAddUpdateRemoveComponentInsideSystems() { 198 | entitasLogger.willExecute("S1") 199 | let e = ctx.createEntity() 200 | let e2 = ctx2.createEntity() 201 | e += Position(x: 1, y: 2) 202 | e += Position(x: 2, y: 2) 203 | e -= Position.cid 204 | e2 += God() 205 | entitasLogger.didExecute("S1") 206 | 207 | XCTAssertEqual( 208 | entitasLogger.eventTypes, 209 | [ 210 | .willExec, 211 | .created, 212 | .created, 213 | .added, 214 | .replaced, 215 | .removed, 216 | .added, 217 | .didExec 218 | ] 219 | ) 220 | XCTAssertEqual(entitasLogger.entityIds, [EntityId.max, 1, 1, 1, 1, 1, 1, .max]) 221 | XCTAssertEqual(entitasLogger.contextIds, [ContextId.max, 0, 1, 0, 0, 0, 1, .max]) 222 | XCTAssertEqual(entitasLogger.compNames, ["Position", "God"]) 223 | XCTAssertEqual(entitasLogger.compNameIds, [CompNameId.max, .max, .max, 0, 0, 0, 1, .max]) 224 | XCTAssertEqual(entitasLogger.systemNames, ["S1"]) 225 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 0, 0, 0, 0, 0, 0, 0]) 226 | XCTAssertEqual(entitasLogger.infos, ["x:1, y:2", "x:2, y:2"]) 227 | 228 | } 229 | 230 | func testTickIncrease() { 231 | entitasLogger.willExecute("S1") 232 | entitasLogger.didExecute("S1") 233 | entitasLogger.willExecute("S2") 234 | entitasLogger.didExecute("S2") 235 | entitasLogger.willExecute("S3") 236 | entitasLogger.didExecute("S3") 237 | entitasLogger.willExecute("S1") 238 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 239 | entitasLogger.didExecute("S1") 240 | entitasLogger.willExecute("S1") 241 | entitasLogger.didExecute("S1") 242 | entitasLogger.willExecute("S2") 243 | Thread.sleep(until: Date(timeIntervalSinceNow: 0.01)) 244 | entitasLogger.didExecute("S2") 245 | XCTAssertEqual(entitasLogger.systemNames, ["S1", "S2", "S3"]) 246 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willExec, .didExec, .willExec, .didExec]) 247 | XCTAssertEqual(entitasLogger.systemNameIds, [0, 0, 1, 1]) 248 | XCTAssertEqual(entitasLogger.ticks, [2, 2, 3, 3]) 249 | } 250 | 251 | func testAddInfo() { 252 | entitasLogger.addInfo("This is an info") 253 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.info]) 254 | XCTAssertEqual(entitasLogger.infoIds, [0]) 255 | XCTAssertEqual(entitasLogger.infos, ["This is an info"]) 256 | } 257 | 258 | func testAddInfoWithSystems() { 259 | entitasLogger.willInit("S0") 260 | entitasLogger.didInit("S0") 261 | entitasLogger.willExecute("S1") 262 | entitasLogger.addInfo("Hi") 263 | entitasLogger.didExecute("S1") 264 | entitasLogger.addInfo("How are you") 265 | 266 | XCTAssertEqual(entitasLogger.systemNames, ["S0", "S1"]) 267 | XCTAssertEqual(entitasLogger.eventTypes, [EventType.willExec, .info, .didExec, .info]) 268 | XCTAssertEqual(entitasLogger.infos, ["Hi", "How are you"]) 269 | XCTAssertEqual(entitasLogger.infoIds, [.max, 0, .max, 1]) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /Sources/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entity.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public final class Entity { 13 | public let creationIndex: Int 14 | private var components: [CID: Component] = [:] 15 | fileprivate let mainObserver: EntityObserver 16 | 17 | private var observers : Set = [] 18 | init(index: Int, mainObserver: EntityObserver) { 19 | creationIndex = index 20 | self.mainObserver = mainObserver 21 | } 22 | 23 | deinit { 24 | observers.removeAll() 25 | } 26 | 27 | public func get() -> T? { 28 | return components[T.cid] as? T 29 | } 30 | 31 | public func get(_ type: T.Type) -> T? { 32 | return components[type.cid] as? T 33 | } 34 | 35 | @discardableResult 36 | public func set(_ comp: T) -> Entity { 37 | let c = components.updateValue(comp, forKey: T.cid) 38 | updated(component: c, with: comp) 39 | return self 40 | } 41 | 42 | @discardableResult 43 | public func remove(_ cid: CID) -> Entity { 44 | if let c = components.removeValue(forKey: cid) { 45 | removed(component: c) 46 | } 47 | return self 48 | } 49 | 50 | public func has(_ cid: CID) -> Bool { 51 | return components[cid] != nil 52 | } 53 | 54 | public func destroy() { 55 | for cid in components.keys { 56 | remove(cid) 57 | } 58 | destroyed() 59 | observers.removeAll() 60 | } 61 | 62 | public var isEmpty: Bool { 63 | return components.isEmpty 64 | } 65 | 66 | public func observer(add o: EntityObserver){ 67 | observers.update(with: ObserverBox(o)) 68 | } 69 | 70 | public func observer(remove o: EntityObserver){ 71 | observers.remove(ObserverBox(o)) 72 | } 73 | 74 | private func updated(component oldComponent: Component?, with newComponent: Component) { 75 | mainObserver.updated(component: oldComponent, with: newComponent, in: self) 76 | for box in observers { 77 | (box.ref as? EntityObserver)?.updated(component: oldComponent, with: newComponent, in: self) 78 | } 79 | } 80 | 81 | private func removed(component: Component) { 82 | mainObserver.removed(component: component, from: self) 83 | for box in observers { 84 | (box.ref as? EntityObserver)?.removed(component: component, from: self) 85 | } 86 | } 87 | 88 | private func destroyed() { 89 | mainObserver.destroyed(entity: self) 90 | for box in observers { 91 | (box.ref as? EntityObserver)?.destroyed(entity: self) 92 | } 93 | } 94 | } 95 | 96 | public protocol EntityObserver : Observer { 97 | func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity) 98 | func removed(component: Component, from entity: Entity) 99 | func destroyed(entity: Entity) 100 | } 101 | 102 | extension Entity: Hashable { 103 | public var hashValue: Int { 104 | return creationIndex 105 | } 106 | public static func ==(a: Entity, b: Entity) -> Bool { 107 | return a.creationIndex == b.creationIndex && a.mainObserver === b.mainObserver 108 | } 109 | } 110 | 111 | extension Entity: Comparable { 112 | public static func <(lhs: Entity, rhs: Entity) -> Bool { 113 | return lhs.creationIndex < rhs.creationIndex 114 | } 115 | } 116 | 117 | public func += (e: Entity, c: T) { 118 | e.set(c) 119 | } 120 | 121 | public func -= (e: Entity, cid: CID) { 122 | e.remove(cid) 123 | } 124 | 125 | extension Entity { 126 | public func with(block: (C1) -> Void) { 127 | if let c = self.get(C1.self) { 128 | block(c) 129 | } 130 | } 131 | public func with(block: (C1, C2) -> Void) { 132 | if let c1 = self.get(C1.self), 133 | let c2 = self.get(C2.self) { 134 | block(c1, c2) 135 | } 136 | } 137 | public func with(block: (C1, C2, C3) -> Void) { 138 | if let c1 = self.get(C1.self), 139 | let c2 = self.get(C2.self), 140 | let c3 = self.get(C3.self) { 141 | block(c1, c2, c3) 142 | } 143 | } 144 | public func with(block: (C1, C2, C3, C4) -> Void) { 145 | if let c1 = self.get(C1.self), 146 | let c2 = self.get(C2.self), 147 | let c3 = self.get(C3.self), 148 | let c4 = self.get(C4.self) { 149 | block(c1, c2, c3, c4) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 05.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Group: Hashable { 12 | fileprivate var entities: Set = [] 13 | private var observers : Set = [] 14 | let matcher: Matcher 15 | var sortedCache: [ObjectIdentifier: [Entity]] = [:] 16 | 17 | init(matcher: Matcher) { 18 | self.matcher = matcher 19 | } 20 | 21 | func add(_ e: Entity) { 22 | entities.insert(e) 23 | sortedCache.removeAll(keepingCapacity: true) 24 | } 25 | 26 | public func observer(add observer:GroupObserver) { 27 | observers.update(with: ObserverBox(observer)) 28 | } 29 | 30 | public func observer(remove observer: GroupObserver) { 31 | observers.remove(ObserverBox(observer)) 32 | } 33 | 34 | func checkOnUpdate(oldComponent: Component?, newComponent: Component, entity: Entity) { 35 | if matcher.matches(entity) { 36 | let (added, _) = entities.insert(entity) 37 | if added { 38 | notify(groupEvent: .added, oldComponent: oldComponent, newComponent: newComponent, entity: entity) 39 | } else { 40 | notify(groupEvent: .updated, oldComponent: oldComponent, newComponent: newComponent, entity: entity) 41 | } 42 | } else { 43 | if entities.remove(entity) != nil { 44 | notify(groupEvent: .removed, oldComponent: oldComponent, newComponent: newComponent, entity: entity) 45 | } 46 | } 47 | } 48 | 49 | func checkOnRemove(oldComponent: Component?, entity: Entity) { 50 | if matcher.matches(entity) { 51 | let (added, _) = entities.insert(entity) 52 | if added { 53 | notify(groupEvent: .added, oldComponent: oldComponent, newComponent: nil, entity: entity) 54 | } else { 55 | notify(groupEvent: .updated, oldComponent: oldComponent, newComponent: nil, entity: entity) 56 | } 57 | } else { 58 | if entities.remove(entity) != nil { 59 | notify(groupEvent: .removed, oldComponent: oldComponent, newComponent: nil, entity: entity) 60 | } 61 | } 62 | } 63 | 64 | private func notify(groupEvent: GroupEvent, oldComponent: Component?, newComponent: Component?, entity: Entity) { 65 | 66 | sortedCache.removeAll(keepingCapacity: true) 67 | 68 | for observer in observers { 69 | guard let observer = observer.ref as? GroupObserver else { 70 | continue 71 | } 72 | switch groupEvent { 73 | case .added: 74 | observer.added(entity: entity, oldComponent: oldComponent, newComponent: newComponent, in: self) 75 | case .updated: 76 | observer.updated(entity: entity, oldComponent: oldComponent, newComponent: newComponent, in: self) 77 | case .removed: 78 | observer.removed(entity: entity, oldComponent: oldComponent, newComponent: newComponent, in: self) 79 | } 80 | } 81 | } 82 | 83 | public var count: Int { 84 | return entities.count 85 | } 86 | public var isEmpty: Bool { 87 | return entities.isEmpty 88 | } 89 | public var hashValue: Int { 90 | return matcher.hashValue 91 | } 92 | public static func ==(a: Group, b: Group) -> Bool { 93 | return a.matcher == b.matcher 94 | } 95 | } 96 | 97 | extension Group : Sequence { 98 | public func makeIterator() -> SetIterator { 99 | return entities.makeIterator() 100 | } 101 | 102 | public func sorted() -> [Entity] { 103 | let id = ObjectIdentifier(self) 104 | if let presorted = sortedCache[id] { 105 | return presorted 106 | } 107 | 108 | let sorted = entities.sorted() 109 | sortedCache[id] = sorted 110 | return sorted 111 | } 112 | 113 | public func sorted(forObject id: ObjectIdentifier, by sortingAlgo: (Entity, Entity) -> (Bool)) -> [Entity] { 114 | if let presorted = sortedCache[id] { 115 | return presorted 116 | } 117 | 118 | let sorted = entities.sorted(by: sortingAlgo) 119 | sortedCache[id] = sorted 120 | return sorted 121 | } 122 | } 123 | 124 | public enum GroupEvent { 125 | case added, updated, removed 126 | } 127 | 128 | public protocol GroupObserver : Observer { 129 | func added(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) 130 | func updated(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) 131 | func removed(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) 132 | } 133 | 134 | extension Sequence where Self.Iterator.Element == Entity { 135 | public func withEach(sorted: Bool = false, block: @escaping (Entity, C1) -> Void) { 136 | if sorted { 137 | for e in self.sorted() { 138 | e.with { (c1: C1) in 139 | block(e, c1) 140 | } 141 | } 142 | } else { 143 | for e in self { 144 | e.with { (c1: C1) in 145 | block(e, c1) 146 | } 147 | } 148 | } 149 | } 150 | public func withEach(sorted: Bool = false, block: @escaping (Entity, C1, C2) -> Void) { 151 | if sorted { 152 | for e in self.sorted() { 153 | e.with { (c1: C1, c2: C2) in 154 | block(e, c1, c2) 155 | } 156 | } 157 | } else { 158 | for e in self { 159 | e.with { (c1: C1, c2: C2) in 160 | block(e, c1, c2) 161 | } 162 | } 163 | } 164 | } 165 | public func withEach(sorted: Bool = false, block: @escaping (Entity, C1, C2, C3) -> Void) { 166 | if sorted { 167 | for e in self.sorted() { 168 | e.with { (c1: C1, c2: C2, c3: C3) in 169 | block(e, c1, c2, c3) 170 | } 171 | } 172 | } else { 173 | for e in self { 174 | e.with { (c1: C1, c2: C2, c3: C3) in 175 | block(e, c1, c2, c3) 176 | } 177 | } 178 | } 179 | } 180 | public func withEach(sorted: Bool = false, 181 | block: @escaping (Entity, C1, C2, C3, C4) -> Void) { 182 | if sorted { 183 | for e in self.sorted() { 184 | e.with { (c1: C1, c2: C2, c3: C3, c4: C4) in 185 | block(e, c1, c2, c3, c4) 186 | } 187 | } 188 | } else { 189 | for e in self.sorted() { 190 | e.with { (c1: C1, c2: C2, c3: C3, c4: C4) in 191 | block(e, c1, c2, c3, c4) 192 | } 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Sources/Index.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Index.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 18.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Index { 12 | 13 | fileprivate var entities: [T: Set] 14 | fileprivate weak var group: Group? 15 | fileprivate let keyBuilder: (C) -> T 16 | 17 | init(ctx: Context, paused: Bool = false, keyBuilder: @escaping (C) -> T) { 18 | self.group = ctx.group(C.matcher) 19 | self.entities = [:] 20 | self.keyBuilder = keyBuilder 21 | self.isPaused = paused 22 | if isPaused == false { 23 | refillIndex() 24 | } 25 | group?.observer(add: self) 26 | } 27 | 28 | public subscript(key: T) -> Set { 29 | return entities[key] ?? [] 30 | } 31 | 32 | public var isPaused : Bool { 33 | didSet { 34 | if isPaused { 35 | entities.removeAll() 36 | } else { 37 | refillIndex() 38 | } 39 | } 40 | } 41 | 42 | private func refillIndex() { 43 | if let group = group { 44 | for e in group { 45 | if let c: C = e.get() { 46 | insert(c, e) 47 | } 48 | } 49 | } 50 | } 51 | 52 | fileprivate func insert(_ c: C, _ entity: Entity) { 53 | let key = keyBuilder(c) 54 | var set: Set = entities[key] ?? [] 55 | set.insert(entity) 56 | entities[key] = set 57 | } 58 | 59 | fileprivate func remove(_ prevC: C, _ entity: Entity) { 60 | let prevKey = keyBuilder(prevC) 61 | var prevSet: Set = entities[prevKey] ?? [] 62 | prevSet.remove(entity) 63 | entities[prevKey] = prevSet 64 | } 65 | } 66 | 67 | extension Index: GroupObserver { 68 | public func added(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 69 | guard let c = newComponent as? C else { 70 | return 71 | } 72 | guard isPaused == false else { 73 | return 74 | } 75 | insert(c, entity) 76 | } 77 | public func updated(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 78 | guard let c = newComponent as? C, 79 | let prevC = oldComponent as? C else { 80 | return 81 | } 82 | guard isPaused == false else { 83 | return 84 | } 85 | 86 | remove(prevC, entity) 87 | insert(c, entity) 88 | } 89 | public func removed(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 90 | guard let prevC = oldComponent as? C else { 91 | return 92 | } 93 | guard isPaused == false else { 94 | return 95 | } 96 | 97 | remove(prevC, entity) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Maxim Zaks. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Loop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loop.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 19.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class Loop: InitSystem, ExecuteSystem, CleanupSystem, TeardownSystem { 12 | public let name: String 13 | private var initSystems: [InitSystem] = [] 14 | private var executeSystems: [ExecuteSystem] = [] 15 | private var cleanupSystems: [CleanupSystem] = [] 16 | private var teardownSystems: [TeardownSystem] = [] 17 | private weak var logger: SystemExecuteLogger? 18 | 19 | public init(name: String, systems: [System], logger: SystemExecuteLogger? = nil) { 20 | self.name = name 21 | self.logger = logger 22 | for system in systems { 23 | if let initSystem = system as? InitSystem { 24 | initSystems.append(initSystem) 25 | } 26 | if let execute = system as? ExecuteSystem { 27 | executeSystems.append(execute) 28 | } 29 | if let cleanup = system as? CleanupSystem { 30 | cleanupSystems.append(cleanup) 31 | } 32 | if let teardown = system as? TeardownSystem { 33 | teardownSystems.append(teardown) 34 | } 35 | } 36 | } 37 | 38 | public func initialise() { 39 | for system in initSystems { 40 | logger?.willInit(system.name) 41 | system.initialise() 42 | logger?.didInit(system.name) 43 | } 44 | } 45 | 46 | public func execute() { 47 | for system in executeSystems { 48 | logger?.willExecute(system.name) 49 | system.execute() 50 | logger?.didExecute(system.name) 51 | } 52 | } 53 | 54 | public func cleanup() { 55 | for system in cleanupSystems { 56 | logger?.willCleanup(system.name) 57 | system.cleanup() 58 | logger?.didCleanup(system.name) 59 | } 60 | } 61 | 62 | public func teardown() { 63 | for system in teardownSystems { 64 | logger?.willTeardown(system.name) 65 | system.teardown() 66 | logger?.didTeardown(system.name) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Matcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Matcher.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Matcher { 12 | let allOf: Set 13 | let anyOf: Set 14 | let noneOf: Set 15 | public func matches(_ e: Entity) -> Bool { 16 | for id in allOf { 17 | if e.has(id) == false { 18 | return false 19 | } 20 | } 21 | for id in noneOf { 22 | if e.has(id) == true { 23 | return false 24 | } 25 | } 26 | if anyOf.isEmpty { 27 | return true 28 | } 29 | for id in anyOf { 30 | if e.has(id) == true { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | 37 | public init(all: Set, any: Set, none: Set) { 38 | allOf = all 39 | anyOf = any 40 | noneOf = none 41 | assert(allOf.isEmpty == false || anyOf.isEmpty == false, "Your matcher does not have elements in allOf or in anyOf set") 42 | assert(isDisjoint, "Your matcher is not disjoint") 43 | } 44 | 45 | public init(all: Set) { 46 | self.init(all: all, any:[], none: []) 47 | } 48 | public init(all: Set, any: Set) { 49 | self.init(all: all, any:any, none: []) 50 | } 51 | public init(any: Set) { 52 | self.init(all: [], any:any, none: []) 53 | } 54 | public init(any: Set, none: Set) { 55 | self.init(all: [], any:any, none: none) 56 | } 57 | public init(all: Set, none: Set) { 58 | self.init(all: all, any:[], none: none) 59 | } 60 | 61 | private var isDisjoint : Bool { 62 | return allOf.isDisjoint(with: anyOf) && allOf.isDisjoint(with: noneOf) && anyOf.isDisjoint(with: noneOf) 63 | } 64 | } 65 | 66 | extension Matcher: Hashable { 67 | public var hashValue: Int { 68 | return allOf.hashValue ^ anyOf.hashValue ^ noneOf.hashValue 69 | } 70 | public static func ==(a: Matcher, b: Matcher) -> Bool { 71 | return a.allOf == b.allOf && a.anyOf == b.anyOf && a.noneOf == b.noneOf 72 | } 73 | } 74 | 75 | extension Component { 76 | public static var matcher: Matcher { 77 | return Matcher(all:[Self.cid]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Observer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Listener.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Observer: class {} 12 | 13 | struct ObserverBox: Hashable { 14 | private(set) weak var ref: Observer? 15 | 16 | init(_ ref: Observer) { 17 | self.ref = ref 18 | } 19 | public var hashValue: Int { 20 | guard let ref = ref else { 21 | return 0 22 | } 23 | return ObjectIdentifier(ref).hashValue 24 | } 25 | public static func ==(a: ObserverBox, b: ObserverBox) -> Bool { 26 | if a.ref == nil || b.ref == nil { 27 | // ⚠️ This is a trick to make the set replace empty observer boxes. 28 | // The trick works iff the ObeserverBox is inserted with `Set.update(with:)` method. 29 | // In case you use `Set.insert()` method. 30 | // This trick will prevent inserting new observers, if the set contains an empty box. 31 | // Which leads to unexpected behaviour. 🔴 32 | return true 33 | } 34 | return a.ref === b.ref 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ReactiveLoop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveLoop.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 19.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class ReactiveLoop: GroupObserver { 12 | private let systems: [ReactiveSystem] 13 | private let group: Group 14 | private let queue: DispatchQueue 15 | private weak var logger: SystemExecuteLogger? 16 | private let delay: DispatchTime? 17 | private var triggered = false 18 | public init(ctx: Context, logger: SystemExecuteLogger? = nil, queue: DispatchQueue = DispatchQueue.main, delay: Double? = nil, systems: [ReactiveSystem]) { 19 | self.systems = systems 20 | self.queue = queue 21 | self.logger = logger 22 | if let delay = delay { 23 | let time = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 24 | self.delay = time 25 | } else { 26 | self.delay = nil 27 | } 28 | var cids : Set = [] 29 | for system in systems { 30 | cids.formUnion(system.collector.matcher.allOf) 31 | cids.formUnion(system.collector.matcher.anyOf) 32 | cids.formUnion(system.collector.matcher.noneOf) 33 | } 34 | group = ctx.group(Matcher(any:cids)) 35 | group.observer(add: self) 36 | } 37 | 38 | private func execute() { 39 | triggered = false 40 | for system in systems { 41 | logger?.willExecute(system.name) 42 | system.execute() 43 | logger?.didExecute(system.name) 44 | } 45 | } 46 | 47 | private func trigger() { 48 | if triggered == false { 49 | triggered = true 50 | if let delay = delay { 51 | queue.asyncAfter(deadline: delay, execute: {[weak self] in 52 | self?.execute() 53 | }) 54 | } else { 55 | queue.async { [weak self] in 56 | self?.execute() 57 | } 58 | } 59 | } 60 | } 61 | 62 | public func added(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 63 | trigger() 64 | } 65 | public func updated(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 66 | trigger() 67 | } 68 | public func removed(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 69 | trigger() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/System.swift: -------------------------------------------------------------------------------- 1 | // 2 | // System.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 18.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SystemExecuteLogger: class { 12 | func willExecute(_ name: String) 13 | func didExecute(_ name: String) 14 | func willInit(_ name: String) 15 | func didInit(_ name: String) 16 | func willCleanup(_ name: String) 17 | func didCleanup(_ name: String) 18 | func willTeardown(_ name: String) 19 | func didTeardown(_ name: String) 20 | } 21 | 22 | public protocol System: class { 23 | var name: String {get} 24 | } 25 | 26 | public protocol ExecuteSystem: System { 27 | func execute() 28 | } 29 | 30 | public protocol ReactiveSystem: ExecuteSystem { 31 | var collector: Collector {get} 32 | func execute(entities: Set) 33 | } 34 | 35 | extension ReactiveSystem { 36 | public func execute() { 37 | if collector.isEmpty == false { 38 | self.execute(entities: collector.collected) 39 | } 40 | } 41 | } 42 | 43 | public protocol StrictReactiveSystem: ExecuteSystem { 44 | var collector: Collector {get} 45 | func execute(entities: [Entity]) 46 | } 47 | 48 | extension StrictReactiveSystem { 49 | func execute() { 50 | if collector.isEmpty == false { 51 | self.execute(entities: collector.collectedAndMatching) 52 | } 53 | } 54 | } 55 | 56 | 57 | public protocol InitSystem: System { 58 | func initialise() 59 | } 60 | 61 | public protocol CleanupSystem: System { 62 | func cleanup() 63 | } 64 | 65 | public protocol TeardownSystem: System { 66 | func teardown() 67 | } 68 | -------------------------------------------------------------------------------- /Tests/CollectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectorTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 18.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class CollectorTests: XCTestCase { 13 | 14 | func testCreateCollectorForAdded() { 15 | let ctx = Context() 16 | let g = ctx.group(Position.matcher) 17 | let collector = Collector(group: g, type: .added) 18 | 19 | XCTAssertNil(collector.first) 20 | 21 | ctx.createEntity().set(Position(x: 1, y: 4)) 22 | 23 | do { 24 | let e = collector.first 25 | XCTAssertEqual(e?.get(Position.self)?.x, 1) 26 | XCTAssertEqual(e?.get(Position.self)?.y, 4) 27 | 28 | e?.set(Position(x:2, y: 2)) 29 | XCTAssertNil(collector.first) 30 | } 31 | 32 | ctx.createEntity().set(Position(x: 2, y: 4)) 33 | ctx.createEntity().set(Position(x: 3, y: 4)) 34 | ctx.createEntity().set(Position(x: 4, y: 4)) 35 | 36 | XCTAssertEqual(collector.collected.count, 3) 37 | XCTAssertEqual(collector.collected.count, 0) 38 | } 39 | 40 | func testCreateCollectorForAddedOrUpdated() { 41 | let ctx = Context() 42 | let g = ctx.group(Position.matcher) 43 | let collector = Collector(group: g, type: .addedOrUpdated) 44 | 45 | XCTAssertNil(collector.first) 46 | 47 | ctx.createEntity().set(Position(x: 1, y: 4)) 48 | 49 | do { 50 | let e = collector.first 51 | XCTAssertEqual(e?.get(Position.self)?.x, 1) 52 | XCTAssertEqual(e?.get(Position.self)?.y, 4) 53 | 54 | e?.set(Position(x:2, y: 2)) 55 | let e1 = collector.first 56 | XCTAssertEqual(e1?.get(Position.self)?.x, 2) 57 | XCTAssertEqual(e1?.get(Position.self)?.y, 2) 58 | } 59 | } 60 | 61 | func testCreateCollectorForRemoved(){ 62 | let ctx = Context() 63 | let g = ctx.group(Position.matcher) 64 | let collector = Collector(group: g, type: .removed) 65 | 66 | XCTAssertNil(collector.first) 67 | 68 | let e = ctx.createEntity().set(Position(x: 1, y: 4)) 69 | 70 | XCTAssertNil(collector.first) 71 | 72 | e.set(Position(x:2, y: 2)) 73 | XCTAssertNil(collector.first) 74 | 75 | e.destroy() 76 | XCTAssert(e === collector.first) 77 | } 78 | 79 | func testDrainAndPause() { 80 | let ctx = Context() 81 | let g = ctx.group(Position.matcher) 82 | let collector = Collector(group: g, type: .addedUpdatedOrRemoved) 83 | 84 | XCTAssertNil(collector.first) 85 | 86 | let e = ctx.createEntity().set(Position(x: 1, y: 4)) 87 | 88 | XCTAssert(collector.first === e) 89 | 90 | e.set(Position(x: 2, y: 2)) 91 | 92 | XCTAssert(collector.drainAndPause().first === e) 93 | 94 | e.set(Position(x: 3, y: 3)) 95 | 96 | ctx.createEntity().set(Position(x: 5, y: 5)) 97 | 98 | e.destroy() 99 | 100 | XCTAssertNil(collector.first) 101 | } 102 | 103 | func testCollectedAndMatching() { 104 | let ctx = Context() 105 | let g = ctx.group(Position.matcher) 106 | let collector = Collector(group: g, type: .added) 107 | 108 | let e = ctx.createEntity().set(Position(x: 1, y: 4)) 109 | e.destroy() 110 | 111 | XCTAssertEqual(collector.collectedAndMatching.count, 0) 112 | } 113 | 114 | func testCollectorIsEmpty() { 115 | let ctx = Context() 116 | let g = ctx.group(Position.matcher) 117 | let collector = Collector(group: g, type: .added) 118 | 119 | XCTAssert(collector.isEmpty) 120 | 121 | ctx.createEntity().set(Position(x: 1, y: 4)) 122 | 123 | XCTAssert(collector.isEmpty == false) 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /Tests/ComponentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entitas_SwiftTests.swift 3 | // Entitas-SwiftTests 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class ComponentTests: XCTestCase { 13 | 14 | func testCreateComponentAndCheckCID() { 15 | let p = Position(x:12, y:14) 16 | XCTAssertEqual(Position.cid, p.cid) 17 | 18 | let g = God() 19 | XCTAssertEqual(God.cid, g.cid) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/ContextTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 05.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class ContextTests: XCTestCase { 13 | 14 | func testCreateEntityAndIncrementIndex() { 15 | let ctx = Context() 16 | let e = ctx.createEntity() 17 | XCTAssertEqual(e.creationIndex, 1) 18 | 19 | let e1 = ctx.createEntity() 20 | XCTAssertEqual(e1.creationIndex, 2) 21 | 22 | let e2 = ctx.createEntity() 23 | XCTAssertEqual(e2.creationIndex, 3) 24 | 25 | let e3 = ctx.createEntity() 26 | XCTAssertEqual(e3.creationIndex, 4) 27 | 28 | XCTAssertEqual(ctx.entities.count, 4) 29 | XCTAssertEqual([e, e1, e2, e3], ctx.entities.sorted(by: <)) 30 | } 31 | 32 | 33 | func testCreateEntityIncrementIndexWitEntityDestroy() { 34 | let ctx = Context() 35 | unowned let e = ctx.createEntity() 36 | XCTAssertEqual(e.creationIndex, 1) 37 | let e1 = ctx.createEntity() 38 | XCTAssertEqual(e1.creationIndex, 2) 39 | 40 | e.destroy() 41 | XCTAssertEqual(ctx.entities.count, 1) 42 | 43 | let e2 = ctx.createEntity() 44 | XCTAssertEqual(e2.creationIndex, 3) 45 | 46 | let e3 = ctx.createEntity() 47 | XCTAssertEqual(e3.creationIndex, 4) 48 | 49 | } 50 | 51 | func testCreateEntityIncrementIndexWitEntityDestroyAndWithoutReuse() { 52 | let ctx = Context() 53 | let e = ctx.createEntity() 54 | XCTAssertEqual(e.creationIndex, 1) 55 | let e1 = ctx.createEntity() 56 | XCTAssertEqual(e1.creationIndex, 2) 57 | e.destroy() 58 | XCTAssertEqual(ctx.entities.count, 1) 59 | 60 | let e2 = ctx.createEntity() 61 | XCTAssertEqual(e2.creationIndex, 3) 62 | let e3 = ctx.createEntity() 63 | XCTAssertEqual(e3.creationIndex, 4) 64 | 65 | XCTAssert(e !== e2) 66 | } 67 | 68 | func testGetGroup() { 69 | let ctx = Context() 70 | let e1 = ctx.createEntity().set(Position(x: 1, y: 2)) 71 | let e2 = ctx.createEntity().set(Position(x: 2, y: 3)) 72 | let e3 = ctx.createEntity().set(Name(value: "Max")) 73 | 74 | let group = ctx.group(Position.matcher) 75 | 76 | XCTAssertEqual(group.count, 2) 77 | 78 | for e in group { 79 | XCTAssert(e.has(Position.cid)) 80 | } 81 | XCTAssertEqual([e1, e2], group.sorted()) 82 | 83 | e3.set(Position(x: 12, y: 14)) 84 | XCTAssertEqual([e1, e2, e3], group.sorted()) 85 | 86 | e3.remove(Position.cid) 87 | XCTAssertEqual([e1, e2], group.sorted()) 88 | } 89 | 90 | func testGetGroupAll() { 91 | let ctx = Context() 92 | let e1 = ctx.createEntity().set(Position(x: 1, y: 2)) 93 | let e2 = ctx.createEntity().set(Position(x: 2, y: 3)) 94 | let e3 = ctx.createEntity().set(Name(value: "Max")).set(Position(x: 5, y: 6)) 95 | let e4 = ctx.createEntity().set(Name(value: "Max0")) 96 | 97 | let group1 = ctx.group(Matcher(all: [Position.cid, Name.cid])) 98 | XCTAssertEqual(group1.sorted(), [e3]) 99 | 100 | let group2 = ctx.group(Matcher(any: [Position.cid, Name.cid])) 101 | XCTAssertEqual(group2.sorted(), [e1, e2, e3, e4]) 102 | 103 | let group3 = ctx.group(Matcher(any: [Position.cid], none: [Name.cid])) 104 | XCTAssertEqual(group3.sorted(), [e1, e2]) 105 | 106 | let group4 = ctx.group(Matcher(all: [Name.cid], none: [Position.cid])) 107 | XCTAssertEqual(group4.sorted(), [e4]) 108 | 109 | let group5 = ctx.group(Matcher(all: [Position.cid], any:[Name.cid])) 110 | XCTAssertEqual(group5.sorted(), [e3]) 111 | } 112 | 113 | func testUniqueEntity() { 114 | let ctx = Context() 115 | ctx.createEntity().set(Position(x: 1, y: 2)) 116 | ctx.createEntity().set(Position(x: 2, y: 3)) 117 | let e3 = ctx.createEntity().set(Name(value: "Max")).set(God()) 118 | 119 | let e = ctx.uniqueEntity(Name.matcher) 120 | 121 | XCTAssertNotNil(e) 122 | XCTAssert(e === e3) 123 | } 124 | 125 | func testUniqueComponent() { 126 | let ctx = Context() 127 | ctx.createEntity().set(Position(x: 1, y: 2)) 128 | ctx.createEntity().set(Position(x: 2, y: 3)) 129 | let e1 = ctx.createEntity().set(Name(value: "Max1")).set(God()) 130 | let e2 = ctx.createEntity().set(Name(value: "Max2")).set(Person()) 131 | 132 | do { 133 | let c : God? = ctx.uniqueComponent() 134 | XCTAssertNotNil(c) 135 | 136 | let e = ctx.uniqueEntity(God.matcher) 137 | XCTAssert(e === e1) 138 | } 139 | 140 | e2.set(God()) 141 | 142 | do { 143 | let c = ctx.uniqueComponent(God.self) 144 | XCTAssertNotNil(c) 145 | 146 | let e = ctx.uniqueEntity(God.matcher) 147 | XCTAssert(e === e2) 148 | } 149 | } 150 | 151 | func testUniqueComponentCreation() { 152 | let ctx = Context() 153 | 154 | XCTAssert(ctx.hasUniqueComponent(God.self) == false) 155 | 156 | ctx.setUniqueComponent(God()) 157 | 158 | XCTAssert(ctx.hasUniqueComponent(God.self)) 159 | 160 | // Replace context 161 | ctx.setUniqueComponent(God()) 162 | 163 | XCTAssert(ctx.hasUniqueComponent(God.self)) 164 | 165 | let e = ctx.uniqueEntity(God.self) 166 | XCTAssertNotNil(e) 167 | 168 | let e2 = ctx.createEntity().set(God()) 169 | 170 | XCTAssert(e !== e2) 171 | XCTAssert(ctx.uniqueEntity(God.self) === e2) 172 | } 173 | 174 | func testCollector() { 175 | let ctx = Context() 176 | let collector = ctx.collector(for: Position.matcher) 177 | let e = ctx.createEntity() 178 | e += Position(x: 1, y: 2) 179 | 180 | XCTAssert(collector.first === e) 181 | } 182 | 183 | func testAllAnyNone() { 184 | let ctx = Context() 185 | let g1 = ctx.all([Position.cid]) 186 | let g2 = ctx.all([Position.cid, Name.cid]) 187 | let g3 = ctx.any([Position.cid, Name.cid]) 188 | let e = ctx.createEntity() 189 | e += Position(x: 1, y: 2) 190 | 191 | XCTAssert(g1.sorted().first === e) 192 | XCTAssertNil(g2.sorted().first) 193 | XCTAssert(g3.sorted().first === e) 194 | } 195 | 196 | class MyContextObserver: ContextObserver { 197 | var entities: [Entity] = [] 198 | func created(entity: Entity, in context: Context) { 199 | entities.append(entity) 200 | } 201 | var groups: [Group] = [] 202 | func created(group: Group, withMatcher matcher: Matcher, in context: Context) { 203 | groups.append(group) 204 | } 205 | var indexes: [AnyObject] = [] 206 | func created(index: Index, in context: Context) { 207 | indexes.append(index) 208 | } 209 | } 210 | 211 | func testContextObserver() { 212 | let ctx = Context() 213 | let o = MyContextObserver() 214 | ctx.observer(add: o) 215 | let g = ctx.all([Position.cid]) 216 | XCTAssert(o.groups[0] === g) 217 | 218 | let e = ctx.createEntity() 219 | XCTAssert(o.entities[0] === e) 220 | 221 | let index = ctx.index { (c: Size) -> Int in 222 | return c.value 223 | } 224 | e += Size(value: 1) 225 | XCTAssert(o.indexes[0] === index) 226 | 227 | o.groups.removeAll() 228 | XCTAssert(o.groups.count == 0) 229 | 230 | ctx.observer(remove: o) 231 | 232 | _ = ctx.all([Size.cid]) 233 | XCTAssert(o.groups.count == 0) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Tests/EntityObserverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityObserverTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import EntitasKit 13 | 14 | class EntityObserverTests: XCTestCase { 15 | 16 | class Observer: EntityObserver { 17 | var updatedData: [(Component?, Component, Entity)] = [] 18 | func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity){ 19 | updatedData.append((oldComponent, newComponent, entity)) 20 | } 21 | var removedData: [(Component, Entity)] = [] 22 | func removed(component: Component, from entity: Entity){ 23 | removedData.append((component, entity)) 24 | } 25 | var destroyedData: [Entity] = [] 26 | func destroyed(entity: Entity){ 27 | destroyedData.append(entity) 28 | } 29 | } 30 | 31 | func testAddComponent() { 32 | let observer = Observer() 33 | let e = Entity(index: 0, mainObserver: observer) 34 | e.set(Position(x: 1, y: 2)) 35 | e.set(Position(x: 3, y: 4)) 36 | 37 | XCTAssertEqual(observer.updatedData.count, 2) 38 | 39 | let first = observer.updatedData[0] 40 | XCTAssertNil(first.0) 41 | let pos1 = first.1 as! Position 42 | XCTAssertEqual(pos1.x, 1) 43 | XCTAssertEqual(pos1.y, 2) 44 | XCTAssert(first.2 === e) 45 | 46 | let second = observer.updatedData[1] 47 | let pos20 = second.0 as! Position 48 | XCTAssertEqual(pos20.x, 1) 49 | XCTAssertEqual(pos20.y, 2) 50 | let pos21 = second.1 as! Position 51 | XCTAssertEqual(pos21.x, 3) 52 | XCTAssertEqual(pos21.y, 4) 53 | XCTAssert(second.2 === e) 54 | } 55 | 56 | func testRemoveComponent() { 57 | let observer = Observer() 58 | let e = Entity(index: 0, mainObserver: observer) 59 | e.set(Position(x: 1, y: 2)) 60 | 61 | e.remove(Position.cid) 62 | e.remove(Position.cid) 63 | 64 | XCTAssertEqual(observer.removedData.count, 1) 65 | 66 | let p = observer.removedData[0].0 as! Position 67 | XCTAssertEqual(p.x, 1) 68 | XCTAssertEqual(p.y, 2) 69 | 70 | XCTAssert(observer.removedData[0].1 === e) 71 | } 72 | 73 | func testDestroyEntity() { 74 | let observer = Observer() 75 | let observer2 = Observer() 76 | let e = Entity(index: 0, mainObserver: observer) 77 | e.observer(add: observer2) 78 | e.set(Position(x: 1, y: 2)) 79 | 80 | e.destroy() 81 | 82 | XCTAssertEqual(observer.removedData.count, 1) 83 | XCTAssertEqual(observer2.removedData.count, 1) 84 | 85 | let p = observer.removedData[0].0 as! Position 86 | XCTAssertEqual(p.x, 1) 87 | XCTAssertEqual(p.y, 2) 88 | 89 | let p2 = observer2.removedData[0].0 as! Position 90 | XCTAssertEqual(p2.x, 1) 91 | XCTAssertEqual(p2.y, 2) 92 | 93 | XCTAssert(observer.removedData[0].1 === e) 94 | XCTAssert(observer2.removedData[0].1 === e) 95 | 96 | XCTAssertEqual(observer.destroyedData.count, 1) 97 | XCTAssertEqual(observer2.destroyedData.count, 1) 98 | XCTAssert(observer.destroyedData[0] === e) 99 | XCTAssert(observer2.destroyedData[0] === e) 100 | } 101 | 102 | func testAddComponentMultipleObservers() { 103 | let observer0 = Observer() 104 | let observer1 = Observer() 105 | let observer2 = Observer() 106 | let e = Entity(index: 0, mainObserver: observer0) 107 | e.observer(add: observer1) 108 | e.observer(add: observer2) 109 | e.set(Position(x: 1, y: 2)) 110 | 111 | for observer in [observer0, observer1, observer2] { 112 | XCTAssertEqual(observer.updatedData.count, 1) 113 | 114 | let first = observer.updatedData[0] 115 | XCTAssertNil(first.0) 116 | let pos1 = first.1 as! Position 117 | XCTAssertEqual(pos1.x, 1) 118 | XCTAssertEqual(pos1.y, 2) 119 | XCTAssert(first.2 === e) 120 | } 121 | 122 | } 123 | 124 | func testAddComponentMultipleObserversWithObserverRemoval() { 125 | let observer0 = Observer() 126 | let observer1 = Observer() 127 | let observer2 = Observer() 128 | let e = Entity(index: 0, mainObserver: observer0) 129 | e.observer(add: observer1) 130 | e.observer(add: observer2) 131 | e.set(Position(x: 1, y: 2)) 132 | 133 | e.observer(remove: observer1) 134 | 135 | e.set(Size(value:13)) 136 | 137 | for observer in [observer0, observer2] { 138 | XCTAssertEqual(observer.updatedData.count, 2) 139 | 140 | let first = observer.updatedData[0] 141 | XCTAssertNil(first.0) 142 | let pos1 = first.1 as! Position 143 | XCTAssertEqual(pos1.x, 1) 144 | XCTAssertEqual(pos1.y, 2) 145 | XCTAssert(first.2 === e) 146 | 147 | let second = observer.updatedData[1] 148 | XCTAssertNil(second.0) 149 | let size = second.1 as! Size 150 | XCTAssertEqual(size.value, 13) 151 | XCTAssert(second.2 === e) 152 | } 153 | 154 | XCTAssertEqual(observer1.updatedData.count, 1) 155 | 156 | let first = observer1.updatedData[0] 157 | XCTAssertNil(first.0) 158 | let pos1 = first.1 as! Position 159 | XCTAssertEqual(pos1.x, 1) 160 | XCTAssertEqual(pos1.y, 2) 161 | XCTAssert(first.2 === e) 162 | 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Tests/EntityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import EntitasKit 13 | 14 | class EntityTests: XCTestCase { 15 | 16 | let observerStub = EntityObserverStub() 17 | 18 | func testCreateEntityGetNilForPsotioncomponent() { 19 | let e = Entity(index: 0, mainObserver: observerStub) 20 | let p : Position? = e.get() 21 | 22 | XCTAssertNil(p) 23 | } 24 | 25 | func testCreateEntitySetAndGetPosition() { 26 | let e = Entity(index: 0, mainObserver: observerStub) 27 | e.set(Position(x: 1, y: 3)) 28 | let p : Position! = e.get() 29 | 30 | XCTAssertEqual(p.x, 1) 31 | XCTAssertEqual(p.y, 3) 32 | } 33 | 34 | func testUpdatePosition() { 35 | let e = Entity(index: 0, mainObserver: observerStub) 36 | e.set(Position(x: 1, y: 3)) 37 | e.set(Position(x: 5, y: 8)) 38 | let p : Position! = e.get() 39 | 40 | XCTAssertEqual(p.x, 5) 41 | XCTAssertEqual(p.y, 8) 42 | } 43 | 44 | func testRemovePosition() { 45 | let e = Entity(index: 0, mainObserver: observerStub) 46 | e.set(Position(x: 1, y: 3)) 47 | e.remove(Position.cid) 48 | let p : Position? = e.get() 49 | 50 | XCTAssertNil(p) 51 | } 52 | 53 | func testHasPosition() { 54 | let e = Entity(index: 0, mainObserver: observerStub) 55 | e.set(Position(x: 1, y: 3)) 56 | 57 | XCTAssertTrue(e.has(Position.cid)) 58 | } 59 | 60 | func testDestroyEntity() { 61 | let e = Entity(index: 0, mainObserver: observerStub) 62 | e.set(Position(x: 1, y: 3)) 63 | XCTAssertTrue(e.has(Position.cid)) 64 | e.destroy() 65 | XCTAssertFalse(e.has(Position.cid)) 66 | } 67 | 68 | func testGetPositionByType() { 69 | let e = Entity(index: 0, mainObserver: observerStub) 70 | e.set(Position(x: 1, y: 3)) 71 | 72 | let position = e.get(Position.self)! 73 | XCTAssertEqual(position.x, 1) 74 | XCTAssertEqual(position.y, 3) 75 | } 76 | 77 | func testExtensions(){ 78 | let e = Entity(index: 0, mainObserver: observerStub) 79 | e += Position(x: 1, y: 2) 80 | e += Size(value: 3) 81 | e += Name(value: "Max") 82 | e += Person() 83 | 84 | do { 85 | var flag = false 86 | e.with { (p: Position, s: Size, n: Name, person: Person) in 87 | flag = true 88 | XCTAssert(n.value == "Max") 89 | XCTAssert(s.value == 3) 90 | XCTAssert(p.x == 1 && p.y == 2) 91 | } 92 | XCTAssert(flag) 93 | } 94 | 95 | 96 | e -= Person.cid 97 | 98 | do { 99 | var flag = false 100 | e.with { (p: Position, s: Size, n: Name, person: Person) in 101 | flag = true 102 | } 103 | XCTAssert(flag == false) 104 | } 105 | 106 | do { 107 | var flag = false 108 | e.with { (p: Position, s: Size, n: Name) in 109 | flag = true 110 | XCTAssert(n.value == "Max") 111 | XCTAssert(s.value == 3) 112 | XCTAssert(p.x == 1 && p.y == 2) 113 | } 114 | XCTAssert(flag) 115 | } 116 | 117 | 118 | e -= Name.cid 119 | 120 | do { 121 | var flag = false 122 | e.with { (p: Position, s: Size, n: Name) in 123 | flag = true 124 | } 125 | XCTAssert(flag == false) 126 | } 127 | 128 | do { 129 | var flag = false 130 | e.with { (p: Position, s: Size) in 131 | flag = true 132 | XCTAssert(s.value == 3) 133 | XCTAssert(p.x == 1 && p.y == 2) 134 | } 135 | XCTAssert(flag) 136 | } 137 | 138 | e -= Position.cid 139 | 140 | do { 141 | var flag = false 142 | e.with { (p: Position, s: Size) in 143 | flag = true 144 | } 145 | XCTAssert(flag == false) 146 | } 147 | 148 | do { 149 | var flag = false 150 | e.with { (s: Size) in 151 | flag = true 152 | XCTAssert(s.value == 3) 153 | } 154 | XCTAssert(flag) 155 | } 156 | 157 | e -= Size.cid 158 | 159 | do { 160 | var flag = false 161 | e.with { (s: Size) in 162 | flag = true 163 | } 164 | XCTAssert(flag == false) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/GroupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 17.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import EntitasKit 13 | 14 | class GroupTests: XCTestCase { 15 | 16 | class Observer: GroupObserver { 17 | 18 | var addedData: [(entity: Entity, oldComponent: Component?, newComponent: Component?)] = [] 19 | func added(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 20 | addedData.append((entity: entity, oldComponent: oldComponent, newComponent:newComponent)) 21 | } 22 | var updatedData: [(entity: Entity, oldComponent: Component?, newComponent: Component?)] = [] 23 | func updated(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 24 | updatedData.append((entity: entity, oldComponent: oldComponent, newComponent:newComponent)) 25 | } 26 | var removedData: [(entity: Entity, oldComponent: Component?, newComponent: Component?)] = [] 27 | func removed(entity: Entity, oldComponent: Component?, newComponent: Component?, in group: Group) { 28 | removedData.append((entity: entity, oldComponent: oldComponent, newComponent:newComponent)) 29 | } 30 | } 31 | 32 | func testObservAllOfGroup() { 33 | let ctx = Context() 34 | let g = ctx.group(Matcher(all:[Position.cid, Size.cid])) 35 | let o = Observer() 36 | g.observer(add:o) 37 | 38 | let e = ctx.createEntity().set(Position(x: 1, y:2)).set(Size(value: 123)) 39 | 40 | XCTAssertEqual(o.addedData.count, 1) 41 | XCTAssertEqual(o.updatedData.count, 0) 42 | XCTAssertEqual(o.removedData.count, 0) 43 | 44 | XCTAssert(o.addedData[0].entity === e) 45 | XCTAssertNil(o.addedData[0].oldComponent) 46 | XCTAssertEqual((o.addedData[0].newComponent as? Size)?.value, 123) 47 | 48 | 49 | e.set(Position(x:3, y:1)) 50 | XCTAssertEqual(o.addedData.count, 1) 51 | XCTAssertEqual(o.updatedData.count, 1) 52 | XCTAssertEqual(o.removedData.count, 0) 53 | 54 | XCTAssert(o.addedData[0].entity === e) 55 | XCTAssertEqual((o.updatedData[0].oldComponent as? Position)?.x, 1) 56 | XCTAssertEqual((o.updatedData[0].oldComponent as? Position)?.y, 2) 57 | XCTAssertEqual((o.updatedData[0].newComponent as? Position)?.x, 3) 58 | XCTAssertEqual((o.updatedData[0].newComponent as? Position)?.y, 1) 59 | 60 | e.remove(Size.cid) 61 | XCTAssertEqual(o.addedData.count, 1) 62 | XCTAssertEqual(o.updatedData.count, 1) 63 | XCTAssertEqual(o.removedData.count, 1) 64 | 65 | XCTAssert(o.removedData[0].entity === e) 66 | XCTAssertNil(o.removedData[0].newComponent) 67 | XCTAssertEqual((o.removedData[0].oldComponent as? Size)?.value, 123) 68 | 69 | e.destroy() // no effect because is already out of the group 70 | XCTAssertEqual(o.addedData.count, 1) 71 | XCTAssertEqual(o.updatedData.count, 1) 72 | XCTAssertEqual(o.removedData.count, 1) 73 | } 74 | 75 | 76 | func testObservAnyOfGroup() { 77 | let ctx = Context() 78 | let g = ctx.group(Matcher(any:[Position.cid, Size.cid])) 79 | let o = Observer() 80 | g.observer(add:o) 81 | 82 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 83 | XCTAssertEqual(o.addedData.count, 1) 84 | XCTAssertEqual(o.updatedData.count, 0) 85 | XCTAssertEqual(o.removedData.count, 0) 86 | 87 | XCTAssert(o.addedData[0].entity === e) 88 | XCTAssertNil(o.addedData[0].oldComponent) 89 | XCTAssertEqual((o.addedData[0].newComponent as? Position)?.x, 1) 90 | XCTAssertEqual((o.addedData[0].newComponent as? Position)?.y, 2) 91 | 92 | e.set(Size(value:34)) 93 | XCTAssertEqual(o.addedData.count, 1) 94 | XCTAssertEqual(o.updatedData.count, 1) 95 | XCTAssertEqual(o.removedData.count, 0) 96 | 97 | XCTAssert(o.updatedData[0].entity === e) 98 | XCTAssertNil(o.updatedData[0].oldComponent) 99 | XCTAssertEqual((o.updatedData[0].newComponent as? Size)?.value, 34) 100 | 101 | e.remove(Size.cid) 102 | XCTAssertEqual(o.addedData.count, 1) 103 | XCTAssertEqual(o.updatedData.count, 2) 104 | XCTAssertEqual(o.removedData.count, 0) 105 | 106 | XCTAssert(o.updatedData[1].entity === e) 107 | XCTAssertNil(o.updatedData[1].newComponent) 108 | XCTAssertEqual((o.updatedData[1].oldComponent as? Size)?.value, 34) 109 | 110 | e.destroy() 111 | XCTAssertEqual(o.addedData.count, 1) 112 | XCTAssertEqual(o.updatedData.count, 2) 113 | XCTAssertEqual(o.removedData.count, 1) 114 | 115 | XCTAssert(o.removedData[0].entity === e) 116 | XCTAssertNil(o.removedData[0].newComponent) 117 | XCTAssertEqual((o.removedData[0].oldComponent as? Position)?.x, 1) 118 | XCTAssertEqual((o.removedData[0].oldComponent as? Position)?.y, 2) 119 | 120 | } 121 | 122 | func testObservAllOfNoneOfGroup(){ 123 | let ctx = Context() 124 | let g = ctx.group(Matcher(all:[Position.cid, Size.cid], none: [Name.cid])) 125 | let o = Observer() 126 | g.observer(add:o) 127 | 128 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 129 | 130 | XCTAssertEqual(o.addedData.count, 0) 131 | XCTAssertEqual(o.updatedData.count, 0) 132 | XCTAssertEqual(o.removedData.count, 0) 133 | 134 | e.set(Size(value: 12)) 135 | XCTAssertEqual(o.addedData.count, 1) 136 | XCTAssertEqual(o.updatedData.count, 0) 137 | XCTAssertEqual(o.removedData.count, 0) 138 | 139 | XCTAssert(o.addedData[0].entity === e) 140 | XCTAssertNil(o.addedData[0].oldComponent) 141 | XCTAssertEqual((o.addedData[0].newComponent as? Size)?.value, 12) 142 | 143 | e.set(Name(value: "Max")) 144 | XCTAssertEqual(o.addedData.count, 1) 145 | XCTAssertEqual(o.updatedData.count, 0) 146 | XCTAssertEqual(o.removedData.count, 1) 147 | 148 | XCTAssert(o.removedData[0].entity === e) 149 | XCTAssertNil(o.removedData[0].oldComponent) 150 | XCTAssertEqual((o.removedData[0].newComponent as? Name)?.value, "Max") 151 | 152 | e.remove(Name.cid) 153 | XCTAssertEqual(o.addedData.count, 2) 154 | XCTAssertEqual(o.updatedData.count, 0) 155 | XCTAssertEqual(o.removedData.count, 1) 156 | 157 | XCTAssert(o.addedData[1].entity === e) 158 | XCTAssertNil(o.addedData[1].newComponent) 159 | XCTAssertEqual((o.addedData[1].oldComponent as? Name)?.value, "Max") 160 | } 161 | 162 | func testRemoveObserving() { 163 | let ctx = Context() 164 | let g = ctx.group(Matcher(any:[Position.cid, Size.cid])) 165 | let o = Observer() 166 | g.observer(add:o) 167 | 168 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 169 | XCTAssertEqual(o.addedData.count, 1) 170 | XCTAssertEqual(o.updatedData.count, 0) 171 | XCTAssertEqual(o.removedData.count, 0) 172 | 173 | g.observer(remove:o) 174 | e.set(Size(value: 123)) 175 | XCTAssertEqual(o.addedData.count, 1) 176 | XCTAssertEqual(o.updatedData.count, 0) 177 | XCTAssertEqual(o.removedData.count, 0) 178 | } 179 | 180 | func testWeakObserverGetRemoved() { 181 | let ctx = Context() 182 | let g = ctx.group(Matcher(any:[Position.cid, Size.cid])) 183 | weak var o0 : Observer? 184 | do { 185 | let o = Observer() 186 | g.observer(add:o) 187 | o0 = o 188 | } 189 | let o1 = Observer() 190 | g.observer(add:o1) 191 | 192 | ctx.createEntity().set(Position(x: 1, y:2)) 193 | XCTAssertNil(o0) 194 | XCTAssertEqual(o1.addedData.count, 1) 195 | XCTAssertEqual(o1.updatedData.count, 0) 196 | XCTAssertEqual(o1.removedData.count, 0) 197 | } 198 | 199 | func testMultipleGroups() { 200 | let ctx = Context() 201 | let g1 = ctx.group(Matcher(any:[Position.cid, Size.cid])) 202 | let g2 = ctx.group(Matcher(all:[Position.cid, Size.cid])) 203 | 204 | let o1 = Observer() 205 | let o2 = Observer() 206 | 207 | g1.observer(add:o1) 208 | g2.observer(add:o2) 209 | 210 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 211 | 212 | XCTAssertEqual(o1.addedData.count, 1) 213 | XCTAssertEqual(o1.updatedData.count, 0) 214 | XCTAssertEqual(o1.removedData.count, 0) 215 | 216 | XCTAssertEqual(o2.addedData.count, 0) 217 | XCTAssertEqual(o2.updatedData.count, 0) 218 | XCTAssertEqual(o2.removedData.count, 0) 219 | 220 | e.set(Size(value: 43)) 221 | 222 | XCTAssertEqual(o1.addedData.count, 1) 223 | XCTAssertEqual(o1.updatedData.count, 1) 224 | XCTAssertEqual(o1.removedData.count, 0) 225 | 226 | XCTAssertEqual(o2.addedData.count, 1) 227 | XCTAssertEqual(o2.updatedData.count, 0) 228 | XCTAssertEqual(o2.removedData.count, 0) 229 | } 230 | 231 | func testSoretedListFromGroup() { 232 | let ctx = Context() 233 | let g = ctx.group(Position.matcher) 234 | 235 | var list = g.sorted() 236 | 237 | XCTAssertEqual(list, []) 238 | 239 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 240 | 241 | let e1 = ctx.createEntity().set(Position(x: 2, y:2)) 242 | 243 | list = g.sorted() 244 | 245 | XCTAssertEqual(list, [e, e1]) 246 | 247 | let list2 = g.sorted() 248 | 249 | XCTAssert(list == list2) 250 | } 251 | 252 | func testSoretedListForObjectFromGroup() { 253 | let ctx = Context() 254 | let g = ctx.group(Position.matcher) 255 | 256 | var list = g.sorted() 257 | 258 | XCTAssertEqual(list, []) 259 | 260 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 261 | 262 | let e1 = ctx.createEntity().set(Position(x: 2, y:2)) 263 | 264 | list = g.sorted(forObject: ObjectIdentifier(self)) { e1, e2 in 265 | return (e1.get(Position.self)?.x ?? 0) > (e2.get(Position.self)?.x ?? 0) 266 | } 267 | 268 | XCTAssertEqual(list, [e1, e]) 269 | 270 | let list2 = g.sorted(forObject: ObjectIdentifier(self)) { e1, e2 in 271 | return (e1.get(Position.self)?.x ?? 0) > (e2.get(Position.self)?.x ?? 0) 272 | } 273 | 274 | XCTAssert(list == list2) 275 | } 276 | 277 | func testGroupIsEmpty() { 278 | let ctx = Context() 279 | let g = ctx.group(Position.matcher) 280 | 281 | XCTAssertEqual(g.isEmpty, true) 282 | 283 | _ = ctx.createEntity().set(Position(x: 1, y:2)) 284 | 285 | XCTAssertEqual(g.isEmpty, false) 286 | } 287 | 288 | func testWithEach() { 289 | let ctx = Context() 290 | let g = ctx.group(Matcher(any: [Position.cid, Name.cid, Size.cid, Person.cid])) 291 | 292 | let e = ctx.createEntity().set(Position(x: 1, y:2)) 293 | e += Name(value: "Max") 294 | e += Size(value: 3) 295 | e += Person() 296 | 297 | var found = false 298 | g.withEach { (e, c: Position) in 299 | found = true 300 | } 301 | XCTAssert(found) 302 | 303 | found = false 304 | g.withEach { (e, c1: Position, c2: Person) in 305 | found = true 306 | } 307 | XCTAssert(found) 308 | 309 | found = false 310 | g.withEach { (e, c1: Position, c2: Person, c3: Size) in 311 | found = true 312 | } 313 | XCTAssert(found) 314 | 315 | found = false 316 | g.withEach { (e, c1: Position, c2: Person, c3: Size, c4: Name) in 317 | found = true 318 | } 319 | XCTAssert(found) 320 | 321 | e -= Person.cid 322 | 323 | found = false 324 | g.withEach { (e, c1: Position, c2: Person, c3: Size, c4: Name) in 325 | found = true 326 | } 327 | XCTAssert(found == false) 328 | } 329 | 330 | func testLoopOverGroupWhenOutOfOrderChangeOccurs(){ 331 | let ctx = Context() 332 | let g = ctx.all([Position.cid, Name.cid]) 333 | 334 | let e1 = ctx.createEntity() 335 | e1 += Position(x: 1, y: 2) 336 | e1 += Name(value: "1") 337 | 338 | let e2 = ctx.createEntity() 339 | e2 += Position(x: 2, y: 2) 340 | e2 += Name(value: "2") 341 | 342 | let e3 = ctx.createEntity() 343 | e3 += Position(x: 3, y: 2) 344 | e3 += Name(value: "3") 345 | 346 | for e in g { 347 | e3 -= Name.cid 348 | if e == e3 { 349 | XCTAssertFalse(e.has(Name.cid)) 350 | } else { 351 | XCTAssert(e.has(Name.cid)) 352 | } 353 | } 354 | } 355 | 356 | func testLoopWithComponentOverGroupWhenOutOfOrderChangeOccurs(){ 357 | let ctx = Context() 358 | let g = ctx.all([Position.cid, Name.cid]) 359 | 360 | let e1 = ctx.createEntity() 361 | e1 += Position(x: 1, y: 2) 362 | e1 += Name(value: "1") 363 | 364 | let e2 = ctx.createEntity() 365 | e2 += Position(x: 2, y: 2) 366 | e2 += Name(value: "2") 367 | 368 | let e3 = ctx.createEntity() 369 | e3 += Position(x: 3, y: 2) 370 | e3 += Name(value: "3") 371 | 372 | var count = 0 373 | g.withEach(sorted: true) { (e, c: Name) in 374 | e3 -= Name.cid 375 | XCTAssertEqual(e.has(Name.cid), true) 376 | count += 1 377 | } 378 | 379 | XCTAssertEqual(2, count) 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /Tests/IndexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 18.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class IndexTests: XCTestCase { 13 | 14 | func testIndex() { 15 | let ctx = Context() 16 | 17 | let e1 = ctx.createEntity().set(Size(value: 1)) 18 | let e2 = ctx.createEntity().set(Size(value: 2)) 19 | let e3 = ctx.createEntity().set(Size(value: 3)) 20 | let e11 = ctx.createEntity().set(Size(value: 1)) 21 | 22 | let index = ctx.index { (s: Size) -> Int in 23 | return s.value 24 | } 25 | 26 | XCTAssertEqual(index[2].first, e2) 27 | XCTAssertEqual(index[3].first, e3) 28 | XCTAssertEqual(index[1].map{$0}.sorted(), [e1, e11]) 29 | 30 | e1.set(Size(value: 4)) 31 | 32 | XCTAssertEqual(index[2].first, e2) 33 | XCTAssertEqual(index[3].first, e3) 34 | XCTAssertEqual(index[4].first, e1) 35 | XCTAssertEqual(index[1].map{$0}.sorted(), [e11]) 36 | 37 | e1.remove(Size.cid) 38 | XCTAssertEqual(index[2].first, e2) 39 | XCTAssertEqual(index[3].first, e3) 40 | XCTAssertEqual(index[4].first, nil) 41 | XCTAssertEqual(index[1].map{$0}.sorted(), [e11]) 42 | 43 | e1.set(Size(value: 1)) 44 | XCTAssertEqual(index[2].first, e2) 45 | XCTAssertEqual(index[3].first, e3) 46 | XCTAssertEqual(index[4].first, nil) 47 | XCTAssertEqual(index[1].map{$0}.sorted(), [e1, e11]) 48 | } 49 | 50 | func testIndexPaused() { 51 | let ctx = Context() 52 | 53 | let e1 = ctx.createEntity().set(Size(value: 1)) 54 | let e2 = ctx.createEntity().set(Size(value: 2)) 55 | let e3 = ctx.createEntity().set(Size(value: 3)) 56 | let e11 = ctx.createEntity().set(Size(value: 1)) 57 | 58 | let index = ctx.index(paused: true) { (s: Size) -> Int in 59 | return s.value 60 | } 61 | 62 | XCTAssertEqual(index[2].first, nil) 63 | XCTAssertEqual(index[3].first, nil) 64 | XCTAssertEqual(index[1].map{$0}.sorted(), []) 65 | 66 | index.isPaused = false 67 | 68 | XCTAssertEqual(index[2].first, e2) 69 | XCTAssertEqual(index[3].first, e3) 70 | XCTAssertEqual(index[1].map{$0}.sorted(), [e1, e11]) 71 | 72 | index.isPaused = true 73 | 74 | XCTAssertEqual(index[2].first, nil) 75 | XCTAssertEqual(index[3].first, nil) 76 | XCTAssertEqual(index[1].map{$0}.sorted(), []) 77 | 78 | e1.set(Size(value: 4)) 79 | e1.remove(Size.cid) 80 | 81 | index.isPaused = false 82 | 83 | XCTAssertEqual(index[2].first, e2) 84 | XCTAssertEqual(index[3].first, e3) 85 | XCTAssertEqual(index[1].map{$0}.sorted(), [e11]) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/LoopTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoopTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 19.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class LoopTests: XCTestCase { 13 | 14 | final class R1: ReactiveSystem, InitSystem, TeardownSystem { 15 | let name = "R1" 16 | let ctx: Context 17 | let collector: Collector 18 | init(ctx: Context) { 19 | self.ctx = ctx 20 | self.collector = Collector(group: self.ctx.group(Position.matcher), type: .addedOrUpdated) 21 | } 22 | 23 | func initialise() { 24 | ctx.createEntity().set(Position(x: 0, y: 0)) 25 | } 26 | 27 | func execute(entities: Set) { 28 | for e in entities { 29 | let pos: Position = e.get()! 30 | e.set(Position(x: pos.x + 1, y: pos.y + 1)) 31 | } 32 | } 33 | func teardown() { 34 | for e in ctx.group(Position.matcher) { 35 | e.destroy() 36 | } 37 | } 38 | } 39 | 40 | final class S1: ExecuteSystem, CleanupSystem, TeardownSystem { 41 | let name = "S1" 42 | let ctx: Context 43 | 44 | init(ctx: Context) { 45 | self.ctx = ctx 46 | } 47 | 48 | func execute() { 49 | ctx.createEntity().set(Name(value: "Max")) 50 | } 51 | func cleanup() { 52 | for e in ctx.group(Name.matcher) { 53 | e.destroy() 54 | } 55 | } 56 | func teardown() { 57 | 58 | } 59 | } 60 | 61 | func testLoop() { 62 | let ctx = Context() 63 | let logger = Logger() 64 | let loop = Loop(name: "Main Loop", systems: [ 65 | S1(ctx: ctx), 66 | R1(ctx: ctx) 67 | ], logger: logger) 68 | 69 | 70 | loop.initialise() 71 | loop.execute() 72 | loop.cleanup() 73 | loop.execute() 74 | loop.cleanup() 75 | 76 | let g1 = ctx.group(Position.matcher) 77 | XCTAssertEqual(g1.count, 1) 78 | XCTAssertEqual(g1.first(where: {_ in true})?.get(Position.self)?.x, 2) 79 | XCTAssertEqual(g1.first(where: {_ in true})?.get(Position.self)?.y, 2) 80 | 81 | let g2 = ctx.group(Name.matcher) 82 | XCTAssertEqual(g2.count, 0) 83 | 84 | loop.teardown() 85 | 86 | XCTAssertEqual(g1.count, 0) 87 | 88 | XCTAssertEqual(logger.log, [ 89 | "willInitR1", 90 | "didInitR1", 91 | "willExecuteS1", 92 | "didExecuteS1", 93 | "willExecuteR1", 94 | "didExecuteR1", 95 | "willCleanupS1", 96 | "didCleanupS1", 97 | "willExecuteS1", 98 | "didExecuteS1", 99 | "willExecuteR1", 100 | "didExecuteR1", 101 | "willCleanupS1", 102 | "didCleanupS1", 103 | "willTeardownS1", 104 | "didTeardownS1", 105 | "willTeardownR1", 106 | "didTeardownR1", 107 | ]) 108 | } 109 | 110 | final class SR1: StrictReactiveSystem { 111 | let name = "SR1" 112 | let ctx: Context 113 | let collector: Collector 114 | init(ctx: Context) { 115 | self.ctx = ctx 116 | self.collector = Collector(group: self.ctx.group(Name.matcher), type: .addedOrUpdated) 117 | } 118 | 119 | var entities: [Entity] = [] 120 | 121 | func execute(entities: [Entity]) { 122 | for e in entities { 123 | self.entities.append(e) 124 | } 125 | } 126 | 127 | } 128 | 129 | func testStricktReactiveSystem() { 130 | let ctx = Context() 131 | let system = SR1(ctx: ctx) 132 | system.execute() 133 | XCTAssertEqual(system.entities.count, 0) 134 | 135 | let e = ctx.createEntity().set(Name(value: "Maxim")) 136 | 137 | system.execute() 138 | XCTAssertEqual(system.entities.count, 1) 139 | 140 | e.set(Name(value: "Max")) 141 | 142 | system.execute() 143 | XCTAssertEqual(system.entities.count, 2) 144 | 145 | e.set(Name(value: "Maxi")) 146 | e.destroy() 147 | 148 | system.execute() 149 | XCTAssertEqual(system.entities.count, 2) 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /Tests/MatcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatcherTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import EntitasKit 13 | 14 | class MatcherTests: XCTestCase { 15 | 16 | let observer = EntityObserverStub() 17 | 18 | func testPositionMatcher() { 19 | let e = Entity(index: 0, mainObserver: observer) 20 | e.set(Position(x: 1, y: 1)) 21 | XCTAssertTrue(Position.matcher.matches(e)) 22 | } 23 | 24 | func testPositionAndSizeMatcher() { 25 | let e = Entity(index: 0, mainObserver: observer) 26 | let m = Matcher(all: [Position.cid, Size.cid]) 27 | e.set(Position(x: 1, y: 1)) 28 | XCTAssertFalse(m.matches(e)) 29 | e.set(Size(value: 2)) 30 | XCTAssertTrue(m.matches(e)) 31 | } 32 | 33 | func testPositionOrSizeMatcher() { 34 | let e = Entity(index: 0, mainObserver: observer) 35 | let m = Matcher(any: [Position.cid, Size.cid]) 36 | e.set(Position(x: 1, y: 1)) 37 | XCTAssertTrue(m.matches(e)) 38 | e.set(Size(value: 2)) 39 | XCTAssertTrue(m.matches(e)) 40 | e.remove(Position.cid) 41 | XCTAssertTrue(m.matches(e)) 42 | e.remove(Size.cid) 43 | XCTAssertFalse(m.matches(e)) 44 | } 45 | 46 | func testPositionOrSizeButNotNameMatcher() { 47 | let e = Entity(index: 0, mainObserver: observer) 48 | let m = Matcher(any: [Position.cid, Size.cid], none:[Name.cid]) 49 | e.set(Position(x: 1, y: 1)) 50 | XCTAssertTrue(m.matches(e)) 51 | e.set(Size(value: 2)) 52 | XCTAssertTrue(m.matches(e)) 53 | e.set(Name(value: "Max")) 54 | XCTAssertFalse(m.matches(e)) 55 | } 56 | 57 | func testPositionAndSizeButNotNameMatcher() { 58 | let e = Entity(index: 0, mainObserver: observer) 59 | let m = Matcher(all: [Position.cid, Size.cid], none:[Name.cid]) 60 | e.set(Position(x: 1, y: 1)) 61 | XCTAssertFalse(m.matches(e)) 62 | e.set(Size(value: 2)) 63 | XCTAssertTrue(m.matches(e)) 64 | e.set(Name(value: "Max")) 65 | XCTAssertFalse(m.matches(e)) 66 | } 67 | 68 | func testPositionAndSizeNameOrPersonButNotGodMatcher() { 69 | let e = Entity(index: 0, mainObserver: observer) 70 | let m = Matcher(all: [Position.cid, Size.cid], any: [Name.cid, Person.cid], none:[God.cid]) 71 | e.set(Position(x: 1, y: 1)) 72 | XCTAssertFalse(m.matches(e)) 73 | e.set(Size(value: 2)) 74 | XCTAssertFalse(m.matches(e)) 75 | e.set(Name(value: "Max")) 76 | XCTAssertTrue(m.matches(e)) 77 | e.set(Person()) 78 | XCTAssertTrue(m.matches(e)) 79 | e.remove(Name.cid) 80 | XCTAssertTrue(m.matches(e)) 81 | e.set(God()) 82 | XCTAssertFalse(m.matches(e)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/ObserverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObserverTests.swift 3 | // EntitasKit 4 | // 5 | // Created by Maxim Zaks on 03.07.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class ObserverTests: XCTestCase { 13 | 14 | class ObserverMock: EntitasKit.Observer {} 15 | 16 | var box1: ObserverBox! 17 | var box2: ObserverBox! 18 | 19 | func testObserverEqualityFunction() { 20 | let observer = ObserverMock() 21 | do { 22 | box1 = ObserverBox(observer) 23 | box2 = ObserverBox(ObserverMock()) 24 | } 25 | XCTAssert(box1 == box2) 26 | 27 | do { 28 | box1 = ObserverBox(ObserverMock()) 29 | box2 = ObserverBox(observer) 30 | } 31 | XCTAssert(box1 == box2) 32 | 33 | do { 34 | box1 = ObserverBox(observer) 35 | box2 = ObserverBox(observer) 36 | } 37 | XCTAssert(box1 == box2) 38 | 39 | let observer2 = ObserverMock() 40 | do { 41 | box1 = ObserverBox(observer2) 42 | box2 = ObserverBox(observer) 43 | } 44 | XCTAssert(box1 != box2) 45 | } 46 | 47 | func testHashFunction() { 48 | let observer = ObserverMock() 49 | var observer2: ObserverMock? = ObserverMock() 50 | do { 51 | box1 = ObserverBox(observer) 52 | box2 = ObserverBox(observer2!) 53 | observer2 = nil 54 | } 55 | XCTAssert(box1.hashValue != 0) 56 | // FIXME: does not work any more, could be an issue caused by new Swift version 57 | // XCTAssert(box2.hashValue == 0) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Tests/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformanceTests.swift 3 | // EntitasKit 4 | // 5 | // Created by Maxim Zaks on 25.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import EntitasKit 11 | 12 | class PerformanceTests: XCTestCase { 13 | 14 | var ctx : Context! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | ctx = Context() 19 | } 20 | 21 | func testCreatingEntities() { 22 | measure { [unowned self] in 23 | self.createTenThausendEntities() 24 | } 25 | } 26 | 27 | func testCreatingEntitiesAndDestroy() { 28 | measure { [unowned self] in 29 | self.createTenThausendEntities() 30 | for e in self.ctx.entities { 31 | e.destroy() 32 | } 33 | } 34 | } 35 | 36 | func testReCreatingEntities() { 37 | measure { [unowned self] in 38 | self.createTenThausendEntities() 39 | for e in self.ctx.entities { 40 | e.destroy() 41 | } 42 | self.createTenThausendEntities() 43 | } 44 | } 45 | 46 | func testCreateMoveDestroy() { 47 | measure { [unowned self] in 48 | let group = self.ctx.group(Position.matcher) 49 | self.createTenThausendEntitiesWithPosition() 50 | for _ in 1...5 { 51 | for e in group { 52 | let pos: Position = e.get()! 53 | e.set(Position(x: pos.x + 3, y: pos.y - 1)) 54 | } 55 | } 56 | for e in self.ctx.entities { 57 | e.destroy() 58 | } 59 | } 60 | } 61 | 62 | func createTenThausendEntities() { 63 | for _ in 1...10_000 { 64 | _ = ctx.createEntity() 65 | } 66 | } 67 | 68 | func createTenThausendEntitiesWithPosition() { 69 | for i in 1...10_000 { 70 | _ = ctx.createEntity().set(Position(x: i+1, y: i+2)) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/ReactiveLoopTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveLoopTests.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 20.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import EntitasKit 11 | 12 | class ReactiveLoopTests: XCTestCase { 13 | 14 | final class R1: ReactiveSystem { 15 | let name = "R1" 16 | let ctx: Context 17 | let collector: Collector 18 | let block: ()->() 19 | init(ctx: Context, block: @escaping ()->()) { 20 | self.ctx = ctx 21 | self.block = block 22 | self.collector = Collector(group: self.ctx.group(Matcher(all:[Position.cid, Size.cid])), type: .addedOrUpdated) 23 | } 24 | 25 | func execute(entities: Set) { 26 | for _ in entities { 27 | block() 28 | } 29 | } 30 | 31 | } 32 | 33 | final class R2: ReactiveSystem { 34 | let name = "R2" 35 | let ctx: Context 36 | let collector: Collector 37 | let block: ()->() 38 | init(ctx: Context, block: @escaping ()->()) { 39 | self.ctx = ctx 40 | self.block = block 41 | self.collector = Collector(group: self.ctx.group(Name.matcher), type: .addedOrUpdated) 42 | } 43 | 44 | func execute(entities: Set) { 45 | for _ in entities { 46 | block() 47 | } 48 | } 49 | 50 | } 51 | 52 | func testReactiveLoop() { 53 | let ctx = Context() 54 | let expect = expectation(description: "system was executed") 55 | var counter = 0 56 | let logger = Logger() 57 | let loop = ReactiveLoop(ctx:ctx, logger: logger, systems:[ 58 | R1(ctx:ctx){ 59 | counter += 1 60 | expect.fulfill() 61 | }]) 62 | 63 | let e = ctx.createEntity().set(Position(x: 1, y: 2)).set(Size(value: 1)) 64 | 65 | e.destroy() 66 | 67 | waitForExpectations(timeout: 1.0) { (_) in 68 | XCTAssertEqual(counter, 1) 69 | XCTAssertEqual(logger.log, [ 70 | "willExecuteR1", 71 | "didExecuteR1" 72 | ]) 73 | } 74 | XCTAssertNotNil(loop) 75 | } 76 | 77 | func testReactiveWithReTriggeringLoop() { 78 | let ctx = Context() 79 | let expect = expectation(description: "system was executed") 80 | var counter = 0 81 | let logger = Logger() 82 | let r1 = R1(ctx:ctx){ 83 | counter += 1 84 | ctx.createEntity().set(Name(value: "Maxim")) 85 | } 86 | let r2 = R2(ctx:ctx){ 87 | counter += 1 88 | expect.fulfill() 89 | } 90 | let loop = ReactiveLoop(ctx:ctx, logger: logger, systems:[r2, r1]) 91 | 92 | 93 | let e = ctx.createEntity().set(Position(x: 1, y: 2)).set(Size(value: 1)) 94 | 95 | e.destroy() 96 | 97 | waitForExpectations(timeout: 1.0) { (_) in 98 | XCTAssertEqual(counter, 2) 99 | XCTAssertEqual(logger.log, [ 100 | "willExecuteR2", 101 | "didExecuteR2", 102 | "willExecuteR1", 103 | "didExecuteR1", 104 | "willExecuteR2", 105 | "didExecuteR2", 106 | "willExecuteR1", 107 | "didExecuteR1" 108 | ]) 109 | } 110 | XCTAssertNotNil(loop) 111 | } 112 | 113 | func testReactiveLoopWithDelay() { 114 | let ctx = Context() 115 | let expect = expectation(description: "system was executed") 116 | var counter = 0 117 | let logger = Logger() 118 | let time = CFAbsoluteTimeGetCurrent() 119 | let loop = ReactiveLoop(ctx:ctx, logger: logger, delay: 0.1, systems:[ 120 | R1(ctx:ctx){ 121 | counter += 1 122 | expect.fulfill() 123 | }]) 124 | 125 | let e = ctx.createEntity().set(Position(x: 1, y: 2)).set(Size(value: 1)) 126 | 127 | e.destroy() 128 | 129 | waitForExpectations(timeout: 1.0) { (_) in 130 | XCTAssertEqual(counter, 1) 131 | XCTAssertEqual(logger.log, [ 132 | "willExecuteR1", 133 | "didExecuteR1" 134 | ]) 135 | XCTAssertGreaterThanOrEqual(CFAbsoluteTimeGetCurrent() - time, 0.1) 136 | } 137 | XCTAssertNotNil(loop) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /Tests/Stubs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stubs.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import EntitasKit 11 | 12 | class EntityObserverStub: EntityObserver{ 13 | func updated(component oldComponent: Component?, with newComponent: Component, in entity: Entity){} 14 | func removed(component: Component, from entity: Entity){} 15 | func destroyed(entity: Entity){} 16 | } 17 | 18 | final class Logger: SystemExecuteLogger { 19 | var log: [String] = [] 20 | func didCleanup(_ name: String) { 21 | log.append("didCleanup\(name)") 22 | } 23 | func didExecute(_ name: String) { 24 | log.append("didExecute\(name)") 25 | } 26 | func didInit(_ name: String) { 27 | log.append("didInit\(name)") 28 | } 29 | func didTeardown(_ name: String) { 30 | log.append("didTeardown\(name)") 31 | } 32 | func willCleanup(_ name: String) { 33 | log.append("willCleanup\(name)") 34 | } 35 | func willExecute(_ name: String) { 36 | log.append("willExecute\(name)") 37 | } 38 | func willInit(_ name: String) { 39 | log.append("willInit\(name)") 40 | } 41 | func willTeardown(_ name: String) { 42 | log.append("willTeardown\(name)") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/TestComponents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestComponents.swift 3 | // Entitas-Swift 4 | // 5 | // Created by Maxim Zaks on 04.06.17. 6 | // Copyright © 2017 Maxim Zaks. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import EntitasKit 11 | 12 | struct Position: Component { 13 | let x: Int 14 | let y: Int 15 | } 16 | 17 | struct Size: Component { 18 | let value: Int 19 | } 20 | 21 | struct Name: Component { 22 | let value: String 23 | } 24 | 25 | struct Person: Component {} 26 | 27 | struct God: UniqueComponent {} 28 | 29 | extension Position: ComponentInfo { 30 | var info: String { 31 | return "x:\(x), y:\(y)" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/screenshot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzaks/EntitasKit/8df660513cc28a6be58b5d0963cb36f3b48fffed/docs/screenshot001.png --------------------------------------------------------------------------------