diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 74d7240..f65d157 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -6,12 +6,6 @@ import kotlin.math.max import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass -/** - * Type alias for an optional hook function for a [ComponentsHolder]. - * Such a function runs within a [World] and takes the [Entity] and [component][Component] as an argument. - */ -typealias ComponentHook = World.(Entity, T) -> Unit - /** * A class that assigns a unique [id] per type of [Component] starting from 0. * This [id] is used internally by Fleks as an index for some arrays. @@ -53,6 +47,16 @@ interface Component { * Returns the [ComponentType] of a [Component]. */ fun type(): ComponentType + + /** + * Lifecycle method that gets called whenever a [component][Component] gets set for an [entity][Entity]. + */ + fun World.onAddComponent() = Unit + + /** + * Lifecycle method that gets called whenever a [component][Component] gets removed from an [entity][Entity]. + */ + fun World.onRemoveComponent() = Unit } /** @@ -67,18 +71,6 @@ class ComponentsHolder>( private val name: String, private var components: Array, ) { - /** - * An optional [ComponentHook] that gets called whenever a [component][Component] gets set for an [entity][Entity]. - */ - @PublishedApi - internal var addHook: ComponentHook? = null - - /** - * An optional [ComponentHook] that gets called whenever a [component][Component] gets removed from an [entity][Entity]. - */ - @PublishedApi - internal var removeHook: ComponentHook? = null - /** * Sets the [component] for the given [entity]. This function is only * used by [World.loadSnapshot] where we don't have the correct type information @@ -90,8 +82,10 @@ class ComponentsHolder>( /** * Sets the [component] for the given [entity]. - * If a [removeHook] is defined then it gets called if the [entity] already had a component. - * If an [addHook] is defined then it gets called after the [component] is assigned to the [entity]. + * If the [entity] already had a component, the [onRemoveComponent][Component.onRemoveComponent] lifecycle method + * will be called. + * After the [component] is assigned to the [entity], the [onAddComponent][Component.onAddComponent] lifecycle method + * will be called. */ operator fun set(entity: Entity, component: T) { if (entity.id >= components.size) { @@ -99,22 +93,27 @@ class ComponentsHolder>( components = components.copyOf(max(components.size * 2, entity.id + 1)) } - // check if removeHook needs to be called + // check if the remove lifecycle method of the previous component needs to be called components[entity.id]?.let { existingCmp -> - // assign current component to null in order for 'contains' calls inside the hook - // to correctly return false + // assign current component to null in order for 'contains' calls inside the lifecycle + // method to correctly return false components[entity.id] = null - removeHook?.invoke(world, entity, existingCmp) + existingCmp.run { + world.onRemoveComponent() + } } - // set component and call addHook if necessary + // set component and call lifecycle method components[entity.id] = component - addHook?.invoke(world, entity, component) + component.run { + world.onAddComponent() + } } /** * Removes a component of the specific type from the given [entity]. - * If a [removeHook] is defined then it gets called if the [entity] has such a component. + * If the entity has such a component, its [onRemoveComponent][Component.onRemoveComponent] lifecycle method will + * be called. * * @throws [IndexOutOfBoundsException] if the id of the [entity] exceeds the components' capacity. */ @@ -122,10 +121,10 @@ class ComponentsHolder>( if (entity.id < 0 || entity.id >= components.size) throw IndexOutOfBoundsException("$entity.id is not valid for components of size ${components.size}") val existingCmp = components[entity.id] - // assign null before running the removeHook in order for 'contains' calls to correctly return false + // assign null before running the lifecycle method in order for 'contains' calls to correctly return false components[entity.id] = null - if (existingCmp != null) { - removeHook?.invoke(world, entity, existingCmp) + existingCmp?.run { + world.onRemoveComponent() } } diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 8de2ec7..770d884 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -18,7 +18,7 @@ value class Entity(val id: Int) typealias EntityHook = World.(Entity) -> Unit /** - * A class for basic [Entity] extension functions within an add/remove hook of a [Component], [Family], + * A class for basic [Entity] extension functions within a [Family], * [IntervalSystem], [World] or [compareEntity]. */ abstract class EntityComponentContext( @@ -92,12 +92,12 @@ open class EntityCreateContext( /** * Adds the [component] to the [entity][Entity]. * - * If a component [addHook][ComponentsHolder.addHook] is defined then it + * The [onAddComponent][Component.onAddComponent] lifecycle method * gets called after the [component] is assigned to the [entity][Entity]. * - * If a component [removeHook][ComponentsHolder.removeHook] is defined and the [entity][Entity] - * already had such a [component] then it gets called with the previous assigned component before - * the [addHook][ComponentsHolder.addHook] is called. + * If the [entity][Entity] already had such a [component] then the [onRemoveComponent][Component.onRemoveComponent] + * lifecycle method gets called on the previously assigned component before the [onAddComponent][Component.onAddComponent] + * lifecycle method is called on the new component. */ inline operator fun > Entity.plusAssign(component: T) { val compType: ComponentType = component.type() @@ -111,12 +111,12 @@ open class EntityCreateContext( * in exceptional cases. * It is preferred to use the [plusAssign] function whenever possible to have type-safety. * - * If a component [addHook][ComponentsHolder.addHook] is defined then it - * gets called after the component is assigned to the [entity][Entity]. + * The [onAddComponent][Component.onAddComponent] lifecycle method + * gets called after each component is assigned to the [entity][Entity]. * - * If a component [removeHook][ComponentsHolder.removeHook] is defined and the [entity][Entity] - * already had such a component then it gets called with the previous assigned component before - * the [addHook][ComponentsHolder.addHook] is called. + * If the [entity][Entity] already has such a component then the [onRemoveComponent][Component.onRemoveComponent] + * lifecycle method gets called on the previously assigned component before the [onAddComponent][Component.onAddComponent] + * lifecycle method is called on the new component. */ operator fun Entity.plusAssign(components: List>) { components.forEach { cmp -> @@ -139,8 +139,7 @@ class EntityUpdateContext( /** * Removes a [component][Component] of the given [type] from the [entity][Entity]. * - * If a component [removeHook][ComponentsHolder.removeHook] is defined then it gets called - * if the [entity][Entity] has such a component. + * Calls the [onRemoveComponent][Component.onRemoveComponent] lifecycle method on the component being removed. * * @throws [IndexOutOfBoundsException] if the id of the [entity][Entity] exceeds the internal components' capacity. * This can only happen when the [entity][Entity] never had such a component. diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index ed6a64c..2f5355b 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -48,38 +48,6 @@ class InjectableConfiguration(private val world: World) { inline fun add(dependency: T) = add(T::class.simpleName ?: T::class.toString(), dependency) } -/** - * A DSL class to configure [ComponentHook] for specific [components][Component]. - */ -@WorldCfgMarker -class ComponentConfiguration( - @PublishedApi - internal val world: World, -) { - - /** - * Sets the add [hook][ComponentsHolder.addHook] for the given [type]. - * This hook gets called whenever a [component][Component] of the given [type] gets added to an [entity][Entity]. - */ - inline fun > onAdd( - type: ComponentType, - noinline hook: ComponentHook - ) { - world.setComponentAddHook(type, hook) - } - - /** - * Sets the remove [hook][ComponentsHolder.addHook] for the given [type]. - * This hook gets called whenever a [component][Component] of the given [type] gets removed from an [entity][Entity]. - */ - inline fun > onRemove( - type: ComponentType, - noinline hook: ComponentHook - ) { - world.setComponentRemoveHook(type, hook) - } -} - /** * A DSL class to configure [IntervalSystem] of a [WorldConfiguration]. */ @@ -149,7 +117,6 @@ class FamilyConfiguration( class WorldConfiguration(@PublishedApi internal val world: World) { private var injectableCfg: (InjectableConfiguration.() -> Unit)? = null - private var compCfg: (ComponentConfiguration.() -> Unit)? = null private var familyCfg: (FamilyConfiguration.() -> Unit)? = null private var systemCfg: (SystemConfiguration.() -> Unit)? = null @@ -157,10 +124,6 @@ class WorldConfiguration(@PublishedApi internal val world: World) { injectableCfg = cfg } - fun components(cfg: ComponentConfiguration.() -> Unit) { - compCfg = cfg - } - fun families(cfg: FamilyConfiguration.() -> Unit) { familyCfg = cfg } @@ -197,7 +160,6 @@ class WorldConfiguration(@PublishedApi internal val world: World) { fun configure() { injectableCfg?.invoke(InjectableConfiguration(world)) - compCfg?.invoke(ComponentConfiguration(world)) familyCfg?.invoke(FamilyConfiguration(world)) SystemConfiguration().also { systemCfg?.invoke(it) @@ -216,7 +178,7 @@ class WorldConfiguration(@PublishedApi internal val world: World) { * size of some collections and to avoid slow resizing calls. * * @param cfg the [configuration][WorldConfiguration] of the world containing the [systems][IntervalSystem], - * [injectables][Injectable], [ComponentHook]s and [FamilyHook]s. + * [injectables][Injectable] and [FamilyHook]s. */ fun configureWorld(entityCapacity: Int = 512, cfg: WorldConfiguration.() -> Unit): World { val newWorld = World(entityCapacity) @@ -382,40 +344,6 @@ class World internal constructor( throw FleksNoSuchSystemException(T::class) } - /** - * Sets the [hook] as a [ComponentsHolder.addHook] for the given [type]. - * - * @throws FleksHookAlreadyAddedException if the [ComponentsHolder] already has an add hook set. - */ - @PublishedApi - internal inline fun > setComponentAddHook( - type: ComponentType, - noinline hook: ComponentHook - ) { - val holder = componentService.holder(type) - if (holder.addHook != null) { - throw FleksHookAlreadyAddedException("addHook", "Component ${type::class.simpleName}") - } - holder.addHook = hook - } - - /** - * Sets the [hook] as a [ComponentsHolder.removeHook] for the given [type]. - * - * @throws FleksHookAlreadyAddedException if the [ComponentsHolder] already has a remove hook set. - */ - @PublishedApi - internal inline fun > setComponentRemoveHook( - type: ComponentType, - noinline hook: ComponentHook - ) { - val holder = componentService.holder(type) - if (holder.removeHook != null) { - throw FleksHookAlreadyAddedException("removeHook", "Component ${type::class.simpleName}") - } - holder.removeHook = hook - } - /** * Sets the [hook] as an [EntityService.addHook]. * @@ -529,7 +457,7 @@ class World internal constructor( /** * Loads the given [snapshot] of the world. This will first clear any existing * entity of the world. After that it will load all provided entities and components. - * This will also execute [ComponentHook] or [FamilyHook]. + * This will also execute [FamilyHook]. * * @throws FleksSnapshotException if a family iteration is currently in process. */ diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt index dd4395e..9f75466 100644 --- a/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt +++ b/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt @@ -21,6 +21,26 @@ internal class ComponentTest { } } + private data class ComponentTestWithLifecycleComponent( + val expectedWorld: World, + var numAddCalls: Int = 0, + var numRemoveCalls: Int = 0 + ) : Component { + override fun type() = ComponentTestWithLifecycleComponent + + override fun World.onAddComponent() { + assertEquals(expectedWorld, this) + numAddCalls++ + } + + override fun World.onRemoveComponent() { + assertEquals(expectedWorld, this) + numRemoveCalls++ + } + + companion object : ComponentType() + } + private val testWorld = configureWorld { } private val testService = ComponentService().also { it.world = testWorld } private val testHolder = testService.holder(ComponentTestComponent) @@ -127,66 +147,40 @@ internal class ComponentTest { } @Test - fun addComponentWithHook() { - var numAddCalls = 0 - var numRemoveCalls = 0 + fun addAndRemoveComponentWithLifecycleMethod() { val expectedEntity = Entity(1) - val expectedComp = ComponentTestComponent() - - val onAdd: ComponentHook = { entity, component -> - assertEquals(testWorld, this) - assertEquals(expectedEntity, entity) - assertEquals(expectedComp, component) - numAddCalls++ - } - - val onRemove: ComponentHook = { entity, component -> - assertEquals(testWorld, this) - assertEquals(expectedEntity, entity) - assertEquals(expectedComp, component) - numRemoveCalls++ - } - - testHolder.addHook = onAdd - testHolder.removeHook = onRemove + val expectedComp = ComponentTestWithLifecycleComponent(testWorld) - testHolder[expectedEntity] = expectedComp + val testHolderForLifecycleComponent = testService.holder(ComponentTestWithLifecycleComponent) - assertEquals(1, numAddCalls) - assertEquals(0, numRemoveCalls) + assertEquals(0, expectedComp.numAddCalls) + assertEquals(0, expectedComp.numRemoveCalls) + testHolderForLifecycleComponent[expectedEntity] = expectedComp + assertEquals(1, expectedComp.numAddCalls) + assertEquals(0, expectedComp.numRemoveCalls) + testHolderForLifecycleComponent -= expectedEntity + assertEquals(1, expectedComp.numAddCalls) + assertEquals(1, expectedComp.numRemoveCalls) } @Test - fun addComponentWithComponentListenerWhenComponentAlreadyPresent() { - var numAddCalls = 0 - var numRemoveCalls = 0 + fun addAndReplaceComponentWithLifecycleMethod() { val expectedEntity = Entity(1) - val expectedComp1 = ComponentTestComponent() - val expectedComp2 = ComponentTestComponent() - - val onAdd: ComponentHook = { entity, component -> - assertSame(testWorld, this) - assertEquals(expectedEntity, entity) - assertTrue { expectedComp1 === component || expectedComp2 === component } - numAddCalls++ - } - - val onRemove: ComponentHook = { entity, component -> - assertSame(testWorld, this) - assertEquals(expectedEntity, entity) - assertSame(expectedComp1, component) - numRemoveCalls++ - } + val expectedComp1 = ComponentTestWithLifecycleComponent(testWorld) + val expectedComp2 = ComponentTestWithLifecycleComponent(testWorld) - testHolder.addHook = onAdd - testHolder.removeHook = onRemove + val testHolderForLifecycleComponent = testService.holder(ComponentTestWithLifecycleComponent) - testHolder[expectedEntity] = expectedComp1 - // this should trigger onRemove of the first component - testHolder[expectedEntity] = expectedComp2 + testHolderForLifecycleComponent[expectedEntity] = expectedComp1 + assertEquals(1, expectedComp1.numAddCalls) + assertEquals(0, expectedComp1.numRemoveCalls) - assertEquals(2, numAddCalls) - assertEquals(1, numRemoveCalls) + // Should trigger onRemoveComponent on expectedComp1 + testHolderForLifecycleComponent[expectedEntity] = expectedComp2 + assertEquals(1, expectedComp1.numAddCalls) + assertEquals(1, expectedComp1.numRemoveCalls) + assertEquals(1, expectedComp2.numAddCalls) + assertEquals(0, expectedComp2.numRemoveCalls) } @Test diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/Fleks2TDD.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/Fleks2TDD.kt index c4128c5..e8b7997 100644 --- a/src/commonTest/kotlin/com/github/quillraven/fleks/Fleks2TDD.kt +++ b/src/commonTest/kotlin/com/github/quillraven/fleks/Fleks2TDD.kt @@ -20,6 +20,29 @@ private data class Position( companion object : ComponentType() } +abstract class ColliderService { + abstract fun getId(): String +} + +private data class Collider( + val size: Float +) : Component { + override fun type(): ComponentType = Collider + + var colliderId: String? = null + + override fun World.onAddComponent() { + val provider = inject() + colliderId = provider.getId() + } + + override fun World.onRemoveComponent() { + colliderId = null + } + + companion object : ComponentType() +} + private data class Sprite( val background: Boolean, var path: String = "", @@ -142,36 +165,28 @@ class Fleks2TDD { } @Test - fun testComponentHooks() { - val addComponent = Position(0f, 0f) - val removeComponent = Position(0f, 0f) - lateinit var testWorld: World - testWorld = configureWorld { - components { - onAdd(Position) { entity, component -> - component.x = 1f - assertEquals(testWorld, this) - assertTrue { entity.id in 0..1 } - assertTrue { component in listOf(addComponent, removeComponent) } - } - - onRemove(Position) { entity, component -> - component.x = 2f - assertEquals(testWorld, this) - assertEquals(Entity(1), entity) - assertEquals(removeComponent, component) - } + fun testComponentLifecycleMethods() { + val addComponent = Collider(0f) + val removeComponent = Collider(2f) + val testWorld = configureWorld { + injectables { + add(object : ColliderService() { + var nextId = 0 + override fun getId() = (nextId++).toString() + }) } } - // entity that triggers onAdd hook + // entity that triggers onAddComponent lifecycle method + assertEquals(null, addComponent.colliderId) testWorld.entity { it += addComponent } - // entity that triggers onRemove hook - val removeEntity = testWorld.entity { it += removeComponent } - with(testWorld) { removeEntity.configure { it -= Position } } + assertEquals("0", addComponent.colliderId) - assertEquals(1f, addComponent.x) - assertEquals(2f, removeComponent.x) + // entity that triggers onRemoveComponent lifecycle method + val removeEntity = testWorld.entity { it += removeComponent } + assertEquals("1", removeComponent.colliderId) + with(testWorld) { removeEntity.configure { it -= Collider } } + assertEquals(null, removeComponent.colliderId) } @Test diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt index 8ca8c7e..9816441 100644 --- a/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt +++ b/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt @@ -6,10 +6,18 @@ import com.github.quillraven.fleks.collection.compareEntity import com.github.quillraven.fleks.collection.compareEntityBy import kotlin.test.* -private data class WorldTestComponent(var x: Float = 0f) : Component, +private data class WorldTestComponent( + var x: Float = 0f, +) : Component, Comparable { override fun type(): ComponentType = WorldTestComponent + var numAddCalls: Int = 0 + var numRemoveCalls: Int = 0 + + override fun World.onAddComponent() { numAddCalls++ } + override fun World.onRemoveComponent() { numAddCalls-- } + companion object : ComponentType() override fun compareTo(other: WorldTestComponent) = 0 @@ -50,7 +58,7 @@ private class WorldTestIteratingSystem( } } -private class WorldTestInitSystem : IteratingSystem(family { all(WorldTestComponent) }) { +private class WorldTestInitSystem: IteratingSystem(family { all(WorldTestComponent) }) { init { world.entity { it += WorldTestComponent() } } @@ -434,38 +442,21 @@ internal class WorldTest { assertFailsWith { w.family { all().none().any() } } } - @Test - fun createWorldWithComponentHooks() { - val w = configureWorld { - components { - onAdd(WorldTestComponent) { _, _ -> } - onRemove(WorldTestComponent) { _, _ -> } - } - } - - val holder = w.componentService.holder(WorldTestComponent) - assertNotNull(holder.addHook) - assertNotNull(holder.removeHook) - } - @Test fun notifyComponentHooksDuringSystemCreation() { - var numAddCalls = 0 - var numRemoveCalls = 0 - - configureWorld { - components { - onAdd(WorldTestComponent) { _, _ -> ++numAddCalls } - onRemove(WorldTestComponent) { _, _ -> ++numRemoveCalls } - } - + val w = configureWorld { systems { add(WorldTestInitSystem()) } } - assertEquals(1, numAddCalls) - assertEquals(0, numRemoveCalls) + val testComp = with(w) { + val entity = w.family { all(WorldTestComponent) }.first() + return@with entity[WorldTestComponent] + } + + assertEquals(1, testComp.numAddCalls) + assertEquals(0, testComp.numRemoveCalls) } @Test @@ -665,25 +656,20 @@ internal class WorldTest { @Test fun testLoadSnapshotWithThreeEntities() { - var numAddCalls = 0 - var numRemoveCalls = 0 val w = configureWorld { injectables { add("42") } - components { - onAdd(WorldTestComponent) { _, _ -> ++numAddCalls } - onRemove(WorldTestComponent) { _, _ -> ++numRemoveCalls } - } - systems { add(WorldTestIteratingSystem()) } } + val comp1 = WorldTestComponent() + val comp2 = WorldTestComponent() val snapshot = mapOf( - Entity(3) to listOf(WorldTestComponent(), WorldTestComponent2()), - Entity(5) to listOf(WorldTestComponent()), + Entity(3) to listOf(comp1, WorldTestComponent2()), + Entity(5) to listOf(comp2), Entity(7) to listOf() ) @@ -703,9 +689,12 @@ internal class WorldTest { } // 2 out of 3 loaded entities should be part of the IteratingSystem family assertEquals(2, w.system().numCallsEntity) - // 2 out of 3 loaded entities should notify the WorldTestComponentListener - assertEquals(2, numAddCalls) - assertEquals(0, numRemoveCalls) + // 2 out of 3 loaded entities have components with lifecycle methods + assertEquals(1, comp1.numAddCalls) + assertEquals(0, comp1.numRemoveCalls) + assertEquals(1, comp2.numAddCalls) + assertEquals(0, comp2.numRemoveCalls) + assertEquals(3, actual.size) }