diff --git a/ReadMe.md b/ReadMe.md index 2a217cb..a044daf 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -47,7 +47,7 @@ then use [Artemis-odb](https://github.com/junkdog/artemis-odb) or [Ashley](https ## Current Status -Release Candidate version 1.0-RC2 is available on maven central since 19-Jan-2022. Please feel free to contribute to the +Release Candidate version 1.0-RC3 is available on maven central since 30-Jan-2022. Please feel free to contribute to the Discussions or Issues. Help is always appreciated. To use Fleks add it as a dependency to your project: @@ -57,20 +57,20 @@ To use Fleks add it as a dependency to your project: io.github.quillraven.fleks Fleks - 1.0-RC2 + 1.0-RC3 ``` #### Gradle (Groovy) ```kotlin -implementation 'io.github.quillraven.fleks:Fleks:1.0-RC2' +implementation 'io.github.quillraven.fleks:Fleks:1.0-RC3' ``` #### Gradle (Kotlin) ```kotlin -implementation("io.github.quillraven.fleks:Fleks:1.0-RC2") +implementation("io.github.quillraven.fleks:Fleks:1.0-RC3") ``` ## Current API and usage @@ -233,7 +233,11 @@ class AnimationSystem( If you need to modify the component configuration of an entity then this can be done via the `configureEntity` function of an `IteratingSystem`. The purpose of this function is performance reasons to trigger internal calculations -of Fleks only once instead of each time a component gets added or removed. +of Fleks only once instead of each time a component gets added or removed. Inside `configureEntity` you get access to +three special `ComponentMapper` functions: +- `add`: adds a component to an entity +- `remove`: removes a component from an entity +- `addOrUpdate`: adds a component only if it does not exist yet. Otherwise, it just updates the existing component Let's see how a system can look like that adds a `DeadComponent` to an entity and removes a `LifeComponent` when its hitpoints are <= 0: @@ -367,6 +371,25 @@ fun main() { } ``` +If you ever need to iterate over entities outside a system then this is also possible but please note that +systems are always the preferred way of iteration in an entity component system. +The world's `forEach` function allows you to iterate over all active entities: + +```Kotlin + fun main() { + val world = World {} + val e1 = world.entity() + val e2 = world.entity() + val e3 = world.entity() + world.remove(e2) + + // this will iterate over entities e1 and e3 + world.forEach { entity -> + // do something with the entity + } +} +``` + ### Entity and Components We now know how to create a world and add systems to it, but we don't know how to add entities to our world. This can be diff --git a/build.gradle.kts b/build.gradle.kts index a23c8a9..bca8f0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "io.github.quillraven.fleks" -version = "1.0-RC2" +version = "1.0-RC3" java.sourceCompatibility = JavaVersion.VERSION_1_8 val bmSourceSetName = "benchmarks" diff --git a/src/main/kotlin/com/github/quillraven/fleks/component.kt b/src/main/kotlin/com/github/quillraven/fleks/component.kt index 0cab4ea..3abfa48 100644 --- a/src/main/kotlin/com/github/quillraven/fleks/component.kt +++ b/src/main/kotlin/com/github/quillraven/fleks/component.kt @@ -60,6 +60,20 @@ class ComponentMapper( } } + /** + * Creates a new component if the [entity] does not have it yet. Otherwise, updates the existing component. + * Applies the [configuration] in both cases and returns the component. + * Notifies any registered [ComponentListener] if a new component is created. + */ + @PublishedApi + internal inline fun addOrUpdateInternal(entity: Entity, configuration: T.() -> Unit = {}): T { + return if (entity in this) { + this[entity].apply(configuration) + } else { + addInternal(entity, configuration) + } + } + /** * Removes a component of the specific type from the given [entity]. * Notifies any registered [ComponentListener]. diff --git a/src/main/kotlin/com/github/quillraven/fleks/entity.kt b/src/main/kotlin/com/github/quillraven/fleks/entity.kt index b33cb88..c0da713 100644 --- a/src/main/kotlin/com/github/quillraven/fleks/entity.kt +++ b/src/main/kotlin/com/github/quillraven/fleks/entity.kt @@ -47,6 +47,7 @@ class EntityCreateCfg( /** * Adds and returns a component of the given type to the [entity] and * applies the [configuration] to the component. + * Notifies any registered [ComponentListener]. * * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type * does not have a no argument constructor. @@ -72,14 +73,27 @@ class EntityUpdateCfg { * Adds and returns a component of the given type to the [entity] and applies the [configuration] to that component. * If the [entity] already has a component of the given type then no new component is created and instead * the existing one will be updated. + * Notifies any registered [ComponentListener]. */ inline fun ComponentMapper.add(entity: Entity, configuration: T.() -> Unit = {}): T { cmpMask.set(this.id) return this.addInternal(entity, configuration) } + /** + * Adds a new component of the given type to the [entity] if it does not have it yet. + * Otherwise, updates the already existing component. + * Applies the [configuration] in both cases and returns the component. + * Notifies any registered [ComponentListener] if a new component is created. + */ + inline fun ComponentMapper.addOrUpdate(entity: Entity, configuration: T.() -> Unit = {}): T { + cmpMask.set(this.id) + return this.addOrUpdateInternal(entity, configuration) + } + /** * Removes a component of the given type from the [entity]. + * Notifies any registered [ComponentListener]. * * @throws [ArrayIndexOutOfBoundsException] if the id of the [entity] exceeds the mapper's capacity. */ @@ -240,6 +254,19 @@ class EntityService( } } + /** + * Performs the given [action] on each active [entity][Entity]. + */ + fun forEach(action: (Entity) -> Unit) { + for (id in 0 until nextId) { + val entity = Entity(id) + if (removedEntities[entity.id]) { + continue + } + entity.run(action) + } + } + /** * Clears the [delayRemoval] flag and removes [entities][Entity] which are part of the [delayedEntities]. */ diff --git a/src/main/kotlin/com/github/quillraven/fleks/world.kt b/src/main/kotlin/com/github/quillraven/fleks/world.kt index 6f019a8..6e13e44 100644 --- a/src/main/kotlin/com/github/quillraven/fleks/world.kt +++ b/src/main/kotlin/com/github/quillraven/fleks/world.kt @@ -177,6 +177,13 @@ class World( entityService.removeAll() } + /** + * Performs the given [action] on each active [entity][Entity]. + */ + fun forEach(action: (Entity) -> Unit) { + entityService.forEach(action) + } + /** * Returns the specified [system][IntervalSystem] of the world. * diff --git a/src/test/kotlin/com/github/quillraven/fleks/ComponentTest.kt b/src/test/kotlin/com/github/quillraven/fleks/ComponentTest.kt index 283f865..8c1af2c 100644 --- a/src/test/kotlin/com/github/quillraven/fleks/ComponentTest.kt +++ b/src/test/kotlin/com/github/quillraven/fleks/ComponentTest.kt @@ -229,4 +229,34 @@ internal class ComponentTest { { assertEquals("add", listener.lastCall) } ) } + + @Test + fun `add component if it does not exist yet`() { + val cmpService = ComponentService() + val mapper = cmpService.mapper() + val entity = Entity(1) + + val cmp = mapper.addOrUpdateInternal(entity) { x++ } + + assertAll( + { assertTrue(entity in mapper) }, + { assertEquals(1, cmp.x) } + ) + } + + @Test + fun `update component if it already exists`() { + val cmpService = ComponentService() + val mapper = cmpService.mapper() + val entity = Entity(1) + val expectedCmp = mapper.addOrUpdateInternal(entity) { x++ } + + val actualCmp = mapper.addOrUpdateInternal(entity) { x++ } + + assertAll( + { assertTrue(entity in mapper) }, + { assertEquals(expectedCmp, actualCmp) }, + { assertEquals(2, actualCmp.x) } + ) + } } diff --git a/src/test/kotlin/com/github/quillraven/fleks/EntityTest.kt b/src/test/kotlin/com/github/quillraven/fleks/EntityTest.kt index 4a6993a..4ad687e 100644 --- a/src/test/kotlin/com/github/quillraven/fleks/EntityTest.kt +++ b/src/test/kotlin/com/github/quillraven/fleks/EntityTest.kt @@ -119,6 +119,27 @@ internal class EntityTest { ) } + @Test + fun `update component of entity if it already exists with custom listener`() { + val cmpService = ComponentService() + val entityService = EntityService(32, cmpService) + val listener = EntityTestListener() + val expectedEntity = entityService.create { } + val mapper = cmpService.mapper() + entityService.addEntityListener(listener) + + entityService.configureEntity(expectedEntity) { + mapper.add(expectedEntity) { ++x } + mapper.addOrUpdate(expectedEntity) { x++ } + } + + assertAll( + { assertTrue(expectedEntity in mapper) }, + { assertEquals(2f, mapper[expectedEntity].x) }, + { assertEquals(1, listener.numCalls) } + ) + } + @Test fun `remove entity with a component immediately with custom listener`() { val cmpService = ComponentService() diff --git a/src/test/kotlin/com/github/quillraven/fleks/WorldTest.kt b/src/test/kotlin/com/github/quillraven/fleks/WorldTest.kt index 4e245e3..9d4ff28 100644 --- a/src/test/kotlin/com/github/quillraven/fleks/WorldTest.kt +++ b/src/test/kotlin/com/github/quillraven/fleks/WorldTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import kotlin.test.assertContentEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -255,4 +256,18 @@ internal class WorldTest { } } } + + @Test + fun `iterate over all active entities`() { + val w = World {} + val e1 = w.entity() + val e2 = w.entity() + val e3 = w.entity() + w.remove(e2) + val actualEntities = mutableListOf() + + w.forEach { actualEntities.add(it) } + + assertContentEquals(listOf(e1, e3), actualEntities) + } }