From 627f45aa47647400e1dc4b651cb25e685d8cccd7 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 21 Jan 2022 16:31:58 +0100 Subject: [PATCH 01/27] Add fleks ECS unchanged --- samples/fleks-ecs/.gitignore | 1 + samples/fleks-ecs/build.gradle | 4 + .../github/quillraven/fleks/collection/bag.kt | 270 ++++++++++++ .../quillraven/fleks/collection/bitArray.kt | 156 +++++++ .../com/github/quillraven/fleks/component.kt | 177 ++++++++ .../com/github/quillraven/fleks/entity.kt | 267 ++++++++++++ .../com/github/quillraven/fleks/exception.kt | 35 ++ .../com/github/quillraven/fleks/family.kt | 121 ++++++ .../com/github/quillraven/fleks/reflection.kt | 77 ++++ .../com/github/quillraven/fleks/system.kt | 399 ++++++++++++++++++ .../com/github/quillraven/fleks/world.kt | 213 ++++++++++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 55 +++ 12 files changed, 1775 insertions(+) create mode 100644 samples/fleks-ecs/.gitignore create mode 100644 samples/fleks-ecs/build.gradle create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/main.kt diff --git a/samples/fleks-ecs/.gitignore b/samples/fleks-ecs/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/samples/fleks-ecs/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/fleks-ecs/build.gradle b/samples/fleks-ecs/build.gradle new file mode 100644 index 000000000..0653ded39 --- /dev/null +++ b/samples/fleks-ecs/build.gradle @@ -0,0 +1,4 @@ +dependencies { + add("commonMainApi", project(":korge")) +} + diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt new file mode 100644 index 000000000..2eddb9bd7 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt @@ -0,0 +1,270 @@ +package com.github.quillraven.fleks.collection + +import com.github.quillraven.fleks.Entity +import kotlin.math.max +import kotlin.math.min + +/** + * Creates a new [Bag] of the given [capacity] and type. Default capacity is 64. + */ +inline fun bag(capacity: Int = 64): Bag { + return Bag(arrayOfNulls(capacity)) +} + +/** + * A bag implementation in Kotlin containing only the necessary functions for Fleks. + */ +class Bag( + @PublishedApi + internal var values: Array +) { + var size: Int = 0 + private set + + val capacity: Int + get() = values.size + + fun add(value: T) { + if (size == values.size) { + values = values.copyOf(max(1, size * 2)) + } + values[size++] = value + } + + operator fun set(index: Int, value: T) { + if (index >= values.size) { + values = values.copyOf(max(size * 2, index + 1)) + } + size = max(size, index + 1) + values[index] = value + } + + operator fun get(index: Int): T { + return values[index] ?: throw NoSuchElementException("Bag has no value at index $index") + } + + fun removeValue(value: T): Boolean { + for (i in 0 until size) { + if (values[i] == value) { + values[i] = values[--size] + values[size] = null + return true + } + } + return false + } + + operator fun contains(value: T): Boolean { + for (i in 0 until size) { + if (values[i] == value) { + return true + } + } + return false + } + + inline fun forEach(action: (T) -> Unit) { + for (i in 0 until size) { + values[i]?.let(action) + } + } +} + +/** + * A bag implementation for integer values in Kotlin to avoid autoboxing. It is used for [entities][Entity] + * and contains only the necessary functions for Fleks. + */ +class IntBag( + size: Int = 64 +) { + @PublishedApi + internal var values: IntArray = IntArray(size) + + var size: Int = 0 + private set + + val capacity: Int + get() = values.size + + val isNotEmpty: Boolean + get() = size > 0 + + fun add(value: Int) { + if (size == values.size) { + values = values.copyOf(max(1, size * 2)) + } + values[size++] = value + } + + internal fun unsafeAdd(value: Int) { + values[size++] = value + } + + operator fun get(index: Int): Int { + return values[index] + } + + fun clear() { + values.fill(0) + size = 0 + } + + fun ensureCapacity(capacity: Int) { + if (capacity >= values.size) { + values = values.copyOf(capacity + 1) + } + } + + operator fun contains(value: Int): Boolean { + for (i in 0 until size) { + if (values[i] == value) { + return true + } + } + return false + } + + inline fun forEach(action: (Int) -> Unit) { + for (i in 0 until size) { + action(values[i]) + } + } + + fun sort(comparator: EntityComparator) { + values.quickSort(0, size, comparator) + } +} + +/** + * Sorting of int[] logic taken from: https://github.com/karussell/fastutil/blob/master/src/it/unimi/dsi/fastutil/ints/IntArrays.java + */ +interface EntityComparator { + fun compare(entityA: Entity, entityB: Entity): Int +} + +fun compareEntity(compareFun: (Entity, Entity) -> Int): EntityComparator { + return object : EntityComparator { + override fun compare(entityA: Entity, entityB: Entity): Int { + return compareFun(entityA, entityB) + } + } +} + +private const val SMALL = 7 +private const val MEDIUM = 50 + +private fun IntArray.swap(idxA: Int, idxB: Int) { + val tmp = this[idxA] + this[idxA] = this[idxB] + this[idxB] = tmp +} + +private fun IntArray.vecSwap(idxA: Int, idxB: Int, n: Int) { + var a = idxA + var b = idxB + for (i in 0 until n) { + this.swap(a++, b++) + } +} + +private fun IntArray.med3(idxA: Int, idxB: Int, idxC: Int, comparator: EntityComparator): Int { + val ab = comparator.compare(Entity(this[idxA]), Entity(this[idxB])) + val ac = comparator.compare(Entity(this[idxA]), Entity(this[idxC])) + val bc = comparator.compare(Entity(this[idxB]), Entity(this[idxC])) + + return when { + ab < 0 -> { + when { + bc < 0 -> idxB + ac < 0 -> idxC + else -> idxA + } + } + bc > 0 -> idxB + ac > 0 -> idxC + else -> idxA + } +} + +private fun IntArray.selectionSort(fromIdx: Int, toIdx: Int, comparator: EntityComparator) { + for (i in fromIdx until toIdx - 1) { + var m = i + for (j in i + 1 until toIdx) { + if (comparator.compare(Entity(this[j]), Entity(this[m])) < 0) { + m = j + } + } + if (m != i) { + this.swap(i, m) + } + } +} + +private fun IntArray.quickSort(fromIdx: Int, toIdx: Int, comparator: EntityComparator) { + val len = toIdx - fromIdx + + // Selection sort on smallest arrays + if (len < SMALL) { + this.selectionSort(fromIdx, toIdx, comparator) + return + } + + // Choose a partition element, v + var m = fromIdx + len / 2 // Small arrays, middle element + if (len > SMALL) { + var l = fromIdx + var n = toIdx - 1 + if (len > MEDIUM) { + // Big arrays, pseudo median of 9 + val s = len / 8 + l = this.med3(l, l + s, l + 2 * s, comparator) + m = this.med3(m - s, m, m + s, comparator) + n = this.med3(n - 2 * s, n - s, n, comparator) + } + // Mid-size, med of 3 + m = this.med3(l, m, n, comparator) + } + + val v = this[m] + // Establish Invariant: v* (v)* v* + var a = fromIdx + var b = a + var c = toIdx - 1 + var d = c + while (true) { + var comparison = 0 + + while (b <= c && comparator.compare(Entity(this[b]), Entity(v)).also { comparison = it } <= 0) { + if (comparison == 0) { + this.swap(a++, b) + } + b++ + } + + while (c >= b && comparator.compare(Entity(this[c]), Entity(v)).also { comparison = it } >= 0) { + if (comparison == 0) { + this.swap(c, d--) + } + c-- + } + + if (b > c) { + break + } + + this.swap(b++, c--) + } + + // Swap partition elements back to middle + var s = min(a - fromIdx, b - a) + this.vecSwap(fromIdx, b - s, s) + s = min(d - c, toIdx - d - 1) + this.vecSwap(b, toIdx - s, s) + // Recursively sort non-partition-elements + if ((b - a).also { s = it } > 1) { + this.quickSort(fromIdx, fromIdx + s, comparator) + } + if ((d - c).also { s = it } > 1) { + this.quickSort(toIdx - s, toIdx, comparator) + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt new file mode 100644 index 000000000..cc454b555 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -0,0 +1,156 @@ +package com.github.quillraven.fleks.collection + +import kotlin.math.min + +/** + * A BitArray implementation in Kotlin containing only the necessary functions for Fleks. + * + * Boolean[] gives a better performance when iterating over a BitArray, but uses more memory and + * also the amount of array resizing is increased when enlarging the array which makes it then slower in the end. + * + * For that reason I used a Long[] implementation which is similar to the one of java.util with inspirations also from + * https://github.com/lemire/javaewah/blob/master/src/main/java/com/googlecode/javaewah/datastructure/BitSet.java. + * It is more memory efficient and requires less resizing calls since one Long can store up to 64 bits. + */ +class BitArray( + nBits: Int = 0 +) { + @PublishedApi + internal var bits = LongArray((nBits + 63) / 64) + + val capacity: Int + get() = bits.size * 64 + + operator fun get(idx: Int): Boolean { + val word = idx / 64 + return if (word >= bits.size) { + false + } else { + (bits[word] and (1L shl (idx % 64))) != 0L + } + } + + fun set(idx: Int) { + val word = idx / 64 + if (word >= bits.size) { + bits = bits.copyOf(word + 1) + } + bits[word] = bits[word] or (1L shl (idx % 64)) + } + + fun clearAll() { + bits.fill(0L) + } + + fun clear(idx: Int) { + val word = idx / 64 + if (word < bits.size) { + bits[word] = bits[word] and (1L shl (idx % 64)).inv() + } + } + + fun intersects(other: BitArray): Boolean { + val otherBits = other.bits + val start = min(bits.size, otherBits.size) - 1 + for (i in start downTo 0) { + if ((bits[i] and otherBits[i]) != 0L) { + return true + } + } + return false + } + + fun contains(other: BitArray): Boolean { + val otherBits = other.bits + + // check if other BitArray is larger and if there is any of those bits set + for (i in bits.size until otherBits.size) { + if (otherBits[i] != 0L) { + return false + } + } + + // check overlapping bits + val start = min(bits.size, otherBits.size) - 1 + for (i in start downTo 0) { + if ((bits[i] and otherBits[i]) != otherBits[i]) { + return false + } + } + + return true + } + + /** + * Returns the logical size of the [BitArray] which is equal to the highest index of the + * bit that is set. + * + * Returns zero if the [BitArray] is empty. + */ + fun length(): Int { + for (word in bits.size - 1 downTo 0) { + val bitsAtWord = bits[word] + if (bitsAtWord != 0L) { + for (bit in 63 downTo 0) { + if ((bitsAtWord and (1L shl (bit % 64))) != 0L) { + return (word shl 6) + bit + 1 + } + } + } + } + return 0 + } + + inline fun forEachSetBit(action: (Int) -> Unit) { + for (word in bits.size - 1 downTo 0) { + val bitsAtWord = bits[word] + if (bitsAtWord != 0L) { + for (bit in 63 downTo 0) { + if ((bitsAtWord and (1L shl (bit % 64))) != 0L) { + action((word shl 6) + bit) + } + } + } + } + } + + fun toIntBag(bag: IntBag) { + var checkSize = true + bag.clear() + forEachSetBit { idx -> + if (checkSize) { + checkSize = false + bag.ensureCapacity(idx) + } + bag.unsafeAdd(idx) + } + } + + override fun hashCode(): Int { + if (bits.isEmpty()) { + return 0 + } + + val word = length() / 64 + var hash = 0 + for (i in 0..word) { + hash = 127 * hash + (bits[i] xor (bits[i] ushr 32)).toInt() + } + return hash + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BitArray + val otherBits = other.bits + + val commonWords: Int = min(bits.size, otherBits.size) + for (i in 0 until commonWords) { + if (bits[i] != otherBits[i]) return false + } + + return if (bits.size == otherBits.size) true else length() == other.length() + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt new file mode 100644 index 000000000..0cab4ea58 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -0,0 +1,177 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.Bag +import com.github.quillraven.fleks.collection.bag +import java.lang.reflect.Constructor +import kotlin.math.max +import kotlin.reflect.KClass + +/** + * Interface of a component listener that gets notified when a component of a specific type + * gets added or removed from an [entity][Entity]. + */ +interface ComponentListener { + fun onComponentAdded(entity: Entity, component: T) + fun onComponentRemoved(entity: Entity, component: T) +} + +/** + * A class that is responsible to store components of a specific type for all [entities][Entity] in a [world][World]. + * Each component is assigned a unique [id] for fast access and to avoid lookups via a class which is slow. + * + * Refer to [ComponentService] for more details. + */ +class ComponentMapper( + @PublishedApi + internal val id: Int, + @PublishedApi + internal var components: Array, + @PublishedApi + internal val cstr: Constructor +) { + @PublishedApi + internal val listeners = bag>(2) + + /** + * Creates and returns a new component of the specific type for the given [entity] and applies the [configuration]. + * If the [entity] already has a component of that type then no new instance will be created. + * Notifies any registered [ComponentListener]. + */ + @PublishedApi + internal inline fun addInternal(entity: Entity, configuration: T.() -> Unit = {}): T { + if (entity.id >= components.size) { + components = components.copyOf(max(components.size * 2, entity.id + 1)) + } + val cmp = components[entity.id] + return if (cmp == null) { + val newCmp = cstr.newInstance().apply(configuration) + components[entity.id] = newCmp + listeners.forEach { it.onComponentAdded(entity, newCmp) } + newCmp + } else { + // component already added -> reuse it and do not create a new instance. + // Call onComponentRemoved first in case users do something special in onComponentAdded. + // Otherwise, onComponentAdded will be executed twice on a single component without executing onComponentRemoved + // which is not correct. + listeners.forEach { it.onComponentRemoved(entity, cmp) } + val existingCmp = cmp.apply(configuration) + listeners.forEach { it.onComponentAdded(entity, existingCmp) } + existingCmp + } + } + + /** + * Removes a component of the specific type from the given [entity]. + * Notifies any registered [ComponentListener]. + * + * @throws [ArrayIndexOutOfBoundsException] if the id of the [entity] exceeds the components' capacity. + */ + @PublishedApi + internal fun removeInternal(entity: Entity) { + components[entity.id]?.let { cmp -> + listeners.forEach { it.onComponentRemoved(entity, cmp) } + } + components[entity.id] = null + } + + /** + * Returns a component of the specific type of the given [entity]. + * + * @throws [FleksNoSuchComponentException] if the [entity] does not have such a component. + */ + operator fun get(entity: Entity): T { + return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.name) + } + + /** + * Returns true if and only if the given [entity] has a component of the specific type. + */ + operator fun contains(entity: Entity): Boolean = components.size > entity.id && components[entity.id] != null + + /** + * Adds the given [listener] to the list of [ComponentListener]. + */ + fun addComponentListener(listener: ComponentListener) = listeners.add(listener) + + /** + * Adds the given [listener] to the list of [ComponentListener]. This function is only used internally + * to add listeners through the [WorldConfiguration]. + */ + @Suppress("UNCHECKED_CAST") + internal fun addComponentListenerInternal(listener: ComponentListener<*>) = + addComponentListener(listener as ComponentListener) + + /** + * Removes the given [listener] from the list of [ComponentListener]. + */ + fun removeComponentListener(listener: ComponentListener) = listeners.removeValue(listener) + + /** + * Returns true if and only if the given [listener] is part of the list of [ComponentListener]. + */ + operator fun contains(listener: ComponentListener) = listener in listeners + + override fun toString(): String { + return "ComponentMapper(id=$id, component=${cstr.name})" + } +} + +/** + * A service class that is responsible for managing [ComponentMapper] instances. + * It creates a [ComponentMapper] for every unique component type and assigns a unique id for each mapper. + */ +class ComponentService { + /** + * Returns map of [ComponentMapper] that stores mappers by its component type. + * It is used by the [SystemService] during system creation and by the [EntityService] for entity creation. + */ + @PublishedApi + internal val mappers = HashMap, ComponentMapper<*>>() + + /** + * Returns [Bag] of [ComponentMapper]. The id of the mapper is the index of the bag. + * It is used by the [EntityService] to fasten up the cleanup process of delayed entity removals. + */ + private val mappersBag = bag>() + + /** + * Returns a [ComponentMapper] for the given [type]. If the mapper does not exist then it will be created. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given [type] does not have + * a no argument constructor. + */ + @Suppress("UNCHECKED_CAST") + fun mapper(type: KClass): ComponentMapper { + var mapper = mappers[type] + + if (mapper == null) { + try { + mapper = ComponentMapper( + mappers.size, + Array(64) { null } as Array, + // use java constructor because it is ~4x faster than calling Kotlin's createInstance on a KClass + type.java.getDeclaredConstructor() + ) + mappers[type] = mapper + mappersBag.add(mapper) + } catch (e: Exception) { + throw FleksMissingNoArgsComponentConstructorException(type) + } + } + + return mapper as ComponentMapper + } + + /** + * Returns a [ComponentMapper] for the specific type. If the mapper does not exist then it will be created. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the specific type does not have + * a no argument constructor. + */ + inline fun mapper(): ComponentMapper = mapper(T::class) + + /** + * Returns an already existing [ComponentMapper] for the given [cmpId]. + */ + fun mapper(cmpId: Int) = mappersBag[cmpId] +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt new file mode 100644 index 000000000..8284c3350 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -0,0 +1,267 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import com.github.quillraven.fleks.collection.IntBag +import com.github.quillraven.fleks.collection.bag + +/** + * An entity of a [world][World]. It represents a unique id. + */ +class Entity(val id: Int) + +/** + * Interface of an [entity][Entity] listener that gets notified when the component configuration changes. + * The [onEntityCfgChanged] function gets also called when an [entity][Entity] gets created and removed. + */ +interface EntityListener { + /** + * Function that gets called when an [entity's][Entity] component configuration changes. + * This happens when a component gets added or removed or the [entity] gets added or removed from the [world][World]. + * + * @param entity the [entity][Entity] with the updated component configuration. + * + * @param cmpMask the [BitArray] representing what type of components the entity has. Each component type has a + * unique id. Refer to [ComponentMapper] for more details. + */ + fun onEntityCfgChanged(entity: Entity, cmpMask: BitArray) = Unit +} + +@DslMarker +annotation class EntityCfgMarker + +/** + * A DSL class to add components to a newly created [entity][Entity]. + */ +@EntityCfgMarker +class EntityCreateCfg( + @PublishedApi + internal val cmpService: ComponentService +) { + @PublishedApi + internal var entity = Entity(0) + + @PublishedApi + internal lateinit var cmpMask: BitArray + + /** + * Adds and returns a component of the given type to the [entity] and + * applies the [configuration] to the component. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type + * does not have a no argument constructor. + */ + inline fun add(configuration: T.() -> Unit = {}): T { + val mapper = cmpService.mapper() + cmpMask.set(mapper.id) + return mapper.addInternal(entity, configuration) + } +} + +/** + * A DSL class to update components of an already existing [entity][Entity]. + * It contains extension functions for [ComponentMapper] which is how the component configuration of + * existing entities is changed. This usually happens within [IteratingSystem] classes. + */ +@EntityCfgMarker +class EntityUpdateCfg { + @PublishedApi + internal lateinit var cmpMask: BitArray + + /** + * 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. + */ + inline fun ComponentMapper.add(entity: Entity, configuration: T.() -> Unit = {}): T { + cmpMask.set(this.id) + return this.addInternal(entity, configuration) + } + + /** + * Removes a component of the given type from the [entity]. + * + * @throws [ArrayIndexOutOfBoundsException] if the id of the [entity] exceeds the mapper's capacity. + */ + inline fun ComponentMapper.remove(entity: Entity) { + cmpMask.clear(this.id) + this.removeInternal(entity) + } +} + +/** + * A service class that is responsible for creation and removal of [entities][Entity]. + * It also stores the component configuration of each entity as a [BitArray] to have quick access to + * what kind of components an entity has or doesn't have. + */ +class EntityService( + initialEntityCapacity: Int, + private val cmpService: ComponentService +) { + /** + * The id that will be given to a newly created [entity][Entity] if there are no [recycledEntities]. + */ + @PublishedApi + internal var nextId = 0 + + /** + * Separate BitArray to remember if an [entity][Entity] was already removed. + * This is faster than looking up the [recycledEntities]. + */ + @PublishedApi + internal val removedEntities = BitArray(initialEntityCapacity) + + /** + * The already removed [entities][Entity] which can be reused whenever a new entity is needed. + */ + @PublishedApi + internal val recycledEntities = ArrayDeque() + + /** + * Returns the amount of active entities. + */ + val numEntities: Int + get() = nextId - recycledEntities.size + + /** + * Returns the maximum capacity of active entities. + */ + val capacity: Int + get() = cmpMasks.capacity + + /** + * The component configuration per [entity][Entity]. + */ + @PublishedApi + internal val cmpMasks = bag(initialEntityCapacity) + + @PublishedApi + internal val createCfg = EntityCreateCfg(cmpService) + + @PublishedApi + internal val updateCfg = EntityUpdateCfg() + + @PublishedApi + internal val listeners = bag() + + /** + * Flag that indicates if an iteration of an [IteratingSystem] is currently in progress. + * In such cases entities will not be removed immediately. + * Refer to [IteratingSystem.onTick] for more details. + */ + internal var delayRemoval = false + + /** + * The entities that get removed at the end of an [IteratingSystem] iteration. + */ + private val delayedEntities = IntBag() + + /** + * Creates and returns a new [entity][Entity] and applies the given [configuration]. + * If there are [recycledEntities] then they will be preferred over creating new entities. + * Notifies any registered [EntityListener]. + */ + inline fun create(configuration: EntityCreateCfg.(Entity) -> Unit): Entity { + val newEntity = if (recycledEntities.isEmpty()) { + Entity(nextId++) + } else { + val recycled = recycledEntities.removeLast() + removedEntities.clear(recycled.id) + recycled + } + + if (newEntity.id >= cmpMasks.size) { + cmpMasks[newEntity.id] = BitArray(64) + } + val cmpMask = cmpMasks[newEntity.id] + createCfg.run { + this.entity = newEntity + this.cmpMask = cmpMask + configuration(this.entity) + } + listeners.forEach { it.onEntityCfgChanged(newEntity, cmpMask) } + + return newEntity + } + + /** + * Updates an [entity] with the given [configuration]. + * Notifies any registered [EntityListener]. + */ + inline fun configureEntity(entity: Entity, configuration: EntityUpdateCfg.(Entity) -> Unit) { + val cmpMask = cmpMasks[entity.id] + updateCfg.run { + this.cmpMask = cmpMask + configuration(entity) + } + listeners.forEach { it.onEntityCfgChanged(entity, cmpMask) } + } + + /** + * Removes the given [entity] and adds it to the [recycledEntities] for future use. + * + * If [delayRemoval] is set then the [entity] is not removed immediately and instead will be cleaned up + * within the [cleanupDelays] function. + * + * Notifies any registered [EntityListener] when the [entity] gets removed. + */ + fun remove(entity: Entity) { + if (removedEntities[entity.id]) { + // entity is already removed + return + } + + if (delayRemoval) { + delayedEntities.add(entity.id) + } else { + removedEntities.set(entity.id) + val cmpMask = cmpMasks[entity.id] + recycledEntities.add(entity) + cmpMask.forEachSetBit { cmpId -> + cmpService.mapper(cmpId).removeInternal(entity) + } + cmpMask.clearAll() + listeners.forEach { it.onEntityCfgChanged(entity, cmpMask) } + } + } + + /** + * Removes all [entities][Entity] and adds them to the [recycledEntities] for future use. + * + * Refer to [remove] for more details. + */ + fun removeAll() { + for (id in 0 until nextId) { + val entity = Entity(id) + if (removedEntities[entity.id]) { + continue + } + remove(entity) + } + } + + /** + * Clears the [delayRemoval] flag and removes [entities][Entity] which are part of the [delayedEntities]. + */ + fun cleanupDelays() { + delayRemoval = false + if (delayedEntities.isNotEmpty) { + delayedEntities.forEach { remove(Entity(it)) } + delayedEntities.clear() + } + } + + /** + * Adds the given [listener] to the list of [EntityListener]. + */ + fun addEntityListener(listener: EntityListener) = listeners.add(listener) + + /** + * Removes the given [listener] from the list of [EntityListener]. + */ + fun removeEntityListener(listener: EntityListener) = listeners.removeValue(listener) + + /** + * Returns true if and only if the given [listener] is part of the list of [EntityListener]. + */ + operator fun contains(listener: EntityListener) = listener in listeners +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt new file mode 100644 index 000000000..60f43cf86 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -0,0 +1,35 @@ +package com.github.quillraven.fleks + +import kotlin.reflect.KClass + +abstract class FleksException(message: String) : RuntimeException(message) + +class FleksSystemAlreadyAddedException(system: KClass<*>) : + FleksException("System ${system.simpleName} is already part of the ${WorldConfiguration::class.simpleName}") + +class FleksSystemCreationException(system: KClass<*>, details: String) : + FleksException("Cannot create ${system.simpleName}. Did you add all necessary injectables?\nDetails: $details") + +class FleksNoSuchSystemException(system: KClass<*>) : + FleksException("There is no system of type ${system.simpleName} in the world") + +class FleksInjectableAlreadyAddedException(name: String) : + FleksException("Injectable with name $name is already part of the ${WorldConfiguration::class.simpleName}") + +class FleksInjectableWithoutNameException : + FleksException("Injectables must be registered with a non-null name") + +class FleksMissingNoArgsComponentConstructorException(component: KClass<*>) : + FleksException("Component ${component.simpleName} is missing a no-args constructor") + +class FleksNoSuchComponentException(entity: Entity, component: String) : + FleksException("Entity $entity has no component of type $component") + +class FleksComponentListenerAlreadyAddedException(listener: KClass>) : + FleksException("ComponentListener ${listener.simpleName} is already part of the ${WorldConfiguration::class.simpleName}") + +class FleksUnusedInjectablesException(unused: List>) : + FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") + +class FleksReflectionException(type: KClass<*>, details: String) : + FleksException("Cannot create ${type.simpleName}.\nDetails: $details") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt new file mode 100644 index 000000000..80523016d --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -0,0 +1,121 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import com.github.quillraven.fleks.collection.EntityComparator +import com.github.quillraven.fleks.collection.IntBag +import kotlin.reflect.KClass + +/** + * An annotation for an [IteratingSystem] to define a [Family]. + * [Entities][Entity] must have all [components] specified to be part of the [family][Family]. + */ +@Target(AnnotationTarget.CLASS) +annotation class AllOf(val components: Array> = []) + +/** + * An annotation for an [IteratingSystem] to define a [Family]. + * [Entities][Entity] must not have any [components] specified to be part of the [family][Family]. + */ +@Target(AnnotationTarget.CLASS) +annotation class NoneOf(val components: Array> = []) + +/** + * An annotation for an [IteratingSystem] to define a [Family]. + * [Entities][Entity] must have at least one of the [components] specified to be part of the [family][Family]. + */ +@Target(AnnotationTarget.CLASS) +annotation class AnyOf(val components: Array> = []) + +/** + * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. + * A configuration is defined via the three annotations: [AllOf], [NoneOf] and [AnyOf]. + * Each component is assigned to a unique index. That index is set in the [allOf], [noneOf] or [anyOf][] [BitArray]. + * + * A family is an [EntityListener] and gets notified when an [entity][Entity] is added to the world or the + * entity's component configuration changes. + * + * Every [IteratingSystem] is linked to exactly one family. Families are created by the [SystemService] automatically + * when a [world][World] gets created. + * + * @param allOf all the components that an [entity][Entity] must have. Default value is null. + * @param noneOf all the components that an [entity][Entity] must not have. Default value is null. + * @param anyOf the components that an [entity][Entity] must have at least one. Default value is null. + */ +data class Family( + internal val allOf: BitArray? = null, + internal val noneOf: BitArray? = null, + internal val anyOf: BitArray? = null +) : EntityListener { + /** + * Return the [entities] in form of an [IntBag] for better iteration performance. + */ + @PublishedApi + internal val entitiesBag = IntBag() + + /** + * Returns the [entities][Entity] that belong to this family. + */ + private val entities = BitArray(1) + + /** + * Flag to indicate if there are changes in the [entities]. If it is true then the [entitiesBag] should get + * updated via a call to [updateActiveEntities]. + * + * Refer to [IteratingSystem.onTick] for an example implementation. + */ + var isDirty = false + private set + + /** + * Returns true if the specified [cmpMask] matches the family's component configuration. + * + * @param cmpMask the component configuration of an [entity][Entity]. + */ + operator fun contains(cmpMask: BitArray): Boolean { + return (allOf == null || cmpMask.contains(allOf)) + && (noneOf == null || !cmpMask.intersects(noneOf)) + && (anyOf == null || cmpMask.intersects(anyOf)) + } + + /** + * Updates the [entitiesBag] and clears the [isDirty] flag. + * This should be called when [isDirty] is true. + */ + fun updateActiveEntities() { + isDirty = false + entities.toIntBag(entitiesBag) + } + + /** + * Iterates over the [entities][Entity] of this family and runs the given [action]. + */ + inline fun forEach(action: (Entity) -> Unit) { + entitiesBag.forEach { action(Entity(it)) } + } + + /** + * Sorts the [entities][Entity] of this family by the given [comparator]. + */ + fun sort(comparator: EntityComparator) { + entitiesBag.sort(comparator) + } + + /** + * Checks if the [entity] is part of the family by analyzing the entity's components. + * The [cmpMask] is a [BitArray] that indicates which components the [entity] currently has. + * + * The [entity] gets either added to the [entities] or removed and [isDirty] is set when needed. + */ + override fun onEntityCfgChanged(entity: Entity, cmpMask: BitArray) { + val entityInFamily = cmpMask in this + if (entityInFamily && !entities[entity.id]) { + // new entity gets added + isDirty = true + entities.set(entity.id) + } else if (!entityInFamily && entities[entity.id]) { + // existing entity gets removed + isDirty = true + entities.clear(entity.id) + } + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt new file mode 100644 index 000000000..58fc16cdc --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt @@ -0,0 +1,77 @@ +package com.github.quillraven.fleks + +import java.lang.reflect.Constructor +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +/** + * Creates a new instance of type [T] by using dependency injection if necessary. + * Dependencies are looked up in the [injectables] map. + * If it is a [ComponentMapper] then the mapper is retrieved from the [cmpService]. + * + * @throws [FleksReflectionException] if there is no single primary constructor or if dependencies are missing. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if a component of a [ComponentMapper] has no no-args constructor. + */ +@Suppress("UNCHECKED_CAST") +fun newInstance( + type: KClass, + cmpService: ComponentService, + injectables: Map +): T { + val constructors = type.java.declaredConstructors + if (constructors.size != 1) { + throw FleksReflectionException( + type, + "Found ${constructors.size} constructors. Only a single primary constructor is supported!" + ) + } + + // get constructor arguments + val args = systemArgs(constructors.first(), cmpService, injectables, type) + // create new instance using arguments from above + return constructors.first().newInstance(*args) as T +} + +/** + * Returns array of arguments for the given [primaryConstructor]. + * Arguments are either an object of [injectables] or [ComponentMapper] instances. + * + * @throws [FleksReflectionException] if [injectables] are missing for the [primaryConstructor]. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the [primaryConstructor] requires a [ComponentMapper] + * with a component type that does not have a no argument constructor. + */ +@PublishedApi +internal fun systemArgs( + primaryConstructor: Constructor<*>, + cmpService: ComponentService, + injectables: Map, + type: KClass +): Array { + val params = primaryConstructor.parameters + val paramAnnotations = primaryConstructor.parameterAnnotations + + val args = Array(params.size) { idx -> + val param = params[idx] + val paramClass = param.type.kotlin + if (paramClass == ComponentMapper::class) { + val cmpType = (param.parameterizedType as ParameterizedType).actualTypeArguments[0] as Class<*> + cmpService.mapper(cmpType.kotlin) + } else { + // check if qualifier annotation is specified + val qualifierAnn = paramAnnotations[idx].firstOrNull { it is Qualifier } as Qualifier? + // if yes -> use qualifier name + // if no -> use qualified class name + val name = qualifierAnn?.name ?: paramClass.qualifiedName + val injectable = injectables[name] ?: throw FleksReflectionException( + type, + "Missing injectable of type ${paramClass.qualifiedName}" + ) + injectable.used = true + injectable.injObj + } + } + + return args +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt new file mode 100644 index 000000000..2bdcdfc7b --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -0,0 +1,399 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import com.github.quillraven.fleks.collection.EntityComparator +import java.lang.reflect.Field +import kotlin.reflect.KClass + +/** + * An interval for an [IntervalSystem]. There are two kind of intervals: + * - [EachFrame] + * - [Fixed] + * + * [EachFrame] means that the [IntervalSystem] is updated every time the [world][World] gets updated. + * [Fixed] means that the [IntervalSystem] is updated at a fixed rate given in seconds. + */ +sealed interface Interval +object EachFrame : Interval + +/** + * @param step the time in seconds when an [IntervalSystem] gets updated. + */ +data class Fixed(val step: Float) : Interval + +/** + * A basic system of a [world][World] without a context to [entities][Entity]. + * It is mandatory to implement [onTick] which gets called whenever the system gets updated + * according to its [interval][Interval]. + * + * If the system uses a [Fixed] interval then [onAlpha] can be overridden in case interpolation logic is needed. + * + * @param interval the [interval][Interval] in which the system gets updated. Default is [EachFrame]. + * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. + */ +abstract class IntervalSystem( + val interval: Interval = EachFrame, + var enabled: Boolean = true +) { + /** + * Returns the [world][World] to which this system belongs. + * This reference gets updated by the [SystemService] when the system gets created via reflection. + */ + lateinit var world: World + internal set + + private var accumulator: Float = 0.0f + + /** + * Returns the time in seconds since the last time [onUpdate] was called. + * + * If the [interval] is [EachFrame] then the [world's][World] delta time is returned which is passed to [World.update]. + * + * Otherwise, the [step][Fixed.step] value is returned. + */ + val deltaTime: Float + get() = if (interval is Fixed) interval.step else world.deltaTime + + /** + * Optional function for any initialization logic that requires access to the [world]. + * This is necessary because the normal init block does not have an initialized [world] yet. + */ + open fun onInit() = Unit + + /** + * Updates the system according to its [interval]. This function gets called from [World.update] when + * the system is [enabled]. + * + * If the [interval] is [EachFrame] then [onTick] gets called. + * + * Otherwise, the world's [delta time][World.deltaTime] is analyzed and [onTick] is called at a fixed rate. + * This could be multiple or zero times with a single call to [onUpdate]. At the end [onAlpha] is called. + */ + open fun onUpdate() { + when (interval) { + is EachFrame -> onTick() + is Fixed -> { + accumulator += world.deltaTime + val stepRate = interval.step + while (accumulator >= stepRate) { + onTick() + accumulator -= stepRate + } + + onAlpha(accumulator / stepRate) + } + } + } + + /** + * Function that contains the update logic of the system. Gets called whenever this system should get processed + * according to its [interval]. + */ + abstract fun onTick() + + /** + * Optional function for interpolation logic when using a [Fixed] interval. This function is not called for + * an [EachFrame] interval. + * + * @param alpha a value between 0 (inclusive) and 1 (exclusive) that describes the progress between two ticks. + */ + open fun onAlpha(alpha: Float) = Unit + + /** + * Optional function to dispose any resources of the system if needed. Gets called when the world's [dispose][World.dispose] + * function is called. + */ + open fun onDispose() = Unit +} + +/** + * A sorting type for an [IteratingSystem]. There are two sorting options: + * - [Automatic] + * - [Manual] + * + * [Automatic] means that the sorting of [entities][Entity] is happening automatically each time + * [IteratingSystem.onTick] gets called. + * + * [Manual] means that sorting must be called programmatically by setting [IteratingSystem.doSort] to true. + * [Entities][Entity] are then sorted the next time [IteratingSystem.onTick] gets called. + */ +sealed interface SortingType +object Automatic : SortingType +object Manual : SortingType + +/** + * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. It must be linked to a + * [family][Family] using at least one of the [AllOf], [AnyOf] or [NoneOf] annotations. + * + * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. + * Default value is an empty comparator which means no sorting. + * @param sortingType the [type][SortingType] of sorting for entities when using a [comparator]. + * @param interval the [interval][Interval] in which the system gets updated. Default is [EachFrame]. + * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. + */ +abstract class IteratingSystem( + private val comparator: EntityComparator = EMPTY_COMPARATOR, + private val sortingType: SortingType = Automatic, + interval: Interval = EachFrame, + enabled: Boolean = true +) : IntervalSystem(interval, enabled) { + /** + * Returns the [family][Family] of this system. + * This reference gets updated by the [SystemService] when the system gets created via reflection. + */ + private lateinit var family: Family + + /** + * Returns the [entityService][EntityService] of this system. + * This reference gets updated by the [SystemService] when the system gets created via reflection. + */ + @PublishedApi + internal lateinit var entityService: EntityService + private set + + /** + * Flag that defines if sorting of [entities][Entity] will be performed the next time [onTick] is called. + * + * If a [comparator] is defined and [sortingType] is [Automatic] then this flag is always true. + * + * Otherwise, it must be set programmatically to perform sorting. The flag gets cleared after sorting. + */ + var doSort = sortingType == Automatic && comparator != EMPTY_COMPARATOR + + /** + * Updates an [entity] using the given [configuration] to add and remove components. + */ + inline fun configureEntity(entity: Entity, configuration: EntityUpdateCfg.(Entity) -> Unit) { + entityService.configureEntity(entity, configuration) + } + + /** + * Updates the [family] if needed and calls [onTickEntity] for each [entity][Entity] of the [family]. + * If [doSort] is true then [entities][Entity] are sorted using the [comparator] before calling [onTickEntity]. + * + * **Important note**: There is a potential risk when iterating over entities and one of those entities + * gets removed. Removing the entity immediately and cleaning up its components could + * cause problems because if you access a component which is mandatory for the family, you will get + * a FleksNoSuchComponentException. To avoid that you could check if an entity really has the component + * before accessing it but that is redundant in context of a family. + * + * To avoid these kinds of problems, entity removals are delayed until the end of the iteration. This also means + * that a removed entity of this family will still be part of the [onTickEntity] for the current iteration. + */ + override fun onTick() { + if (family.isDirty) { + family.updateActiveEntities() + } + if (doSort) { + doSort = sortingType == Automatic + family.sort(comparator) + } + + entityService.delayRemoval = true + family.forEach { onTickEntity(it) } + entityService.cleanupDelays() + } + + /** + * Function that contains the update logic for each [entity][Entity] of the system. + */ + abstract fun onTickEntity(entity: Entity) + + /** + * Optional function for interpolation logic when using a [Fixed] interval. This function is not called for + * an [EachFrame] interval. Calls [onAlphaEntity] for each [entity][Entity] of the system. + * + * @param alpha a value between 0 (inclusive) and 1 (exclusive) that describes the progress between two ticks. + */ + override fun onAlpha(alpha: Float) { + if (family.isDirty) { + family.updateActiveEntities() + } + + entityService.delayRemoval = true + family.forEach { onAlphaEntity(it, alpha) } + entityService.cleanupDelays() + } + + /** + * Optional function for interpolation logic for each [entity][Entity] of the system. + * + * @param alpha a value between 0 (inclusive) and 1 (exclusive) that describes the progress between two ticks. + */ + open fun onAlphaEntity(entity: Entity, alpha: Float) = Unit + + companion object { + private val EMPTY_COMPARATOR = object : EntityComparator { + override fun compare(entityA: Entity, entityB: Entity): Int = 0 + } + } +} + +/** + * A service class for any [IntervalSystem] of a [world][World]. It is responsible to create systems using + * constructor dependency injection. It also stores [systems] and updates [enabled][IntervalSystem.enabled] systems + * each time [update] is called. + * + * @param world the [world][World] the service belongs to. + * @param systemTypes the [systems][IntervalSystem] to be created. + * @param injectables the required dependencies to create the [systems][IntervalSystem]. + */ +class SystemService( + world: World, + systemTypes: List>, + injectables: Map +) { + @PublishedApi + internal val systems: Array + + init { + // create systems + val entityService = world.entityService + val cmpService = world.componentService + val allFamilies = mutableListOf() + systems = Array(systemTypes.size) { sysIdx -> + val sysType = systemTypes[sysIdx] + val newSystem = newInstance(sysType, cmpService, injectables) + + // set world reference of newly created system + val worldField = field(newSystem, "world") + worldField.isAccessible = true + worldField.set(newSystem, world) + + if (IteratingSystem::class.java.isAssignableFrom(sysType.java)) { + // set family and entity service reference of newly created iterating system + @Suppress("UNCHECKED_CAST") + val family = family(sysType as KClass, entityService, cmpService, allFamilies) + val famField = field(newSystem, "family") + famField.isAccessible = true + famField.set(newSystem, family) + + val eServiceField = field(newSystem, "entityService") + eServiceField.isAccessible = true + eServiceField.set(newSystem, entityService) + } + + newSystem.apply { onInit() } + } + } + + /** + * Returns [Annotation] of the specific type if the class has that annotation. Otherwise, returns null. + */ + private inline fun KClass<*>.annotation(): T? { + return this.java.getAnnotation(T::class.java) + } + + /** + * Creates or returns an already created [family][Family] for the given [IteratingSystem] + * by analyzing the system's [AllOf], [AnyOf] and [NoneOf] annotations. + * + * @throws [FleksSystemCreationException] if the [IteratingSystem] does not contain at least one + * [AllOf], [AnyOf] or [NoneOf] annotation. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the [AllOf], [NoneOf] or [AnyOf] annotations + * of the system have a component type that does not have a no argument constructor. + */ + private fun family( + sysType: KClass, + entityService: EntityService, + cmpService: ComponentService, + allFamilies: MutableList + ): Family { + val allOfAnn = sysType.annotation() + val allOfCmps = if (allOfAnn != null && allOfAnn.components.isNotEmpty()) { + allOfAnn.components.map { cmpService.mapper(it) } + } else { + null + } + + val noneOfAnn = sysType.annotation() + val noneOfCmps = if (noneOfAnn != null && noneOfAnn.components.isNotEmpty()) { + noneOfAnn.components.map { cmpService.mapper(it) } + } else { + null + } + + val anyOfAnn = sysType.annotation() + val anyOfCmps = if (anyOfAnn != null && anyOfAnn.components.isNotEmpty()) { + anyOfAnn.components.map { cmpService.mapper(it) } + } else { + null + } + + if ((allOfCmps == null || allOfCmps.isEmpty()) + && (noneOfCmps == null || noneOfCmps.isEmpty()) + && (anyOfCmps == null || anyOfCmps.isEmpty()) + ) { + throw FleksSystemCreationException( + sysType, + "IteratingSystem must define at least one of AllOf, NoneOf or AnyOf" + ) + } + + val allBs = if (allOfCmps == null) null else BitArray().apply { allOfCmps.forEach { this.set(it.id) } } + val noneBs = if (noneOfCmps == null) null else BitArray().apply { noneOfCmps.forEach { this.set(it.id) } } + val anyBs = if (anyOfCmps == null) null else BitArray().apply { anyOfCmps.forEach { this.set(it.id) } } + + var family = allFamilies.find { it.allOf == allBs && it.noneOf == noneBs && it.anyOf == anyBs } + if (family == null) { + family = Family(allBs, noneBs, anyBs) + entityService.addEntityListener(family) + allFamilies.add(family) + } + return family + } + + /** + * Returns a [Field] of name [fieldName] of the given [system]. + * + * @throws [FleksSystemCreationException] if the [system] does not have a [Field] of name [fieldName]. + */ + private fun field(system: IntervalSystem, fieldName: String): Field { + var sysClass: Class<*> = system::class.java + var classField: Field? = null + while (classField == null) { + try { + classField = sysClass.getDeclaredField(fieldName) + } catch (e: NoSuchFieldException) { + val supC = sysClass.superclass ?: throw FleksSystemCreationException(system::class, "No '$fieldName' field found") + sysClass = supC + } + + } + return classField + } + + /** + * Returns the specified [system][IntervalSystem]. + * + * @throws [FleksNoSuchSystemException] if there is no such system. + */ + inline fun system(): T { + systems.forEach { system -> + if (system is T) { + return system + } + } + throw FleksNoSuchSystemException(T::class) + } + + /** + * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] by calling + * their [IntervalSystem.onUpdate] function. + */ + fun update() { + systems.forEach { system -> + if (system.enabled) { + system.onUpdate() + } + } + } + + /** + * Calls the [onDispose][IntervalSystem.onDispose] function of all [systems]. + */ + fun dispose() { + systems.forEach { it.onDispose() } + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt new file mode 100644 index 000000000..6f019a858 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -0,0 +1,213 @@ +package com.github.quillraven.fleks + +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +/** + * An optional annotation for an [IntervalSystem] constructor parameter to + * inject a dependency exactly by that qualifier's [name]. + */ +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class Qualifier(val name: String) + +/** + * Wrapper class for injectables of the [WorldConfiguration]. + * It is used in the [SystemService] to find out any unused injectables. + */ +data class Injectable(val injObj: Any, var used: Boolean = false) + +/** + * A configuration for an entity [world][World] to define the initial maximum entity capacity, + * the systems of the [world][World] and the systems' dependencies to be injected. + * Additionally, you can define [ComponentListener] to define custom logic when a specific component is + * added or removed from an [entity][Entity]. + */ +class WorldConfiguration { + /** + * Initial maximum entity capacity. + * Will be used internally when a [world][World] is created to set the initial + * size of some collections and to avoid slow resizing calls. + */ + var entityCapacity = 512 + + @PublishedApi + internal val systemTypes = mutableListOf>() + + @PublishedApi + internal val injectables = mutableMapOf() + + @PublishedApi + internal val cmpListenerTypes = mutableListOf>>() + + /** + * Adds the specified [IntervalSystem] to the [world][World]. + * The order in which systems are added is the order in which they will be executed when calling [World.update]. + * + * @throws [FleksSystemAlreadyAddedException] if the system was already added before. + */ + inline fun system() { + val systemType = T::class + if (systemType in systemTypes) { + throw FleksSystemAlreadyAddedException(systemType) + } + systemTypes.add(systemType) + } + + /** + * Adds the specified [dependency] under the given [name] which can then be injected to any [IntervalSystem]. + * + * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. + */ + fun inject(name: String, dependency: T) { + if (name in injectables) { + throw FleksInjectableAlreadyAddedException(name) + } + + injectables[name] = Injectable(dependency) + } + + /** + * Adds the specified dependency which can then be injected to any [IntervalSystem]. + * Refer to [inject]: the name is the qualifiedName of the class of the [dependency]. + * + * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. + * @throws [FleksInjectableWithoutNameException] if the qualifiedName of the [dependency] is null. + */ + inline fun inject(dependency: T) { + val key = T::class.qualifiedName ?: throw FleksInjectableWithoutNameException() + inject(key, dependency) + } + + /** + * Adds the specified [ComponentListener] to the [world][World]. + * + * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. + */ + inline fun > componentListener() { + val listenerType = T::class + if (listenerType in cmpListenerTypes) { + throw FleksComponentListenerAlreadyAddedException(listenerType) + } + cmpListenerTypes.add(listenerType) + } +} + +/** + * A world to handle [entities][Entity] and [systems][IntervalSystem]. + * + * @param cfg the [configuration][WorldConfiguration] of the world containing the initial maximum entity capacity + * and the [systems][IntervalSystem] to be processed. + */ +class World( + cfg: WorldConfiguration.() -> Unit +) { + /** + * Returns the time that is passed to [update][World.update]. + * It represents the time in seconds between two frames. + */ + var deltaTime = 0f + private set + + @PublishedApi + internal val systemService: SystemService + + @PublishedApi + internal val componentService = ComponentService() + + @PublishedApi + internal val entityService: EntityService + + /** + * Returns the amount of active entities. + */ + val numEntities: Int + get() = entityService.numEntities + + /** + * Returns the maximum capacity of active entities. + */ + val capacity: Int + get() = entityService.capacity + + init { + val worldCfg = WorldConfiguration().apply(cfg) + entityService = EntityService(worldCfg.entityCapacity, componentService) + val injectables = worldCfg.injectables + systemService = SystemService(this, worldCfg.systemTypes, injectables) + + // create and register ComponentListener + worldCfg.cmpListenerTypes.forEach { listenerType -> + val listener = newInstance(listenerType, componentService, injectables) + val genInter = listener.javaClass.genericInterfaces.first { + it is ParameterizedType && it.rawType == ComponentListener::class.java + } + val cmpType = (genInter as ParameterizedType).actualTypeArguments[0] + val mapper = componentService.mapper((cmpType as Class<*>).kotlin) + mapper.addComponentListenerInternal(listener) + } + + // verify that there are no unused injectables + val unusedInjectables = injectables.filterValues { !it.used }.map { it.value.injObj::class } + if (unusedInjectables.isNotEmpty()) { + throw FleksUnusedInjectablesException(unusedInjectables) + } + } + + /** + * Adds a new [entity][Entity] to the world using the given [configuration][EntityCreateCfg]. + */ + inline fun entity(configuration: EntityCreateCfg.(Entity) -> Unit = {}): Entity { + return entityService.create(configuration) + } + + /** + * Removes the given [entity] from the world. The [entity] will be recycled and reused for + * future calls to [World.entity]. + */ + fun remove(entity: Entity) { + entityService.remove(entity) + } + + /** + * Removes all [entities][Entity] from the world. The entities will be recycled and reused for + * future calls to [World.entity]. + */ + fun removeAll() { + entityService.removeAll() + } + + /** + * Returns the specified [system][IntervalSystem] of the world. + * + * @throws [FleksNoSuchSystemException] if there is no such [system][IntervalSystem]. + */ + inline fun system(): T { + return systemService.system() + } + + /** + * Returns a [ComponentMapper] for the given type. If the mapper does not exist then it will be created. + * + * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type does not have + * a no argument constructor. + */ + inline fun mapper(): ComponentMapper = componentService.mapper(T::class) + + /** + * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world + * using the given [deltaTime]. + */ + fun update(deltaTime: Float) { + this.deltaTime = deltaTime + systemService.update() + } + + /** + * Removes all [entities][Entity] of the world and calls the [onDispose][IntervalSystem.onDispose] function of each system. + */ + fun dispose() { + entityService.removeAll() + systemService.dispose() + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt new file mode 100644 index 000000000..1ec97c010 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -0,0 +1,55 @@ +import com.soywiz.klock.* +import com.soywiz.korge.Korge +import com.soywiz.korge.scene.MaskTransition +import com.soywiz.korge.scene.Scene +import com.soywiz.korge.scene.sceneContainer +import com.soywiz.korge.view.* +import com.soywiz.korge.view.filter.TransitionFilter +import com.soywiz.korim.atlas.MutableAtlasUnit +import com.soywiz.korim.color.Colors +import com.soywiz.korim.format.* +import com.soywiz.korio.file.std.resourcesVfs +import ecs.entitysystem.EntitySystem +import ecs.entitytypes.SpawnerEntity +import ecs.subsystems.ImageAnimationSystem +import ecs.subsystems.MovingSystem +import ecs.subsystems.SpawningSystem + +import com.github.quillraven.fleks.World + +const val scaleFactor = 1 + +suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor, bgcolor = Colors["#000000"]) { + + injector.mapPrototype { ExampleScene() } + + val rootSceneContainer = sceneContainer() + views.debugViews = true + + rootSceneContainer.changeTo( + transition = MaskTransition(transition = TransitionFilter.Transition.CIRCULAR, reversed = false, smooth = true), + time = 0.5.seconds + ) +} + +var aseImage: ImageData? = null + +class ExampleScene : Scene() { + + private val atlas = MutableAtlasUnit(1024, 1024) + private val entitySystem: EntitySystem = EntitySystem() + + override suspend fun Container.sceneInit() { + val sw = Stopwatch().start() + aseImage = resourcesVfs["sprites2.ase"].readImageData(ASE, atlas = atlas) + println("loaded resources in ${sw.elapsed}") + } + + override suspend fun Container.sceneMain() { + container { + scale(scaleFactor) + + + } + } +} From 82938ebcc7ca6144c3c99f4f2c2017d3d7cec696 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 24 Jan 2022 12:00:18 +0100 Subject: [PATCH 02/27] Adapt registering of systems in World Instantiation of generic classes is not possible in Kotlin multiplatform. Thus, instead of instantiating a generic class in the SystemService the system function gets a factory method for calling the constructor of the system. --- .../quillraven/fleks/collection/bitArray.kt | 2 +- .../com/github/quillraven/fleks/component.kt | 27 +++-- .../com/github/quillraven/fleks/entity.kt | 11 +- .../com/github/quillraven/fleks/reflection.kt | 8 +- .../com/github/quillraven/fleks/system.kt | 104 ++++++++++-------- .../com/github/quillraven/fleks/world.kt | 42 ++++--- .../fleks-ecs/src/commonMain/kotlin/main.kt | 45 +++++--- 7 files changed, 147 insertions(+), 92 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index cc454b555..0626f0d30 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -141,7 +141,7 @@ class BitArray( override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false +// MK if (javaClass != other?.javaClass) return false other as BitArray val otherBits = other.bits diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 0cab4ea58..b24de6408 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -2,7 +2,7 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.Bag import com.github.quillraven.fleks.collection.bag -import java.lang.reflect.Constructor +// MK import java.lang.reflect.Constructor import kotlin.math.max import kotlin.reflect.KClass @@ -27,7 +27,8 @@ class ComponentMapper( @PublishedApi internal var components: Array, @PublishedApi - internal val cstr: Constructor +// MK internal val cstr: Constructor + internal val cstr: () -> T ) { @PublishedApi internal val listeners = bag>(2) @@ -44,7 +45,8 @@ class ComponentMapper( } val cmp = components[entity.id] return if (cmp == null) { - val newCmp = cstr.newInstance().apply(configuration) +// MK val newCmp = cstr.newInstance().apply(configuration) + val newCmp = cstr.invoke().apply(configuration) components[entity.id] = newCmp listeners.forEach { it.onComponentAdded(entity, newCmp) } newCmp @@ -80,7 +82,8 @@ class ComponentMapper( * @throws [FleksNoSuchComponentException] if the [entity] does not have such a component. */ operator fun get(entity: Entity): T { - return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.name) +// MK return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.name) + return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.toString()) } /** @@ -112,7 +115,8 @@ class ComponentMapper( operator fun contains(listener: ComponentListener) = listener in listeners override fun toString(): String { - return "ComponentMapper(id=$id, component=${cstr.name})" +// MK return "ComponentMapper(id=$id, component=${cstr.name})" + return "ComponentMapper(id=$id, component=${cstr})" } } @@ -137,11 +141,13 @@ class ComponentService { /** * Returns a [ComponentMapper] for the given [type]. If the mapper does not exist then it will be created. * + * @param gen The generator function for creating [type] component objects. * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given [type] does not have * a no argument constructor. */ @Suppress("UNCHECKED_CAST") - fun mapper(type: KClass): ComponentMapper { +// MK fun mapper(type: KClass): ComponentMapper { + fun mapper(type: KClass, gen: () -> T): ComponentMapper { var mapper = mappers[type] if (mapper == null) { @@ -149,8 +155,9 @@ class ComponentService { mapper = ComponentMapper( mappers.size, Array(64) { null } as Array, - // use java constructor because it is ~4x faster than calling Kotlin's createInstance on a KClass - type.java.getDeclaredConstructor() +// MK // use java constructor because it is ~4x faster than calling Kotlin's createInstance on a KClass +// type.java.getDeclaredConstructor() + gen ) mappers[type] = mapper mappersBag.add(mapper) @@ -165,10 +172,12 @@ class ComponentService { /** * Returns a [ComponentMapper] for the specific type. If the mapper does not exist then it will be created. * + * @param gen The generator function for creating [T] objects. * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the specific type does not have * a no argument constructor. */ - inline fun mapper(): ComponentMapper = mapper(T::class) +// MK inline fun mapper(): ComponentMapper = mapper(T::class) + inline fun mapper(noinline gen: () -> T): ComponentMapper = mapper(T::class, gen) /** * Returns an already existing [ComponentMapper] for the given [cmpId]. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 8284c3350..96d0ebfc8 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -7,7 +7,9 @@ import com.github.quillraven.fleks.collection.bag /** * An entity of a [world][World]. It represents a unique id. */ -class Entity(val id: Int) +// MK @JvmInline +// value class Entity(val id: Int) +data class Entity(val id: Int) /** * Interface of an [entity][Entity] listener that gets notified when the component configuration changes. @@ -50,8 +52,11 @@ class EntityCreateCfg( * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type * does not have a no argument constructor. */ - inline fun add(configuration: T.() -> Unit = {}): T { - val mapper = cmpService.mapper() +// MK inline fun add(configuration: T.() -> Unit = {}): T { +// val mapper = cmpService.mapper() + inline fun add(noinline gen: () -> T, configuration: T.() -> Unit = {}): T { + val mapper = cmpService.mapper(gen) + cmpMask.set(mapper.id) return mapper.addInternal(entity, configuration) } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt index 58fc16cdc..ceb0365cd 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt @@ -1,9 +1,11 @@ package com.github.quillraven.fleks -import java.lang.reflect.Constructor -import java.lang.reflect.ParameterizedType +// MK import java.lang.reflect.Constructor +// Mk import java.lang.reflect.ParameterizedType import kotlin.reflect.KClass +/* Mk + /** * Creates a new instance of type [T] by using dependency injection if necessary. * Dependencies are looked up in the [injectables] map. @@ -75,3 +77,5 @@ internal fun systemArgs( return args } + +Mk */ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 2bdcdfc7b..b31e20d1a 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -2,8 +2,10 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator -import java.lang.reflect.Field +// MK import java.lang.reflect.Field import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * An interval for an [IntervalSystem]. There are two kind of intervals: @@ -141,7 +143,8 @@ abstract class IteratingSystem( * Returns the [family][Family] of this system. * This reference gets updated by the [SystemService] when the system gets created via reflection. */ - private lateinit var family: Family + lateinit var family: Family + internal set /** * Returns the [entityService][EntityService] of this system. @@ -149,7 +152,6 @@ abstract class IteratingSystem( */ @PublishedApi internal lateinit var entityService: EntityService - private set /** * Flag that defines if sorting of [entities][Entity] will be performed the next time [onTick] is called. @@ -181,17 +183,18 @@ abstract class IteratingSystem( * that a removed entity of this family will still be part of the [onTickEntity] for the current iteration. */ override fun onTick() { - if (family.isDirty) { - family.updateActiveEntities() - } - if (doSort) { - doSort = sortingType == Automatic - family.sort(comparator) - } - - entityService.delayRemoval = true - family.forEach { onTickEntity(it) } - entityService.cleanupDelays() +// if (family.isDirty) { +// family.updateActiveEntities() +// } +// if (doSort) { +// doSort = sortingType == Automatic +// family.sort(comparator) +// } +// +// entityService.delayRemoval = true +// family.forEach { onTickEntity(it) } +// entityService.cleanupDelays() + println("IteratingSystem: onTick") } /** @@ -206,13 +209,13 @@ abstract class IteratingSystem( * @param alpha a value between 0 (inclusive) and 1 (exclusive) that describes the progress between two ticks. */ override fun onAlpha(alpha: Float) { - if (family.isDirty) { - family.updateActiveEntities() - } - - entityService.delayRemoval = true - family.forEach { onAlphaEntity(it, alpha) } - entityService.cleanupDelays() +// if (family.isDirty) { +// family.updateActiveEntities() +// } +// +// entityService.delayRemoval = true +// family.forEach { onAlphaEntity(it, alpha) } +// entityService.cleanupDelays() } /** @@ -235,12 +238,12 @@ abstract class IteratingSystem( * each time [update] is called. * * @param world the [world][World] the service belongs to. - * @param systemTypes the [systems][IntervalSystem] to be created. + * @param systemFactorys the factory methods to create the [systems][IntervalSystem]. * @param injectables the required dependencies to create the [systems][IntervalSystem]. */ class SystemService( world: World, - systemTypes: List>, + systemFactorys: MutableList IntervalSystem>>, injectables: Map ) { @PublishedApi @@ -251,26 +254,37 @@ class SystemService( val entityService = world.entityService val cmpService = world.componentService val allFamilies = mutableListOf() - systems = Array(systemTypes.size) { sysIdx -> - val sysType = systemTypes[sysIdx] - val newSystem = newInstance(sysType, cmpService, injectables) + val systemList = systemFactorys.toList() + systems = Array(systemFactorys.size) { sysIdx -> + val sysType = systemList[sysIdx].first + val newSystem = systemList[sysIdx].second.invoke() + +// // set world reference of newly created system +// val worldField = field(newSystem, "world") +// worldField.isAccessible = true +// worldField.set(newSystem, world) +// +// if (IteratingSystem::class.java.isAssignableFrom(sysType.java)) { +// // set family and entity service reference of newly created iterating system +// @Suppress("UNCHECKED_CAST") +// val family = family(sysType as KClass, entityService, cmpService, allFamilies) +// val famField = field(newSystem, "family") +// famField.isAccessible = true +// famField.set(newSystem, family) +// +// val eServiceField = field(newSystem, "entityService") +// eServiceField.isAccessible = true +// eServiceField.set(newSystem, entityService) +// } +// // set world reference of newly created system - val worldField = field(newSystem, "world") - worldField.isAccessible = true - worldField.set(newSystem, world) + newSystem.world = world - if (IteratingSystem::class.java.isAssignableFrom(sysType.java)) { + if (sysType == typeOf()) { // set family and entity service reference of newly created iterating system - @Suppress("UNCHECKED_CAST") - val family = family(sysType as KClass, entityService, cmpService, allFamilies) - val famField = field(newSystem, "family") - famField.isAccessible = true - famField.set(newSystem, family) - - val eServiceField = field(newSystem, "entityService") - eServiceField.isAccessible = true - eServiceField.set(newSystem, entityService) +// TODO (newSystem as IteratingSystem).family = family(sysType as KClass, entityService, cmpService, allFamilies) + (newSystem as IteratingSystem).entityService = entityService } newSystem.apply { onInit() } @@ -280,9 +294,11 @@ class SystemService( /** * Returns [Annotation] of the specific type if the class has that annotation. Otherwise, returns null. */ - private inline fun KClass<*>.annotation(): T? { - return this.java.getAnnotation(T::class.java) - } +// MK private inline fun KClass<*>.annotation(): T? { +// return this.java.getAnnotation(T::class.java) +// } + +/* /** * Creates or returns an already created [family][Family] for the given [IteratingSystem] @@ -343,7 +359,7 @@ class SystemService( } return family } - +/*MK */ /** * Returns a [Field] of name [fieldName] of the given [system]. * @@ -363,7 +379,7 @@ class SystemService( } return classField } - +Mk */ /** * Returns the specified [system][IntervalSystem]. * diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 6f019a858..6346c7bc6 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -1,7 +1,9 @@ package com.github.quillraven.fleks -import java.lang.reflect.ParameterizedType +// MK import java.lang.reflect.ParameterizedType import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * An optional annotation for an [IntervalSystem] constructor parameter to @@ -32,7 +34,7 @@ class WorldConfiguration { var entityCapacity = 512 @PublishedApi - internal val systemTypes = mutableListOf>() + internal val systemFactorys = mutableListOf IntervalSystem>>() @PublishedApi internal val injectables = mutableMapOf() @@ -40,18 +42,24 @@ class WorldConfiguration { @PublishedApi internal val cmpListenerTypes = mutableListOf>>() + private val systemTypes = mutableListOf>() + /** * Adds the specified [IntervalSystem] to the [world][World]. * The order in which systems are added is the order in which they will be executed when calling [World.update]. * + * @param factory A function which creates an object of type [T]. * @throws [FleksSystemAlreadyAddedException] if the system was already added before. */ - inline fun system() { - val systemType = T::class + fun system(factory: () -> T) { + val systemType = factory()::class if (systemType in systemTypes) { throw FleksSystemAlreadyAddedException(systemType) } systemTypes.add(systemType) + // Save factory method for creation of system together with its base type class + val type: KType = if (factory() is IteratingSystem) typeOf() else typeOf() + systemFactorys.add(Pair(type, factory)) } /** @@ -75,7 +83,8 @@ class WorldConfiguration { * @throws [FleksInjectableWithoutNameException] if the qualifiedName of the [dependency] is null. */ inline fun inject(dependency: T) { - val key = T::class.qualifiedName ?: throw FleksInjectableWithoutNameException() +// MK val key = T::class.qualifiedName ?: throw FleksInjectableWithoutNameException() + val key = typeOf().toString() inject(key, dependency) } @@ -134,18 +143,18 @@ class World( val worldCfg = WorldConfiguration().apply(cfg) entityService = EntityService(worldCfg.entityCapacity, componentService) val injectables = worldCfg.injectables - systemService = SystemService(this, worldCfg.systemTypes, injectables) + systemService = SystemService(this, worldCfg.systemFactorys, injectables) // create and register ComponentListener - worldCfg.cmpListenerTypes.forEach { listenerType -> - val listener = newInstance(listenerType, componentService, injectables) - val genInter = listener.javaClass.genericInterfaces.first { - it is ParameterizedType && it.rawType == ComponentListener::class.java - } - val cmpType = (genInter as ParameterizedType).actualTypeArguments[0] - val mapper = componentService.mapper((cmpType as Class<*>).kotlin) - mapper.addComponentListenerInternal(listener) - } +// Mk worldCfg.cmpListenerTypes.forEach { listenerType -> +// val listener = newInstance(listenerType, componentService, injectables) +// val genInter = listener.javaClass.genericInterfaces.first { +// it is ParameterizedType && it.rawType == ComponentListener::class.java +// } +// val cmpType = (genInter as ParameterizedType).actualTypeArguments[0] +// val mapper = componentService.mapper((cmpType as Class<*>).kotlin) +// mapper.addComponentListenerInternal(listener) +// } // verify that there are no unused injectables val unusedInjectables = injectables.filterValues { !it.used }.map { it.value.injObj::class } @@ -192,7 +201,8 @@ class World( * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type does not have * a no argument constructor. */ - inline fun mapper(): ComponentMapper = componentService.mapper(T::class) +// MK inline fun mapper(): ComponentMapper = componentService.mapper(T::class) + inline fun mapper(noinline gen: () -> T): ComponentMapper = componentService.mapper(T::class, gen) /** * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 1ec97c010..737818eb8 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -1,3 +1,4 @@ +import com.github.quillraven.fleks.* import com.soywiz.klock.* import com.soywiz.korge.Korge import com.soywiz.korge.scene.MaskTransition @@ -7,15 +8,6 @@ import com.soywiz.korge.view.* import com.soywiz.korge.view.filter.TransitionFilter import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors -import com.soywiz.korim.format.* -import com.soywiz.korio.file.std.resourcesVfs -import ecs.entitysystem.EntitySystem -import ecs.entitytypes.SpawnerEntity -import ecs.subsystems.ImageAnimationSystem -import ecs.subsystems.MovingSystem -import ecs.subsystems.SpawningSystem - -import com.github.quillraven.fleks.World const val scaleFactor = 1 @@ -32,24 +24,43 @@ suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor ) } -var aseImage: ImageData? = null - class ExampleScene : Scene() { private val atlas = MutableAtlasUnit(1024, 1024) - private val entitySystem: EntitySystem = EntitySystem() override suspend fun Container.sceneInit() { - val sw = Stopwatch().start() - aseImage = resourcesVfs["sprites2.ase"].readImageData(ASE, atlas = atlas) - println("loaded resources in ${sw.elapsed}") } override suspend fun Container.sceneMain() { - container { - scale(scaleFactor) + val world = World { + entityCapacity = 20 + + system(::MoveSystem) + system(::PositionSystem) + } + addUpdater() { dt -> + world.update(dt.milliseconds.toFloat()) } } } + +class MoveSystem : IntervalSystem( + interval = Fixed(1000f) // every second +) { + + override fun onTick() { + println("MoveSystem: onTick") + } +} + +class PositionSystem : IteratingSystem( + interval = Fixed(500f) // every 500 millisecond +) { + + override fun onTickEntity(entity: Entity) { + println("PositionSystem: onTickEntity") + } + +} From 4b2437e5334f61f1b24019686f1bc31e73273824 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 24 Jan 2022 22:18:35 +0100 Subject: [PATCH 03/27] Add Injector to IntervalSystem --- .../com/github/quillraven/fleks/exception.kt | 13 ++++---- .../com/github/quillraven/fleks/system.kt | 32 ++++++++++++++++--- .../com/github/quillraven/fleks/world.kt | 16 ++++------ .../fleks-ecs/src/commonMain/kotlin/main.kt | 26 ++++++++------- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 60f43cf86..7d74a0b42 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -1,20 +1,21 @@ package com.github.quillraven.fleks import kotlin.reflect.KClass +import kotlin.reflect.KType abstract class FleksException(message: String) : RuntimeException(message) class FleksSystemAlreadyAddedException(system: KClass<*>) : FleksException("System ${system.simpleName} is already part of the ${WorldConfiguration::class.simpleName}") -class FleksSystemCreationException(system: KClass<*>, details: String) : - FleksException("Cannot create ${system.simpleName}. Did you add all necessary injectables?\nDetails: $details") +class FleksSystemCreationException(injectType: KType) : + FleksException("Cannot create system. Injection object of type $injectType cannot be found. Did you add all necessary injectables?") class FleksNoSuchSystemException(system: KClass<*>) : FleksException("There is no system of type ${system.simpleName} in the world") -class FleksInjectableAlreadyAddedException(name: String) : - FleksException("Injectable with name $name is already part of the ${WorldConfiguration::class.simpleName}") +class FleksInjectableAlreadyAddedException(type: KType) : + FleksException("Injectable with name $type is already part of the ${WorldConfiguration::class.simpleName}") class FleksInjectableWithoutNameException : FleksException("Injectables must be registered with a non-null name") @@ -31,5 +32,5 @@ class FleksComponentListenerAlreadyAddedException(listener: KClass>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") -class FleksReflectionException(type: KClass<*>, details: String) : - FleksException("Cannot create ${type.simpleName}.\nDetails: $details") +class FleksReflectionException(type: KType, details: String) : + FleksException("Cannot create $type.\nDetails: $details") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index b31e20d1a..075842b53 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -1,9 +1,7 @@ package com.github.quillraven.fleks -import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator // MK import java.lang.reflect.Field -import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -44,6 +42,12 @@ abstract class IntervalSystem( lateinit var world: World internal set + /** + * An [Injector] which is used to inject objects from outside the [IntervalSystem]. + */ + @PublishedApi + internal lateinit var injector: Injector + private var accumulator: Float = 0.0f /** @@ -244,7 +248,7 @@ abstract class IteratingSystem( class SystemService( world: World, systemFactorys: MutableList IntervalSystem>>, - injectables: Map + injectables: MutableMap ) { @PublishedApi internal val systems: Array @@ -252,8 +256,8 @@ class SystemService( init { // create systems val entityService = world.entityService - val cmpService = world.componentService - val allFamilies = mutableListOf() + val cmpService = world.componentService // TODO add to newSystem + val allFamilies = mutableListOf() // TODO add to newSystem val systemList = systemFactorys.toList() systems = Array(systemFactorys.size) { sysIdx -> val sysType = systemList[sysIdx].first @@ -287,6 +291,8 @@ class SystemService( (newSystem as IteratingSystem).entityService = entityService } + newSystem.injector = Injector(injectables) + newSystem.apply { onInit() } } } @@ -413,3 +419,19 @@ Mk */ systems.forEach { it.onDispose() } } } + +/** + * An [Injector] which is used to inject objects from outside the [IntervalSystem]. + */ +class Injector( + @PublishedApi + internal val injectObjects: Map +) { + inline fun get(): T { + val injectType = typeOf() + if (injectType in injectObjects) { + injectObjects[injectType]!!.used = true + return injectObjects[injectType]!!.injObj as T + } else throw FleksSystemCreationException(injectType) + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 6346c7bc6..346642502 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -37,7 +37,7 @@ class WorldConfiguration { internal val systemFactorys = mutableListOf IntervalSystem>>() @PublishedApi - internal val injectables = mutableMapOf() + internal val injectables = mutableMapOf() @PublishedApi internal val cmpListenerTypes = mutableListOf>>() @@ -63,16 +63,16 @@ class WorldConfiguration { } /** - * Adds the specified [dependency] under the given [name] which can then be injected to any [IntervalSystem]. + * Adds the specified [dependency] under the given [type] which can then be injected to any [IntervalSystem]. * * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. */ - fun inject(name: String, dependency: T) { - if (name in injectables) { - throw FleksInjectableAlreadyAddedException(name) + fun inject(type: KType, dependency: T) { + if (type in injectables) { + throw FleksInjectableAlreadyAddedException(type) } - injectables[name] = Injectable(dependency) + injectables[type] = Injectable(dependency) } /** @@ -80,11 +80,9 @@ class WorldConfiguration { * Refer to [inject]: the name is the qualifiedName of the class of the [dependency]. * * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. - * @throws [FleksInjectableWithoutNameException] if the qualifiedName of the [dependency] is null. */ inline fun inject(dependency: T) { -// MK val key = T::class.qualifiedName ?: throw FleksInjectableWithoutNameException() - val key = typeOf().toString() + val key = typeOf() inject(key, dependency) } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 737818eb8..4444647b6 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -1,13 +1,10 @@ -import com.github.quillraven.fleks.* -import com.soywiz.klock.* import com.soywiz.korge.Korge -import com.soywiz.korge.scene.MaskTransition import com.soywiz.korge.scene.Scene import com.soywiz.korge.scene.sceneContainer import com.soywiz.korge.view.* -import com.soywiz.korge.view.filter.TransitionFilter import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors +import com.github.quillraven.fleks.* const val scaleFactor = 1 @@ -18,10 +15,7 @@ suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor val rootSceneContainer = sceneContainer() views.debugViews = true - rootSceneContainer.changeTo( - transition = MaskTransition(transition = TransitionFilter.Transition.CIRCULAR, reversed = false, smooth = true), - time = 0.5.seconds - ) + rootSceneContainer.changeTo() } class ExampleScene : Scene() { @@ -33,25 +27,36 @@ class ExampleScene : Scene() { override suspend fun Container.sceneMain() { + val dummy = MyClass(text = "Hello injector!") + val world = World { entityCapacity = 20 system(::MoveSystem) system(::PositionSystem) + + inject(dummy) } - addUpdater() { dt -> + addUpdater { dt -> world.update(dt.milliseconds.toFloat()) } } } +data class MyClass(val text: String = "n/a") + class MoveSystem : IntervalSystem( interval = Fixed(1000f) // every second ) { + private lateinit var dummy: MyClass + + override fun onInit() { + dummy = injector.get() + } override fun onTick() { - println("MoveSystem: onTick") + println("MoveSystem: onTick (text: ${dummy.text})") } } @@ -62,5 +67,4 @@ class PositionSystem : IteratingSystem( override fun onTickEntity(entity: Entity) { println("PositionSystem: onTickEntity") } - } From da21e7ce57cc31f0915cc46671814990998aea09 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Tue, 25 Jan 2022 00:22:16 +0100 Subject: [PATCH 04/27] Integate creation of ComponentMapper objects in systems --- .../com/github/quillraven/fleks/component.kt | 36 ++++++++++--------- .../com/github/quillraven/fleks/entity.kt | 4 +-- .../com/github/quillraven/fleks/exception.kt | 4 +-- .../com/github/quillraven/fleks/system.kt | 16 ++++----- .../com/github/quillraven/fleks/world.kt | 2 +- .../fleks-ecs/src/commonMain/kotlin/main.kt | 17 ++++++++- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index b24de6408..089085f45 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -4,7 +4,8 @@ import com.github.quillraven.fleks.collection.Bag import com.github.quillraven.fleks.collection.bag // MK import java.lang.reflect.Constructor import kotlin.math.max -import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * Interface of a component listener that gets notified when a component of a specific type @@ -21,14 +22,15 @@ interface ComponentListener { * * Refer to [ComponentService] for more details. */ +@Suppress("UNCHECKED_CAST") class ComponentMapper( @PublishedApi - internal val id: Int, +// MK internal val cstr: Constructor + internal val factory: () -> T, @PublishedApi - internal var components: Array, + internal val id: Int = 0, @PublishedApi -// MK internal val cstr: Constructor - internal val cstr: () -> T + internal var components: Array = Array(64) { null } as Array ) { @PublishedApi internal val listeners = bag>(2) @@ -46,7 +48,7 @@ class ComponentMapper( val cmp = components[entity.id] return if (cmp == null) { // MK val newCmp = cstr.newInstance().apply(configuration) - val newCmp = cstr.invoke().apply(configuration) + val newCmp = factory.invoke().apply(configuration) components[entity.id] = newCmp listeners.forEach { it.onComponentAdded(entity, newCmp) } newCmp @@ -83,7 +85,7 @@ class ComponentMapper( */ operator fun get(entity: Entity): T { // MK return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.name) - return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.toString()) + return components[entity.id] ?: throw FleksNoSuchComponentException(entity, factory.toString()) } /** @@ -116,7 +118,7 @@ class ComponentMapper( override fun toString(): String { // MK return "ComponentMapper(id=$id, component=${cstr.name})" - return "ComponentMapper(id=$id, component=${cstr})" + return "ComponentMapper(id=$id, component=${factory})" } } @@ -130,7 +132,7 @@ class ComponentService { * It is used by the [SystemService] during system creation and by the [EntityService] for entity creation. */ @PublishedApi - internal val mappers = HashMap, ComponentMapper<*>>() + internal val mappers = HashMap>() /** * Returns [Bag] of [ComponentMapper]. The id of the mapper is the index of the bag. @@ -147,23 +149,23 @@ class ComponentService { */ @Suppress("UNCHECKED_CAST") // MK fun mapper(type: KClass): ComponentMapper { - fun mapper(type: KClass, gen: () -> T): ComponentMapper { + fun mapper(type: KType, factory: () -> T): ComponentMapper { var mapper = mappers[type] if (mapper == null) { - try { +// try { mapper = ComponentMapper( + factory, mappers.size, Array(64) { null } as Array, // MK // use java constructor because it is ~4x faster than calling Kotlin's createInstance on a KClass // type.java.getDeclaredConstructor() - gen ) mappers[type] = mapper mappersBag.add(mapper) - } catch (e: Exception) { - throw FleksMissingNoArgsComponentConstructorException(type) - } +// } catch (e: Exception) { +// throw FleksMissingNoArgsComponentConstructorException(type) +// } } return mapper as ComponentMapper @@ -172,12 +174,12 @@ class ComponentService { /** * Returns a [ComponentMapper] for the specific type. If the mapper does not exist then it will be created. * - * @param gen The generator function for creating [T] objects. + * @param factory The generator function for creating [T] objects. * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the specific type does not have * a no argument constructor. */ // MK inline fun mapper(): ComponentMapper = mapper(T::class) - inline fun mapper(noinline gen: () -> T): ComponentMapper = mapper(T::class, gen) + inline fun mapper(noinline factory: () -> T): ComponentMapper = mapper(typeOf>(), factory) /** * Returns an already existing [ComponentMapper] for the given [cmpId]. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 96d0ebfc8..3fffa5db3 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -54,8 +54,8 @@ class EntityCreateCfg( */ // MK inline fun add(configuration: T.() -> Unit = {}): T { // val mapper = cmpService.mapper() - inline fun add(noinline gen: () -> T, configuration: T.() -> Unit = {}): T { - val mapper = cmpService.mapper(gen) + inline fun add(noinline factory: () -> T, configuration: T.() -> Unit = {}): T { + val mapper = cmpService.mapper(factory) cmpMask.set(mapper.id) return mapper.addInternal(entity, configuration) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 7d74a0b42..7c0fd36c3 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -20,8 +20,8 @@ class FleksInjectableAlreadyAddedException(type: KType) : class FleksInjectableWithoutNameException : FleksException("Injectables must be registered with a non-null name") -class FleksMissingNoArgsComponentConstructorException(component: KClass<*>) : - FleksException("Component ${component.simpleName} is missing a no-args constructor") +class FleksMissingNoArgsComponentConstructorException(component: KType) : + FleksException("Component $component is missing a no-args constructor") class FleksNoSuchComponentException(entity: Entity, component: String) : FleksException("Entity $entity has no component of type $component") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 075842b53..c4468c53b 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -247,8 +247,8 @@ abstract class IteratingSystem( */ class SystemService( world: World, - systemFactorys: MutableList IntervalSystem>>, - injectables: MutableMap + systemFactorys: List IntervalSystem>>, + injectables: Map ) { @PublishedApi internal val systems: Array @@ -256,14 +256,14 @@ class SystemService( init { // create systems val entityService = world.entityService - val cmpService = world.componentService // TODO add to newSystem +// Mk val cmpService = world.componentService val allFamilies = mutableListOf() // TODO add to newSystem val systemList = systemFactorys.toList() systems = Array(systemFactorys.size) { sysIdx -> val sysType = systemList[sysIdx].first val newSystem = systemList[sysIdx].second.invoke() -// // set world reference of newly created system +// Mk // set world reference of newly created system // val worldField = field(newSystem, "world") // worldField.isAccessible = true // worldField.set(newSystem, world) @@ -429,9 +429,9 @@ class Injector( ) { inline fun get(): T { val injectType = typeOf() - if (injectType in injectObjects) { - injectObjects[injectType]!!.used = true - return injectObjects[injectType]!!.injObj as T - } else throw FleksSystemCreationException(injectType) + return if (injectType in injectObjects) { + injectObjects[injectType]!!.used = true + injectObjects[injectType]!!.injObj as T + } else throw FleksSystemCreationException(injectType) } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 346642502..91d9a09ff 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -200,7 +200,7 @@ class World( * a no argument constructor. */ // MK inline fun mapper(): ComponentMapper = componentService.mapper(T::class) - inline fun mapper(noinline gen: () -> T): ComponentMapper = componentService.mapper(T::class, gen) + inline fun mapper(noinline factory: () -> T): ComponentMapper = componentService.mapper(typeOf>(), factory) /** * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 4444647b6..8f72b89a2 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -38,17 +38,26 @@ class ExampleScene : Scene() { inject(dummy) } + val entity = world.entity { + add(::Position) { + x = 50f + y = 100f + } + } + addUpdater { dt -> world.update(dt.milliseconds.toFloat()) } } } -data class MyClass(val text: String = "n/a") +data class MyClass(val text: String = "") +data class Position(var x: Float = 0f, var y: Float = 0f) class MoveSystem : IntervalSystem( interval = Fixed(1000f) // every second ) { + private lateinit var dummy: MyClass override fun onInit() { @@ -64,7 +73,13 @@ class PositionSystem : IteratingSystem( interval = Fixed(500f) // every 500 millisecond ) { + private val position = ComponentMapper(::Position) + + override fun onInit() { + } + override fun onTickEntity(entity: Entity) { println("PositionSystem: onTickEntity") } } + From a550710dfa4fc82b97263cc6b3246b0a169c7d85 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sat, 29 Jan 2022 10:16:35 +0100 Subject: [PATCH 05/27] Integrate Family creation First version which works. --- .../com/github/quillraven/fleks/component.kt | 76 +++---- .../com/github/quillraven/fleks/entity.kt | 12 +- .../com/github/quillraven/fleks/exception.kt | 33 ++- .../com/github/quillraven/fleks/family.kt | 15 +- .../com/github/quillraven/fleks/reflection.kt | 81 ------- .../com/github/quillraven/fleks/system.kt | 203 +++++++----------- .../com/github/quillraven/fleks/world.kt | 57 +++-- .../fleks-ecs/src/commonMain/kotlin/main.kt | 11 +- settings.gradle.kts | 2 + 9 files changed, 176 insertions(+), 314 deletions(-) delete mode 100644 samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 089085f45..51decfbe6 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -2,10 +2,8 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.Bag import com.github.quillraven.fleks.collection.bag -// MK import java.lang.reflect.Constructor import kotlin.math.max -import kotlin.reflect.KType -import kotlin.reflect.typeOf +import kotlin.reflect.KClass /** * Interface of a component listener that gets notified when a component of a specific type @@ -25,12 +23,11 @@ interface ComponentListener { @Suppress("UNCHECKED_CAST") class ComponentMapper( @PublishedApi -// MK internal val cstr: Constructor - internal val factory: () -> T, - @PublishedApi internal val id: Int = 0, @PublishedApi - internal var components: Array = Array(64) { null } as Array + internal var components: Array = Array(64) { null } as Array, + @PublishedApi + internal val factory: () -> T ) { @PublishedApi internal val listeners = bag>(2) @@ -47,7 +44,6 @@ class ComponentMapper( } val cmp = components[entity.id] return if (cmp == null) { -// MK val newCmp = cstr.newInstance().apply(configuration) val newCmp = factory.invoke().apply(configuration) components[entity.id] = newCmp listeners.forEach { it.onComponentAdded(entity, newCmp) } @@ -81,12 +77,9 @@ class ComponentMapper( /** * Returns a component of the specific type of the given [entity]. * - * @throws [FleksNoSuchComponentException] if the [entity] does not have such a component. + * @throws [FleksNoSuchEntityComponentException] if the [entity] does not have such a component. */ - operator fun get(entity: Entity): T { -// MK return components[entity.id] ?: throw FleksNoSuchComponentException(entity, cstr.name) - return components[entity.id] ?: throw FleksNoSuchComponentException(entity, factory.toString()) - } + operator fun get(entity: Entity): T = components[entity.id] ?: throw FleksNoSuchEntityComponentException(entity, factory.toString()) /** * Returns true if and only if the given [entity] has a component of the specific type. @@ -116,23 +109,22 @@ class ComponentMapper( */ operator fun contains(listener: ComponentListener) = listener in listeners - override fun toString(): String { -// MK return "ComponentMapper(id=$id, component=${cstr.name})" - return "ComponentMapper(id=$id, component=${factory})" - } + override fun toString(): String = "ComponentMapper(id=$id, component=${factory})" } /** * A service class that is responsible for managing [ComponentMapper] instances. * It creates a [ComponentMapper] for every unique component type and assigns a unique id for each mapper. */ -class ComponentService { +class ComponentService( + componentFactory: Map, () -> Any> +) { /** * Returns map of [ComponentMapper] that stores mappers by its component type. * It is used by the [SystemService] during system creation and by the [EntityService] for entity creation. */ @PublishedApi - internal val mappers = HashMap>() + internal val mappers: Map, ComponentMapper<*>> /** * Returns [Bag] of [ComponentMapper]. The id of the mapper is the index of the bag. @@ -140,46 +132,32 @@ class ComponentService { */ private val mappersBag = bag>() + init { + // Create component mappers with help of constructor functions from component factory + mappers = componentFactory.mapValues { entry -> + ComponentMapper(factory = entry.value) + } + } + /** - * Returns a [ComponentMapper] for the given [type]. If the mapper does not exist then it will be created. + * Returns a [ComponentMapper] for the given [type]. * - * @param gen The generator function for creating [type] component objects. - * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given [type] does not have - * a no argument constructor. + * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the + * world configuration. */ @Suppress("UNCHECKED_CAST") -// MK fun mapper(type: KClass): ComponentMapper { - fun mapper(type: KType, factory: () -> T): ComponentMapper { - var mapper = mappers[type] - - if (mapper == null) { -// try { - mapper = ComponentMapper( - factory, - mappers.size, - Array(64) { null } as Array, -// MK // use java constructor because it is ~4x faster than calling Kotlin's createInstance on a KClass -// type.java.getDeclaredConstructor() - ) - mappers[type] = mapper - mappersBag.add(mapper) -// } catch (e: Exception) { -// throw FleksMissingNoArgsComponentConstructorException(type) -// } - } - + fun mapper(type: KClass): ComponentMapper { + val mapper = mappers[type] ?: throw FleksNoSuchComponentException(type) return mapper as ComponentMapper } /** - * Returns a [ComponentMapper] for the specific type. If the mapper does not exist then it will be created. + * Returns a [ComponentMapper] for the specific type. * - * @param factory The generator function for creating [T] objects. - * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the specific type does not have - * a no argument constructor. + * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the + * world configuration. */ -// MK inline fun mapper(): ComponentMapper = mapper(T::class) - inline fun mapper(noinline factory: () -> T): ComponentMapper = mapper(typeOf>(), factory) + inline fun mapper(): ComponentMapper = mapper(T::class) /** * Returns an already existing [ComponentMapper] for the given [cmpId]. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 3fffa5db3..6413feeba 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -7,8 +7,6 @@ import com.github.quillraven.fleks.collection.bag /** * An entity of a [world][World]. It represents a unique id. */ -// MK @JvmInline -// value class Entity(val id: Int) data class Entity(val id: Int) /** @@ -48,15 +46,9 @@ class EntityCreateCfg( /** * Adds and returns a component of the given type to the [entity] and * applies the [configuration] to the component. - * - * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type - * does not have a no argument constructor. */ -// MK inline fun add(configuration: T.() -> Unit = {}): T { -// val mapper = cmpService.mapper() - inline fun add(noinline factory: () -> T, configuration: T.() -> Unit = {}): T { - val mapper = cmpService.mapper(factory) - + inline fun add(configuration: T.() -> Unit = {}): T { + val mapper = cmpService.mapper() cmpMask.set(mapper.id) return mapper.addInternal(entity, configuration) } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 7c0fd36c3..4374fa5cc 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -1,36 +1,35 @@ package com.github.quillraven.fleks import kotlin.reflect.KClass -import kotlin.reflect.KType abstract class FleksException(message: String) : RuntimeException(message) class FleksSystemAlreadyAddedException(system: KClass<*>) : - FleksException("System ${system.simpleName} is already part of the ${WorldConfiguration::class.simpleName}") + FleksException("System ${system.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") -class FleksSystemCreationException(injectType: KType) : - FleksException("Cannot create system. Injection object of type $injectType cannot be found. Did you add all necessary injectables?") +class FleksComponentAlreadyAddedException(comp: KClass<*>) : + FleksException("Component ${comp.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") + +class FleksSystemCreationException(system: IteratingSystem) : + FleksException("Cannot create system '$system'. IteratingSystem must define at least one of AllOf, NoneOf or AnyOf properties.") class FleksNoSuchSystemException(system: KClass<*>) : - FleksException("There is no system of type ${system.simpleName} in the world") + FleksException("There is no system of type ${system.simpleName} in the world.") -class FleksInjectableAlreadyAddedException(type: KType) : - FleksException("Injectable with name $type is already part of the ${WorldConfiguration::class.simpleName}") +class FleksNoSuchComponentException(component: KClass<*>) : + FleksException("There is no component of type ${component.simpleName} in the ComponentMapper. Did you add the component to the ${WorldConfiguration::class.simpleName}?") -class FleksInjectableWithoutNameException : - FleksException("Injectables must be registered with a non-null name") +class FleksInjectableAlreadyAddedException(type: KClass<*>) : + FleksException("Injectable with name ${type} is already part of the ${WorldConfiguration::class.simpleName}.") -class FleksMissingNoArgsComponentConstructorException(component: KType) : - FleksException("Component $component is missing a no-args constructor") +class FleksSystemInjectException(injectType: KClass<*>) : + FleksException("Injection object of type ${injectType.simpleName} cannot be found. Did you add all necessary injectables?") -class FleksNoSuchComponentException(entity: Entity, component: String) : - FleksException("Entity $entity has no component of type $component") +class FleksNoSuchEntityComponentException(entity: Entity, component: String) : + FleksException("Entity $entity has no component of type $component.") class FleksComponentListenerAlreadyAddedException(listener: KClass>) : - FleksException("ComponentListener ${listener.simpleName} is already part of the ${WorldConfiguration::class.simpleName}") + FleksException("ComponentListener ${listener.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") class FleksUnusedInjectablesException(unused: List>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") - -class FleksReflectionException(type: KType, details: String) : - FleksException("Cannot create $type.\nDetails: $details") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index 80523016d..ab512a431 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -5,26 +5,23 @@ import com.github.quillraven.fleks.collection.EntityComparator import com.github.quillraven.fleks.collection.IntBag import kotlin.reflect.KClass -/** +/** TODO change readme * An annotation for an [IteratingSystem] to define a [Family]. * [Entities][Entity] must have all [components] specified to be part of the [family][Family]. */ -@Target(AnnotationTarget.CLASS) -annotation class AllOf(val components: Array> = []) +data class AllOf(val components: Array>) -/** +/** TODO change readme * An annotation for an [IteratingSystem] to define a [Family]. * [Entities][Entity] must not have any [components] specified to be part of the [family][Family]. */ -@Target(AnnotationTarget.CLASS) -annotation class NoneOf(val components: Array> = []) +data class NoneOf(val components: Array>) -/** +/** TODO change readme * An annotation for an [IteratingSystem] to define a [Family]. * [Entities][Entity] must have at least one of the [components] specified to be part of the [family][Family]. */ -@Target(AnnotationTarget.CLASS) -annotation class AnyOf(val components: Array> = []) +data class AnyOf(val components: Array>) /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt deleted file mode 100644 index ceb0365cd..000000000 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/reflection.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.quillraven.fleks - -// MK import java.lang.reflect.Constructor -// Mk import java.lang.reflect.ParameterizedType -import kotlin.reflect.KClass - -/* Mk - -/** - * Creates a new instance of type [T] by using dependency injection if necessary. - * Dependencies are looked up in the [injectables] map. - * If it is a [ComponentMapper] then the mapper is retrieved from the [cmpService]. - * - * @throws [FleksReflectionException] if there is no single primary constructor or if dependencies are missing. - * - * @throws [FleksMissingNoArgsComponentConstructorException] if a component of a [ComponentMapper] has no no-args constructor. - */ -@Suppress("UNCHECKED_CAST") -fun newInstance( - type: KClass, - cmpService: ComponentService, - injectables: Map -): T { - val constructors = type.java.declaredConstructors - if (constructors.size != 1) { - throw FleksReflectionException( - type, - "Found ${constructors.size} constructors. Only a single primary constructor is supported!" - ) - } - - // get constructor arguments - val args = systemArgs(constructors.first(), cmpService, injectables, type) - // create new instance using arguments from above - return constructors.first().newInstance(*args) as T -} - -/** - * Returns array of arguments for the given [primaryConstructor]. - * Arguments are either an object of [injectables] or [ComponentMapper] instances. - * - * @throws [FleksReflectionException] if [injectables] are missing for the [primaryConstructor]. - * - * @throws [FleksMissingNoArgsComponentConstructorException] if the [primaryConstructor] requires a [ComponentMapper] - * with a component type that does not have a no argument constructor. - */ -@PublishedApi -internal fun systemArgs( - primaryConstructor: Constructor<*>, - cmpService: ComponentService, - injectables: Map, - type: KClass -): Array { - val params = primaryConstructor.parameters - val paramAnnotations = primaryConstructor.parameterAnnotations - - val args = Array(params.size) { idx -> - val param = params[idx] - val paramClass = param.type.kotlin - if (paramClass == ComponentMapper::class) { - val cmpType = (param.parameterizedType as ParameterizedType).actualTypeArguments[0] as Class<*> - cmpService.mapper(cmpType.kotlin) - } else { - // check if qualifier annotation is specified - val qualifierAnn = paramAnnotations[idx].firstOrNull { it is Qualifier } as Qualifier? - // if yes -> use qualifier name - // if no -> use qualified class name - val name = qualifierAnn?.name ?: paramClass.qualifiedName - val injectable = injectables[name] ?: throw FleksReflectionException( - type, - "Missing injectable of type ${paramClass.qualifiedName}" - ) - injectable.used = true - injectable.injObj - } - } - - return args -} - -Mk */ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index c4468c53b..37f3ae0f3 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -1,7 +1,8 @@ package com.github.quillraven.fleks +import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator -// MK import java.lang.reflect.Field +import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -128,8 +129,10 @@ object Automatic : SortingType object Manual : SortingType /** - * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. It must be linked to a - * [family][Family] using at least one of the [AllOf], [AnyOf] or [NoneOf] annotations. + * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. + * + * It must have at least one of [AllOf], [AnyOf] or [NoneOf] objects defined. These objects define + * a [Family] to which this [IteratingSystem] belongs. * * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. * Default value is an empty comparator which means no sorting. @@ -138,6 +141,9 @@ object Manual : SortingType * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. */ abstract class IteratingSystem( + val allOf: AllOf? = null, + val noneOf: NoneOf? = null, + val anyOf: AnyOf? = null, private val comparator: EntityComparator = EMPTY_COMPARATOR, private val sortingType: SortingType = Automatic, interval: Interval = EachFrame, @@ -187,18 +193,17 @@ abstract class IteratingSystem( * that a removed entity of this family will still be part of the [onTickEntity] for the current iteration. */ override fun onTick() { -// if (family.isDirty) { -// family.updateActiveEntities() -// } -// if (doSort) { -// doSort = sortingType == Automatic -// family.sort(comparator) -// } -// -// entityService.delayRemoval = true -// family.forEach { onTickEntity(it) } -// entityService.cleanupDelays() - println("IteratingSystem: onTick") + if (family.isDirty) { + family.updateActiveEntities() + } + if (doSort) { + doSort = sortingType == Automatic + family.sort(comparator) + } + + entityService.delayRemoval = true + family.forEach { onTickEntity(it) } + entityService.cleanupDelays() } /** @@ -213,13 +218,13 @@ abstract class IteratingSystem( * @param alpha a value between 0 (inclusive) and 1 (exclusive) that describes the progress between two ticks. */ override fun onAlpha(alpha: Float) { -// if (family.isDirty) { -// family.updateActiveEntities() -// } -// -// entityService.delayRemoval = true -// family.forEach { onAlphaEntity(it, alpha) } -// entityService.cleanupDelays() + if (family.isDirty) { + family.updateActiveEntities() + } + + entityService.delayRemoval = true + family.forEach { onAlphaEntity(it, alpha) } + entityService.cleanupDelays() } /** @@ -247,65 +252,38 @@ abstract class IteratingSystem( */ class SystemService( world: World, - systemFactorys: List IntervalSystem>>, - injectables: Map + systemFactory: MutableMap, () -> IntervalSystem>, + injectables: MutableMap, Injectable> ) { @PublishedApi internal val systems: Array init { - // create systems + // Create systems val entityService = world.entityService -// Mk val cmpService = world.componentService - val allFamilies = mutableListOf() // TODO add to newSystem - val systemList = systemFactorys.toList() - systems = Array(systemFactorys.size) { sysIdx -> - val sysType = systemList[sysIdx].first + val compService = world.componentService + val allFamilies = mutableListOf() + val systemList = systemFactory.toList() +// val systemList = systemFactory.mapValuesTo(destination = , ) toList() +// val systemList: Array = systemFactory.fil to mapKeysTo() //toList() toList() +// TODO check if we can use here some map lambda to directly create an Array object with fix size + systems = Array(systemFactory.size) { sysIdx -> val newSystem = systemList[sysIdx].second.invoke() -// Mk // set world reference of newly created system -// val worldField = field(newSystem, "world") -// worldField.isAccessible = true -// worldField.set(newSystem, world) -// -// if (IteratingSystem::class.java.isAssignableFrom(sysType.java)) { -// // set family and entity service reference of newly created iterating system -// @Suppress("UNCHECKED_CAST") -// val family = family(sysType as KClass, entityService, cmpService, allFamilies) -// val famField = field(newSystem, "family") -// famField.isAccessible = true -// famField.set(newSystem, family) -// -// val eServiceField = field(newSystem, "entityService") -// eServiceField.isAccessible = true -// eServiceField.set(newSystem, entityService) -// } -// - - // set world reference of newly created system + // Set world reference of newly created system newSystem.world = world - if (sysType == typeOf()) { - // set family and entity service reference of newly created iterating system -// TODO (newSystem as IteratingSystem).family = family(sysType as KClass, entityService, cmpService, allFamilies) - (newSystem as IteratingSystem).entityService = entityService + // Set family and entity service reference of newly created iterating system + if (newSystem is IteratingSystem) { + newSystem.family = family(newSystem, entityService, compService, allFamilies) + newSystem.entityService = entityService } - newSystem.injector = Injector(injectables) - + newSystem.injector = Injector(injectables, compService.mappers) newSystem.apply { onInit() } } } - /** - * Returns [Annotation] of the specific type if the class has that annotation. Otherwise, returns null. - */ -// MK private inline fun KClass<*>.annotation(): T? { -// return this.java.getAnnotation(T::class.java) -// } - -/* - /** * Creates or returns an already created [family][Family] for the given [IteratingSystem] * by analyzing the system's [AllOf], [AnyOf] and [NoneOf] annotations. @@ -313,49 +291,29 @@ class SystemService( * @throws [FleksSystemCreationException] if the [IteratingSystem] does not contain at least one * [AllOf], [AnyOf] or [NoneOf] annotation. * - * @throws [FleksMissingNoArgsComponentConstructorException] if the [AllOf], [NoneOf] or [AnyOf] annotations - * of the system have a component type that does not have a no argument constructor. + * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the + * world configuration. */ private fun family( - sysType: KClass, + system: IteratingSystem, entityService: EntityService, cmpService: ComponentService, allFamilies: MutableList ): Family { - val allOfAnn = sysType.annotation() - val allOfCmps = if (allOfAnn != null && allOfAnn.components.isNotEmpty()) { - allOfAnn.components.map { cmpService.mapper(it) } - } else { - null - } + val allOfComps = system.allOf?.components?.map { cmpService.mapper(it) } + val noneOfComps = system.noneOf?.components?.map { cmpService.mapper(it) } + val anyOfComps = system.anyOf?.components?.map { cmpService.mapper(it) } - val noneOfAnn = sysType.annotation() - val noneOfCmps = if (noneOfAnn != null && noneOfAnn.components.isNotEmpty()) { - noneOfAnn.components.map { cmpService.mapper(it) } - } else { - null - } - - val anyOfAnn = sysType.annotation() - val anyOfCmps = if (anyOfAnn != null && anyOfAnn.components.isNotEmpty()) { - anyOfAnn.components.map { cmpService.mapper(it) } - } else { - null - } - - if ((allOfCmps == null || allOfCmps.isEmpty()) - && (noneOfCmps == null || noneOfCmps.isEmpty()) - && (anyOfCmps == null || anyOfCmps.isEmpty()) + if ((allOfComps == null || allOfComps.isEmpty()) + && (noneOfComps == null || noneOfComps.isEmpty()) + && (anyOfComps == null || anyOfComps.isEmpty()) ) { - throw FleksSystemCreationException( - sysType, - "IteratingSystem must define at least one of AllOf, NoneOf or AnyOf" - ) + throw FleksSystemCreationException(system) } - val allBs = if (allOfCmps == null) null else BitArray().apply { allOfCmps.forEach { this.set(it.id) } } - val noneBs = if (noneOfCmps == null) null else BitArray().apply { noneOfCmps.forEach { this.set(it.id) } } - val anyBs = if (anyOfCmps == null) null else BitArray().apply { anyOfCmps.forEach { this.set(it.id) } } + val allBs = if (allOfComps == null) null else BitArray().apply { allOfComps.forEach { this.set(it.id) } } + val noneBs = if (noneOfComps == null) null else BitArray().apply { noneOfComps.forEach { this.set(it.id) } } + val anyBs = if (anyOfComps == null) null else BitArray().apply { anyOfComps.forEach { this.set(it.id) } } var family = allFamilies.find { it.allOf == allBs && it.noneOf == noneBs && it.anyOf == anyBs } if (family == null) { @@ -365,27 +323,7 @@ class SystemService( } return family } -/*MK */ - /** - * Returns a [Field] of name [fieldName] of the given [system]. - * - * @throws [FleksSystemCreationException] if the [system] does not have a [Field] of name [fieldName]. - */ - private fun field(system: IntervalSystem, fieldName: String): Field { - var sysClass: Class<*> = system::class.java - var classField: Field? = null - while (classField == null) { - try { - classField = sysClass.getDeclaredField(fieldName) - } catch (e: NoSuchFieldException) { - val supC = sysClass.superclass ?: throw FleksSystemCreationException(system::class, "No '$fieldName' field found") - sysClass = supC - } - } - return classField - } -Mk */ /** * Returns the specified [system][IntervalSystem]. * @@ -422,16 +360,35 @@ Mk */ /** * An [Injector] which is used to inject objects from outside the [IntervalSystem]. + * + * @throws [FleksSystemInjectException] if the Injector does not contain an entry + * for the given type in its internal maps. */ class Injector( @PublishedApi - internal val injectObjects: Map + internal val injectObjects: Map, Injectable>, + @PublishedApi + internal val mapperObjects: Map, ComponentMapper<*>> ) { - inline fun get(): T { - val injectType = typeOf() - return if (injectType in injectObjects) { + inline fun dependency(): T { + val injectType = T::class + return when { + (injectType in injectObjects) -> { injectObjects[injectType]!!.used = true injectObjects[injectType]!!.injObj as T - } else throw FleksSystemCreationException(injectType) + } + (injectType in mapperObjects) -> { + mapperObjects[injectType]!! as T + } + else -> throw FleksSystemInjectException(injectType) + } + } + + @Suppress("UNCHECKED_CAST") + inline fun componentMapper(): ComponentMapper { + val injectType = T::class + return if (injectType in mapperObjects) { + mapperObjects[injectType]!! as ComponentMapper + } else throw FleksSystemInjectException(injectType) } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 91d9a09ff..69cb05428 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -1,6 +1,5 @@ package com.github.quillraven.fleks -// MK import java.lang.reflect.ParameterizedType import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -34,15 +33,17 @@ class WorldConfiguration { var entityCapacity = 512 @PublishedApi - internal val systemFactorys = mutableListOf IntervalSystem>>() + internal val systemFactory = mutableMapOf, () -> IntervalSystem>() @PublishedApi - internal val injectables = mutableMapOf() + internal val injectables = mutableMapOf, Injectable>() @PublishedApi - internal val cmpListenerTypes = mutableListOf>>() + internal val compListenerTypes = mutableListOf>>() + + @PublishedApi + internal val componentFactory = mutableMapOf, () -> Any>() - private val systemTypes = mutableListOf>() /** * Adds the specified [IntervalSystem] to the [world][World]. @@ -51,15 +52,24 @@ class WorldConfiguration { * @param factory A function which creates an object of type [T]. * @throws [FleksSystemAlreadyAddedException] if the system was already added before. */ - fun system(factory: () -> T) { - val systemType = factory()::class - if (systemType in systemTypes) { + inline fun system(noinline factory: () -> T) { + val systemType = T::class + if (systemType in systemFactory) { throw FleksSystemAlreadyAddedException(systemType) } - systemTypes.add(systemType) - // Save factory method for creation of system together with its base type class - val type: KType = if (factory() is IteratingSystem) typeOf() else typeOf() - systemFactorys.add(Pair(type, factory)) + systemFactory[systemType] = factory + } + + // TODO Add the specified [Component] to the [World]. +// fun component(factory: () -> T) { + inline fun component(noinline factory: () -> T) { +// val compType: ComponentMapper = ComponentMapper(factory = factory) // ()> //::class //factory()::class + val compType = T::class + if (compType in componentFactory) { + throw FleksComponentAlreadyAddedException(compType) + } + componentFactory[compType] = factory +// val compMapper = ComponentMapper(factory = factory) } /** @@ -67,7 +77,7 @@ class WorldConfiguration { * * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. */ - fun inject(type: KType, dependency: T) { + fun inject(type: KClass, dependency: T) { if (type in injectables) { throw FleksInjectableAlreadyAddedException(type) } @@ -82,7 +92,7 @@ class WorldConfiguration { * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. */ inline fun inject(dependency: T) { - val key = typeOf() + val key = T::class inject(key, dependency) } @@ -93,10 +103,10 @@ class WorldConfiguration { */ inline fun > componentListener() { val listenerType = T::class - if (listenerType in cmpListenerTypes) { + if (listenerType in compListenerTypes) { throw FleksComponentListenerAlreadyAddedException(listenerType) } - cmpListenerTypes.add(listenerType) + compListenerTypes.add(listenerType) } } @@ -120,7 +130,7 @@ class World( internal val systemService: SystemService @PublishedApi - internal val componentService = ComponentService() + internal val componentService: ComponentService @PublishedApi internal val entityService: EntityService @@ -139,9 +149,11 @@ class World( init { val worldCfg = WorldConfiguration().apply(cfg) + // Create first ComponentService + componentService = ComponentService(worldCfg.componentFactory) entityService = EntityService(worldCfg.entityCapacity, componentService) val injectables = worldCfg.injectables - systemService = SystemService(this, worldCfg.systemFactorys, injectables) + systemService = SystemService(this, worldCfg.systemFactory, injectables) // create and register ComponentListener // Mk worldCfg.cmpListenerTypes.forEach { listenerType -> @@ -193,14 +205,15 @@ class World( return systemService.system() } +// inline fun registerComponent() + /** * Returns a [ComponentMapper] for the given type. If the mapper does not exist then it will be created. * - * @throws [FleksMissingNoArgsComponentConstructorException] if the component of the given type does not have - * a no argument constructor. + * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the + * world configuration. */ -// MK inline fun mapper(): ComponentMapper = componentService.mapper(T::class) - inline fun mapper(noinline factory: () -> T): ComponentMapper = componentService.mapper(typeOf>(), factory) + inline fun mapper(): ComponentMapper = componentService.mapper(T::class) /** * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 8f72b89a2..af44f8330 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -36,10 +36,13 @@ class ExampleScene : Scene() { system(::PositionSystem) inject(dummy) + + // Register all needed components + component(::Position) } val entity = world.entity { - add(::Position) { + add { x = 50f y = 100f } @@ -61,7 +64,7 @@ class MoveSystem : IntervalSystem( private lateinit var dummy: MyClass override fun onInit() { - dummy = injector.get() + dummy = injector.dependency() } override fun onTick() { @@ -70,12 +73,14 @@ class MoveSystem : IntervalSystem( } class PositionSystem : IteratingSystem( + allOf = AllOf(arrayOf(Position::class)), interval = Fixed(500f) // every 500 millisecond ) { - private val position = ComponentMapper(::Position) + private lateinit var position: ComponentMapper override fun onInit() { + position = injector.componentMapper() } override fun onTickEntity(entity: Entity) { diff --git a/settings.gradle.kts b/settings.gradle.kts index f384599d3..7c8288781 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,6 +59,8 @@ if (System.getenv("DISABLED_EXTRA_KORGE_LIBS") != "true") { //include(":tensork") //include(":samples:parallax-scrolling-aseprite") //include(":samples:tiled-background") +//include(":samples:ase-animations") +include(":samples:fleks-ecs") if (!inCI) { include(":korge-sandbox") From e781ba7234e41c443f3a61c475a01724b181f4d8 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sat, 29 Jan 2022 10:47:06 +0100 Subject: [PATCH 06/27] Add singleton for injecting objects into systems --- .../com/github/quillraven/fleks/system.kt | 25 ++++++++++--------- .../fleks-ecs/src/commonMain/kotlin/main.kt | 10 +++++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 37f3ae0f3..09b7a44b6 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -2,6 +2,7 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator +import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -46,8 +47,8 @@ abstract class IntervalSystem( /** * An [Injector] which is used to inject objects from outside the [IntervalSystem]. */ - @PublishedApi - internal lateinit var injector: Injector +// @PublishedApi +// internal lateinit var injector: Injector private var accumulator: Float = 0.0f @@ -259,14 +260,14 @@ class SystemService( internal val systems: Array init { + // Configure injector before instantiating systems + val compService = world.componentService + Inject.injectObjects = injectables + Inject.mapperObjects = compService.mappers // Create systems val entityService = world.entityService - val compService = world.componentService val allFamilies = mutableListOf() val systemList = systemFactory.toList() -// val systemList = systemFactory.mapValuesTo(destination = , ) toList() -// val systemList: Array = systemFactory.fil to mapKeysTo() //toList() toList() -// TODO check if we can use here some map lambda to directly create an Array object with fix size systems = Array(systemFactory.size) { sysIdx -> val newSystem = systemList[sysIdx].second.invoke() @@ -279,7 +280,6 @@ class SystemService( newSystem.entityService = entityService } - newSystem.injector = Injector(injectables, compService.mappers) newSystem.apply { onInit() } } } @@ -359,17 +359,18 @@ class SystemService( } /** - * An [Injector] which is used to inject objects from outside the [IntervalSystem]. + * An [injector][Inject] which is used to inject objects from outside the [IntervalSystem]. * * @throws [FleksSystemInjectException] if the Injector does not contain an entry * for the given type in its internal maps. */ -class Injector( +@ThreadLocal +object Inject { @PublishedApi - internal val injectObjects: Map, Injectable>, + internal lateinit var injectObjects: Map, Injectable> @PublishedApi - internal val mapperObjects: Map, ComponentMapper<*>> -) { + internal lateinit var mapperObjects: Map, ComponentMapper<*>> + inline fun dependency(): T { val injectType = T::class return when { diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index af44f8330..9d183462c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -5,6 +5,8 @@ import com.soywiz.korge.view.* import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors import com.github.quillraven.fleks.* +import kotlin.native.concurrent.ThreadLocal +import kotlin.reflect.KClass const val scaleFactor = 1 @@ -38,6 +40,7 @@ class ExampleScene : Scene() { inject(dummy) // Register all needed components + // TODO remove and create components directly on system creation time component(::Position) } @@ -61,10 +64,9 @@ class MoveSystem : IntervalSystem( interval = Fixed(1000f) // every second ) { - private lateinit var dummy: MyClass + private val dummy: MyClass = Inject.dependency() override fun onInit() { - dummy = injector.dependency() } override fun onTick() { @@ -77,14 +79,14 @@ class PositionSystem : IteratingSystem( interval = Fixed(500f) // every 500 millisecond ) { - private lateinit var position: ComponentMapper + private val position: ComponentMapper = Inject.componentMapper() override fun onInit() { - position = injector.componentMapper() } override fun onTickEntity(entity: Entity) { println("PositionSystem: onTickEntity") + println("pos id: ${position.id} x: ${position[entity].x} y: ${position[entity].y}") } } From 1dba117fa709f496e4902ad5986018bb21ff5ed0 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 31 Jan 2022 12:29:59 +0100 Subject: [PATCH 07/27] Implement adding of component listener in world configuration The component listener will be added together with registering the component in the world configuration. That was done because when registering the component listener the base type of the component is needed. Without reflections that is not possible to detect it from the component listener template type. Thus, adding component and listener objects together is the most elegant workaround - I guess. --- .../com/github/quillraven/fleks/component.kt | 6 +- .../com/github/quillraven/fleks/exception.kt | 4 +- .../com/github/quillraven/fleks/system.kt | 15 ++--- .../com/github/quillraven/fleks/world.kt | 65 +++++++++++-------- .../fleks-ecs/src/commonMain/kotlin/main.kt | 31 ++++++--- 5 files changed, 73 insertions(+), 48 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 51decfbe6..cfa80d0b5 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -134,8 +134,10 @@ class ComponentService( init { // Create component mappers with help of constructor functions from component factory - mappers = componentFactory.mapValues { entry -> - ComponentMapper(factory = entry.value) + mappers = componentFactory.mapValues { + val compMapper = ComponentMapper(factory = it.value) + mappersBag.add(compMapper) + compMapper } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 4374fa5cc..50fb6714c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -20,7 +20,7 @@ class FleksNoSuchComponentException(component: KClass<*>) : FleksException("There is no component of type ${component.simpleName} in the ComponentMapper. Did you add the component to the ${WorldConfiguration::class.simpleName}?") class FleksInjectableAlreadyAddedException(type: KClass<*>) : - FleksException("Injectable with name ${type} is already part of the ${WorldConfiguration::class.simpleName}.") + FleksException("Injectable with name ${type.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") class FleksSystemInjectException(injectType: KClass<*>) : FleksException("Injection object of type ${injectType.simpleName} cannot be found. Did you add all necessary injectables?") @@ -28,7 +28,7 @@ class FleksSystemInjectException(injectType: KClass<*>) : class FleksNoSuchEntityComponentException(entity: Entity, component: String) : FleksException("Entity $entity has no component of type $component.") -class FleksComponentListenerAlreadyAddedException(listener: KClass>) : +class FleksComponentListenerAlreadyAddedException(listener: KClass<*>) : FleksException("ComponentListener ${listener.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") class FleksUnusedInjectablesException(unused: List>) : diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 09b7a44b6..450caa919 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -4,8 +4,6 @@ import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.typeOf /** * An interval for an [IntervalSystem]. There are two kind of intervals: @@ -142,9 +140,9 @@ object Manual : SortingType * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. */ abstract class IteratingSystem( - val allOf: AllOf? = null, - val noneOf: NoneOf? = null, - val anyOf: AnyOf? = null, + val allOfComponents: Array>? = null, + val noneOfComponents: Array>? = null, + val anyOfComponents: Array>? = null, private val comparator: EntityComparator = EMPTY_COMPARATOR, private val sortingType: SortingType = Automatic, interval: Interval = EachFrame, @@ -300,9 +298,10 @@ class SystemService( cmpService: ComponentService, allFamilies: MutableList ): Family { - val allOfComps = system.allOf?.components?.map { cmpService.mapper(it) } - val noneOfComps = system.noneOf?.components?.map { cmpService.mapper(it) } - val anyOfComps = system.anyOf?.components?.map { cmpService.mapper(it) } +// val allOfComps = system.allOfComponents?.components?.map { cmpService.mapper(it) } + val allOfComps = system.allOfComponents?.map { cmpService.mapper(it) } + val noneOfComps = system.noneOfComponents?.map { cmpService.mapper(it) } + val anyOfComps = system.anyOfComponents?.map { cmpService.mapper(it) } if ((allOfComps == null || allOfComps.isEmpty()) && (noneOfComps == null || noneOfComps.isEmpty()) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 69cb05428..8cba693e1 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -1,8 +1,6 @@ package com.github.quillraven.fleks import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.typeOf /** * An optional annotation for an [IntervalSystem] constructor parameter to @@ -36,10 +34,10 @@ class WorldConfiguration { internal val systemFactory = mutableMapOf, () -> IntervalSystem>() @PublishedApi - internal val injectables = mutableMapOf, Injectable>() + internal val injectables = mutableMapOf, Injectable>() @PublishedApi - internal val compListenerTypes = mutableListOf>>() + internal val compListenerFactory = mutableMapOf, () -> ComponentListener<*>>() @PublishedApi internal val componentFactory = mutableMapOf, () -> Any>() @@ -60,17 +58,14 @@ class WorldConfiguration { systemFactory[systemType] = factory } - // TODO Add the specified [Component] to the [World]. -// fun component(factory: () -> T) { - inline fun component(noinline factory: () -> T) { -// val compType: ComponentMapper = ComponentMapper(factory = factory) // ()> //::class //factory()::class - val compType = T::class - if (compType in componentFactory) { - throw FleksComponentAlreadyAddedException(compType) - } - componentFactory[compType] = factory -// val compMapper = ComponentMapper(factory = factory) - } +// // TODO Add the specified [Component] to the [World]. +// inline fun component(noinline factory: () -> T) { +// val compType = T::class +// if (compType in componentFactory) { +// throw FleksComponentAlreadyAddedException(compType) +// } +// componentFactory[compType] = factory +// } /** * Adds the specified [dependency] under the given [type] which can then be injected to any [IntervalSystem]. @@ -99,14 +94,28 @@ class WorldConfiguration { /** * Adds the specified [ComponentListener] to the [world][World]. * + * TODO + * @param factory * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. */ - inline fun > componentListener() { - val listenerType = T::class - if (listenerType in compListenerTypes) { - throw FleksComponentListenerAlreadyAddedException(listenerType) +// inline fun > componentListener() { +// inline fun > componentListener(noinline factory: () -> ComponentListener) { + inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener<*>)? = null) { + val compType = T::class + + if (compType in componentFactory) { + throw FleksComponentAlreadyAddedException(compType) } - compListenerTypes.add(listenerType) + componentFactory[compType] = compFactory + +// val listenerType = factory::class +// if (listenerType in compListenerFactory) { +// throw FleksComponentListenerAlreadyAddedException(listenerType) +// } + if (compType in compListenerFactory) { + throw FleksComponentListenerAlreadyAddedException(compType) + } + if (listenerFactory != null) compListenerFactory[compType] = listenerFactory } } @@ -149,22 +158,28 @@ class World( init { val worldCfg = WorldConfiguration().apply(cfg) - // Create first ComponentService componentService = ComponentService(worldCfg.componentFactory) entityService = EntityService(worldCfg.entityCapacity, componentService) val injectables = worldCfg.injectables systemService = SystemService(this, worldCfg.systemFactory, injectables) // create and register ComponentListener -// Mk worldCfg.cmpListenerTypes.forEach { listenerType -> + worldCfg.compListenerFactory.forEach { + val compType = it.key + val listener = it.value.invoke() + // val listener = newInstance(listenerType, componentService, injectables) // val genInter = listener.javaClass.genericInterfaces.first { // it is ParameterizedType && it.rawType == ComponentListener::class.java // } // val cmpType = (genInter as ParameterizedType).actualTypeArguments[0] // val mapper = componentService.mapper((cmpType as Class<*>).kotlin) -// mapper.addComponentListenerInternal(listener) -// } + + println("ComponentListener type '${compType}'") + val mapper = componentService.mapper(compType) + println("Component mapper '$mapper' of type '${compType}' found!") + mapper.addComponentListenerInternal(listener) + } // verify that there are no unused injectables val unusedInjectables = injectables.filterValues { !it.used }.map { it.value.injObj::class } @@ -205,8 +220,6 @@ class World( return systemService.system() } -// inline fun registerComponent() - /** * Returns a [ComponentMapper] for the given type. If the mapper does not exist then it will be created. * diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 9d183462c..ba350e4ce 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -5,8 +5,6 @@ import com.soywiz.korge.view.* import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors import com.github.quillraven.fleks.* -import kotlin.native.concurrent.ThreadLocal -import kotlin.reflect.KClass const val scaleFactor = 1 @@ -29,7 +27,7 @@ class ExampleScene : Scene() { override suspend fun Container.sceneMain() { - val dummy = MyClass(text = "Hello injector!") + val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") val world = World { entityCapacity = 20 @@ -37,17 +35,18 @@ class ExampleScene : Scene() { system(::MoveSystem) system(::PositionSystem) - inject(dummy) + inject(dummyInMoveSystem) + + // Register all needed components and its listener if needed to the world + component(::Position, ::PositionListener) + component(::ImageAnimation) - // Register all needed components - // TODO remove and create components directly on system creation time - component(::Position) } val entity = world.entity { add { x = 50f - y = 100f + y = 120f } } @@ -57,13 +56,25 @@ class ExampleScene : Scene() { } } -data class MyClass(val text: String = "") +class PositionListener : ComponentListener { + override fun onComponentAdded(entity: Entity, component: Position) { + println("Component $component added to $entity!") + } + + override fun onComponentRemoved(entity: Entity, component: Position) { + println("Component $component removed from $entity!") + } +} + data class Position(var x: Float = 0f, var y: Float = 0f) +data class ImageAnimation(var imageData: String = "", var isPlaying: Boolean = false) class MoveSystem : IntervalSystem( interval = Fixed(1000f) // every second ) { + class MyClass(val text: String = "") + private val dummy: MyClass = Inject.dependency() override fun onInit() { @@ -75,7 +86,7 @@ class MoveSystem : IntervalSystem( } class PositionSystem : IteratingSystem( - allOf = AllOf(arrayOf(Position::class)), + allOfComponents = arrayOf(Position::class), interval = Fixed(500f) // every 500 millisecond ) { From 8a5a763e30a2932106e192e355b597d03fb5caa9 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Wed, 2 Feb 2022 18:04:58 +0100 Subject: [PATCH 08/27] Update with latest changes from Fleks project --- .../quillraven/fleks/collection/bitArray.kt | 1 - .../com/github/quillraven/fleks/component.kt | 22 ++++++++++- .../com/github/quillraven/fleks/entity.kt | 27 ++++++++++++++ .../com/github/quillraven/fleks/family.kt | 17 --------- .../com/github/quillraven/fleks/system.kt | 10 +---- .../com/github/quillraven/fleks/world.kt | 37 +++++-------------- .../fleks-ecs/src/commonMain/kotlin/main.kt | 2 +- 7 files changed, 58 insertions(+), 58 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index 0626f0d30..8defa3c5d 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -141,7 +141,6 @@ class BitArray( override fun equals(other: Any?): Boolean { if (this === other) return true -// MK if (javaClass != other?.javaClass) return false other as BitArray val otherBits = other.bits diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index cfa80d0b5..23452f756 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/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]. @@ -79,7 +93,9 @@ class ComponentMapper( * * @throws [FleksNoSuchEntityComponentException] if the [entity] does not have such a component. */ - operator fun get(entity: Entity): T = components[entity.id] ?: throw FleksNoSuchEntityComponentException(entity, factory.toString()) + operator fun get(entity: Entity): T { + return components[entity.id] ?: throw FleksNoSuchEntityComponentException(entity, factory.toString()) + } /** * Returns true if and only if the given [entity] has a component of the specific type. @@ -109,7 +125,9 @@ class ComponentMapper( */ operator fun contains(listener: ComponentListener) = listener in listeners - override fun toString(): String = "ComponentMapper(id=$id, component=${factory})" + override fun toString(): String { + return "ComponentMapper(id=$id, component=${factory})" + } } /** diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 6413feeba..165ed083a 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -46,6 +46,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]. */ inline fun add(configuration: T.() -> Unit = {}): T { val mapper = cmpService.mapper() @@ -68,14 +69,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. */ @@ -236,6 +250,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/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index ab512a431..6acc6239b 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -5,23 +5,6 @@ import com.github.quillraven.fleks.collection.EntityComparator import com.github.quillraven.fleks.collection.IntBag import kotlin.reflect.KClass -/** TODO change readme - * An annotation for an [IteratingSystem] to define a [Family]. - * [Entities][Entity] must have all [components] specified to be part of the [family][Family]. - */ -data class AllOf(val components: Array>) - -/** TODO change readme - * An annotation for an [IteratingSystem] to define a [Family]. - * [Entities][Entity] must not have any [components] specified to be part of the [family][Family]. - */ -data class NoneOf(val components: Array>) - -/** TODO change readme - * An annotation for an [IteratingSystem] to define a [Family]. - * [Entities][Entity] must have at least one of the [components] specified to be part of the [family][Family]. - */ -data class AnyOf(val components: Array>) /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 450caa919..f04fe54b5 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -42,12 +42,6 @@ abstract class IntervalSystem( lateinit var world: World internal set - /** - * An [Injector] which is used to inject objects from outside the [IntervalSystem]. - */ -// @PublishedApi -// internal lateinit var injector: Injector - private var accumulator: Float = 0.0f /** @@ -152,8 +146,7 @@ abstract class IteratingSystem( * Returns the [family][Family] of this system. * This reference gets updated by the [SystemService] when the system gets created via reflection. */ - lateinit var family: Family - internal set + internal lateinit var family: Family /** * Returns the [entityService][EntityService] of this system. @@ -298,7 +291,6 @@ class SystemService( cmpService: ComponentService, allFamilies: MutableList ): Family { -// val allOfComps = system.allOfComponents?.components?.map { cmpService.mapper(it) } val allOfComps = system.allOfComponents?.map { cmpService.mapper(it) } val noneOfComps = system.noneOfComponents?.map { cmpService.mapper(it) } val anyOfComps = system.anyOfComponents?.map { cmpService.mapper(it) } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 8cba693e1..4403fc984 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -42,7 +42,6 @@ class WorldConfiguration { @PublishedApi internal val componentFactory = mutableMapOf, () -> Any>() - /** * Adds the specified [IntervalSystem] to the [world][World]. * The order in which systems are added is the order in which they will be executed when calling [World.update]. @@ -58,15 +57,6 @@ class WorldConfiguration { systemFactory[systemType] = factory } -// // TODO Add the specified [Component] to the [World]. -// inline fun component(noinline factory: () -> T) { -// val compType = T::class -// if (compType in componentFactory) { -// throw FleksComponentAlreadyAddedException(compType) -// } -// componentFactory[compType] = factory -// } - /** * Adds the specified [dependency] under the given [type] which can then be injected to any [IntervalSystem]. * @@ -95,11 +85,10 @@ class WorldConfiguration { * Adds the specified [ComponentListener] to the [world][World]. * * TODO - * @param factory + * @param compFactory + * @param listenerFactory * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. */ -// inline fun > componentListener() { -// inline fun > componentListener(noinline factory: () -> ComponentListener) { inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener<*>)? = null) { val compType = T::class @@ -107,11 +96,6 @@ class WorldConfiguration { throw FleksComponentAlreadyAddedException(compType) } componentFactory[compType] = compFactory - -// val listenerType = factory::class -// if (listenerType in compListenerFactory) { -// throw FleksComponentListenerAlreadyAddedException(listenerType) -// } if (compType in compListenerFactory) { throw FleksComponentListenerAlreadyAddedException(compType) } @@ -167,17 +151,7 @@ class World( worldCfg.compListenerFactory.forEach { val compType = it.key val listener = it.value.invoke() - -// val listener = newInstance(listenerType, componentService, injectables) -// val genInter = listener.javaClass.genericInterfaces.first { -// it is ParameterizedType && it.rawType == ComponentListener::class.java -// } -// val cmpType = (genInter as ParameterizedType).actualTypeArguments[0] -// val mapper = componentService.mapper((cmpType as Class<*>).kotlin) - - println("ComponentListener type '${compType}'") val mapper = componentService.mapper(compType) - println("Component mapper '$mapper' of type '${compType}' found!") mapper.addComponentListenerInternal(listener) } @@ -211,6 +185,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/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index ba350e4ce..ff3dbc12d 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -37,7 +37,7 @@ class ExampleScene : Scene() { inject(dummyInMoveSystem) - // Register all needed components and its listener if needed to the world + // Register all needed components and its listeners (if needed) to the world component(::Position, ::PositionListener) component(::ImageAnimation) From 8034b1d8071a2d909509281a36c0c896792b9ced Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Wed, 2 Feb 2022 18:25:55 +0100 Subject: [PATCH 09/27] Add comments and rename cmp to comp --- .../com/github/quillraven/fleks/component.kt | 18 +++---- .../com/github/quillraven/fleks/entity.kt | 54 +++++++++---------- .../com/github/quillraven/fleks/family.kt | 21 ++++---- .../com/github/quillraven/fleks/system.kt | 20 +++---- .../com/github/quillraven/fleks/world.kt | 9 ++-- 5 files changed, 61 insertions(+), 61 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 23452f756..83e6b78ca 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -20,11 +20,11 @@ interface ComponentListener { * * Refer to [ComponentService] for more details. */ -@Suppress("UNCHECKED_CAST") class ComponentMapper( @PublishedApi internal val id: Int = 0, @PublishedApi + @Suppress("UNCHECKED_CAST") internal var components: Array = Array(64) { null } as Array, @PublishedApi internal val factory: () -> T @@ -42,8 +42,8 @@ class ComponentMapper( if (entity.id >= components.size) { components = components.copyOf(max(components.size * 2, entity.id + 1)) } - val cmp = components[entity.id] - return if (cmp == null) { + val comp = components[entity.id] + return if (comp == null) { val newCmp = factory.invoke().apply(configuration) components[entity.id] = newCmp listeners.forEach { it.onComponentAdded(entity, newCmp) } @@ -53,8 +53,8 @@ class ComponentMapper( // Call onComponentRemoved first in case users do something special in onComponentAdded. // Otherwise, onComponentAdded will be executed twice on a single component without executing onComponentRemoved // which is not correct. - listeners.forEach { it.onComponentRemoved(entity, cmp) } - val existingCmp = cmp.apply(configuration) + listeners.forEach { it.onComponentRemoved(entity, comp) } + val existingCmp = comp.apply(configuration) listeners.forEach { it.onComponentAdded(entity, existingCmp) } existingCmp } @@ -82,8 +82,8 @@ class ComponentMapper( */ @PublishedApi internal fun removeInternal(entity: Entity) { - components[entity.id]?.let { cmp -> - listeners.forEach { it.onComponentRemoved(entity, cmp) } + components[entity.id]?.let { comp -> + listeners.forEach { it.onComponentRemoved(entity, comp) } } components[entity.id] = null } @@ -180,7 +180,7 @@ class ComponentService( inline fun mapper(): ComponentMapper = mapper(T::class) /** - * Returns an already existing [ComponentMapper] for the given [cmpId]. + * Returns an already existing [ComponentMapper] for the given [compId]. */ - fun mapper(cmpId: Int) = mappersBag[cmpId] + fun mapper(compId: Int) = mappersBag[compId] } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt index 165ed083a..ca40ca2f3 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt @@ -20,10 +20,10 @@ interface EntityListener { * * @param entity the [entity][Entity] with the updated component configuration. * - * @param cmpMask the [BitArray] representing what type of components the entity has. Each component type has a + * @param compMask the [BitArray] representing what type of components the entity has. Each component type has a * unique id. Refer to [ComponentMapper] for more details. */ - fun onEntityCfgChanged(entity: Entity, cmpMask: BitArray) = Unit + fun onEntityCfgChanged(entity: Entity, compMask: BitArray) = Unit } @DslMarker @@ -35,13 +35,13 @@ annotation class EntityCfgMarker @EntityCfgMarker class EntityCreateCfg( @PublishedApi - internal val cmpService: ComponentService + internal val compService: ComponentService ) { @PublishedApi internal var entity = Entity(0) @PublishedApi - internal lateinit var cmpMask: BitArray + internal lateinit var compMask: BitArray /** * Adds and returns a component of the given type to the [entity] and @@ -49,8 +49,8 @@ class EntityCreateCfg( * Notifies any registered [ComponentListener]. */ inline fun add(configuration: T.() -> Unit = {}): T { - val mapper = cmpService.mapper() - cmpMask.set(mapper.id) + val mapper = compService.mapper() + compMask.set(mapper.id) return mapper.addInternal(entity, configuration) } } @@ -63,7 +63,7 @@ class EntityCreateCfg( @EntityCfgMarker class EntityUpdateCfg { @PublishedApi - internal lateinit var cmpMask: BitArray + internal lateinit var compMask: BitArray /** * Adds and returns a component of the given type to the [entity] and applies the [configuration] to that component. @@ -72,7 +72,7 @@ class EntityUpdateCfg { * Notifies any registered [ComponentListener]. */ inline fun ComponentMapper.add(entity: Entity, configuration: T.() -> Unit = {}): T { - cmpMask.set(this.id) + compMask.set(this.id) return this.addInternal(entity, configuration) } @@ -83,7 +83,7 @@ class EntityUpdateCfg { * Notifies any registered [ComponentListener] if a new component is created. */ inline fun ComponentMapper.addOrUpdate(entity: Entity, configuration: T.() -> Unit = {}): T { - cmpMask.set(this.id) + compMask.set(this.id) return this.addOrUpdateInternal(entity, configuration) } @@ -94,7 +94,7 @@ class EntityUpdateCfg { * @throws [ArrayIndexOutOfBoundsException] if the id of the [entity] exceeds the mapper's capacity. */ inline fun ComponentMapper.remove(entity: Entity) { - cmpMask.clear(this.id) + compMask.clear(this.id) this.removeInternal(entity) } } @@ -106,7 +106,7 @@ class EntityUpdateCfg { */ class EntityService( initialEntityCapacity: Int, - private val cmpService: ComponentService + private val compService: ComponentService ) { /** * The id that will be given to a newly created [entity][Entity] if there are no [recycledEntities]. @@ -137,16 +137,16 @@ class EntityService( * Returns the maximum capacity of active entities. */ val capacity: Int - get() = cmpMasks.capacity + get() = compMasks.capacity /** * The component configuration per [entity][Entity]. */ @PublishedApi - internal val cmpMasks = bag(initialEntityCapacity) + internal val compMasks = bag(initialEntityCapacity) @PublishedApi - internal val createCfg = EntityCreateCfg(cmpService) + internal val createCfg = EntityCreateCfg(compService) @PublishedApi internal val updateCfg = EntityUpdateCfg() @@ -180,16 +180,16 @@ class EntityService( recycled } - if (newEntity.id >= cmpMasks.size) { - cmpMasks[newEntity.id] = BitArray(64) + if (newEntity.id >= compMasks.size) { + compMasks[newEntity.id] = BitArray(64) } - val cmpMask = cmpMasks[newEntity.id] + val compMask = compMasks[newEntity.id] createCfg.run { this.entity = newEntity - this.cmpMask = cmpMask + this.compMask = compMask configuration(this.entity) } - listeners.forEach { it.onEntityCfgChanged(newEntity, cmpMask) } + listeners.forEach { it.onEntityCfgChanged(newEntity, compMask) } return newEntity } @@ -199,12 +199,12 @@ class EntityService( * Notifies any registered [EntityListener]. */ inline fun configureEntity(entity: Entity, configuration: EntityUpdateCfg.(Entity) -> Unit) { - val cmpMask = cmpMasks[entity.id] + val compMask = compMasks[entity.id] updateCfg.run { - this.cmpMask = cmpMask + this.compMask = compMask configuration(entity) } - listeners.forEach { it.onEntityCfgChanged(entity, cmpMask) } + listeners.forEach { it.onEntityCfgChanged(entity, compMask) } } /** @@ -225,13 +225,13 @@ class EntityService( delayedEntities.add(entity.id) } else { removedEntities.set(entity.id) - val cmpMask = cmpMasks[entity.id] + val compMask = compMasks[entity.id] recycledEntities.add(entity) - cmpMask.forEachSetBit { cmpId -> - cmpService.mapper(cmpId).removeInternal(entity) + compMask.forEachSetBit { compId -> + compService.mapper(compId).removeInternal(entity) } - cmpMask.clearAll() - listeners.forEach { it.onEntityCfgChanged(entity, cmpMask) } + compMask.clearAll() + listeners.forEach { it.onEntityCfgChanged(entity, compMask) } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index 6acc6239b..b1d4fd7df 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -3,12 +3,11 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator import com.github.quillraven.fleks.collection.IntBag -import kotlin.reflect.KClass /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. - * A configuration is defined via the three annotations: [AllOf], [NoneOf] and [AnyOf]. + * A configuration is defined via the three system properties "allOfComponents", "noneOfComponents" and "anyOfComponents. * Each component is assigned to a unique index. That index is set in the [allOf], [noneOf] or [anyOf][] [BitArray]. * * A family is an [EntityListener] and gets notified when an [entity][Entity] is added to the world or the @@ -47,14 +46,14 @@ data class Family( private set /** - * Returns true if the specified [cmpMask] matches the family's component configuration. + * Returns true if the specified [compMask] matches the family's component configuration. * - * @param cmpMask the component configuration of an [entity][Entity]. + * @param compMask the component configuration of an [entity][Entity]. */ - operator fun contains(cmpMask: BitArray): Boolean { - return (allOf == null || cmpMask.contains(allOf)) - && (noneOf == null || !cmpMask.intersects(noneOf)) - && (anyOf == null || cmpMask.intersects(anyOf)) + operator fun contains(compMask: BitArray): Boolean { + return (allOf == null || compMask.contains(allOf)) + && (noneOf == null || !compMask.intersects(noneOf)) + && (anyOf == null || compMask.intersects(anyOf)) } /** @@ -82,12 +81,12 @@ data class Family( /** * Checks if the [entity] is part of the family by analyzing the entity's components. - * The [cmpMask] is a [BitArray] that indicates which components the [entity] currently has. + * The [compMask] is a [BitArray] that indicates which components the [entity] currently has. * * The [entity] gets either added to the [entities] or removed and [isDirty] is set when needed. */ - override fun onEntityCfgChanged(entity: Entity, cmpMask: BitArray) { - val entityInFamily = cmpMask in this + override fun onEntityCfgChanged(entity: Entity, compMask: BitArray) { + val entityInFamily = compMask in this if (entityInFamily && !entities[entity.id]) { // new entity gets added isDirty = true diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index f04fe54b5..548884642 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -124,8 +124,8 @@ object Manual : SortingType /** * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. * - * It must have at least one of [AllOf], [AnyOf] or [NoneOf] objects defined. These objects define - * a [Family] to which this [IteratingSystem] belongs. + * It must have at least one of [allOfComponents], [anyOfComponents] or [noneOfComponents] objects defined. + * These objects define a [Family] to which this [IteratingSystem] belongs. * * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. * Default value is an empty comparator which means no sorting. @@ -239,7 +239,7 @@ abstract class IteratingSystem( * each time [update] is called. * * @param world the [world][World] the service belongs to. - * @param systemFactorys the factory methods to create the [systems][IntervalSystem]. + * @param systemFactory the factory methods to create the [systems][IntervalSystem]. * @param injectables the required dependencies to create the [systems][IntervalSystem]. */ class SystemService( @@ -277,23 +277,23 @@ class SystemService( /** * Creates or returns an already created [family][Family] for the given [IteratingSystem] - * by analyzing the system's [AllOf], [AnyOf] and [NoneOf] annotations. + * by analyzing the system's "allOfComponents", "anyOfComponents" and "noneOfComponents" properties. * * @throws [FleksSystemCreationException] if the [IteratingSystem] does not contain at least one - * [AllOf], [AnyOf] or [NoneOf] annotation. + * "allOfComponents", "anyOfComponents" and "noneOfComponents" property. * - * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the + * @throws [FleksNoSuchComponentException] if the component of the given type from the family does not exist in the * world configuration. */ private fun family( system: IteratingSystem, entityService: EntityService, - cmpService: ComponentService, + compService: ComponentService, allFamilies: MutableList ): Family { - val allOfComps = system.allOfComponents?.map { cmpService.mapper(it) } - val noneOfComps = system.noneOfComponents?.map { cmpService.mapper(it) } - val anyOfComps = system.anyOfComponents?.map { cmpService.mapper(it) } + val allOfComps = system.allOfComponents?.map { compService.mapper(it) } + val noneOfComps = system.noneOfComponents?.map { compService.mapper(it) } + val anyOfComps = system.anyOfComponents?.map { compService.mapper(it) } if ((allOfComps == null || allOfComps.isEmpty()) && (noneOfComps == null || noneOfComps.isEmpty()) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 4403fc984..d6017965e 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -82,11 +82,12 @@ class WorldConfiguration { } /** - * Adds the specified [ComponentListener] to the [world][World]. + * Adds the specified [Component] and its [ComponentListener] to the [world][World]. If a component listener + * is not needed than it can be omitted. * - * TODO - * @param compFactory - * @param listenerFactory + * @param compFactory the constructor method for creating the component. + * @param listenerFactory the constructor method for creating the component listener. + * @throws [FleksComponentAlreadyAddedException] if the component was already added before. * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. */ inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener<*>)? = null) { From 9cda2f6604280e5bf96d4ffd494915656dcd1f0c Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 4 Feb 2022 18:22:18 +0100 Subject: [PATCH 10/27] Add sources for ECS example - wip --- .../com/github/quillraven/fleks/system.kt | 2 +- .../kotlin/components/ImageAnimation.kt | 33 +++++ .../commonMain/kotlin/components/Position.kt | 28 ++++ .../commonMain/kotlin/components/Spawner.kt | 14 ++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 105 +++++++------- .../kotlin/systems/ImageAnimationSystem.kt | 131 ++++++++++++++++++ .../commonMain/kotlin/systems/MoveSystem.kt | 59 ++++++++ .../kotlin/systems/SpawnerSystem.kt | 88 ++++++++++++ .../src/commonMain/resources/sprites.ase | Bin 0 -> 3355 bytes 9 files changed, 402 insertions(+), 58 deletions(-) create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt create mode 100644 samples/fleks-ecs/src/commonMain/resources/sprites.ase diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 548884642..bcddd1f18 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -125,7 +125,7 @@ object Manual : SortingType * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. * * It must have at least one of [allOfComponents], [anyOfComponents] or [noneOfComponents] objects defined. - * These objects define a [Family] to which this [IteratingSystem] belongs. + * These objects define a [Family] of entities for which the [IteratingSystem] will get active and. * * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. * Default value is an empty comparator which means no sorting. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt new file mode 100644 index 000000000..a9c20c2e3 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt @@ -0,0 +1,33 @@ +package components + +import com.github.quillraven.fleks.ComponentListener +import com.github.quillraven.fleks.Entity + +data class ImageAnimation( + var lifeCycle: LifeCycle = LifeCycle.INACTIVE, + var imageData: String = "", + var animation: String = "", + var isPlaying: Boolean = false, + var forwardDirection: Boolean = true, + var loop: Boolean = false +) { + enum class LifeCycle { + INACTIVE, INIT, ACTIVE, DESTROY + } +} + +class ImageAnimationListener : ComponentListener { + override fun onComponentAdded(entity: Entity, component: ImageAnimation) { + + // Init component view + + println("Component $component added to $entity!") + } + + override fun onComponentRemoved(entity: Entity, component: ImageAnimation) { + + // Reset details for reusing in another entity + + println("Component $component removed from $entity!") + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt new file mode 100644 index 000000000..2bb81ca93 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt @@ -0,0 +1,28 @@ +package components + +import com.github.quillraven.fleks.ComponentListener +import com.github.quillraven.fleks.Entity + +data class Position( + var x: Double = 0.0, + var y: Double = 0.0, + var xAcceleration: Double = 0.0, + var yAcceleration: Double = 0.0, +) + +class PositionListener : ComponentListener { + override fun onComponentAdded(entity: Entity, component: Position) { + println("Component $component added to $entity!") + } + + override fun onComponentRemoved(entity: Entity, component: Position) { + + // Reset details for reusing in another entity + component.x = 0.0 + component.y = 0.0 + component.xAcceleration = 0.0 + component.yAcceleration = 0.0 + + println("Component $component removed from $entity!") + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt new file mode 100644 index 000000000..33b34c457 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt @@ -0,0 +1,14 @@ +package components + +data class Spawner( + // config + var numberOfObjects: Int = 1, + var interval: Int = 0, // 0 - disabled, 1 - every frame, 2 - every second frame, 3 - every third frame,... + var timeVariation: Int = 0, // 0 - no variation, 1 - one frame variation, 2 - two frames variation, ... + var xPosVariation: Double = 0.0, + var yPosVariation: Double = 0.0, + var xAccel: Double = 0.0, + var yAccel: Double = 0.0, + // internal state + var nextSpawnIn: Int = 0 +) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index ff3dbc12d..64333e712 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -5,8 +5,10 @@ import com.soywiz.korge.view.* import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors import com.github.quillraven.fleks.* +import systems.* +import components.* -const val scaleFactor = 1 +const val scaleFactor = 2 suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor, bgcolor = Colors["#000000"]) { @@ -27,77 +29,66 @@ class ExampleScene : Scene() { override suspend fun Container.sceneMain() { - val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") +// val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") + // This is the world object of the entity component system (ECS) + // It contains all ECS related configuration val world = World { entityCapacity = 20 + // Register all needed systems system(::MoveSystem) - system(::PositionSystem) + system(::SpawnerSystem) + system(::ImageAnimationSystem) - inject(dummyInMoveSystem) - - // Register all needed components and its listeners (if needed) to the world + // Register all needed components and its listeners (if needed) component(::Position, ::PositionListener) - component(::ImageAnimation) + component(::ImageAnimation, ::ImageAnimationListener) + component(::Spawner) + // Register external objects which are used by systems and component listeners +// inject(dummyInMoveSystem) } - val entity = world.entity { - add { - x = 50f - y = 120f + val spawner = world.entity { + add { // Position of spawner + x = 130.0 + y = 100.0 + } + add { // Config for spawner object + numberOfObjects = 7 + interval = 1 + timeVariation = 0 + xPosVariation = 50.0 + yPosVariation = 7.0 + xAccel = -0.8 + yAccel = -1.0 + } + add { // Config for spawner object + imageData = "sprite2" + animation = "FireTrail" // "FireTrail" - "TestNum" + isPlaying = true } } +/* + // Initialize entity data and config + entity.spawnerComponent.numberOfObjects = 7 + entity.positionComponent.x = 130.0 + entity.positionComponent.y = 100.0 + entity.spawnerComponent.interval = 1 + entity.spawnerComponent.timeVariation = 0 + entity.spawnerComponent.xPosVariation = 50.0 + entity.spawnerComponent.yPosVariation = 7.0 + entity.spawnerComponent.xAccel = -0.8 + entity.spawnerComponent.yAccel = -1.0 + + entity.imageAnimationComponent.imageData = "sprite2" + entity.imageAnimationComponent.animation = "FireTrail" // "FireTrail" - "TestNum" + entity.imageAnimationComponent.isPlaying = true +*/ addUpdater { dt -> world.update(dt.milliseconds.toFloat()) } } } - -class PositionListener : ComponentListener { - override fun onComponentAdded(entity: Entity, component: Position) { - println("Component $component added to $entity!") - } - - override fun onComponentRemoved(entity: Entity, component: Position) { - println("Component $component removed from $entity!") - } -} - -data class Position(var x: Float = 0f, var y: Float = 0f) -data class ImageAnimation(var imageData: String = "", var isPlaying: Boolean = false) - -class MoveSystem : IntervalSystem( - interval = Fixed(1000f) // every second -) { - - class MyClass(val text: String = "") - - private val dummy: MyClass = Inject.dependency() - - override fun onInit() { - } - - override fun onTick() { - println("MoveSystem: onTick (text: ${dummy.text})") - } -} - -class PositionSystem : IteratingSystem( - allOfComponents = arrayOf(Position::class), - interval = Fixed(500f) // every 500 millisecond -) { - - private val position: ComponentMapper = Inject.componentMapper() - - override fun onInit() { - } - - override fun onTickEntity(entity: Entity) { - println("PositionSystem: onTickEntity") - println("pos id: ${position.id} x: ${position[entity].x} y: ${position[entity].y}") - } -} - diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt new file mode 100644 index 000000000..3aab80e85 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt @@ -0,0 +1,131 @@ +package systems + +//import aseImage +import com.github.quillraven.fleks.* +import com.soywiz.kds.Pool +import com.soywiz.kds.iterators.fastForEach +import com.soywiz.klock.TimeSpan +import com.soywiz.korge.view.* +import com.soywiz.korge.view.animation.ImageAnimationView +import com.soywiz.korim.bitmap.Bitmaps +import components.* +import components.ImageAnimation.LifeCycle + +/** + * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the configuration from + * [ImageAnimationComponent] to setup graphics from Assets and create an ImageAnimationView object for displaying in the Container. + * + */ +class ImageAnimationSystem : IteratingSystem( + allOfComponents = arrayOf(ImageAnimation::class, Position::class), + interval = Fixed(500f) // every 500 millisecond +) { + + private val position: ComponentMapper = Inject.componentMapper() + private val imageAnimation: ComponentMapper = Inject.componentMapper() + + override fun onInit() { + } + + override fun onTickEntity(entity: Entity) { + println("[Entity: ${entity.id}] image animation on tick") +// println("pos id: ${position.id} x: ${position[entity].x} y: ${position[entity].y}") + } +} + +/* +class ImageAnimationSystem( + private val container: Container +) : SubSystemBase() { + + private val doSmoothing = false + + private val imageAnimViewMap: MutableMap> = mutableMapOf() + + override fun registerEntity(id: Int, type: EntityAspects) { + if (type.imageAnimation) { + activeEntities.add(id) + getEntityComponents(id).imageAnimationComponent.lifeCycle = LifeCycle.INIT + } + } + + // Special implementation for this sub-system + override fun unregisterEntity(id: Int) { + val imageAnimView = imageAnimViewMap.remove(id)!! + imageAnimationViewPool.free(imageAnimView) + imageAnimView.removeFromParent() + activeEntities.remove(id) + getEntityComponents(id).imageAnimationComponent.lifeCycle = LifeCycle.INACTIVE + } + + override fun fixedUpdate() { + } + + override fun update(dt: TimeSpan, tmod: Double) { + activeEntities.fastForEach { id -> + val entry = getEntityComponents(id) + when (entry.imageAnimationComponent.lifeCycle) { + LifeCycle.INIT -> createImageAnimationView(entry, id) + LifeCycle.ACTIVE -> { + imageAnimViewMap[id]?.updateAnimation(dt) + } + else -> {} + } + } + } + + override fun postUpdate(dt: TimeSpan, tmod: Double) { + activeEntities.fastForEach { id -> + val imageAnimation = getEntityComponents(id) + when (imageAnimation.imageAnimationComponent.lifeCycle) { + LifeCycle.INIT -> {} + LifeCycle.ACTIVE -> { + // sync view position + val container = imageAnimViewMap[id] + container?.x = imageAnimation.positionComponent.x + container?.y = imageAnimation.positionComponent.y + } + LifeCycle.DESTROY -> { + // Object is going to be recycled + destroyEntity(id) + } + else -> {} + } + } + } + + private fun createImageAnimationView(entry: EntityComponents, id: Int) { + // initialize component data and config + entry.let { + val imageAnimView = imageAnimationViewPool.alloc() + // If Asset is not available destroy the object again + it.imageAnimationComponent.lifeCycle = if (aseImage == null) LifeCycle.DESTROY else LifeCycle.ACTIVE + + // Set animation object + imageAnimView.animation = + // TODO get this from Assets object with "imageData" string + aseImage?.animationsByName?.getOrElse(it.imageAnimationComponent.animation) { aseImage?.defaultAnimation } + imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } + imageAnimView.onPlayFinished = { it.imageAnimationComponent.lifeCycle = LifeCycle.DESTROY } + imageAnimView.addTo(container) + // Set play status + imageAnimView.direction = when { + it.imageAnimationComponent.forwardDirection && !it.imageAnimationComponent.loop -> ImageAnimation.Direction.ONCE_FORWARD + !it.imageAnimationComponent.forwardDirection && it.imageAnimationComponent.loop -> ImageAnimation.Direction.REVERSE + !it.imageAnimationComponent.forwardDirection && !it.imageAnimationComponent.loop -> ImageAnimation.Direction.ONCE_REVERSE + else -> ImageAnimation.Direction.FORWARD + } + if (it.imageAnimationComponent.isPlaying) imageAnimView.play() + + imageAnimViewMap[id] = imageAnimView + } + } + + private val imageAnimationViewPool = Pool(reset = { it.rewind() }) { + ImageAnimationView(enableUpdater = false) { imageBitmapTransparentPool.alloc() }.apply { smoothing = doSmoothing } + } + private val imageBitmapTransparentPool = Pool(reset = { it.bitmap = Bitmaps.transparent }, preallocate = 20) { + Image(Bitmaps.transparent) + } +} +*/ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt new file mode 100644 index 000000000..db7816dfa --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -0,0 +1,59 @@ +package systems + +import com.github.quillraven.fleks.* +import components.* + +class MoveSystem : IteratingSystem( + allOfComponents = arrayOf(Position::class), + interval = Fixed(500f) // every 500 millisecond +) { + + private val position: ComponentMapper = Inject.componentMapper() + + override fun onInit() { + } + + override fun onTickEntity(entity: Entity) { + val pos = position[entity] + pos.x += pos.xAcceleration * deltaTime + pos.y += pos.yAcceleration * deltaTime + + println("[Entity: ${entity.id}] move on tick") + } +} + +/* +class MovingSystem : SubSystemBase() { + + override fun registerEntity(id: Int, type: EntityAspects) { + if (type.position) { + activeEntities.add(id) + + // initialize component data and config + getEntityComponents(id).positionComponent.let { + it.x = 0.0 + it.y = 0.0 + } + } + } + + override fun fixedUpdate() { + } + + override fun update(dt: TimeSpan, tmod: Double) { + + // TODO for stesting only... + activeEntities.fastForEach { id -> + val positionComponent = getEntityComponents(id).positionComponent + // TODO further implement dynamic moving + positionComponent.x += positionComponent.xAcceleration * tmod + positionComponent.y += positionComponent.yAcceleration * tmod + } + + } + + override fun postUpdate(dt: TimeSpan, tmod: Double) { + } + +} +*/ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt new file mode 100644 index 000000000..779d70dec --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -0,0 +1,88 @@ +package systems + +import com.github.quillraven.fleks.* +import components.ImageAnimation +import components.Position +import components.Spawner +import kotlin.random.Random + +fun ClosedFloatingPointRange.random() = Random.nextDouble(start, endInclusive) +fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble(), endInclusive.toDouble()).toFloat() +fun IntRange.random() = Random.nextInt(start, endInclusive) + +class SpawnerSystem : IteratingSystem( + allOfComponents = arrayOf(Spawner::class), + interval = Fixed(500f) // every 500 millisecond +) { + + private val imageAnimation: ComponentMapper = Inject.componentMapper() + private val position: ComponentMapper = Inject.componentMapper() + private val spawner: ComponentMapper = Inject.componentMapper() + + override fun onInit() { + } + + override fun onTickEntity(entity: Entity) { + spawn(entity) + println("[Entity: ${entity.id}] spawner on tick - create new entity") + } + + private fun spawn(entity: Entity) { + world.entity { + add { // Position of spawner + x = position[entity].x + if (spawner[entity].xPosVariation != 0.0) x += (-spawner[entity].xPosVariation..spawner[entity].xPosVariation).random() + y = position[entity].y + if (spawner[entity].yPosVariation != 0.0) y += (-spawner[entity].yPosVariation..spawner[entity].yPosVariation).random() + xAcceleration = spawner[entity].xAccel + yAcceleration = spawner[entity].yAccel + } + add { // Config for spawner object + imageData = imageAnimation[entity].imageData + animation = imageAnimation[entity].animation + isPlaying = imageAnimation[entity].isPlaying + } + } + } +} +/* +class SpawnerSystem : SubSystemBase() { + + override fun registerEntity(id: Int, type: EntityAspects) { + if (type.spawner) { + activeEntities.add(id) + + // initialize component data and config + getEntityComponents(id).spawnerComponent.let { + it.nextSpawnIn = 0 + } + } + } + + override fun fixedUpdate() { + // Do nothing + } + + override fun update(dt: TimeSpan, tmod: Double) { + activeEntities.fastForEach { id -> + val entity = getEntityComponents(id) + entity.lock = true + val spawner = entity.spawnerComponent + if (spawner.interval > 0) { + if (spawner.nextSpawnIn <= 0) { + spawn(id) + spawner.nextSpawnIn = spawner.interval + if (spawner.timeVariation != 0) spawner.nextSpawnIn += (-spawner.timeVariation..spawner.timeVariation).random() + } else { + spawner.nextSpawnIn-- + } + } + entity.lock = false + } + } + + override fun postUpdate(dt: TimeSpan, tmod: Double) { + // Do nothing + } +} +*/ diff --git a/samples/fleks-ecs/src/commonMain/resources/sprites.ase b/samples/fleks-ecs/src/commonMain/resources/sprites.ase new file mode 100644 index 0000000000000000000000000000000000000000..e7e181ce01d0ef34d8d530d9e015ff711930dff5 GIT binary patch literal 3355 zcmb_e4OCNQ7=Fh#w#oQWN(GT^V4wxKiGtFA0XaQFKtTQ=M>i%YL>M|bNJTRU1eP0> z0^%{t%0ps_^e3HD7>II^fW#wICg$L%m7OV`5W(Af=N-GdN9r8w?%ZeJ{rR5vd7tn7 zzVFU)1{nO%7u@hC1{MIE#nX1+J@TgIx$W-%*V^m=FlL@+i8>8wXX42dTSC%;{D^H2 zv6#9eqCK%R2>>7QV#MY1<%pTfWQc~*p5hU!&_6tuH1CJcv{8%-AND$?(F* zZfFINoRg)=*tj(}gVqUajjT5SIy*sx;~lOTfQ@bulWFkrt%3vh2qLoeHNKK3+%mgb zUVo!L&=3^=q<6qg|4uoWqKb@R*m=y%vXmK`4Np>J0;IE%Ilwdv*}GzuqO}0u8;$)u zp3yUc>iI7+YK-!rJ0oxTUz`}p_m}LLcTxnmdh%;x7;M258`mx=M=B{1TdI%+`| zB%u_^zYH2xJCW~Sa)k>gN_RhcW@HH_1m5P?CcI$rY1j8fMzW;G%(exVoRDNq&c}bU zCPVNf3!IBTTTMFRRlxy`N~IcgVR8SD-H7OwoN4 zUTYSW=FLBK^q{e5(@w2EAVFXFN@3&4GUJA%J6CRn9vex_o>i}|2z2S_ElyY{%hK0q zxolXpxwd*6BUbu9$KqVxGdyv|NAIrd^VLLWs*}qa_8H|7Uzf|O-P(uaqi+amqdwX2 zidz4@UweGcmC9~;^v!vpaplidI=LO@!G(a8iDU%2SxHnx2fMlBY?(V6m4f@J|Kk%Y z+J#G@IV^gNUw`H!(VRCE0#D|<9o;u0)a|HfO~K*cmJeS&UmUv-R~YRtx1Z85A|y{n zpbv*F!kHL{`^8fBboglIT=%2ibxUQ#SDOrL!_OA>W<}?o9?DFgSfPLU?CG`=_7`8V z;O@1v8`m>J#6C2HkV>M3Q<|Phn6#GXS1?Qyzc%dPveGeb?o-n)1#_NK(b^ z+Q8r?7p;e-Pi}2wO)Crc5mhp_w_;DKR;Jy4yhGFERJS2#Oi=$s`=!xwUH8_KpJVug z$xblZrIbwN1jla12;hMeI`9qeB(Bx41cxedes&2rEg3akf5W2(WZ5shK~~vNQ|iD} zOBTm&;p5|%1F3kD)8Hl1H@Mb$wC(TIagNtky7Uae7OuQ%Dv4a+&{jYmMW<3pe`p$a z{Z6Iau_ySpbeGF&O!_a@vH6 zs*?5A2eH1xP Date: Tue, 8 Feb 2022 09:50:42 +0100 Subject: [PATCH 11/27] wip --- .../com/github/quillraven/fleks/component.kt | 17 +++++++------- .../com/github/quillraven/fleks/family.kt | 15 +++++++++++++ .../com/github/quillraven/fleks/system.kt | 14 ++++++------ .../kotlin/components/ImageAnimation.kt | 13 +++++++++++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 22 +++++-------------- .../kotlin/systems/ImageAnimationSystem.kt | 14 +++--------- .../commonMain/kotlin/systems/MoveSystem.kt | 2 +- .../kotlin/systems/SpawnerSystem.kt | 2 +- 8 files changed, 54 insertions(+), 45 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index 83e6b78ca..c5c900f9c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -17,6 +17,7 @@ interface ComponentListener { /** * A class that is responsible to store components of a specific type for all [entities][Entity] in a [world][World]. * Each component is assigned a unique [id] for fast access and to avoid lookups via a class which is slow. + * Hint: A component at index [id] in the [components] array belongs to [Entity] with the same [id]. * * Refer to [ComponentService] for more details. */ @@ -44,19 +45,19 @@ class ComponentMapper( } val comp = components[entity.id] return if (comp == null) { - val newCmp = factory.invoke().apply(configuration) - components[entity.id] = newCmp - listeners.forEach { it.onComponentAdded(entity, newCmp) } - newCmp + val newComp = factory.invoke().apply(configuration) + components[entity.id] = newComp + listeners.forEach { it.onComponentAdded(entity, newComp) } + newComp } else { // component already added -> reuse it and do not create a new instance. // Call onComponentRemoved first in case users do something special in onComponentAdded. // Otherwise, onComponentAdded will be executed twice on a single component without executing onComponentRemoved // which is not correct. listeners.forEach { it.onComponentRemoved(entity, comp) } - val existingCmp = comp.apply(configuration) - listeners.forEach { it.onComponentAdded(entity, existingCmp) } - existingCmp + val existingComp = comp.apply(configuration) + listeners.forEach { it.onComponentAdded(entity, existingComp) } + existingComp } } @@ -153,7 +154,7 @@ class ComponentService( init { // Create component mappers with help of constructor functions from component factory mappers = componentFactory.mapValues { - val compMapper = ComponentMapper(factory = it.value) + val compMapper = ComponentMapper(id = mappersBag.size, factory = it.value) mappersBag.add(compMapper) compMapper } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index b1d4fd7df..6c6058e18 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -3,7 +3,22 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator import com.github.quillraven.fleks.collection.IntBag +import kotlin.reflect.KClass +/** + * [Entities][Entity] must have all [components] specified to be part of the [family][Family]. + */ +class AllOf(val components: Array>) + +/** + * [Entities][Entity] must not have any [components] specified to be part of the [family][Family]. + */ +class NoneOf(val components: Array>) + +/** + * [Entities][Entity] must have at least one of the [components] specified to be part of the [family][Family]. + */ +class AnyOf(val components: Array>) /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index bcddd1f18..d23672ade 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -124,7 +124,7 @@ object Manual : SortingType /** * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. * - * It must have at least one of [allOfComponents], [anyOfComponents] or [noneOfComponents] objects defined. + * It must have at least one of [allOf], [anyOf] or [noneOf] objects defined. * These objects define a [Family] of entities for which the [IteratingSystem] will get active and. * * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. @@ -134,9 +134,9 @@ object Manual : SortingType * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. */ abstract class IteratingSystem( - val allOfComponents: Array>? = null, - val noneOfComponents: Array>? = null, - val anyOfComponents: Array>? = null, + val allOf: AllOf? = null, + val noneOf: NoneOf? = null, + val anyOf: AnyOf? = null, private val comparator: EntityComparator = EMPTY_COMPARATOR, private val sortingType: SortingType = Automatic, interval: Interval = EachFrame, @@ -291,9 +291,9 @@ class SystemService( compService: ComponentService, allFamilies: MutableList ): Family { - val allOfComps = system.allOfComponents?.map { compService.mapper(it) } - val noneOfComps = system.noneOfComponents?.map { compService.mapper(it) } - val anyOfComps = system.anyOfComponents?.map { compService.mapper(it) } + val allOfComps = system.allOf?.components?.map { compService.mapper(it) } + val noneOfComps = system.noneOf?.components?.map { compService.mapper(it) } + val anyOfComps = system.anyOf?.components?.map { compService.mapper(it) } if ((allOfComps == null || allOfComps.isEmpty()) && (noneOfComps == null || noneOfComps.isEmpty()) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt index a9c20c2e3..c0d8e753a 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt @@ -2,6 +2,10 @@ package components import com.github.quillraven.fleks.ComponentListener import com.github.quillraven.fleks.Entity +import com.soywiz.kds.Pool +import com.soywiz.korge.view.Image +import com.soywiz.korge.view.animation.ImageAnimationView +import com.soywiz.korim.bitmap.Bitmaps data class ImageAnimation( var lifeCycle: LifeCycle = LifeCycle.INACTIVE, @@ -17,6 +21,15 @@ data class ImageAnimation( } class ImageAnimationListener : ComponentListener { + + private val imageAnimationViewPool = Pool(reset = { it.rewind() }) { + ImageAnimationView(/* TODO enableUpdater = false*/) { imageBitmapTransparentPool.alloc() }.apply { smoothing = true } + } + private val imageBitmapTransparentPool = Pool(reset = { it.bitmap = Bitmaps.transparent }, preallocate = 20) { + Image(Bitmaps.transparent) + } + + override fun onComponentAdded(entity: Entity, component: ImageAnimation) { // Init component view diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 64333e712..06aa27b84 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -29,6 +29,7 @@ class ExampleScene : Scene() { override suspend fun Container.sceneMain() { + // val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") // This is the world object of the entity component system (ECS) @@ -47,9 +48,12 @@ class ExampleScene : Scene() { component(::Spawner) // Register external objects which are used by systems and component listeners -// inject(dummyInMoveSystem) +// inject(imageAnimationViewPool) +// inject(imageBitmapTransparentPool) } + val pos = world.mapper() + val spawner = world.entity { add { // Position of spawner x = 130.0 @@ -70,22 +74,6 @@ class ExampleScene : Scene() { isPlaying = true } } -/* - // Initialize entity data and config - entity.spawnerComponent.numberOfObjects = 7 - entity.positionComponent.x = 130.0 - entity.positionComponent.y = 100.0 - entity.spawnerComponent.interval = 1 - entity.spawnerComponent.timeVariation = 0 - entity.spawnerComponent.xPosVariation = 50.0 - entity.spawnerComponent.yPosVariation = 7.0 - entity.spawnerComponent.xAccel = -0.8 - entity.spawnerComponent.yAccel = -1.0 - - entity.imageAnimationComponent.imageData = "sprite2" - entity.imageAnimationComponent.animation = "FireTrail" // "FireTrail" - "TestNum" - entity.imageAnimationComponent.isPlaying = true -*/ addUpdater { dt -> world.update(dt.milliseconds.toFloat()) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt index 3aab80e85..6e0f7136b 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt @@ -3,13 +3,9 @@ package systems //import aseImage import com.github.quillraven.fleks.* import com.soywiz.kds.Pool -import com.soywiz.kds.iterators.fastForEach -import com.soywiz.klock.TimeSpan import com.soywiz.korge.view.* import com.soywiz.korge.view.animation.ImageAnimationView -import com.soywiz.korim.bitmap.Bitmaps import components.* -import components.ImageAnimation.LifeCycle /** * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the configuration from @@ -17,10 +13,12 @@ import components.ImageAnimation.LifeCycle * */ class ImageAnimationSystem : IteratingSystem( - allOfComponents = arrayOf(ImageAnimation::class, Position::class), + allOf = AllOf(arrayOf(ImageAnimation::class, Position::class)), interval = Fixed(500f) // every 500 millisecond ) { + private val imageAnimationViewPool: Pool> = Inject.dependency() + private val imageBitmapTransparentPool: Pool = Inject.dependency() private val position: ComponentMapper = Inject.componentMapper() private val imageAnimation: ComponentMapper = Inject.componentMapper() @@ -121,11 +119,5 @@ class ImageAnimationSystem( } } - private val imageAnimationViewPool = Pool(reset = { it.rewind() }) { - ImageAnimationView(enableUpdater = false) { imageBitmapTransparentPool.alloc() }.apply { smoothing = doSmoothing } - } - private val imageBitmapTransparentPool = Pool(reset = { it.bitmap = Bitmaps.transparent }, preallocate = 20) { - Image(Bitmaps.transparent) - } } */ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt index db7816dfa..97ce62c94 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -4,7 +4,7 @@ import com.github.quillraven.fleks.* import components.* class MoveSystem : IteratingSystem( - allOfComponents = arrayOf(Position::class), + AllOf(arrayOf(Position::class)), interval = Fixed(500f) // every 500 millisecond ) { diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index 779d70dec..a50055500 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -11,7 +11,7 @@ fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble( fun IntRange.random() = Random.nextInt(start, endInclusive) class SpawnerSystem : IteratingSystem( - allOfComponents = arrayOf(Spawner::class), + AllOf(arrayOf(Spawner::class)), interval = Fixed(500f) // every 500 millisecond ) { From 62fc79e714bd0454c4cab5e66956d936a0e847ea Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sat, 12 Feb 2022 12:00:14 +0100 Subject: [PATCH 12/27] Update working example with enhanced spawner The spawner should be able to spawn objects which can spawn objects themselves. This is needed for the meteor object. It spawns fire trails. --- .../view/animation/ImageAnimationView.kt | 33 ++++- .../com/soywiz/korim/format/ImageAnimation.kt | 3 +- .../com/soywiz/korim/format/ImageData.kt | 2 +- .../quillraven/fleks/collection/bitArray.kt | 1 + .../com/github/quillraven/fleks/family.kt | 2 +- .../com/github/quillraven/fleks/system.kt | 9 +- .../kotlin/components/ImageAnimation.kt | 46 ------- .../commonMain/kotlin/components/Position.kt | 20 --- .../commonMain/kotlin/components/Spawner.kt | 21 ++- .../commonMain/kotlin/components/Sprite.kt | 20 +++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 93 +++++++------ .../kotlin/systems/ImageAnimationSystem.kt | 123 ------------------ .../commonMain/kotlin/systems/MoveSystem.kt | 49 +------ .../kotlin/systems/SpawnerSystem.kt | 104 ++++++--------- .../commonMain/kotlin/systems/SpriteSystem.kt | 88 +++++++++++++ 15 files changed, 260 insertions(+), 354 deletions(-) delete mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt delete mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt diff --git a/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt b/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt index 53a5a264b..8cd61077e 100644 --- a/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt +++ b/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt @@ -28,6 +28,9 @@ open class ImageAnimationView( ) : Container() { private var nframes: Int = 1 + var onPlayFinished: (() -> Unit)? = null + var onDestroyLayer: ((T) -> Unit)? = null + var animation: ImageAnimation? = animation set(value) { if (field !== value) { @@ -36,6 +39,12 @@ open class ImageAnimationView( } } var direction: ImageAnimation.Direction? = direction + set(value) { + if (field !== value) { + field = value + setFirstFrame() + } + } private val computedDirection: ImageAnimation.Direction get() = direction ?: animation?.direction ?: ImageAnimation.Direction.FORWARD private val layers = fastArrayListOf() @@ -58,7 +67,7 @@ open class ImageAnimationView( private fun setFrame(frameIndex: Int) { - val frame = animation?.frames?.getCyclicOrNull(frameIndex) + val frame = if (animation?.frames?.isNotEmpty() == true) animation?.frames?.getCyclicOrNull(frameIndex) else null if (frame != null) { frame.layerData.fastForEach { val image = layers[it.layer.index] @@ -70,6 +79,8 @@ open class ImageAnimationView( ImageAnimation.Direction.FORWARD -> +1 ImageAnimation.Direction.REVERSE -> -1 ImageAnimation.Direction.PING_PONG -> if (frameIndex + dir !in 0 until nframes) -dir else dir + ImageAnimation.Direction.ONCE_FORWARD -> if (frameIndex < nframes - 1) +1 else 0 + ImageAnimation.Direction.ONCE_REVERSE -> if (frameIndex == 0) 0 else -1 } nextFrameIndex = (frameIndex + dir) umod nframes } else { @@ -78,15 +89,17 @@ open class ImageAnimationView( } private fun setFirstFrame() { - if (computedDirection == ImageAnimation.Direction.REVERSE) { - setFrame(nframes - 1) - } else { - setFrame(0) - } + if (computedDirection == ImageAnimation.Direction.REVERSE || computedDirection == ImageAnimation.Direction.ONCE_REVERSE) { + setFrame(nframes - 1) + } else { + setFrame(0) + } } private fun didSetAnimation() { nframes = animation?.frames?.size ?: 1 + // Before clearing layers let parent possibly recycle layer objects (e.g. return to pool, etc.) + for (layer in layers) { onDestroyLayer?.invoke(layer) } layers.clear() removeChildren() dir = +1 @@ -115,7 +128,13 @@ open class ImageAnimationView( if (running) { nextFrameIn -= it if (nextFrameIn <= 0.0.milliseconds) { - setFrame(nextFrameIndex) + // Check if animation should be played only once + if (dir == 0) { + running = false + onPlayFinished?.invoke() + } else { + setFrame(nextFrameIndex) + } } } } diff --git a/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageAnimation.kt b/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageAnimation.kt index 5a86f18cb..dbb805ef3 100644 --- a/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageAnimation.kt +++ b/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageAnimation.kt @@ -6,5 +6,4 @@ open class ImageAnimation( val name: String, val layers: List = frames.flatMap { it.layerData }.map { it.layer }.distinct().sortedBy { it.index } ) { - enum class Direction { FORWARD, REVERSE, PING_PONG } -} + enum class Direction { ONCE_FORWARD, ONCE_REVERSE, FORWARD, REVERSE, PING_PONG }} diff --git a/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageData.kt b/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageData.kt index 3263cbd41..1fed5daf5 100644 --- a/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageData.kt +++ b/korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageData.kt @@ -21,7 +21,7 @@ open class ImageData constructor( animations: List = fastArrayListOf(), name: String? = null, ): ImageData = - ImageData(animations.first().frames, loopCount = loopCount, layers = layers, animations = animations, name = name) + ImageData(frames = fastArrayListOf(), loopCount = loopCount, layers = layers, animations = animations, name = name) } val defaultAnimation = ImageAnimation(frames, ImageAnimation.Direction.FORWARD, "default") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index 8defa3c5d..13303d4cd 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -140,6 +140,7 @@ class BitArray( } override fun equals(other: Any?): Boolean { + if (other == null) return false if (this === other) return true other as BitArray diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index 6c6058e18..1e69e39a6 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -22,7 +22,7 @@ class AnyOf(val components: Array>) /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. - * A configuration is defined via the three system properties "allOfComponents", "noneOfComponents" and "anyOfComponents. + * A configuration is defined via the three [IteratingSystem] properties "allOf", "noneOf" and "anyOf". * Each component is assigned to a unique index. That index is set in the [allOf], [noneOf] or [anyOf][] [BitArray]. * * A family is an [EntityListener] and gets notified when an [entity][Entity] is added to the world or the diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index d23672ade..7787df272 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -2,7 +2,6 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator -import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass /** @@ -125,8 +124,13 @@ object Manual : SortingType * An [IntervalSystem] of a [world][World] with a context to [entities][Entity]. * * It must have at least one of [allOf], [anyOf] or [noneOf] objects defined. - * These objects define a [Family] of entities for which the [IteratingSystem] will get active and. + * These objects define a [Family] of entities for which the [IteratingSystem] will get active. + * The [IteratingSystem] will use those components which are part of the family config for + * any specific processing within this system. * + * @param allOf is specifying the family to which this system belongs. + * @param noneOf is specifying the family to which this system belongs. + * @param anyOf is specifying the family to which this system belongs. * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. * Default value is an empty comparator which means no sorting. * @param sortingType the [type][SortingType] of sorting for entities when using a [comparator]. @@ -355,7 +359,6 @@ class SystemService( * @throws [FleksSystemInjectException] if the Injector does not contain an entry * for the given type in its internal maps. */ -@ThreadLocal object Inject { @PublishedApi internal lateinit var injectObjects: Map, Injectable> diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt deleted file mode 100644 index c0d8e753a..000000000 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/ImageAnimation.kt +++ /dev/null @@ -1,46 +0,0 @@ -package components - -import com.github.quillraven.fleks.ComponentListener -import com.github.quillraven.fleks.Entity -import com.soywiz.kds.Pool -import com.soywiz.korge.view.Image -import com.soywiz.korge.view.animation.ImageAnimationView -import com.soywiz.korim.bitmap.Bitmaps - -data class ImageAnimation( - var lifeCycle: LifeCycle = LifeCycle.INACTIVE, - var imageData: String = "", - var animation: String = "", - var isPlaying: Boolean = false, - var forwardDirection: Boolean = true, - var loop: Boolean = false -) { - enum class LifeCycle { - INACTIVE, INIT, ACTIVE, DESTROY - } -} - -class ImageAnimationListener : ComponentListener { - - private val imageAnimationViewPool = Pool(reset = { it.rewind() }) { - ImageAnimationView(/* TODO enableUpdater = false*/) { imageBitmapTransparentPool.alloc() }.apply { smoothing = true } - } - private val imageBitmapTransparentPool = Pool(reset = { it.bitmap = Bitmaps.transparent }, preallocate = 20) { - Image(Bitmaps.transparent) - } - - - override fun onComponentAdded(entity: Entity, component: ImageAnimation) { - - // Init component view - - println("Component $component added to $entity!") - } - - override fun onComponentRemoved(entity: Entity, component: ImageAnimation) { - - // Reset details for reusing in another entity - - println("Component $component removed from $entity!") - } -} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt index 2bb81ca93..246e76820 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt @@ -1,28 +1,8 @@ package components -import com.github.quillraven.fleks.ComponentListener -import com.github.quillraven.fleks.Entity - data class Position( var x: Double = 0.0, var y: Double = 0.0, var xAcceleration: Double = 0.0, var yAcceleration: Double = 0.0, ) - -class PositionListener : ComponentListener { - override fun onComponentAdded(entity: Entity, component: Position) { - println("Component $component added to $entity!") - } - - override fun onComponentRemoved(entity: Entity, component: Position) { - - // Reset details for reusing in another entity - component.x = 0.0 - component.y = 0.0 - component.xAcceleration = 0.0 - component.yAcceleration = 0.0 - - println("Component $component removed from $entity!") - } -} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt index 33b34c457..0a186c234 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt @@ -5,10 +5,23 @@ data class Spawner( var numberOfObjects: Int = 1, var interval: Int = 0, // 0 - disabled, 1 - every frame, 2 - every second frame, 3 - every third frame,... var timeVariation: Int = 0, // 0 - no variation, 1 - one frame variation, 2 - two frames variation, ... - var xPosVariation: Double = 0.0, - var yPosVariation: Double = 0.0, - var xAccel: Double = 0.0, - var yAccel: Double = 0.0, + // + var spawnerNumberOfObjects: Int = 0, // 0 - Disable spawning feature for spawned object + var spawnerInterval: Int = 0, + var spawnerTimeVariation: Int = 0, + // + var positionX: Double = 0.0, + var positionY: Double = 0.0, + var positionVariationX: Double = 0.0, + var positionVariationY: Double = 0.0, + var positionAccelerationX: Double = 0.0, + var positionAccelerationY: Double = 0.0, + // + var spriteImageData: String = "", // "" - Disable sprite graphic for spawned object + var spriteAnimation: String = "", + var spriteIsPlaying: Boolean = false, + var spriteForwardDirection: Boolean = true, + var spriteLoop: Boolean = false, // internal state var nextSpawnIn: Int = 0 ) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt new file mode 100644 index 000000000..3d25d5981 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt @@ -0,0 +1,20 @@ +package components + +import com.soywiz.korge.view.Image +import com.soywiz.korge.view.animation.ImageAnimationView +import com.soywiz.korim.bitmap.Bitmaps + +data class Sprite( + var lifeCycle: LifeCycle = LifeCycle.INACTIVE, + var imageData: String = "", + var animation: String = "", + var isPlaying: Boolean = false, + var forwardDirection: Boolean = true, + var loop: Boolean = false, + // internal data + var imageAnimView: ImageAnimationView = ImageAnimationView { Image(Bitmaps.transparent) }.apply { smoothing = false } +) { + enum class LifeCycle { + INACTIVE, INIT, ACTIVE, DESTROY + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 06aa27b84..02925d503 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -1,11 +1,20 @@ import com.soywiz.korge.Korge import com.soywiz.korge.scene.Scene import com.soywiz.korge.scene.sceneContainer -import com.soywiz.korge.view.* +import com.soywiz.korge.view.Container +import com.soywiz.korge.view.container +import com.soywiz.korge.view.addUpdater import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors +import com.soywiz.klock.Stopwatch +import com.soywiz.korim.format.ASE +import com.soywiz.korim.format.ImageData +import com.soywiz.korim.format.readImageData +import com.soywiz.korio.file.std.resourcesVfs + import com.github.quillraven.fleks.* import systems.* +import systems.SpriteSystem.SpriteListener import components.* const val scaleFactor = 2 @@ -20,63 +29,69 @@ suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor rootSceneContainer.changeTo() } +var aseImage: ImageData? = null + class ExampleScene : Scene() { private val atlas = MutableAtlasUnit(1024, 1024) override suspend fun Container.sceneInit() { + val sw = Stopwatch().start() + aseImage = resourcesVfs["sprites.ase"].readImageData(ASE, atlas = atlas) + println("loaded resources in ${sw.elapsed}") } override suspend fun Container.sceneMain() { + container { + scale = scaleFactor.toDouble() // val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") - // This is the world object of the entity component system (ECS) - // It contains all ECS related configuration - val world = World { - entityCapacity = 20 + // TODO build a views container for handling layers for the ImageAnimationSystem of Fleks ECS + val layerContainer = container() - // Register all needed systems - system(::MoveSystem) - system(::SpawnerSystem) - system(::ImageAnimationSystem) + // This is the world object of the entity component system (ECS) + // It contains all ECS related configuration + val world = World { + entityCapacity = 512 - // Register all needed components and its listeners (if needed) - component(::Position, ::PositionListener) - component(::ImageAnimation, ::ImageAnimationListener) - component(::Spawner) - - // Register external objects which are used by systems and component listeners -// inject(imageAnimationViewPool) -// inject(imageBitmapTransparentPool) - } + // Register all needed systems + system(::MoveSystem) + system(::SpawnerSystem) + system(::SpriteSystem) - val pos = world.mapper() + // Register all needed components and its listeners (if needed) + component(::Position) + component(::Sprite, ::SpriteListener) + component(::Spawner) - val spawner = world.entity { - add { // Position of spawner - x = 130.0 - y = 100.0 + // Register external objects which are used by systems and component listeners + inject(layerContainer) } - add { // Config for spawner object - numberOfObjects = 7 - interval = 1 - timeVariation = 0 - xPosVariation = 50.0 - yPosVariation = 7.0 - xAccel = -0.8 - yAccel = -1.0 - } - add { // Config for spawner object - imageData = "sprite2" - animation = "FireTrail" // "FireTrail" - "TestNum" - isPlaying = true + + val spawner = world.entity { + add { // Position of spawner + x = 130.0 + y = 100.0 + } + add { // Config for spawner object + numberOfObjects = 1 // which will be created at once + interval = 30 // every 30 frames + timeVariation = 0 + positionVariationX = 50.0 + positionVariationY = 0.0 + positionAccelerationX = 40.0 + positionAccelerationY = 50.0 +// spriteImageData = "sprite" +// spriteAnimation = "FireTrail" // "FireTrail" - "TestNum" +// spriteIsPlaying = true + } } - } - addUpdater { dt -> - world.update(dt.milliseconds.toFloat()) + addUpdater { dt -> + world.update(dt.seconds.toFloat()) + } } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt deleted file mode 100644 index 6e0f7136b..000000000 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/ImageAnimationSystem.kt +++ /dev/null @@ -1,123 +0,0 @@ -package systems - -//import aseImage -import com.github.quillraven.fleks.* -import com.soywiz.kds.Pool -import com.soywiz.korge.view.* -import com.soywiz.korge.view.animation.ImageAnimationView -import components.* - -/** - * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the configuration from - * [ImageAnimationComponent] to setup graphics from Assets and create an ImageAnimationView object for displaying in the Container. - * - */ -class ImageAnimationSystem : IteratingSystem( - allOf = AllOf(arrayOf(ImageAnimation::class, Position::class)), - interval = Fixed(500f) // every 500 millisecond -) { - - private val imageAnimationViewPool: Pool> = Inject.dependency() - private val imageBitmapTransparentPool: Pool = Inject.dependency() - private val position: ComponentMapper = Inject.componentMapper() - private val imageAnimation: ComponentMapper = Inject.componentMapper() - - override fun onInit() { - } - - override fun onTickEntity(entity: Entity) { - println("[Entity: ${entity.id}] image animation on tick") -// println("pos id: ${position.id} x: ${position[entity].x} y: ${position[entity].y}") - } -} - -/* -class ImageAnimationSystem( - private val container: Container -) : SubSystemBase() { - - private val doSmoothing = false - - private val imageAnimViewMap: MutableMap> = mutableMapOf() - - override fun registerEntity(id: Int, type: EntityAspects) { - if (type.imageAnimation) { - activeEntities.add(id) - getEntityComponents(id).imageAnimationComponent.lifeCycle = LifeCycle.INIT - } - } - - // Special implementation for this sub-system - override fun unregisterEntity(id: Int) { - val imageAnimView = imageAnimViewMap.remove(id)!! - imageAnimationViewPool.free(imageAnimView) - imageAnimView.removeFromParent() - activeEntities.remove(id) - getEntityComponents(id).imageAnimationComponent.lifeCycle = LifeCycle.INACTIVE - } - - override fun fixedUpdate() { - } - - override fun update(dt: TimeSpan, tmod: Double) { - activeEntities.fastForEach { id -> - val entry = getEntityComponents(id) - when (entry.imageAnimationComponent.lifeCycle) { - LifeCycle.INIT -> createImageAnimationView(entry, id) - LifeCycle.ACTIVE -> { - imageAnimViewMap[id]?.updateAnimation(dt) - } - else -> {} - } - } - } - - override fun postUpdate(dt: TimeSpan, tmod: Double) { - activeEntities.fastForEach { id -> - val imageAnimation = getEntityComponents(id) - when (imageAnimation.imageAnimationComponent.lifeCycle) { - LifeCycle.INIT -> {} - LifeCycle.ACTIVE -> { - // sync view position - val container = imageAnimViewMap[id] - container?.x = imageAnimation.positionComponent.x - container?.y = imageAnimation.positionComponent.y - } - LifeCycle.DESTROY -> { - // Object is going to be recycled - destroyEntity(id) - } - else -> {} - } - } - } - - private fun createImageAnimationView(entry: EntityComponents, id: Int) { - // initialize component data and config - entry.let { - val imageAnimView = imageAnimationViewPool.alloc() - // If Asset is not available destroy the object again - it.imageAnimationComponent.lifeCycle = if (aseImage == null) LifeCycle.DESTROY else LifeCycle.ACTIVE - - // Set animation object - imageAnimView.animation = - // TODO get this from Assets object with "imageData" string - aseImage?.animationsByName?.getOrElse(it.imageAnimationComponent.animation) { aseImage?.defaultAnimation } - imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } - imageAnimView.onPlayFinished = { it.imageAnimationComponent.lifeCycle = LifeCycle.DESTROY } - imageAnimView.addTo(container) - // Set play status - imageAnimView.direction = when { - it.imageAnimationComponent.forwardDirection && !it.imageAnimationComponent.loop -> ImageAnimation.Direction.ONCE_FORWARD - !it.imageAnimationComponent.forwardDirection && it.imageAnimationComponent.loop -> ImageAnimation.Direction.REVERSE - !it.imageAnimationComponent.forwardDirection && !it.imageAnimationComponent.loop -> ImageAnimation.Direction.ONCE_REVERSE - else -> ImageAnimation.Direction.FORWARD - } - if (it.imageAnimationComponent.isPlaying) imageAnimView.play() - - imageAnimViewMap[id] = imageAnimView - } - } - -} -*/ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt index 97ce62c94..2d33990aa 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -4,56 +4,21 @@ import com.github.quillraven.fleks.* import components.* class MoveSystem : IteratingSystem( - AllOf(arrayOf(Position::class)), - interval = Fixed(500f) // every 500 millisecond + allOf = AllOf(arrayOf(Position::class)), + interval = EachFrame +// interval = Fixed(500f) // for testing every 500 millisecond ) { - private val position: ComponentMapper = Inject.componentMapper() + private val positions: ComponentMapper = Inject.componentMapper() override fun onInit() { } override fun onTickEntity(entity: Entity) { - val pos = position[entity] +// println("[Entity: ${entity.id}] MoveSystem onTickEntity") + + val pos = positions[entity] pos.x += pos.xAcceleration * deltaTime pos.y += pos.yAcceleration * deltaTime - - println("[Entity: ${entity.id}] move on tick") - } -} - -/* -class MovingSystem : SubSystemBase() { - - override fun registerEntity(id: Int, type: EntityAspects) { - if (type.position) { - activeEntities.add(id) - - // initialize component data and config - getEntityComponents(id).positionComponent.let { - it.x = 0.0 - it.y = 0.0 - } - } - } - - override fun fixedUpdate() { - } - - override fun update(dt: TimeSpan, tmod: Double) { - - // TODO for stesting only... - activeEntities.fastForEach { id -> - val positionComponent = getEntityComponents(id).positionComponent - // TODO further implement dynamic moving - positionComponent.x += positionComponent.xAcceleration * tmod - positionComponent.y += positionComponent.yAcceleration * tmod - } - - } - - override fun postUpdate(dt: TimeSpan, tmod: Double) { } - } -*/ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index a50055500..ff2fe073f 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -1,88 +1,60 @@ package systems -import com.github.quillraven.fleks.* -import components.ImageAnimation -import components.Position -import components.Spawner import kotlin.random.Random - -fun ClosedFloatingPointRange.random() = Random.nextDouble(start, endInclusive) -fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble(), endInclusive.toDouble()).toFloat() -fun IntRange.random() = Random.nextInt(start, endInclusive) +import com.github.quillraven.fleks.* +import components.* class SpawnerSystem : IteratingSystem( - AllOf(arrayOf(Spawner::class)), - interval = Fixed(500f) // every 500 millisecond + allOf = AllOf(arrayOf(Spawner::class)), + interval = EachFrame +// interval = Fixed(500f) // for testing every 500 millisecond ) { - private val imageAnimation: ComponentMapper = Inject.componentMapper() - private val position: ComponentMapper = Inject.componentMapper() - private val spawner: ComponentMapper = Inject.componentMapper() + private val sprites: ComponentMapper = Inject.componentMapper() + private val positions: ComponentMapper = Inject.componentMapper() + private val spawners: ComponentMapper = Inject.componentMapper() override fun onInit() { } override fun onTickEntity(entity: Entity) { - spawn(entity) - println("[Entity: ${entity.id}] spawner on tick - create new entity") - } - - private fun spawn(entity: Entity) { - world.entity { - add { // Position of spawner - x = position[entity].x - if (spawner[entity].xPosVariation != 0.0) x += (-spawner[entity].xPosVariation..spawner[entity].xPosVariation).random() - y = position[entity].y - if (spawner[entity].yPosVariation != 0.0) y += (-spawner[entity].yPosVariation..spawner[entity].yPosVariation).random() - xAcceleration = spawner[entity].xAccel - yAcceleration = spawner[entity].yAccel - } - add { // Config for spawner object - imageData = imageAnimation[entity].imageData - animation = imageAnimation[entity].animation - isPlaying = imageAnimation[entity].isPlaying - } - } - } -} -/* -class SpawnerSystem : SubSystemBase() { - - override fun registerEntity(id: Int, type: EntityAspects) { - if (type.spawner) { - activeEntities.add(id) - - // initialize component data and config - getEntityComponents(id).spawnerComponent.let { - it.nextSpawnIn = 0 + val spawner = spawners[entity] + if (spawner.interval > 0) { + if (spawner.nextSpawnIn <= 0) { +// println("[Entity: ${entity.id}] SpawnerSystem onTickEntity - create new entity") + spawn(entity) + spawner.nextSpawnIn = spawner.interval + if (spawner.timeVariation != 0) spawner.nextSpawnIn += (-spawner.timeVariation..spawner.timeVariation).random() + } else { + spawner.nextSpawnIn-- } } } - override fun fixedUpdate() { - // Do nothing - } - - override fun update(dt: TimeSpan, tmod: Double) { - activeEntities.fastForEach { id -> - val entity = getEntityComponents(id) - entity.lock = true - val spawner = entity.spawnerComponent - if (spawner.interval > 0) { - if (spawner.nextSpawnIn <= 0) { - spawn(id) - spawner.nextSpawnIn = spawner.interval - if (spawner.timeVariation != 0) spawner.nextSpawnIn += (-spawner.timeVariation..spawner.timeVariation).random() - } else { - spawner.nextSpawnIn-- + private fun spawn(entity: Entity) { + val pos = positions[entity] + val spawner = spawners[entity] + for (i in 0 until spawner.numberOfObjects) { + world.entity { + add { // Position of spawner + x = pos.x + if (spawner.positionVariationX != 0.0) x += (-spawner.positionVariationX..spawner.positionVariationX).random() + y = pos.y + if (spawner.positionVariationY != 0.0) y += (-spawner.positionVariationY..spawner.positionVariationY).random() + xAcceleration = spawner.positionAccelerationX + yAcceleration = spawner.positionAccelerationY + } + add { // Config for spawned object + imageData = spawner.spriteImageData + animation = spawner.spriteAnimation + isPlaying = spawner.spriteIsPlaying + forwardDirection = spawner.spriteForwardDirection + loop = spawner.spriteLoop } } - entity.lock = false } } - override fun postUpdate(dt: TimeSpan, tmod: Double) { - // Do nothing - } + private fun ClosedFloatingPointRange.random() = Random.nextDouble(start, endInclusive) + private fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble(), endInclusive.toDouble()).toFloat() } -*/ diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt new file mode 100644 index 000000000..77b8702b9 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt @@ -0,0 +1,88 @@ +package systems + +import com.github.quillraven.fleks.ComponentListener +import com.github.quillraven.fleks.Entity +import com.github.quillraven.fleks.Inject +import com.soywiz.korge.view.Container +import com.soywiz.korge.view.addTo +import com.soywiz.korim.format.ImageAnimation + +import com.github.quillraven.fleks.* +import components.* +import components.Sprite +import components.Sprite.LifeCycle +import aseImage + +/** + * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the configuration from + * [Sprite] component to setup graphics from Assets and create an ImageAnimationView object for displaying in the Container. + * + */ +class SpriteSystem : IteratingSystem( + allOf = AllOf(arrayOf(Sprite::class, Position::class)), + interval = EachFrame +// interval = Fixed(500f) // for testing every 500 millisecond +) { + + private val positions: ComponentMapper = Inject.componentMapper() + private val sprites: ComponentMapper = Inject.componentMapper() + + override fun onInit() { + } + + override fun onTickEntity(entity: Entity) { +// println("[Entity: ${entity.id}] SpriteSystem onTickEntity") + + val sprite = sprites[entity] + val pos = positions[entity] + when (sprite.lifeCycle) { + LifeCycle.INIT -> { + sprite.lifeCycle = LifeCycle.ACTIVE + } + LifeCycle.ACTIVE -> { + // sync view position + sprite.imageAnimView.x = pos.x + sprite.imageAnimView.y = pos.y + } + LifeCycle.DESTROY -> { + // Object is going to be recycled + world.remove(entity) + } + else -> {} + } + } + + class SpriteListener : ComponentListener { + + private val layerContainer: Container = Inject.dependency() + + override fun onComponentAdded(entity: Entity, component: Sprite) { + // Set animation object + component.imageAnimView.animation = + // TODO get this from Assets object with "imageData" string + aseImage?.animationsByName?.getOrElse(component.animation) { aseImage?.defaultAnimation } +// component.imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } + component.imageAnimView.onPlayFinished = { component.lifeCycle = Sprite.LifeCycle.DESTROY } + component.imageAnimView.addTo(layerContainer) + // Set play status + component.imageAnimView.direction = when { + component.forwardDirection && !component.loop -> ImageAnimation.Direction.ONCE_FORWARD + !component.forwardDirection && component.loop -> ImageAnimation.Direction.REVERSE + !component.forwardDirection && !component.loop -> ImageAnimation.Direction.ONCE_REVERSE + else -> ImageAnimation.Direction.FORWARD + } + if (component.isPlaying) { component.imageAnimView.play() } + component.lifeCycle = Sprite.LifeCycle.ACTIVE + +// println("Component $component") +// println(" added to Entity '${entity.id}'!") + } + + override fun onComponentRemoved(entity: Entity, component: Sprite) { +// println("Component $component") +// println(" removed from Entity '${entity.id}'!") + + component.imageAnimView.removeFromParent() + } + } +} From e81c16c0ece113b19b6f37524854e963bdb29898 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sat, 12 Feb 2022 19:17:25 +0100 Subject: [PATCH 13/27] Implemented spawning of meteorid objects - It is now possible to spawn objects which themselves spawn another objects. - Added variation of acceleration which makes the animation of the file trails looking more dynamic. --- .../commonMain/kotlin/components/Spawner.kt | 23 +++++++-- .../fleks-ecs/src/commonMain/kotlin/main.kt | 31 ++++++++---- .../kotlin/systems/CollisionSystem.kt | 25 ++++++++++ .../kotlin/systems/SpawnerSystem.kt | 50 +++++++++++++++---- 4 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt index 0a186c234..6f8960a49 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt @@ -5,18 +5,31 @@ data class Spawner( var numberOfObjects: Int = 1, var interval: Int = 0, // 0 - disabled, 1 - every frame, 2 - every second frame, 3 - every third frame,... var timeVariation: Int = 0, // 0 - no variation, 1 - one frame variation, 2 - two frames variation, ... - // + // Spawner details for spawned objects (spawned objects do also spawn objects itself) var spawnerNumberOfObjects: Int = 0, // 0 - Disable spawning feature for spawned object var spawnerInterval: Int = 0, - var spawnerTimeVariation: Int = 0, - // - var positionX: Double = 0.0, + var spawnerTimeVariation: Int = 0, + var spawnerPositionX: Double = 0.0, // Position of spawned object relative to spawner position + var spawnerPositionY: Double = 0.0, + var spawnerPositionVariationX: Double = 0.0, + var spawnerPositionVariationY: Double = 0.0, + var spawnerPositionAccelerationX: Double = 0.0, + var spawnerPositionAccelerationY: Double = 0.0, + var spawnerPositionAccelerationVariation: Double = 0.0, + var spawnerSpriteImageData: String = "", // "" - Disable sprite graphic for spawned object + var spawnerSpriteAnimation: String = "", + var spawnerSpriteIsPlaying: Boolean = false, + var spawnerSpriteForwardDirection: Boolean = true, + var spawnerSpriteLoop: Boolean = false, + // Position details for spawned objects + var positionX: Double = 0.0, // Position of spawned object relative to spawner position var positionY: Double = 0.0, var positionVariationX: Double = 0.0, var positionVariationY: Double = 0.0, var positionAccelerationX: Double = 0.0, var positionAccelerationY: Double = 0.0, - // + var positionAccelerationVariation: Double = 0.0, + // Sprite animation details for spawned objects var spriteImageData: String = "", // "" - Disable sprite graphic for spawned object var spriteAnimation: String = "", var spriteIsPlaying: Boolean = false, diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 02925d503..ed68e6027 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -17,7 +17,7 @@ import systems.* import systems.SpriteSystem.SpriteListener import components.* -const val scaleFactor = 2 +const val scaleFactor = 1 suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor, bgcolor = Colors["#000000"]) { @@ -60,6 +60,7 @@ class ExampleScene : Scene() { system(::MoveSystem) system(::SpawnerSystem) system(::SpriteSystem) + system(::CollisionSystem) // Register all needed components and its listeners (if needed) component(::Position) @@ -72,20 +73,30 @@ class ExampleScene : Scene() { val spawner = world.entity { add { // Position of spawner - x = 130.0 - y = 100.0 + x = 100.0 + y = -10.0 } add { // Config for spawner object numberOfObjects = 1 // which will be created at once - interval = 30 // every 30 frames + interval = 60 // every 60 frames timeVariation = 0 - positionVariationX = 50.0 + // Spawner details for spawned objects (spawned objects do also spawn objects itself) + spawnerNumberOfObjects = 5 // Enable spawning feature for spawned object + spawnerInterval = 1 + spawnerPositionVariationX = 10.0 + spawnerPositionVariationY = 10.0 + spawnerPositionAccelerationX = -80.0 + spawnerPositionAccelerationY = -100.0 + spawnerPositionAccelerationVariation = 10.0 + spawnerSpriteImageData = "sprite" // "" - Disable sprite graphic for spawned object + spawnerSpriteAnimation = "FireTrail" // "FireTrail" - "TestNum" + spawnerSpriteIsPlaying = true + // Set position details for spawned objects + positionVariationX = 100.0 positionVariationY = 0.0 - positionAccelerationX = 40.0 - positionAccelerationY = 50.0 -// spriteImageData = "sprite" -// spriteAnimation = "FireTrail" // "FireTrail" - "TestNum" -// spriteIsPlaying = true + positionAccelerationX = 160.0 + positionAccelerationY = 200.0 + positionAccelerationVariation = 10.0 } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt new file mode 100644 index 000000000..5ca3da562 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt @@ -0,0 +1,25 @@ +package systems + +import com.github.quillraven.fleks.* +import components.Position + +class CollisionSystem : IteratingSystem( + allOf = AllOf(arrayOf(Position::class)), + interval = EachFrame +// interval = Fixed(500f) // for testing every 500 millisecond +) { + + private val positions: ComponentMapper = Inject.componentMapper() + + override fun onInit() { + } + + override fun onTickEntity(entity: Entity) { +// println("[Entity: ${entity.id}] MoveSystem onTickEntity") + val pos = positions[entity] + + if (pos.y > 200) { + world.remove(entity) + } + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index ff2fe073f..6c626d937 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -2,6 +2,7 @@ package systems import kotlin.random.Random import com.github.quillraven.fleks.* +import com.soywiz.korge.ui.uiObservable import components.* class SpawnerSystem : IteratingSystem( @@ -10,7 +11,6 @@ class SpawnerSystem : IteratingSystem( // interval = Fixed(500f) // for testing every 500 millisecond ) { - private val sprites: ComponentMapper = Inject.componentMapper() private val positions: ComponentMapper = Inject.componentMapper() private val spawners: ComponentMapper = Inject.componentMapper() @@ -32,24 +32,54 @@ class SpawnerSystem : IteratingSystem( } private fun spawn(entity: Entity) { - val pos = positions[entity] + val spawnerPosition = positions[entity] val spawner = spawners[entity] for (i in 0 until spawner.numberOfObjects) { world.entity { add { // Position of spawner - x = pos.x + x = spawnerPosition.x + spawner.positionX if (spawner.positionVariationX != 0.0) x += (-spawner.positionVariationX..spawner.positionVariationX).random() - y = pos.y + y = spawnerPosition.y + spawner.positionY if (spawner.positionVariationY != 0.0) y += (-spawner.positionVariationY..spawner.positionVariationY).random() xAcceleration = spawner.positionAccelerationX yAcceleration = spawner.positionAccelerationY + if (spawner.positionAccelerationVariation != 0.0) { + val variation = (-spawner.positionAccelerationVariation..spawner.positionAccelerationVariation).random() + xAcceleration += variation + xAcceleration += variation + } } - add { // Config for spawned object - imageData = spawner.spriteImageData - animation = spawner.spriteAnimation - isPlaying = spawner.spriteIsPlaying - forwardDirection = spawner.spriteForwardDirection - loop = spawner.spriteLoop + // Add spawner feature + if (spawner.spawnerNumberOfObjects != 0) { + add { + numberOfObjects = spawner.spawnerNumberOfObjects + interval = spawner.spawnerInterval + timeVariation = spawner.spawnerTimeVariation + // Position details for spawned objects + positionX = spawner.spawnerPositionX + positionY = spawner.spawnerPositionY + positionVariationX = spawner.spawnerPositionVariationX + positionVariationY = spawner.spawnerPositionVariationY + positionAccelerationX = spawner.spawnerPositionAccelerationX + positionAccelerationY = spawner.spawnerPositionAccelerationY + positionAccelerationVariation = spawner.spawnerPositionAccelerationVariation + // Sprite animation details for spawned objects + spriteImageData = spawner.spawnerSpriteImageData + spriteAnimation = spawner.spawnerSpriteAnimation + spriteIsPlaying = spawner.spawnerSpriteIsPlaying + spriteForwardDirection = spawner.spawnerSpriteForwardDirection + spriteLoop = spawner.spawnerSpriteLoop + } + } + // Add sprite animations + if (spawner.spriteImageData.isNotEmpty()) { + add { // Config for spawned object + imageData = spawner.spriteImageData + animation = spawner.spriteAnimation + isPlaying = spawner.spriteIsPlaying + forwardDirection = spawner.spriteForwardDirection + loop = spawner.spriteLoop + } } } } From 2ee52350fa87baf347bb157bf1223ebf48849b11 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Wed, 16 Feb 2022 04:07:05 +0100 Subject: [PATCH 14/27] Implement dependency injection by type names In case of injection of multiple dependencies with the same type it is now possible to specify the type name on injection to the world configuration. When passing that dependency in a system or component listener than the type name can be given as an argument to Inject.dependency() function call. Updated the readme to reflect that injection of dependencies is possible by providing an optional type name as parameter together with the depencency object. --- .../com/github/quillraven/fleks/component.kt | 17 +++---- .../com/github/quillraven/fleks/exception.kt | 30 +++++++------ .../com/github/quillraven/fleks/system.kt | 44 ++++++++++-------- .../com/github/quillraven/fleks/world.kt | 45 ++++++++++--------- .../fleks-ecs/src/commonMain/kotlin/main.kt | 11 +++-- .../commonMain/kotlin/systems/SpriteSystem.kt | 9 ++-- 6 files changed, 84 insertions(+), 72 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt index c5c900f9c..e1894165a 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt @@ -3,7 +3,6 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.Bag import com.github.quillraven.fleks.collection.bag import kotlin.math.max -import kotlin.reflect.KClass /** * Interface of a component listener that gets notified when a component of a specific type @@ -136,14 +135,14 @@ class ComponentMapper( * It creates a [ComponentMapper] for every unique component type and assigns a unique id for each mapper. */ class ComponentService( - componentFactory: Map, () -> Any> + componentFactory: Map Any> ) { /** * Returns map of [ComponentMapper] that stores mappers by its component type. * It is used by the [SystemService] during system creation and by the [EntityService] for entity creation. */ @PublishedApi - internal val mappers: Map, ComponentMapper<*>> + internal val mappers: Map> /** * Returns [Bag] of [ComponentMapper]. The id of the mapper is the index of the bag. @@ -166,10 +165,8 @@ class ComponentService( * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the * world configuration. */ - @Suppress("UNCHECKED_CAST") - fun mapper(type: KClass): ComponentMapper { - val mapper = mappers[type] ?: throw FleksNoSuchComponentException(type) - return mapper as ComponentMapper + fun mapper(type: String): ComponentMapper<*> { + return mappers[type] ?: throw FleksNoSuchComponentException(type) } /** @@ -178,7 +175,11 @@ class ComponentService( * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the * world configuration. */ - inline fun mapper(): ComponentMapper = mapper(T::class) + @Suppress("UNCHECKED_CAST") + inline fun mapper(): ComponentMapper { + val type = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + return mapper(type) as ComponentMapper + } /** * Returns an already existing [ComponentMapper] for the given [compId]. diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 50fb6714c..1ef201b81 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -5,31 +5,35 @@ import kotlin.reflect.KClass abstract class FleksException(message: String) : RuntimeException(message) class FleksSystemAlreadyAddedException(system: KClass<*>) : - FleksException("System ${system.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") + FleksException("System '${system.simpleName}' is already part of the '${WorldConfiguration::class.simpleName}'.") -class FleksComponentAlreadyAddedException(comp: KClass<*>) : - FleksException("Component ${comp.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") +class FleksComponentAlreadyAddedException(comp: String) : + FleksException("Component '$comp' is already part of the '${WorldConfiguration::class.simpleName}'.") class FleksSystemCreationException(system: IteratingSystem) : FleksException("Cannot create system '$system'. IteratingSystem must define at least one of AllOf, NoneOf or AnyOf properties.") class FleksNoSuchSystemException(system: KClass<*>) : - FleksException("There is no system of type ${system.simpleName} in the world.") + FleksException("There is no system of type '${system.simpleName}' in the world.") -class FleksNoSuchComponentException(component: KClass<*>) : - FleksException("There is no component of type ${component.simpleName} in the ComponentMapper. Did you add the component to the ${WorldConfiguration::class.simpleName}?") +class FleksNoSuchComponentException(component: String) : + FleksException("There is no component of type '$component' in the ComponentMapper. Did you add the component to the '${WorldConfiguration::class.simpleName}'?") -class FleksInjectableAlreadyAddedException(type: KClass<*>) : - FleksException("Injectable with name ${type.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") +class FleksInjectableAlreadyAddedException(type: String) : + FleksException("Injectable with type name '$type' is already part of the '${WorldConfiguration::class.simpleName}'. Please add a unique 'type' string as parameter " + + "to inject() function in world configuration and to Inject.dependency() in your systems or component listeners.") -class FleksSystemInjectException(injectType: KClass<*>) : - FleksException("Injection object of type ${injectType.simpleName} cannot be found. Did you add all necessary injectables?") +class FleksInjectableTypeHasNoName(type: KClass<*>) : + FleksException("Injectable '$type' does not have simpleName in its class type.") + +class FleksSystemInjectException(injectType: String) : + FleksException("Injection object of type '$injectType' cannot be found. Did you add all necessary injectables?") class FleksNoSuchEntityComponentException(entity: Entity, component: String) : - FleksException("Entity $entity has no component of type $component.") + FleksException("Entity '$entity' has no component of type '$component'.") -class FleksComponentListenerAlreadyAddedException(listener: KClass<*>) : - FleksException("ComponentListener ${listener.simpleName} is already part of the ${WorldConfiguration::class.simpleName}.") +class FleksComponentListenerAlreadyAddedException(listener: String) : + FleksException("ComponentListener '$listener' is already part of the '${WorldConfiguration::class.simpleName}'.") class FleksUnusedInjectablesException(unused: List>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 7787df272..3630d9678 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -249,7 +249,7 @@ abstract class IteratingSystem( class SystemService( world: World, systemFactory: MutableMap, () -> IntervalSystem>, - injectables: MutableMap, Injectable> + injectables: MutableMap ) { @PublishedApi internal val systems: Array @@ -295,9 +295,15 @@ class SystemService( compService: ComponentService, allFamilies: MutableList ): Family { - val allOfComps = system.allOf?.components?.map { compService.mapper(it) } - val noneOfComps = system.noneOf?.components?.map { compService.mapper(it) } - val anyOfComps = system.anyOf?.components?.map { compService.mapper(it) } + val allOfComps = system.allOf?.components?.map { + val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) + compService.mapper(type) } + val noneOfComps = system.noneOf?.components?.map { + val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) + compService.mapper(type) } + val anyOfComps = system.anyOf?.components?.map { + val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) + compService.mapper(type) } if ((allOfComps == null || allOfComps.isEmpty()) && (noneOfComps == null || noneOfComps.isEmpty()) @@ -358,30 +364,32 @@ class SystemService( * * @throws [FleksSystemInjectException] if the Injector does not contain an entry * for the given type in its internal maps. + * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ object Inject { @PublishedApi - internal lateinit var injectObjects: Map, Injectable> + internal lateinit var injectObjects: Map @PublishedApi - internal lateinit var mapperObjects: Map, ComponentMapper<*>> + internal lateinit var mapperObjects: Map> inline fun dependency(): T { - val injectType = T::class - return when { - (injectType in injectObjects) -> { - injectObjects[injectType]!!.used = true - injectObjects[injectType]!!.injObj as T - } - (injectType in mapperObjects) -> { - mapperObjects[injectType]!! as T - } - else -> throw FleksSystemInjectException(injectType) - } + val injectType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + return if (injectType in injectObjects) { + injectObjects[injectType]!!.used = true + injectObjects[injectType]!!.injObj as T + } else throw FleksSystemInjectException(injectType) + } + + inline fun dependency(type: String): T { + return if (type in injectObjects) { + injectObjects[type]!!.used = true + injectObjects[type]!!.injObj as T + } else throw FleksSystemInjectException(type) } @Suppress("UNCHECKED_CAST") inline fun componentMapper(): ComponentMapper { - val injectType = T::class + val injectType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) return if (injectType in mapperObjects) { mapperObjects[injectType]!! as ComponentMapper } else throw FleksSystemInjectException(injectType) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index d6017965e..4a9cfacee 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -2,14 +2,6 @@ package com.github.quillraven.fleks import kotlin.reflect.KClass -/** - * An optional annotation for an [IntervalSystem] constructor parameter to - * inject a dependency exactly by that qualifier's [name]. - */ -@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -annotation class Qualifier(val name: String) - /** * Wrapper class for injectables of the [WorldConfiguration]. * It is used in the [SystemService] to find out any unused injectables. @@ -34,13 +26,13 @@ class WorldConfiguration { internal val systemFactory = mutableMapOf, () -> IntervalSystem>() @PublishedApi - internal val injectables = mutableMapOf, Injectable>() + internal val injectables = mutableMapOf() @PublishedApi - internal val compListenerFactory = mutableMapOf, () -> ComponentListener<*>>() + internal val compListenerFactory = mutableMapOf ComponentListener<*>>() @PublishedApi - internal val componentFactory = mutableMapOf, () -> Any>() + internal val componentFactory = mutableMapOf Any>() /** * Adds the specified [IntervalSystem] to the [world][World]. @@ -58,11 +50,11 @@ class WorldConfiguration { } /** - * Adds the specified [dependency] under the given [type] which can then be injected to any [IntervalSystem]. + * Adds the specified [dependency] under the given [type] which can then be injected to any [IntervalSystem] or [ComponentListener]. * * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. */ - fun inject(type: KClass, dependency: T) { + fun inject(type: String, dependency: T) { if (type in injectables) { throw FleksInjectableAlreadyAddedException(type) } @@ -71,14 +63,15 @@ class WorldConfiguration { } /** - * Adds the specified dependency which can then be injected to any [IntervalSystem]. - * Refer to [inject]: the name is the qualifiedName of the class of the [dependency]. + * Adds the specified dependency which can then be injected to any [IntervalSystem] or [ComponentListener]. + * Refer to [inject]: the type is the simpleName of the class of the [dependency]. * * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before. + * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ inline fun inject(dependency: T) { - val key = T::class - inject(key, dependency) + val type = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + inject(type, dependency) } /** @@ -89,18 +82,21 @@ class WorldConfiguration { * @param listenerFactory the constructor method for creating the component listener. * @throws [FleksComponentAlreadyAddedException] if the component was already added before. * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. + * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener<*>)? = null) { - val compType = T::class + val compType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) if (compType in componentFactory) { throw FleksComponentAlreadyAddedException(compType) } componentFactory[compType] = compFactory - if (compType in compListenerFactory) { - throw FleksComponentListenerAlreadyAddedException(compType) + if (listenerFactory != null) { + if (compType in compListenerFactory) { + throw FleksComponentListenerAlreadyAddedException(compType) + } + compListenerFactory[compType] = listenerFactory } - if (listenerFactory != null) compListenerFactory[compType] = listenerFactory } } @@ -207,8 +203,13 @@ class World( * * @throws [FleksNoSuchComponentException] if the component of the given [type] does not exist in the * world configuration. + * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ - inline fun mapper(): ComponentMapper = componentService.mapper(T::class) + @Suppress("UNCHECKED_CAST") + inline fun mapper(): ComponentMapper { + val type = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + return componentService.mapper(type) as ComponentMapper + } /** * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index ed68e6027..1f662d8bc 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -17,7 +17,7 @@ import systems.* import systems.SpriteSystem.SpriteListener import components.* -const val scaleFactor = 1 +const val scaleFactor = 3 suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor, bgcolor = Colors["#000000"]) { @@ -45,11 +45,9 @@ class ExampleScene : Scene() { container { scale = scaleFactor.toDouble() - -// val dummyInMoveSystem = MoveSystem.MyClass(text = "Hello injector!") - // TODO build a views container for handling layers for the ImageAnimationSystem of Fleks ECS - val layerContainer = container() + val layer0 = container() + val layer1 = container() // This is the world object of the entity component system (ECS) // It contains all ECS related configuration @@ -68,7 +66,8 @@ class ExampleScene : Scene() { component(::Spawner) // Register external objects which are used by systems and component listeners - inject(layerContainer) + inject("layer0", layer0) +// TODO inject("layer1", layer1) } val spawner = world.entity { diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt index 77b8702b9..efaeaba44 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt @@ -2,12 +2,11 @@ package systems import com.github.quillraven.fleks.ComponentListener import com.github.quillraven.fleks.Entity -import com.github.quillraven.fleks.Inject +import com.github.quillraven.fleks.* import com.soywiz.korge.view.Container import com.soywiz.korge.view.addTo import com.soywiz.korim.format.ImageAnimation -import com.github.quillraven.fleks.* import components.* import components.Sprite import components.Sprite.LifeCycle @@ -54,7 +53,7 @@ class SpriteSystem : IteratingSystem( class SpriteListener : ComponentListener { - private val layerContainer: Container = Inject.dependency() + private val layerContainer: Container = Inject.dependency("layer0") override fun onComponentAdded(entity: Entity, component: Sprite) { // Set animation object @@ -62,7 +61,7 @@ class SpriteSystem : IteratingSystem( // TODO get this from Assets object with "imageData" string aseImage?.animationsByName?.getOrElse(component.animation) { aseImage?.defaultAnimation } // component.imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } - component.imageAnimView.onPlayFinished = { component.lifeCycle = Sprite.LifeCycle.DESTROY } + component.imageAnimView.onPlayFinished = { component.lifeCycle = LifeCycle.DESTROY } component.imageAnimView.addTo(layerContainer) // Set play status component.imageAnimView.direction = when { @@ -72,7 +71,7 @@ class SpriteSystem : IteratingSystem( else -> ImageAnimation.Direction.FORWARD } if (component.isPlaying) { component.imageAnimView.play() } - component.lifeCycle = Sprite.LifeCycle.ACTIVE + component.lifeCycle = LifeCycle.ACTIVE // println("Component $component") // println(" added to Entity '${entity.id}'!") From 96f91a0b7eb3f79e6fa3d27d16c3afeb2f6147a9 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 18 Feb 2022 16:02:20 +0100 Subject: [PATCH 15/27] Add rigidbody and destruct component and update systems Rigidbody and destruct components were added which are use to control movement and destruction of entities. For that a new system called DestructSystem was added. With it the destruction of an entity can triger creation of new objects like explosions, etc. Rigidbody component details will control how an entity is influenced by gravity, friction or damping. This still needs to be implemented yet in the MoveSystem. --- .../com/github/quillraven/fleks/exception.kt | 5 +- .../com/github/quillraven/fleks/family.kt | 15 ------ .../com/github/quillraven/fleks/system.kt | 30 ++++++----- .../commonMain/kotlin/components/Destruct.kt | 16 ++++++ .../commonMain/kotlin/components/Rigidbody.kt | 12 +++++ .../commonMain/kotlin/components/Spawner.kt | 3 ++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 13 +++-- .../kotlin/systems/CollisionSystem.kt | 19 +++++-- .../kotlin/systems/DestructSystem.kt | 54 +++++++++++++++++++ .../commonMain/kotlin/systems/MoveSystem.kt | 25 +++++++-- .../kotlin/systems/SpawnerSystem.kt | 16 +++--- .../commonMain/kotlin/systems/SpriteSystem.kt | 2 +- .../src/commonMain/kotlin/utils/Utils.kt | 8 +++ 13 files changed, 168 insertions(+), 50 deletions(-) create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/utils/Utils.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 1ef201b81..6377d0051 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -26,9 +26,12 @@ class FleksInjectableAlreadyAddedException(type: String) : class FleksInjectableTypeHasNoName(type: KClass<*>) : FleksException("Injectable '$type' does not have simpleName in its class type.") -class FleksSystemInjectException(injectType: String) : +class FleksSystemDependencyInjectException(injectType: String) : FleksException("Injection object of type '$injectType' cannot be found. Did you add all necessary injectables?") +class FleksSystemComponentInjectException(injectType: String) : + FleksException("Component mapper for type '$injectType' cannot be found. Did you add that component to the world configuration?") + class FleksNoSuchEntityComponentException(entity: Entity, component: String) : FleksException("Entity '$entity' has no component of type '$component'.") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index 1e69e39a6..f01cc04aa 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -5,21 +5,6 @@ import com.github.quillraven.fleks.collection.EntityComparator import com.github.quillraven.fleks.collection.IntBag import kotlin.reflect.KClass -/** - * [Entities][Entity] must have all [components] specified to be part of the [family][Family]. - */ -class AllOf(val components: Array>) - -/** - * [Entities][Entity] must not have any [components] specified to be part of the [family][Family]. - */ -class NoneOf(val components: Array>) - -/** - * [Entities][Entity] must have at least one of the [components] specified to be part of the [family][Family]. - */ -class AnyOf(val components: Array>) - /** * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components. * A configuration is defined via the three [IteratingSystem] properties "allOf", "noneOf" and "anyOf". diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 3630d9678..5a2fcae91 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -128,9 +128,9 @@ object Manual : SortingType * The [IteratingSystem] will use those components which are part of the family config for * any specific processing within this system. * - * @param allOf is specifying the family to which this system belongs. - * @param noneOf is specifying the family to which this system belongs. - * @param anyOf is specifying the family to which this system belongs. + * @param allOfComponents is specifying the family to which this system belongs. + * @param noneOfComponents is specifying the family to which this system belongs. + * @param anyOfComponents is specifying the family to which this system belongs. * @param comparator an optional [EntityComparator] that is used to sort [entities][Entity]. * Default value is an empty comparator which means no sorting. * @param sortingType the [type][SortingType] of sorting for entities when using a [comparator]. @@ -138,9 +138,9 @@ object Manual : SortingType * @param enabled defines if the system gets updated when the [world][World] gets updated. Default is true. */ abstract class IteratingSystem( - val allOf: AllOf? = null, - val noneOf: NoneOf? = null, - val anyOf: AnyOf? = null, + val allOfComponents: Array>? = null, + val noneOfComponents: Array>? = null, + val anyOfComponents: Array>? = null, private val comparator: EntityComparator = EMPTY_COMPARATOR, private val sortingType: SortingType = Automatic, interval: Interval = EachFrame, @@ -295,13 +295,13 @@ class SystemService( compService: ComponentService, allFamilies: MutableList ): Family { - val allOfComps = system.allOf?.components?.map { + val allOfComps = system.allOfComponents?.map { val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) compService.mapper(type) } - val noneOfComps = system.noneOf?.components?.map { + val noneOfComps = system.noneOfComponents?.map { val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) compService.mapper(type) } - val anyOfComps = system.anyOf?.components?.map { + val anyOfComps = system.anyOfComponents?.map { val type = it.simpleName ?: throw FleksInjectableTypeHasNoName(it) compService.mapper(type) } @@ -362,8 +362,10 @@ class SystemService( /** * An [injector][Inject] which is used to inject objects from outside the [IntervalSystem]. * - * @throws [FleksSystemInjectException] if the Injector does not contain an entry - * for the given type in its internal maps. + * @throws [FleksSystemDependencyInjectException] if the Injector does not contain an entry + * for the given type in its internal map. + * @throws [FleksSystemComponentInjectException] if the Injector does not contain a component mapper + * for the given type in its internal map. * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ object Inject { @@ -377,14 +379,14 @@ object Inject { return if (injectType in injectObjects) { injectObjects[injectType]!!.used = true injectObjects[injectType]!!.injObj as T - } else throw FleksSystemInjectException(injectType) + } else throw FleksSystemDependencyInjectException(injectType) } inline fun dependency(type: String): T { return if (type in injectObjects) { injectObjects[type]!!.used = true injectObjects[type]!!.injObj as T - } else throw FleksSystemInjectException(type) + } else throw FleksSystemDependencyInjectException(type) } @Suppress("UNCHECKED_CAST") @@ -392,6 +394,6 @@ object Inject { val injectType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) return if (injectType in mapperObjects) { mapperObjects[injectType]!! as ComponentMapper - } else throw FleksSystemInjectException(injectType) + } else throw FleksSystemComponentInjectException(injectType) } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt new file mode 100644 index 000000000..cf5918944 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt @@ -0,0 +1,16 @@ +package components + +/** + * This component contains details on destruction of the entity like if other entities should be spawned + * or if other systems should be fed with data (score, player health or damage, enemy damage, + * collectable rewards, etc.) + * + */ +data class Destruct( + // Setting this to true triggers the DestructSystem to execute destruction of the entity + var triggerDestruction: Boolean = false, + // details about what explosion animation should be spawned, etc. + var spawnExplosion: Boolean = false, + var explosionParticleRange: Double = 0.0, + var explosionParticleAcceleration: Double = 0.0, +) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt new file mode 100644 index 000000000..85b1daa26 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt @@ -0,0 +1,12 @@ +package components + +/** + * This is a very basic definition of a rigidbody which does not take rotation into account. + */ +data class Rigidbody( + var mass: Double = 0.0, + var velocityX: Double = 0.0, + var velocityY: Double = 0.0, + var damping: Double = 0.0, // e.g. air resistance of the object when falling + var friction: Double = 0.0, // e.g. friction of the object when it moves over surfaces +) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt index 6f8960a49..2253c0a65 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt @@ -35,6 +35,9 @@ data class Spawner( var spriteIsPlaying: Boolean = false, var spriteForwardDirection: Boolean = true, var spriteLoop: Boolean = false, + // Destruct info for spawned objects + var destruct: Boolean = false, // true - spawned object gets a destruct component, false - no destruct component spawned + // internal state var nextSpawnIn: Int = 0 ) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 1f662d8bc..2c8459249 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -59,15 +59,18 @@ class ExampleScene : Scene() { system(::SpawnerSystem) system(::SpriteSystem) system(::CollisionSystem) + system(::DestructSystem) // Register all needed components and its listeners (if needed) component(::Position) component(::Sprite, ::SpriteListener) component(::Spawner) + component(::Destruct) + component(::Rigidbody) // Register external objects which are used by systems and component listeners inject("layer0", layer0) -// TODO inject("layer1", layer1) +// inject("layer1", layer1) TODO add more layers for explosion objects to be on top } val spawner = world.entity { @@ -84,18 +87,20 @@ class ExampleScene : Scene() { spawnerInterval = 1 spawnerPositionVariationX = 10.0 spawnerPositionVariationY = 10.0 - spawnerPositionAccelerationX = -80.0 + spawnerPositionAccelerationX = -30.0 spawnerPositionAccelerationY = -100.0 - spawnerPositionAccelerationVariation = 10.0 + spawnerPositionAccelerationVariation = 20.0 spawnerSpriteImageData = "sprite" // "" - Disable sprite graphic for spawned object spawnerSpriteAnimation = "FireTrail" // "FireTrail" - "TestNum" spawnerSpriteIsPlaying = true // Set position details for spawned objects positionVariationX = 100.0 positionVariationY = 0.0 - positionAccelerationX = 160.0 + positionAccelerationX = 90.0 positionAccelerationY = 200.0 positionAccelerationVariation = 10.0 + // Destruct info for spawned objects + destruct = true } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt index 5ca3da562..a078b541b 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt @@ -1,15 +1,17 @@ package systems import com.github.quillraven.fleks.* +import components.Destruct import components.Position class CollisionSystem : IteratingSystem( - allOf = AllOf(arrayOf(Position::class)), + allOfComponents = arrayOf(Position::class), interval = EachFrame // interval = Fixed(500f) // for testing every 500 millisecond ) { - private val positions: ComponentMapper = Inject.componentMapper() + private val positions = Inject.componentMapper() + private val destructs = Inject.componentMapper() override fun onInit() { } @@ -18,8 +20,17 @@ class CollisionSystem : IteratingSystem( // println("[Entity: ${entity.id}] MoveSystem onTickEntity") val pos = positions[entity] - if (pos.y > 200) { - world.remove(entity) + // To make collision detection easy we check here just the Y position if it is below 200 which means + // that the object is colliding - In real games here is a more sophisticated collision check necessary ;-) + if (pos.y > 200.0) { + // Check if entity has a destruct component + if (destructs.contains(entity)) { + // yes - then delegate "destruction" of the entity to the DestructSystem - it will destroy the entity after some other task are done + destructs[entity].triggerDestruction = true + } else { + // no - else the entity gets destroyed immediately + world.remove(entity) + } } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt new file mode 100644 index 000000000..cc00f78dd --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt @@ -0,0 +1,54 @@ +package systems + +import com.github.quillraven.fleks.Entity +import com.github.quillraven.fleks.Inject +import com.github.quillraven.fleks.IteratingSystem +import components.Destruct +import components.Position +import components.Sprite +import utils.random + +/** + * This system controls the "destruction" of an game object / entity. + * + */ +class DestructSystem : IteratingSystem( + allOfComponents = arrayOf(Destruct::class) +) { + + private val positions = Inject.componentMapper() + private val destructs = Inject.componentMapper() + + override fun onTickEntity(entity: Entity) { + val destruct = destructs[entity] + if (destruct.triggerDestruction) { + val pos = positions[entity] + // The spawning of exposion objects is hardcoded here to 10 objects - that should be put into some component later + for (i in 0 until 20) { + world.entity { + add { // Position of explosion object + // set initial position of explosion object to collision position + x = pos.x + y = pos.y - 10.0 // TODO remove hard coded value + if (destruct.explosionParticleRange != 0.0) { + x += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() + y += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() + } + // make sure that all spawned objects are above 200 - this is hardcoded for now since we only have some basic collision detection at y > 200 + // otherwise they will be destroyed immediately and false appear at position 0x0 + if (y > 200.0) { y = 199.0 } + xAcceleration = pos.xAcceleration + random(destruct.explosionParticleAcceleration) + yAcceleration = -pos.yAcceleration + random(destruct.explosionParticleAcceleration) + } + add { + imageData = "sprite" // "" - Disable sprite graphic for spawned object + animation = "FireTrail" // "FireTrail" - "TestNum" + isPlaying = true + } + } + } + // now destroy entity + world.remove(entity) + } + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt index 2d33990aa..83aefd71d 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -3,22 +3,37 @@ package systems import com.github.quillraven.fleks.* import components.* +/** + * A system which moves entities. It either takes the rididbody of an entity into account or if not + * it moves the entity linear without caring about gravity. + */ class MoveSystem : IteratingSystem( - allOf = AllOf(arrayOf(Position::class)), + allOfComponents = arrayOf(Position::class), // Position component absolutely needed for movement of entity objects + anyOfComponents = arrayOf(Position::class, Rigidbody::class), // Rigidbody not necessarily needed for movement interval = EachFrame // interval = Fixed(500f) // for testing every 500 millisecond ) { - private val positions: ComponentMapper = Inject.componentMapper() + private val positions = Inject.componentMapper() + private val rigidbodies = Inject.componentMapper() override fun onInit() { } override fun onTickEntity(entity: Entity) { // println("[Entity: ${entity.id}] MoveSystem onTickEntity") - val pos = positions[entity] - pos.x += pos.xAcceleration * deltaTime - pos.y += pos.yAcceleration * deltaTime + + if (rigidbodies.contains(entity)) { + // Entity has a rigidbody - that means the movement will be calculated depending on it + val rigidbody = rigidbodies[entity] +// pos.x += pos.a + // TODO implement movement with rigidbody + + } else { + // Do movement without rigidbody which means that the object will not react to gravity, friction and damping + pos.x += pos.xAcceleration * deltaTime + pos.y += pos.yAcceleration * deltaTime + } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index 6c626d937..2211d42de 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -1,12 +1,11 @@ package systems -import kotlin.random.Random import com.github.quillraven.fleks.* -import com.soywiz.korge.ui.uiObservable import components.* +import utils.random class SpawnerSystem : IteratingSystem( - allOf = AllOf(arrayOf(Spawner::class)), + allOfComponents = arrayOf(Spawner::class), interval = EachFrame // interval = Fixed(500f) // for testing every 500 millisecond ) { @@ -81,10 +80,15 @@ class SpawnerSystem : IteratingSystem( loop = spawner.spriteLoop } } + // Add destruct details + if (spawner.destruct) { + add { + spawnExplosion = true + explosionParticleRange = 20.0 + explosionParticleAcceleration = 200.0 + } + } } } } - - private fun ClosedFloatingPointRange.random() = Random.nextDouble(start, endInclusive) - private fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble(), endInclusive.toDouble()).toFloat() } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt index efaeaba44..3da1b3344 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt @@ -18,7 +18,7 @@ import aseImage * */ class SpriteSystem : IteratingSystem( - allOf = AllOf(arrayOf(Sprite::class, Position::class)), + allOfComponents = arrayOf(Sprite::class, Position::class), interval = EachFrame // interval = Fixed(500f) // for testing every 500 millisecond ) { diff --git a/samples/fleks-ecs/src/commonMain/kotlin/utils/Utils.kt b/samples/fleks-ecs/src/commonMain/kotlin/utils/Utils.kt new file mode 100644 index 000000000..8521b1e02 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/utils/Utils.kt @@ -0,0 +1,8 @@ +package utils + +import kotlin.random.Random + +fun ClosedFloatingPointRange.random() = Random.nextDouble(start, endInclusive) +fun ClosedFloatingPointRange.random() = Random.nextDouble(start.toDouble(), endInclusive.toDouble()).toFloat() + +fun random(radius: Double) = (-radius..radius).random() From 62bd7094a6c4bc564f1572ed39014b5957aa9906 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 25 Feb 2022 18:20:13 +0100 Subject: [PATCH 16/27] Add gravity and impulse to explosions --- .../view/animation/ImageAnimationView.kt | 3 +-- .../commonMain/kotlin/components/Impulse.kt | 6 +++++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 21 ++++++++++++------ .../kotlin/systems/CollisionSystem.kt | 13 ++++++++--- .../kotlin/systems/DestructSystem.kt | 10 +++++---- .../commonMain/kotlin/systems/MoveSystem.kt | 11 +++++---- .../kotlin/systems/SpawnerSystem.kt | 6 ++--- .../commonMain/kotlin/systems/SpriteSystem.kt | 7 +++++- .../src/commonMain/resources/sprites.ase | Bin 3355 -> 6749 bytes 9 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt diff --git a/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt b/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt index 8cd61077e..157a09bf5 100644 --- a/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt +++ b/korge/src/commonMain/kotlin/com/soywiz/korge/view/animation/ImageAnimationView.kt @@ -128,12 +128,11 @@ open class ImageAnimationView( if (running) { nextFrameIn -= it if (nextFrameIn <= 0.0.milliseconds) { + setFrame(nextFrameIndex) // Check if animation should be played only once if (dir == 0) { running = false onPlayFinished?.invoke() - } else { - setFrame(nextFrameIndex) } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt new file mode 100644 index 000000000..ea508292e --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt @@ -0,0 +1,6 @@ +package components + +data class Impulse( + var xForce: Double = 0.0, // not used currently + var yForce: Double = 0.0 +) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 2c8459249..253f08ac1 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -50,7 +50,7 @@ class ExampleScene : Scene() { val layer1 = container() // This is the world object of the entity component system (ECS) - // It contains all ECS related configuration + // It contains all ECS related system and component configuration val world = World { entityCapacity = 512 @@ -67,29 +67,36 @@ class ExampleScene : Scene() { component(::Spawner) component(::Destruct) component(::Rigidbody) + component(::Impulse) // Register external objects which are used by systems and component listeners inject("layer0", layer0) // inject("layer1", layer1) TODO add more layers for explosion objects to be on top } - val spawner = world.entity { + // This is the config for the spawner entity which sits on top of the screen and which + // spawns the meteorite objects. + // - The spawner get a "Position" component which set the position of it 10 pixels + // above the visible area. + // - Secondly it gets a "Spawner" component. That tells the system that the spawned + // meteorite objects itself are spawning objects. These are the visible fire trails. + world.entity { add { // Position of spawner x = 100.0 y = -10.0 } add { // Config for spawner object - numberOfObjects = 1 // which will be created at once - interval = 60 // every 60 frames + numberOfObjects = 1 // The spawner will generate one object per second + interval = 60 // every 60 frames which means once per second timeVariation = 0 // Spawner details for spawned objects (spawned objects do also spawn objects itself) spawnerNumberOfObjects = 5 // Enable spawning feature for spawned object spawnerInterval = 1 - spawnerPositionVariationX = 10.0 - spawnerPositionVariationY = 10.0 + spawnerPositionVariationX = 5.0 + spawnerPositionVariationY = 5.0 spawnerPositionAccelerationX = -30.0 spawnerPositionAccelerationY = -100.0 - spawnerPositionAccelerationVariation = 20.0 + spawnerPositionAccelerationVariation = 15.0 spawnerSpriteImageData = "sprite" // "" - Disable sprite graphic for spawned object spawnerSpriteAnimation = "FireTrail" // "FireTrail" - "TestNum" spawnerSpriteIsPlaying = true diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt index a078b541b..cdb4379bf 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt @@ -2,6 +2,7 @@ package systems import com.github.quillraven.fleks.* import components.Destruct +import components.Impulse import components.Position class CollisionSystem : IteratingSystem( @@ -12,6 +13,7 @@ class CollisionSystem : IteratingSystem( private val positions = Inject.componentMapper() private val destructs = Inject.componentMapper() + private val impulses = Inject.componentMapper() override fun onInit() { } @@ -23,12 +25,17 @@ class CollisionSystem : IteratingSystem( // To make collision detection easy we check here just the Y position if it is below 200 which means // that the object is colliding - In real games here is a more sophisticated collision check necessary ;-) if (pos.y > 200.0) { - // Check if entity has a destruct component + // Check if entity has a destruct or impulse component if (destructs.contains(entity)) { - // yes - then delegate "destruction" of the entity to the DestructSystem - it will destroy the entity after some other task are done + // Delegate "destruction" of the entity to the DestructSystem - it will destroy the entity after some other task are done destructs[entity].triggerDestruction = true + } else if (impulses.contains(entity)) { + // Do not destruct entity but let it bounce on the surface + pos.xAcceleration = pos.xAcceleration * 0.7 + pos.yAcceleration = -pos.yAcceleration * 0.9 + pos.y = 199.0 } else { - // no - else the entity gets destroyed immediately + // Entity gets destroyed immediately world.remove(entity) } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt index cc00f78dd..98f82df82 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt @@ -3,9 +3,7 @@ package systems import com.github.quillraven.fleks.Entity import com.github.quillraven.fleks.Inject import com.github.quillraven.fleks.IteratingSystem -import components.Destruct -import components.Position -import components.Sprite +import components.* import utils.random /** @@ -29,7 +27,7 @@ class DestructSystem : IteratingSystem( add { // Position of explosion object // set initial position of explosion object to collision position x = pos.x - y = pos.y - 10.0 // TODO remove hard coded value + y = pos.y - (destruct.explosionParticleRange * 0.5) if (destruct.explosionParticleRange != 0.0) { x += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() y += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() @@ -45,6 +43,10 @@ class DestructSystem : IteratingSystem( animation = "FireTrail" // "FireTrail" - "TestNum" isPlaying = true } + add { + mass = 2.0 + } + add {} } } // now destroy entity diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt index 83aefd71d..5dd62bbee 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -27,13 +27,12 @@ class MoveSystem : IteratingSystem( if (rigidbodies.contains(entity)) { // Entity has a rigidbody - that means the movement will be calculated depending on it val rigidbody = rigidbodies[entity] -// pos.x += pos.a +// pos.xAcceleration * deltaTime // TODO implement movement with rigidbody - - } else { - // Do movement without rigidbody which means that the object will not react to gravity, friction and damping - pos.x += pos.xAcceleration * deltaTime - pos.y += pos.yAcceleration * deltaTime + pos.yAcceleration += rigidbody.mass * 9.81 } + + pos.x += pos.xAcceleration * deltaTime + pos.y += pos.yAcceleration * deltaTime } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index 2211d42de..91f68e1a9 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -10,8 +10,8 @@ class SpawnerSystem : IteratingSystem( // interval = Fixed(500f) // for testing every 500 millisecond ) { - private val positions: ComponentMapper = Inject.componentMapper() - private val spawners: ComponentMapper = Inject.componentMapper() + private val positions = Inject.componentMapper() + private val spawners = Inject.componentMapper() override fun onInit() { } @@ -84,7 +84,7 @@ class SpawnerSystem : IteratingSystem( if (spawner.destruct) { add { spawnExplosion = true - explosionParticleRange = 20.0 + explosionParticleRange = 10.0 explosionParticleAcceleration = 200.0 } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt index 3da1b3344..fc16f5482 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt @@ -53,7 +53,8 @@ class SpriteSystem : IteratingSystem( class SpriteListener : ComponentListener { - private val layerContainer: Container = Inject.dependency("layer0") + private val world = Inject.dependency() + private val layerContainer = Inject.dependency("layer0") override fun onComponentAdded(entity: Entity, component: Sprite) { // Set animation object @@ -62,6 +63,10 @@ class SpriteSystem : IteratingSystem( aseImage?.animationsByName?.getOrElse(component.animation) { aseImage?.defaultAnimation } // component.imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } component.imageAnimView.onPlayFinished = { component.lifeCycle = LifeCycle.DESTROY } +// component.imageAnimView.onPlayFinished = { +// component.imageAnimView.removeFromParent() +// world.remove(entity) +// } component.imageAnimView.addTo(layerContainer) // Set play status component.imageAnimView.direction = when { diff --git a/samples/fleks-ecs/src/commonMain/resources/sprites.ase b/samples/fleks-ecs/src/commonMain/resources/sprites.ase index e7e181ce01d0ef34d8d530d9e015ff711930dff5..70daa6fbfce7aa8f351777a6003a7d617b84a2f7 100644 GIT binary patch literal 6749 zcmeI0dsGwGzQ=b65Rj07v=AQVVHB+8k%*!Qc>qFCK@>eG3N`_1ks=C4l*l6r^3Y&{ zT8ZLSs?iqZa3Qt@@mPT%!Xi`Ol|dL00U`wS;qHH5O}@QWf& zG^x?ZKjFS5k*GdG&^R-(s{~*!aWRbE3*2Gc=1PT87~}@yqgY!QTl2TU_)>Nn#*x=9 zFrIq07RIQbDKIWQ5d-7u?M^TTuAT?uB6n9984NCr6!t+Fx5PYwF+5KR;};DbFxEd9 zg;5DN0P_A`3S(Mw28`RIqF@XT3WPD<^DvBsix$Dyx-lHa`}@mb6y~mi@$gQHG75G; zSCw}n+<~2dJ%IR-9fCt@hzxllEF^`PkP!kxI*10jAQU8mI4TyU5>h|}*gk9=whWtv zZNdg&Yp|(s*Z_`*gmG$9$i%)3SauN&jVURr9-YBjxS zozho#ih!grsgAK+!RoUieWVw7knb?JcP0tsw5}MayF$R=8Y#>3zj6OjPL81acOV)% zu;5?p6HKkDPv5nSA)m!?6QmEZ6MgzWs(mw3w?w!h;jkPDd}PTYZ>@qf{_8zcm0*Cw8-r5;Ur*%ez%R)n zlL@E~57P~5$OZ7PswP*!ud2Xx;pan58o)bX-O?Qo5Zd6OCfz9&)e}_(ax)wtGM-iBM z|84i_m@qx(&*Bh%ur{6wV_4y#>r^T7|Xzq_-c}Um&Z?q^5sVP(g`(Y@t6Ss9Z zyTW*Dz`|jnz_37e@s9^h?uElm_c+b(UeuxCUPDczQ430kXqC5>d$q6i$Z7`ZUAU3k z`B5X8@3<{4Xsxme$RAaAl37mj9h9ZAe$zN-o?^DyZAI^V+a0s7U z)bWo(G5JqP2qg!Xhby;nOyG|40-KaOS#K&9`!T4)Oq}G82gfbhR-sOE_rMq47ItY( zW2*!=D1HmK0HHmEJgB(>-?c zn-`Jz5GN|H@rBn>S4id&wPJ zL&nKIPDR}GB8zjagZ-v=kzk`eX-k0?~|VEjILth~9>yPJ_|8$|QF1CiKPdG2&U zG@%?v5;U|`hlRCe+*uQGQ#LBBpVJwmR{(1dE65p;b>uW0?DG0Gwb0^{xh?j5Z1?^} z$b&ikRksc-jKeW4qH{E7RWxsfaD7M=xNhf=AkhLnp$65N zR&d7Zf+M1J1_za?l#)0~iT}dD1#y(vFiHWhxHZ6NKbyzy&C)SRIY`O$_r*$ip2P4+ zx2iyuFt6hDLk?26#(|2K6bR@?exP@p<$}tk5ott$J*GW;H$jjcZ7ZeKKFPMx?~JP* zul%%h@UA^L7SJ<$reyv(BNMpc&nhW$CetPu9g1IpdexUomEuU~^#ONvy~JczkPy%6jE)8%*R!LgTq~KEKRW6ax<0&|6CU_vIL;x@LXlb7?1{h9*n-YXtiPLGJ??#<83C_>G-&_mXf_0PU=azkmD z(dQknbhCB_<_s%&fu`b`OBOVY$2NYo2k+-O4ajCi6)M!pkKi~02cn%Qh_OS<+HFE@ zww(6B*oAC2+-P+RC2Bx|5$vx!u4$30Qm&dpX1IUwioRE)EDPtmS4V|{k}ku?PnFna zqxGnEa=3V_*ai+iJtQto?ggKD`nmXsBn3rkQ~oHn%i}(*##VQ+va#(*13luV|76+E z@(XW1@^X!dwFD#ieLymol3l+>v`o6y^X<1I2pWxj#7n$i)FAtm&>4Hk=BDBnOfQ0r_SbIKy7kZRAZN66uur7MC!K~+w=yE$Odh8H`SluLMPok z)=uzDmR&^U-emKbTtHyhggX=30w^>yJrmxp)+6@m(J1Qpyc2ZMGfpBHJM-hn9xaY9 z-*f!gBwm3P^xMOmn;wg^p7Zp$>>@r?I0uVOUoOJAl(N?g<#zXI9m9ycWiBbiE2uhI z3-OBgUWSwxS7nxKot3?xO9#bkCD&C>LDM;?yw7AQX*y(MU^F?GV0n z+-ohW=XwFJlY^Q+PVy!c6_e*5eRb*p8dQv)WNbn^q&WI#X}r784%;`~t6< z>hCKro)v8t^TJx1W*=`4@=PYXL>u1H^TRs2f`TUrq#!MEpZDULMrsyZvP_sBK&I6z zYsCm7=EhWi8huG^RMt9{{iRDcB*PY=4uWiZ6!*k(5`qMps)ld{qEelF zDwG(=o{6ZWgor(59ln2K`5Kc-^RHXC`9-*b6Hi&!ru^iTJDXZd=mI?roABQ8O@LCY z(k(>*dK9aWxD#QC@y=|7q66d%(LYB$7Sx+2s1IRPJ?2jc66O3FR?T3?E=~ju4F%k+@GDjM*DNKM zKh@ELdDPGm$bD)$+tI$mIA{tCem2eek6r$^wab^Z3go1^Kt67l`xe}4eD@;qHziyM zF)1Sd!sOzu`FI>rLTF8*xaA-+h5k>dX0A+C$cYjRxMACnS*k_( z-3*kIbJUc74iz9!pzg-m4gJaoP5)5B1r}(oeNKGSG*CGJs0RE2_d5g;^ zOSl5bLtQ-=c_XdYdl4a+Aid6sj^`_#iy2>BFj@(u;F#GjV^U@1F-bYb_ zWi?@R7siw9uYc%AI9t;SOD?Vq=`#3Yc%Jth0L~Wr9M;0-sj(5$mVm|+?+4&r%!ej_ zJe6{yzIdhgtY2#ME4t6g`D~)K1aB#;ZoDT0<@B-y-x#6<)ryafe-dZCJblQ*TtBUa z+*6rRGGv%%a~S`9ipOFfF}coEiXz(=4;lr}0iNFA*8^H4v1$_S#i)8pF^2AEMERml zpd?y_#+(PrlnC^CBOOIQ`}>T`hY%)&+|41~B?YqQ_%PV`fotbBxkjLOGs@c(axcTL z-N$9-*tF3MhhK_^5TVB4{Bx6lEG>Y9j{-3HfIhAy%9)^|<=dyKw5UAq!2{6;yAO1; zK{d4a@-5`9blU~Lp< z*dwYkao#|goV8Bai?@l&xo397uz*zxmxkPw%Av?{&r&2gB)>an>`)9U=v{unE&W|} zND#jCp>!sS8R~Uv_QvuHhSWSnT&>Tq8HM`6XoTSsR5#*w%Apz07yma@t_Kao!jOfT89e`e*iBnz=CgU%41hjU>}ne*&jf zW;YY8Fx#-YA|%2p^bAh0+CiS|Im~(K8Sm`k+&Jb@n;4-CbGF{HiGv($(E(|KtW{rW z4ZueA08Dp9Q^sX-02rb$ipswqwaf>S=dd!A9*bS}bVkJ%cm!nx7)v+v3Q`Xt7ghY8 z91U`<;QD6g<*Bz}Q?_I3ZP>?bN2eGew7Ef{k3jPQZPq`#SFQ`xqV-hMswVOeFF8=^ z_f>4R3KLs}js&(x?S+2f$I0dwuH61=sT5c4o@oq1$En+Aaxlnge#j8ok9}uUiKL0YDv=&dLMwMDBk32JGvV?3Jew7daTt#WU3LhU#U%cwn<9_9gLSi_GWZ3g@> zYs;y3n^Oa^kof4UPg{(i#rKx)SL1e;2 zb`*0&HsIh(P!>j*W16p9;HzU Us>l4)^}0Sxt26|uRk!Yc04U2PGFysWnwNp$!BXXk?3KLfObiUaK5{XfU|?Y2+SnV*$nFMYNhmN){>W%Q*_TOX zaygT{gCS5%Nr4ea!T={ja871&YJ7fPDohAO3qUa+kXBFtnhga1|JzP>VA0%sfN43? zWCsDMdRB)25E-B(8v`pt#hm1XgoGdbtUMVH?4=HD8I3=aSa%&jxYrh0!iLXrT(&amOvDshWFtcYy2`e*$QToj3ML=_rjZK0Y%Ts?`sYkfsigL13 zT4HefnrRbk>~?n=P8T@#%+Ki+=S;_SaW#oO=hTk*@Z{ze%#oVVeu$x<8F+0Om z9glz{G;@Q1e318q!LZB9=@-Y!92cF^@*cLT9~)(U}X6D;zWEjnvvc>BgI`HZaly; zJ7mefWWi&r_+*nW_xsA4)xN0eyx8m`+?}~_p3HtBX}2dw7qSK5FqAt_hF`e>L zRKA|$!Q=n^yv7e03@l@QUHB>-)V?m=VCC|*n>P;eM9+9+%pPtd&}Gh`#?Y7=jOGMq zARjXZxEVO0F)+)C>HkGTX0ZmK&um++9K0g5xG(1G6|>1V3K&i@Gt4WEvJFW=Gt&i! znVe8Fm2Cd}Pn3w{nQf!zv&Jgu>LsD%Vs^dl6JOaoJv(rPfnldye18O*nI4nZGmF;4 z+;xDr=Znt&!v3OUxAQcFAm#vhh3m(k$q1&)NG&W^|smX9!C0X^cZN#}9|w zU@6Y6YTEyahRkvfeNiQbCB+*~CU54R9hdWqbEWFByY)Z*G@Q?Gx#P_7Aefus$5}VU z5H#aqUS~l`(3}hquQxC#+n)aaVg@7Uf^J~;Im?q``s2yVR>=zthFJxzpj7g7bJR^- zWdsYuWV#U_-9KVnRU8>Wy8#Fz6(zoGFWXUVmM^7hJ?Pz;kJ!4+w;Wjc27Kd f;i+Q|!+#~s!p)fyUl;{ICEKr$4B&zi$YcNjguj?k From e729b3499a02f6c645f1e3210567ff40d7c6481a Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sun, 27 Feb 2022 22:52:18 +0100 Subject: [PATCH 17/27] My pixels against Putin --- .../kotlin/com/github/quillraven/fleks/exception.kt | 3 --- .../kotlin/com/github/quillraven/fleks/world.kt | 12 +++++++----- .../src/commonMain/kotlin/systems/DestructSystem.kt | 4 ++-- .../src/commonMain/kotlin/systems/SpawnerSystem.kt | 4 ++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 6377d0051..268ae037a 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -35,8 +35,5 @@ class FleksSystemComponentInjectException(injectType: String) : class FleksNoSuchEntityComponentException(entity: Entity, component: String) : FleksException("Entity '$entity' has no component of type '$component'.") -class FleksComponentListenerAlreadyAddedException(listener: String) : - FleksException("ComponentListener '$listener' is already part of the '${WorldConfiguration::class.simpleName}'.") - class FleksUnusedInjectablesException(unused: List>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt index 4a9cfacee..f38db24a9 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt @@ -81,10 +81,9 @@ class WorldConfiguration { * @param compFactory the constructor method for creating the component. * @param listenerFactory the constructor method for creating the component listener. * @throws [FleksComponentAlreadyAddedException] if the component was already added before. - * @throws [FleksComponentListenerAlreadyAddedException] if the listener was already added before. * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ - inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener<*>)? = null) { + inline fun component(noinline compFactory: () -> T, noinline listenerFactory: (() -> ComponentListener)? = null) { val compType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) if (compType in componentFactory) { @@ -92,9 +91,7 @@ class WorldConfiguration { } componentFactory[compType] = compFactory if (listenerFactory != null) { - if (compType in compListenerFactory) { - throw FleksComponentListenerAlreadyAddedException(compType) - } + // No need to check compType again in compListenerFactory - it is already guarded with check in componentFactory compListenerFactory[compType] = listenerFactory } } @@ -142,6 +139,11 @@ class World( componentService = ComponentService(worldCfg.componentFactory) entityService = EntityService(worldCfg.entityCapacity, componentService) val injectables = worldCfg.injectables + + // Add world to inject object so that component listeners can get it form injectables, too + // Set "used" to true to make this injectable not mandatory + injectables["World"] = Injectable(this, true) + systemService = SystemService(this, worldCfg.systemFactory, injectables) // create and register ComponentListener diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt index 98f82df82..f3b131d7c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt @@ -21,8 +21,8 @@ class DestructSystem : IteratingSystem( val destruct = destructs[entity] if (destruct.triggerDestruction) { val pos = positions[entity] - // The spawning of exposion objects is hardcoded here to 10 objects - that should be put into some component later - for (i in 0 until 20) { + // The spawning of exposion objects is hardcoded here to 40 objects - that should be put into some component later + for (i in 0 until 40) { world.entity { add { // Position of explosion object // set initial position of explosion object to collision position diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index 91f68e1a9..830254818 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -84,8 +84,8 @@ class SpawnerSystem : IteratingSystem( if (spawner.destruct) { add { spawnExplosion = true - explosionParticleRange = 10.0 - explosionParticleAcceleration = 200.0 + explosionParticleRange = 15.0 + explosionParticleAcceleration = 300.0 } } } From 9f9339a3474b253d422d00ec573ecd667d40f9e2 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Wed, 9 Mar 2022 13:35:00 +0100 Subject: [PATCH 18/27] Move Fleks under korge-fleks and update fleks-ecs example code Source code for Fleks ECS was moved under korge-fleks to decouple it from the example code. Updated fleks-ecs example code with source comments. --- korge-fleks/.gitignore | 1 + korge-fleks/LICENSE | 21 + korge-fleks/build.gradle.kts | 5 + .../quillraven/fleks/benchmark/artemis.kt | 139 ++++++ .../quillraven/fleks/benchmark/ashley.kt | 138 ++++++ .../github/quillraven/fleks/benchmark/data.kt | 7 + .../quillraven/fleks/benchmark/fleks.kt | 148 ++++++ .../quillraven/fleks/benchmark/manual.kt | 70 +++ .../github/quillraven/fleks/collection/bag.kt | 0 .../quillraven/fleks/collection/bitArray.kt | 0 .../com/github/quillraven/fleks/component.kt | 0 .../com/github/quillraven/fleks/entity.kt | 0 .../com/github/quillraven/fleks/exception.kt | 0 .../com/github/quillraven/fleks/family.kt | 0 .../com/github/quillraven/fleks/system.kt | 0 .../com/github/quillraven/fleks/world.kt | 0 .../github/quillraven/fleks/ComponentTest.kt | 266 ++++++++++ .../com/github/quillraven/fleks/EntityTest.kt | 297 +++++++++++ .../com/github/quillraven/fleks/FamilyTest.kt | 171 +++++++ .../com/github/quillraven/fleks/SystemTest.kt | 461 ++++++++++++++++++ .../com/github/quillraven/fleks/WorldTest.kt | 270 ++++++++++ .../quillraven/fleks/collection/BagTest.kt | 280 +++++++++++ .../fleks/collection/BitArrayTest.kt | 165 +++++++ samples/fleks-ecs/build.gradle | 4 +- .../src/commonMain/kotlin/assets/Assets.kt | 43 ++ .../commonMain/kotlin/components/Destruct.kt | 4 + .../commonMain/kotlin/components/Impulse.kt | 3 + .../commonMain/kotlin/components/Position.kt | 8 +- .../commonMain/kotlin/components/Rigidbody.kt | 4 +- .../commonMain/kotlin/components/Spawner.kt | 5 +- .../commonMain/kotlin/components/Sprite.kt | 11 +- .../commonMain/kotlin/entities/Explosions.kt | 33 ++ .../kotlin/entities/SpawnerEntities.kt | 112 +++++ .../fleks-ecs/src/commonMain/kotlin/main.kt | 79 +-- .../kotlin/systems/CollisionSystem.kt | 5 +- .../kotlin/systems/DestructSystem.kt | 34 +- .../commonMain/kotlin/systems/MoveSystem.kt | 9 +- .../kotlin/systems/SpawnerSystem.kt | 63 +-- .../commonMain/kotlin/systems/SpriteSystem.kt | 54 +- settings.gradle.kts | 1 + 40 files changed, 2705 insertions(+), 206 deletions(-) create mode 100644 korge-fleks/.gitignore create mode 100644 korge-fleks/LICENSE create mode 100644 korge-fleks/build.gradle.kts create mode 100644 korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/artemis.kt create mode 100644 korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/ashley.kt create mode 100644 korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/data.kt create mode 100644 korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/fleks.kt create mode 100644 korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/manual.kt rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt (100%) rename {samples/fleks-ecs => korge-fleks}/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt (100%) create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt create mode 100644 korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/assets/Assets.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/entities/Explosions.kt create mode 100644 samples/fleks-ecs/src/commonMain/kotlin/entities/SpawnerEntities.kt diff --git a/korge-fleks/.gitignore b/korge-fleks/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/korge-fleks/.gitignore @@ -0,0 +1 @@ +/build diff --git a/korge-fleks/LICENSE b/korge-fleks/LICENSE new file mode 100644 index 000000000..b7371ef7c --- /dev/null +++ b/korge-fleks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simon Klausner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/korge-fleks/build.gradle.kts b/korge-fleks/build.gradle.kts new file mode 100644 index 000000000..f037e91aa --- /dev/null +++ b/korge-fleks/build.gradle.kts @@ -0,0 +1,5 @@ +description = "Multiplatform Game Engine written in Kotlin" + +//dependencies { +// add("commonMainApi", project(":korge")) +//} diff --git a/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/artemis.kt b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/artemis.kt new file mode 100644 index 000000000..4e87f7d0b --- /dev/null +++ b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/artemis.kt @@ -0,0 +1,139 @@ +package com.github.quillraven.fleks.benchmark + +import com.artemis.Component +import com.artemis.ComponentMapper +import com.artemis.World +import com.artemis.WorldConfigurationBuilder +import com.artemis.annotations.All +import com.artemis.annotations.Exclude +import com.artemis.annotations.One +import com.artemis.systems.IteratingSystem +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +data class ArtemisPosition(var x: Float = 0f, var y: Float = 0f) : Component() + +data class ArtemisLife(var life: Float = 0f) : Component() + +data class ArtemisSprite(var path: String = "", var animationTime: Float = 0f) : Component() + +@All(ArtemisPosition::class) +class ArtemisSystemSimple : IteratingSystem() { + private lateinit var mapper: ComponentMapper + + override fun process(entityId: Int) { + mapper[entityId].x++ + } +} + +@All(ArtemisPosition::class) +@Exclude(ArtemisLife::class) +@One(ArtemisSprite::class) +class ArtemisSystemComplex1 : IteratingSystem() { + private var processCalls = 0 + private lateinit var positions: ComponentMapper + private lateinit var lifes: ComponentMapper + private lateinit var sprites: ComponentMapper + + override fun process(entityId: Int) { + if (processCalls % 2 == 0) { + positions[entityId].x++ + lifes.create(entityId) + } else { + positions.remove(entityId) + } + sprites[entityId].animationTime++ + ++processCalls + } +} + +@One(ArtemisPosition::class, ArtemisLife::class, ArtemisSprite::class) +class ArtemisSystemComplex2 : IteratingSystem() { + private lateinit var positions: ComponentMapper + private lateinit var lifes: ComponentMapper + + override fun process(entityId: Int) { + lifes.remove(entityId) + positions.create(entityId) + } +} + +@State(Scope.Benchmark) +open class ArtemisStateAddRemove { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World(WorldConfigurationBuilder().run { + build() + }) + } +} + +@State(Scope.Benchmark) +open class ArtemisStateSimple { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World(WorldConfigurationBuilder().run { + with(ArtemisSystemSimple()) + build() + }) + + repeat(NUM_ENTITIES) { + world.createEntity().edit().create(ArtemisPosition::class.java) + } + } +} + +@State(Scope.Benchmark) +open class ArtemisStateComplex { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World(WorldConfigurationBuilder().run { + with(ArtemisSystemComplex1()) + with(ArtemisSystemComplex2()) + build() + }) + + repeat(NUM_ENTITIES) { + val entityEdit = world.createEntity().edit() + entityEdit.create(ArtemisPosition::class.java) + entityEdit.create(ArtemisSprite::class.java) + } + } +} + +@Fork(1) +@Warmup(iterations = WARMUPS) +@Measurement(iterations = ITERATIONS, time = TIME, timeUnit = TimeUnit.SECONDS) +open class ArtemisBenchmark { + @Benchmark + fun addRemove(state: ArtemisStateAddRemove) { + repeat(NUM_ENTITIES) { + state.world.createEntity().edit().create(ArtemisPosition::class.java) + } + repeat(NUM_ENTITIES) { + state.world.delete(it) + } + } + + @Benchmark + fun simple(state: ArtemisStateSimple) { + repeat(WORLD_UPDATES) { + state.world.delta = 1f + state.world.process() + } + } + + @Benchmark + fun complex(state: ArtemisStateComplex) { + repeat(WORLD_UPDATES) { + state.world.delta = 1f + state.world.process() + } + } +} diff --git a/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/ashley.kt b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/ashley.kt new file mode 100644 index 000000000..e411dad85 --- /dev/null +++ b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/ashley.kt @@ -0,0 +1,138 @@ +package com.github.quillraven.fleks.benchmark + +import com.badlogic.ashley.core.* +import com.badlogic.ashley.systems.IteratingSystem +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +data class AshleyPosition( + var x: Float = 0f, + var y: Float = 0f +) : Component { + companion object { + val MAPPER: ComponentMapper = ComponentMapper.getFor(AshleyPosition::class.java) + } +} + +data class AshleyLife( + var life: Float = 0f +) : Component + +data class AshleySprite( + var path: String = "", + var animationTime: Float = 0f +) : Component { + companion object { + val MAPPER: ComponentMapper = ComponentMapper.getFor(AshleySprite::class.java) + } +} + +class AshleySystemSimple : IteratingSystem(Family.all(AshleyPosition::class.java).get()) { + override fun processEntity(entity: Entity?, deltaTime: Float) { + AshleyPosition.MAPPER.get(entity).x++ + } +} + +class AshleySystemComplex1 : IteratingSystem( + Family + .all(AshleyPosition::class.java) + .exclude(AshleyLife::class.java) + .one(AshleySprite::class.java) + .get() +) { + private var processCalls = 0 + + override fun processEntity(entity: Entity?, deltaTime: Float) { + if (processCalls % 2 == 0) { + AshleyPosition.MAPPER.get(entity).x++ + entity?.add(engine.createComponent(AshleyLife::class.java)) + } else { + entity?.remove(AshleyPosition::class.java) + } + AshleySprite.MAPPER.get(entity).animationTime++ + ++processCalls + } +} + +class AshleySystemComplex2 : IteratingSystem( + Family + .one(AshleyPosition::class.java, AshleyLife::class.java, AshleySprite::class.java) + .get() +) { + override fun processEntity(entity: Entity?, deltaTime: Float) { + entity?.remove(AshleyLife::class.java) + entity?.add(engine.createComponent(AshleyPosition::class.java)) + } +} + +@State(Scope.Benchmark) +open class AshleyStateAddRemove { + lateinit var engine: Engine + + @Setup(value = Level.Iteration) + fun setup() { + engine = Engine() + engine.addSystem(AshleySystemSimple()) + } +} + +@State(Scope.Benchmark) +open class AshleyStateSimple { + lateinit var engine: Engine + + @Setup(value = Level.Iteration) + fun setup() { + engine = Engine() + engine.addSystem(AshleySystemSimple()) + repeat(NUM_ENTITIES) { + val cmp = engine.createComponent(AshleyPosition::class.java) + val entity = engine.createEntity() + entity.add(cmp) + engine.addEntity(entity) + } + } +} + +@State(Scope.Benchmark) +open class AshleyStateComplex { + lateinit var engine: Engine + + @Setup(value = Level.Iteration) + fun setup() { + engine = Engine() + engine.addSystem(AshleySystemComplex1()) + engine.addSystem(AshleySystemComplex2()) + repeat(NUM_ENTITIES) { + val entity = engine.createEntity() + entity.add(engine.createComponent(AshleyPosition::class.java)) + entity.add(engine.createComponent(AshleySprite::class.java)) + engine.addEntity(entity) + } + } +} + +@Fork(1) +@Warmup(iterations = WARMUPS) +@Measurement(iterations = ITERATIONS, time = TIME, timeUnit = TimeUnit.SECONDS) +open class AshleyBenchmark { + @Benchmark + fun addRemove(state: AshleyStateAddRemove) { + repeat(NUM_ENTITIES) { + val cmp = state.engine.createComponent(AshleyPosition::class.java) + val entity = state.engine.createEntity() + entity.add(cmp) + state.engine.addEntity(entity) + } + state.engine.removeAllEntities() + } + + @Benchmark + fun simple(state: AshleyStateSimple) { + repeat(WORLD_UPDATES) { state.engine.update(1f) } + } + + @Benchmark + fun complex(state: AshleyStateComplex) { + repeat(WORLD_UPDATES) { state.engine.update(1f) } + } +} diff --git a/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/data.kt b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/data.kt new file mode 100644 index 000000000..d12e48e8e --- /dev/null +++ b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/data.kt @@ -0,0 +1,7 @@ +package com.github.quillraven.fleks.benchmark + +const val NUM_ENTITIES = 10_000 +const val WORLD_UPDATES = 1_000 +const val WARMUPS = 3 +const val ITERATIONS = 3 +const val TIME = 3 // in seconds diff --git a/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/fleks.kt b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/fleks.kt new file mode 100644 index 000000000..e6d3e509e --- /dev/null +++ b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/fleks.kt @@ -0,0 +1,148 @@ +package com.github.quillraven.fleks.benchmark + +import com.github.quillraven.fleks.* +import org.openjdk.jmh.annotations.* +import java.util.concurrent.TimeUnit + +data class FleksPosition(var x: Float = 0f, var y: Float = 0f) + +data class FleksLife(var life: Float = 0f) + +data class FleksSprite(var path: String = "", var animationTime: Float = 0f) + +class FleksSystemSimple : IteratingSystem( + allOf = AllOf(arrayOf(FleksPosition::class)) + ) { + + private val positions: ComponentMapper = Inject.componentMapper() + + override fun onTickEntity(entity: Entity) { + positions[entity].x++ + } +} + +class FleksSystemComplex1 : IteratingSystem( + allOf = AllOf(arrayOf(FleksPosition::class)), + noneOf = NoneOf(arrayOf(FleksLife::class)), + anyOf = AnyOf(arrayOf(FleksSprite::class)) +) { + + private val positions: ComponentMapper = Inject.componentMapper() + private val lifes: ComponentMapper = Inject.componentMapper() + private val sprites: ComponentMapper = Inject.componentMapper() + + private var actionCalls = 0 + + override fun onTickEntity(entity: Entity) { + if (actionCalls % 2 == 0) { + positions[entity].x++ + configureEntity(entity) { lifes.add(it) } + } else { + configureEntity(entity) { positions.remove(it) } + } + sprites[entity].animationTime++ + ++actionCalls + } +} + +class FleksSystemComplex2 : IteratingSystem( + anyOf = AnyOf(arrayOf(FleksPosition::class, FleksLife::class, FleksSprite::class)) +) { + + private val positions: ComponentMapper = Inject.componentMapper() + private val lifes: ComponentMapper = Inject.componentMapper() + + override fun onTickEntity(entity: Entity) { + configureEntity(entity) { + lifes.remove(it) + positions.add(it) + } + } +} + +@State(Scope.Benchmark) +open class FleksStateAddRemove { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World { + entityCapacity = NUM_ENTITIES + + component(::FleksPosition) + } + } +} + +@State(Scope.Benchmark) +open class FleksStateSimple { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World { + entityCapacity = NUM_ENTITIES + system(::FleksSystemSimple) + + component(::FleksPosition) + } + + repeat(NUM_ENTITIES) { + world.entity { add() } + } + } +} + +@State(Scope.Benchmark) +open class FleksStateComplex { + lateinit var world: World + + @Setup(value = Level.Iteration) + fun setup() { + world = World { + entityCapacity = NUM_ENTITIES + system(::FleksSystemComplex1) + system(::FleksSystemComplex2) + + component(::FleksPosition) + component(::FleksLife) + component(::FleksSprite) + } + + repeat(NUM_ENTITIES) { + world.entity { + add() + add() + } + } + } +} + +@Fork(1) +@Warmup(iterations = WARMUPS) +@Measurement(iterations = ITERATIONS, time = TIME, timeUnit = TimeUnit.SECONDS) +open class FleksBenchmark { + @Benchmark + fun addRemove(state: FleksStateAddRemove) { + repeat(NUM_ENTITIES) { + state.world.entity { add() } + } + repeat(NUM_ENTITIES) { + state.world.remove(Entity(it)) + } + } + + @Benchmark + fun simple(state: FleksStateSimple) { + repeat(WORLD_UPDATES) { + state.world.update(1f) + } + } + + @Benchmark + fun complex(state: FleksStateComplex) { + repeat(WORLD_UPDATES) { + state.world.update(1f) + } + } +} diff --git a/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/manual.kt b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/manual.kt new file mode 100644 index 000000000..9ec53fbdf --- /dev/null +++ b/korge-fleks/src/benchmarks/kotlin/com/github/quillraven/fleks/benchmark/manual.kt @@ -0,0 +1,70 @@ +package com.github.quillraven.fleks.benchmark + +import kotlin.system.measureTimeMillis + +fun main() { + compareArtemisFleksSimple() + compareArtemisFleksComplex() +} + +/* +COMPLEX: +Artemis: max(787) min(720) avg(747.0) +Fleks: max(877) min(800) avg(846.0) + */ +private fun compareArtemisFleksComplex() { + val artemisTimes = mutableListOf() + val artemisState = ArtemisStateComplex().apply { setup() } + val artemisBm = ArtemisBenchmark() + artemisBm.complex(artemisState) + repeat(3) { + artemisTimes.add(measureTimeMillis { artemisBm.complex(artemisState) }) + } + + val fleksTimes = mutableListOf() + val fleksState = FleksStateComplex().apply { setup() } + val fleksBm = FleksBenchmark() + fleksBm.complex(fleksState) + repeat(3) { + fleksTimes.add(measureTimeMillis { fleksBm.complex(fleksState) }) + } + + println( + """ + COMPLEX: + Artemis: max(${artemisTimes.maxOrNull()}) min(${artemisTimes.minOrNull()}) avg(${artemisTimes.average()}) + Fleks: max(${fleksTimes.maxOrNull()}) min(${fleksTimes.minOrNull()}) avg(${fleksTimes.average()}) + """.trimIndent() + ) +} + +/* +SIMPLE: +Artemis: max(38) min(31) avg(33.666666666666664) +Fleks: max(32) min(31) avg(31.333333333333332) + */ +private fun compareArtemisFleksSimple() { + val artemisTimes = mutableListOf() + val artemisState = ArtemisStateSimple().apply { setup() } + val artemisBm = ArtemisBenchmark() + artemisBm.simple(artemisState) + repeat(3) { + artemisTimes.add(measureTimeMillis { artemisBm.simple(artemisState) }) + } + + val fleksTimes = mutableListOf() + val fleksState = FleksStateSimple().apply { setup() } + val fleksBm = FleksBenchmark() + fleksBm.simple(fleksState) + repeat(3) { + fleksTimes.add(measureTimeMillis { fleksBm.simple(fleksState) }) + } + + println( + """ + SIMPLE: + Artemis: max(${artemisTimes.maxOrNull()}) min(${artemisTimes.minOrNull()}) avg(${artemisTimes.average()}) + Fleks: max(${fleksTimes.maxOrNull()}) min(${fleksTimes.minOrNull()}) avg(${fleksTimes.average()}) + """.trimIndent() + ) +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bag.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/component.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt diff --git a/samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt similarity index 100% rename from samples/fleks-ecs/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt rename to korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/world.kt diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt new file mode 100644 index 000000000..b1bce0047 --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt @@ -0,0 +1,266 @@ +package com.github.quillraven.fleks + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertSame + +private data class ComponentTestComponent(var x: Int = 0) + +private class ComponentTestComponentListener : ComponentListener { + var numAddCalls = 0 + var numRemoveCalls = 0 + lateinit var cmpCalled: ComponentTestComponent + var entityCalled = Entity(-1) + var lastCall = "" + + override fun onComponentAdded(entity: Entity, component: ComponentTestComponent) { + numAddCalls++ + cmpCalled = component + entityCalled = entity + lastCall = "add" + } + + override fun onComponentRemoved(entity: Entity, component: ComponentTestComponent) { + numRemoveCalls++ + cmpCalled = component + entityCalled = entity + lastCall = "remove" + } +} + +internal class ComponentTest { + private val componentFactory = mutableMapOf Any>() + + private inline fun initComponentFactory(noinline compFactory: () -> T) { + val compType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + + if (compType in componentFactory) { + throw FleksComponentAlreadyAddedException(compType) + } + componentFactory[compType] = compFactory + } + + init { + initComponentFactory(::ComponentTestComponent) + } + + @Test + fun `add entity to mapper with sufficient capacity`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(0) + + val cmp = mapper.addInternal(entity) { x = 5 } + + assertAll( + { assertTrue(entity in mapper) }, + { assertEquals(5, cmp.x) } + ) + } + + @Test + fun `add entity to mapper with insufficient capacity`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(10_000) + + val cmp = mapper.addInternal(entity) + + assertAll( + { assertTrue(entity in mapper) }, + { assertEquals(0, cmp.x) } + ) + } + + @Test + fun `add already existing entity to mapper`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(10_000) + val expected = mapper.addInternal(entity) + + val actual = mapper.addInternal(entity) { x = 2 } + + assertAll( + { assertSame(expected, actual) }, + { assertEquals(2, actual.x) } + ) + } + + @Test + fun `returns false when entity is not part of mapper`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + + assertAll( + { assertFalse(Entity(0) in mapper) }, + { assertFalse(Entity(10_000) in mapper) } + ) + } + + @Test + fun `remove existing entity from mapper`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(0) + mapper.addInternal(entity) + + mapper.removeInternal(entity) + + assertFalse(entity in mapper) + } + + @Test + fun `cannot remove non-existing entity from mapper with insufficient capacity`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(10_000) + + assertThrows { mapper.removeInternal(entity) } + } + + @Test + fun `get component of existing entity`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(0) + mapper.addInternal(entity) { x = 2 } + + val cmp = mapper[entity] + + assertEquals(2, cmp.x) + } + + @Test + fun `cannot get component of non-existing entity`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val entity = Entity(0) + + assertThrows { mapper[entity] } + } + + @Test + fun `create new mapper`() { + val cmpService = ComponentService(componentFactory) + + val mapper = cmpService.mapper() + + assertEquals(0, mapper.id) + } + + @Test + fun `do not create the same mapper twice`() { + val cmpService = ComponentService(componentFactory) + val expected = cmpService.mapper() + + val actual = cmpService.mapper() + + assertSame(expected, actual) + } + + @Test + fun `get mapper by component id`() { + val cmpService = ComponentService(componentFactory) + val expected = cmpService.mapper() + + val actual = cmpService.mapper(0) + + assertSame(expected, actual) + } + + @Test + fun `add ComponentListener`() { + val cmpService = ComponentService(componentFactory) + val listener = ComponentTestComponentListener() + val mapper = cmpService.mapper() + + mapper.addComponentListenerInternal(listener) + + assertTrue(listener in mapper) + } + + @Test + fun `remove ComponentListener`() { + val cmpService = ComponentService(componentFactory) + val listener = ComponentTestComponentListener() + val mapper = cmpService.mapper() + mapper.addComponentListenerInternal(listener) + + mapper.removeComponentListener(listener) + + assertFalse(listener in mapper) + } + + @Test + fun `add component with ComponentListener`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val listener = ComponentTestComponentListener() + mapper.addComponentListener(listener) + val expectedEntity = Entity(1) + + val expectedCmp = mapper.addInternal(expectedEntity) + + assertAll( + { assertEquals(1, listener.numAddCalls) }, + { assertEquals(0, listener.numRemoveCalls) }, + { assertEquals(expectedEntity, listener.entityCalled) }, + { assertEquals(expectedCmp, listener.cmpCalled) } + ) + } + + @Test + fun `add component with ComponentListener when component already present`() { + val cmpService = ComponentService(componentFactory) + val mapper = cmpService.mapper() + val expectedEntity = Entity(1) + mapper.addInternal(expectedEntity) + val listener = ComponentTestComponentListener() + mapper.addComponentListener(listener) + + val expectedCmp = mapper.addInternal(expectedEntity) + + assertAll( + { assertEquals(1, listener.numAddCalls) }, + { assertEquals(1, listener.numRemoveCalls) }, + { assertEquals(expectedEntity, listener.entityCalled) }, + { assertEquals(expectedCmp, listener.cmpCalled) }, + { assertEquals("add", listener.lastCall) } + ) + } + + @Test + fun `add component if it does not exist yet`() { + val cmpService = ComponentService(componentFactory) + 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(componentFactory) + 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/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt new file mode 100644 index 000000000..666943ea7 --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt @@ -0,0 +1,297 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +private class EntityTestListener : EntityListener { + var numCalls = 0 + var entityReceived = Entity(-1) + lateinit var cmpMaskReceived: BitArray + + override fun onEntityCfgChanged(entity: Entity, compMask: BitArray) { + ++numCalls + entityReceived = entity + cmpMaskReceived = compMask + } +} + +private data class EntityTestComponent(var x: Float = 0f) + +internal class EntityTest { + private val componentFactory = mutableMapOf Any>() + + private inline fun initComponentFactory(noinline compFactory: () -> T) { + val compType = T::class.simpleName ?: throw FleksInjectableTypeHasNoName(T::class) + + if (compType in componentFactory) { + throw FleksComponentAlreadyAddedException(compType) + } + componentFactory[compType] = compFactory + } + + init { + initComponentFactory(::EntityTestComponent) + } + + @Test + fun `create empty service for 32 entities`() { + val cmpService = ComponentService(componentFactory) + + val entityService = EntityService(32, cmpService) + + assertAll( + { assertEquals(0, entityService.numEntities) }, + { assertEquals(32, entityService.capacity) } + ) + } + + @Test + fun `create entity without configuration and sufficient capacity`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + + val entity = entityService.create {} + + assertAll( + { assertEquals(0, entity.id) }, + { assertEquals(1, entityService.numEntities) } + ) + } + + @Test + fun `create entity without configuration and insufficient capacity`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(0, cmpService) + + val entity = entityService.create {} + + assertAll( + { assertEquals(0, entity.id) }, + { assertEquals(1, entityService.numEntities) }, + { assertEquals(1, entityService.capacity) }, + ) + } + + @Test + fun `create entity with configuration and custom listener`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val listener = EntityTestListener() + entityService.addEntityListener(listener) + var processedEntity = Entity(-1) + + val expectedEntity = entityService.create { entity -> + add() + processedEntity = entity + } + val mapper = cmpService.mapper() + + assertAll( + { assertEquals(1, listener.numCalls) }, + { assertEquals(expectedEntity, listener.entityReceived) }, + { assertTrue(listener.cmpMaskReceived[0]) }, + { assertEquals(0f, mapper[listener.entityReceived].x) }, + { assertEquals(expectedEntity, processedEntity) } + ) + } + + @Test + fun `remove component from entity with custom listener`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val listener = EntityTestListener() + val expectedEntity = entityService.create { add() } + val mapper = cmpService.mapper() + entityService.addEntityListener(listener) + + entityService.configureEntity(expectedEntity) { mapper.remove(expectedEntity) } + + assertAll( + { assertEquals(1, listener.numCalls) }, + { assertEquals(expectedEntity, listener.entityReceived) }, + { assertFalse(listener.cmpMaskReceived[0]) }, + { assertFalse(expectedEntity in mapper) } + ) + } + + @Test + fun `add component to entity with custom listener`() { + val cmpService = ComponentService(componentFactory) + 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) } + + assertAll( + { assertEquals(1, listener.numCalls) }, + { assertEquals(expectedEntity, listener.entityReceived) }, + { assertTrue(listener.cmpMaskReceived[0]) }, + { assertTrue(expectedEntity in mapper) } + ) + } + + @Test + fun `update component of entity if it already exists with custom listener`() { + val cmpService = ComponentService(componentFactory) + 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(componentFactory) + val entityService = EntityService(32, cmpService) + val listener = EntityTestListener() + val expectedEntity = entityService.create { add() } + val mapper = cmpService.mapper() + entityService.addEntityListener(listener) + + entityService.remove(expectedEntity) + + assertAll( + { assertEquals(1, listener.numCalls) }, + { assertEquals(expectedEntity, listener.entityReceived) }, + { assertFalse(listener.cmpMaskReceived[0]) }, + { assertFalse(expectedEntity in mapper) } + ) + } + + @Test + fun `remove all entities`() { + val entityService = EntityService(32, ComponentService(componentFactory)) + entityService.create {} + entityService.create {} + + entityService.removeAll() + + assertAll( + { assertEquals(2, entityService.recycledEntities.size) }, + { assertEquals(0, entityService.numEntities) } + ) + } + + @Test + fun `remove all entities with already recycled entities`() { + val entityService = EntityService(32, ComponentService(componentFactory)) + val recycled = entityService.create {} + entityService.create {} + entityService.remove(recycled) + + entityService.removeAll() + + assertAll( + { assertEquals(2, entityService.recycledEntities.size) }, + { assertEquals(0, entityService.numEntities) } + ) + } + + @Test + fun `remove all entities when removal is delayed`() { + val entityService = EntityService(32, ComponentService(componentFactory)) + entityService.create {} + entityService.create {} + entityService.delayRemoval = true + val listener = EntityTestListener() + entityService.addEntityListener(listener) + + entityService.removeAll() + + assertAll( + { assertEquals(0, listener.numCalls) }, + { assertTrue(entityService.delayRemoval) } + ) + } + + @Test + fun `create recycled entity`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val expectedEntity = entityService.create { } + entityService.remove(expectedEntity) + + val actualEntity = entityService.create { } + + assertEquals(expectedEntity, actualEntity) + } + + @Test + fun `delay entity removal`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val entity = entityService.create { } + val listener = EntityTestListener() + entityService.addEntityListener(listener) + entityService.delayRemoval = true + + entityService.remove(entity) + + assertEquals(0, listener.numCalls) + } + + @Test + fun `remove delayed entity`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val entity = entityService.create { } + val listener = EntityTestListener() + entityService.addEntityListener(listener) + entityService.delayRemoval = true + entityService.remove(entity) + + // call two times to make sure that removals are only processed once + entityService.cleanupDelays() + entityService.cleanupDelays() + + assertAll( + { assertFalse(entityService.delayRemoval) }, + { assertEquals(1, listener.numCalls) } + ) + } + + @Test + fun `remove existing listener`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val listener = EntityTestListener() + entityService.addEntityListener(listener) + + entityService.removeEntityListener(listener) + + assertFalse(listener in entityService) + } + + @Test + fun `remove entity twice`() { + val cmpService = ComponentService(componentFactory) + val entityService = EntityService(32, cmpService) + val entity = entityService.create { } + val listener = EntityTestListener() + entityService.addEntityListener(listener) + + entityService.remove(entity) + entityService.remove(entity) + + assertAll( + { assertEquals(1, entityService.recycledEntities.size) }, + { assertEquals(1, listener.numCalls) } + ) + } +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt new file mode 100644 index 000000000..310254264 --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt @@ -0,0 +1,171 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import com.github.quillraven.fleks.collection.compareEntity +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.assertAll +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +internal class FamilyTest { + private fun createCmpBitmask(cmpIdx: Int): BitArray? { + return if (cmpIdx > 0) { + BitArray().apply { set(cmpIdx) } + } else { + null + } + } + + + @TestFactory + fun `test contains`(): Collection { + return listOf( + arrayOf( + "empty family contains entity without components", + BitArray(), // entity component mask + 0, 0, 0, // family allOf, noneOf, anyOf indices + true // expected + ), + arrayOf( + "empty family contains entity with any components", + BitArray().apply { set(1) }, // entity component mask + 0, 0, 0, // family allOf, noneOf, anyOf indices + true // expected + ), + arrayOf( + "family with allOf does not contain entity without components", + BitArray(), // entity component mask + 1, 0, 0, // family allOf, noneOf, anyOf indices + false // expected + ), + arrayOf( + "family with allOf contains entity with specific component", + BitArray().apply { set(1) }, // entity component mask + 1, 0, 0, // family allOf, noneOf, anyOf indices + true // expected + ), + arrayOf( + "family with noneOf contains entity without components", + BitArray(), // entity component mask + 0, 1, 0, // family allOf, noneOf, anyOf indices + true // expected + ), + arrayOf( + "family with noneOf does not contain entity with specific component", + BitArray().apply { set(1) }, // entity component mask + 0, 1, 0, // family allOf, noneOf, anyOf indices + false // expected + ), + arrayOf( + "family with anyOf does not contain entity without components", + BitArray(), // entity component mask + 0, 0, 1, // family allOf, noneOf, anyOf indices + false // expected + ), + arrayOf( + "family with anyOf contains entity with specific component", + BitArray().apply { set(1) }, // entity component mask + 0, 0, 1, // family allOf, noneOf, anyOf indices + true // expected + ), + ).map { + dynamicTest("test ${it[0]}") { + val eCmpMask = it[1] as BitArray + val fAllOf = createCmpBitmask(it[2] as Int) + val fNoneOf = createCmpBitmask(it[3] as Int) + val fAnyOf = createCmpBitmask(it[4] as Int) + val family = Family(fAllOf, fNoneOf, fAnyOf) + val expected = it[5] as Boolean + + assertEquals(expected, eCmpMask in family) + } + } + } + + @Test + fun `update active entities`() { + val family = Family() + + family.onEntityCfgChanged(Entity(0), BitArray()) + family.updateActiveEntities() + + assertAll( + { assertFalse { family.isDirty } }, + { assertEquals(1, family.entitiesBag.size) }, + { assertEquals(0, family.entitiesBag[0]) } + ) + } + + @Test + fun `call action for each entity`() { + val family = Family() + family.onEntityCfgChanged(Entity(0), BitArray()) + family.updateActiveEntities() + var processedEntity = -1 + var numExecutions = 0 + + family.forEach { + numExecutions++ + processedEntity = it.id + } + + assertAll( + { assertEquals(0, processedEntity) }, + { assertEquals(1, numExecutions) } + ) + } + + @Test + fun `sort entities`() { + val family = Family() + family.onEntityCfgChanged(Entity(0), BitArray()) + family.onEntityCfgChanged(Entity(2), BitArray()) + family.onEntityCfgChanged(Entity(1), BitArray()) + family.updateActiveEntities() + + // sort descending by entity id + family.sort(compareEntity { e1, e2 -> e2.id.compareTo(e1.id) }) + + assertAll( + { assertEquals(2, family.entitiesBag[0]) }, + { assertEquals(1, family.entitiesBag[1]) }, + { assertEquals(0, family.entitiesBag[2]) }, + ) + } + + @TestFactory + fun `test onEntityCfgChange`(): Collection { + return listOf( + // first = add entity to family before calling onChange + // second = make entity part of family + Pair(false, false), + Pair(false, true), + Pair(true, false), + Pair(true, true), + ).map { + dynamicTest("addEntityBefore=${it.first}, addEntity=${it.second}") { + val family = Family(BitArray().apply { set(1) }, null, null) + val addEntityBeforeCall = it.first + val addEntityToFamily = it.second + val entity = Entity(1) + if (addEntityBeforeCall) { + family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) + family.updateActiveEntities() + } + + if (addEntityToFamily) { + family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) + + assertEquals(!addEntityBeforeCall, family.isDirty) + } else { + family.onEntityCfgChanged(entity, BitArray()) + + assertEquals(addEntityBeforeCall, family.isDirty) + } + } + } + } +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt new file mode 100644 index 000000000..02dcc1513 --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt @@ -0,0 +1,461 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.EntityComparator +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows +import java.lang.reflect.InvocationTargetException +import kotlin.reflect.KClass +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertSame + +private class SystemTestIntervalSystemEachFrame : IntervalSystem( + interval = EachFrame +) { + var numDisposes = 0 + var numCalls = 0 + + override fun onTick() { + ++numCalls + } + + override fun onDispose() { + numDisposes++ + } +} + +private class SystemTestIntervalSystemFixed : IntervalSystem( + interval = Fixed(0.25f) +) { + var numCalls = 0 + var lastAlpha = 0f + + override fun onTick() { + ++numCalls + } + + override fun onAlpha(alpha: Float) { + lastAlpha = alpha + } +} + +private data class SystemTestComponent(var x: Float = 0f) + + +private class SystemTestInitBlock : IntervalSystem() { + private val someValue: Float = world.deltaTime + + override fun onTick() = Unit +} + +private class SystemTestOnInitBlock : IntervalSystem() { + var someValue: Float = 42f + + override fun onInit() { + someValue = world.deltaTime + } + + override fun onTick() = Unit +} + +private class SystemTestIteratingSystemMapper : IteratingSystem( + allOfComponents = arrayOf(SystemTestComponent::class), + interval = Fixed(0.25f) +) { + var numEntityCalls = 0 + var numAlphaCalls = 0 + var lastAlpha = 0f + var entityToConfigure: Entity? = null + + val mapper = Inject.componentMapper() + + override fun onTickEntity(entity: Entity) { + entityToConfigure?.let { e -> + configureEntity(e) { + mapper.remove(it) + } + } + ++numEntityCalls + } + + override fun onAlphaEntity(entity: Entity, alpha: Float) { + lastAlpha = alpha + ++numAlphaCalls + } +} + +private class SystemTestIteratingSystemSortAutomatic : IteratingSystem( + allOfComponents = arrayOf(SystemTestComponent::class), + comparator = object : EntityComparator { + private val mapper: ComponentMapper = Inject.componentMapper() + override fun compare(entityA: Entity, entityB: Entity) : Int { + return mapper[entityB].x.compareTo(mapper[entityA].x) + } + }, +) { + var numEntityCalls = 0 + var lastEntityProcess = Entity(-1) + var entityToRemove: Entity? = null + + override fun onTickEntity(entity: Entity) { + entityToRemove?.let { + world.remove(it) + entityToRemove = null + } + + lastEntityProcess = entity + ++numEntityCalls + } +} + +private class SystemTestFixedSystemRemoval : IteratingSystem( + allOfComponents = arrayOf(SystemTestComponent::class), + interval = Fixed(1f) +) { + var numEntityCalls = 0 + var lastEntityProcess = Entity(-1) + var entityToRemove: Entity? = null + + private val mapper = Inject.componentMapper() + + override fun onTickEntity(entity: Entity) { + entityToRemove?.let { + world.remove(it) + entityToRemove = null + } + } + + override fun onAlphaEntity(entity: Entity, alpha: Float) { + // the next line would cause an exception if we don't update the family properly in alpha + // because component removal is instantly + mapper[entity].x++ + lastEntityProcess = entity + ++numEntityCalls + } +} + +private class SystemTestIteratingSystemSortManual : IteratingSystem( + allOfComponents = arrayOf(SystemTestComponent::class), + comparator = object : EntityComparator { + private val mapper: ComponentMapper = Inject.componentMapper() + override fun compare(entityA: Entity, entityB: Entity) : Int { + return mapper[entityB].x.compareTo(mapper[entityA].x) + } + }, + sortingType = Manual +) { + var numEntityCalls = 0 + var lastEntityProcess = Entity(-1) + + override fun onTickEntity(entity: Entity) { + lastEntityProcess = entity + ++numEntityCalls + } +} + +private class SystemTestIteratingSystemInjectable : IteratingSystem( + noneOfComponents = arrayOf(SystemTestComponent::class), + anyOfComponents = arrayOf(SystemTestComponent::class) + ) { + val injectable: String = Inject.dependency() + + override fun onTickEntity(entity: Entity) = Unit +} + +private class SystemTestIteratingSystemQualifiedInjectable : IteratingSystem( + noneOfComponents = arrayOf(SystemTestComponent::class), + anyOfComponents = arrayOf(SystemTestComponent::class) +) { + val injectable: String = Inject.dependency() + val injectable2: String = Inject.dependency("q1") + + override fun onTickEntity(entity: Entity) = Unit +} + +internal class SystemTest { + private fun systemService( + systemFactory: MutableMap, () -> IntervalSystem> = mutableMapOf(), + injectables: MutableMap = mutableMapOf(), + world: World = World {} + ) = SystemService( + world, + systemFactory, + injectables + ) + + @Test + fun `system with interval EachFrame gets called every time`() { + val system = SystemTestIntervalSystemEachFrame() + + system.onUpdate() + system.onUpdate() + + assertEquals(2, system.numCalls) + } + + @Test + fun `system with interval EachFrame returns world's delta time`() { + val system = SystemTestIntervalSystemEachFrame().apply { this.world = World {} } + system.world.update(42f) + + assertEquals(42f, system.deltaTime) + } + + @Test + fun `system with fixed interval of 0,25f gets called four times when delta time is 1,1f`() { + val system = SystemTestIntervalSystemFixed().apply { this.world = World {} } + system.world.update(1.1f) + + system.onUpdate() + + assertAll( + { assertEquals(4, system.numCalls) }, + { assertEquals(0.1f / 0.25f, system.lastAlpha, 0.0001f) } + ) + } + + @Test + fun `system with fixed interval returns step rate as delta time`() { + val system = SystemTestIntervalSystemFixed() + + assertEquals(0.25f, system.deltaTime, 0.0001f) + } + + @Test + fun `create IntervalSystem with no-args`() { + val expectedWorld = World { + component(::SystemTestComponent) + } + + val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame), world = expectedWorld) + + assertAll( + { assertEquals(1, service.systems.size) }, + { assertNotNull(service.system()) }, + { assertSame(expectedWorld, service.system().world) } + ) + } + + @Test + fun `create IteratingSystem with a ComponentMapper arg`() { + val expectedWorld = World { + component(::SystemTestComponent) + } + + val service = systemService(mutableMapOf(SystemTestIteratingSystemMapper::class to ::SystemTestIteratingSystemMapper), world = expectedWorld) + + val actualSystem = service.system() + assertAll( + { assertEquals(1, service.systems.size) }, + { assertSame(expectedWorld, actualSystem.world) }, + { assertEquals(SystemTestComponent::class.simpleName, "SystemTestComponent") } + ) + } + + @Test + fun `create IteratingSystem with an injectable arg`() { + val expectedWorld = World { + component(::SystemTestComponent) + } + + val service = systemService( + mutableMapOf(SystemTestIteratingSystemInjectable::class to ::SystemTestIteratingSystemInjectable), + mutableMapOf(String::class.simpleName!! to Injectable("42")), + expectedWorld + ) + + val actualSystem = service.system() + assertAll( + { assertEquals(1, service.systems.size) }, + { assertSame(expectedWorld, actualSystem.world) }, + { assertEquals("42", actualSystem.injectable) }, + ) + } + + @Test + fun `create IteratingSystem with qualified args`() { + val expectedWorld = World { + component(::SystemTestComponent) + } + + val service = systemService( + mutableMapOf(SystemTestIteratingSystemQualifiedInjectable::class to ::SystemTestIteratingSystemQualifiedInjectable), + mutableMapOf(String::class.simpleName!! to Injectable("42"), "q1" to Injectable("43")), + expectedWorld + ) + + val actualSystem = service.system() + assertAll( + { assertEquals(1, service.systems.size) }, + { assertSame(expectedWorld, actualSystem.world) }, + { assertEquals("42", actualSystem.injectable) }, + { assertEquals("43", actualSystem.injectable2) }, + ) + } + + @Test + fun `cannot create IteratingSystem with missing injectables`() { + assertThrows { systemService(mutableMapOf(SystemTestIteratingSystemInjectable::class to ::SystemTestIteratingSystemInjectable)) } + } + + @Test + fun `IteratingSystem calls onTick and onAlpha for each entity of the system`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemMapper::class to ::SystemTestIteratingSystemMapper), world = world) + world.entity { add() } + world.entity { add() } + world.update(0.3f) + + service.update() + + val system = service.system() + assertAll( + { assertEquals(2, system.numEntityCalls) }, + { assertEquals(2, system.numAlphaCalls) }, + { assertEquals(0.05f / 0.25f, system.lastAlpha, 0.0001f) } + ) + } + + @Test + fun `configure entity during iteration`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemMapper::class to ::SystemTestIteratingSystemMapper), world = world) + world.update(0.3f) + val entity = world.entity { add() } + val system = service.system() + system.entityToConfigure = entity + + service.update() + + assertFalse { entity in system.mapper } + } + + @Test + fun `sort entities automatically`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemSortAutomatic::class to ::SystemTestIteratingSystemSortAutomatic), world = world) + world.entity { add { x = 15f } } + world.entity { add { x = 10f } } + val expectedEntity = world.entity { add { x = 5f } } + + service.update() + + assertEquals(expectedEntity, service.system().lastEntityProcess) + } + + @Test + fun `sort entities programmatically`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemSortManual::class to ::SystemTestIteratingSystemSortManual), world = world) + world.entity { add { x = 15f } } + world.entity { add { x = 10f } } + val expectedEntity = world.entity { add { x = 5f } } + val system = service.system() + + system.doSort = true + service.update() + + assertAll( + { assertEquals(expectedEntity, system.lastEntityProcess) }, + { assertFalse(system.doSort) } + ) + } + + @Test + fun `cannot get a non-existing system`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemSortAutomatic::class to ::SystemTestIteratingSystemSortAutomatic), world = world) + + assertThrows { + service.system() + } + } + + @Test + fun `update only calls enabled systems`() { + val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame)) + val system = service.system() + system.enabled = false + + service.update() + + assertEquals(0, system.numCalls) + } + + @Test + fun `removing an entity during update is delayed`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestIteratingSystemSortAutomatic::class to ::SystemTestIteratingSystemSortAutomatic), world = world) + world.entity { add { x = 15f } } + val entityToRemove = world.entity { add { x = 10f } } + world.entity { add { x = 5f } } + val system = service.system() + system.entityToRemove = entityToRemove + + // call it twice - first call still iterates over all three entities + // while the second call will only iterate over the remaining two entities + service.update() + service.update() + + assertEquals(5, system.numEntityCalls) + } + + @Test + fun `removing an entity during alpha is delayed`() { + val world = World { + component(::SystemTestComponent) + } + val service = systemService(mutableMapOf(SystemTestFixedSystemRemoval::class to ::SystemTestFixedSystemRemoval), world = world) + // set delta time to 1f for the fixed interval + world.update(1f) + world.entity { add { x = 15f } } + val entityToRemove = world.entity { add { x = 10f } } + world.entity { add { x = 5f } } + val system = service.system() + system.entityToRemove = entityToRemove + + // call it twice - first call still iterates over all three entities + // while the second call will only iterate over the remaining two entities + service.update() + service.update() + + assertEquals(4, system.numEntityCalls) + } + + @Test + fun `dispose service`() { + val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame)) + + service.dispose() + + assertEquals(1, service.system().numDisposes) + } + + @Test + fun `init block of a system constructor has no access to the world`() { + assertThrows { systemService(mutableMapOf(SystemTestInitBlock::class to ::SystemTestInitBlock)) } + } + + @Test + fun `onInit block is called for any newly created system`() { + val expected = 0f + + val service = systemService(mutableMapOf(SystemTestOnInitBlock::class to ::SystemTestOnInitBlock)) + + assertEquals(expected, service.system().someValue) + } +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt new file mode 100644 index 000000000..e69054866 --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt @@ -0,0 +1,270 @@ +package com.github.quillraven.fleks + +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 + +private data class WorldTestComponent(var x: Float = 0f) + +private class WorldTestIntervalSystem : IntervalSystem() { + var numCalls = 0 + var disposed = false + + override fun onTick() { + ++numCalls + } + + override fun onDispose() { + disposed = true + } +} + +private class WorldTestIteratingSystem : IteratingSystem( + allOfComponents = arrayOf(WorldTestComponent::class) +) { + var numCalls = 0 + + val testInject: String = Inject.dependency() + val mapper: ComponentMapper = Inject.componentMapper() + + override fun onTick() { + ++numCalls + super.onTick() + } + + override fun onTickEntity(entity: Entity) = Unit +} + +private class WorldTestNamedDependencySystem : IntervalSystem() { + val _name: String = Inject.dependency("name") + val level: String = Inject.dependency("level") + + val name: String = _name + + override fun onTick() = Unit +} + +private class WorldTestComponentListener : ComponentListener { + override fun onComponentAdded(entity: Entity, component: WorldTestComponent) = Unit + override fun onComponentRemoved(entity: Entity, component: WorldTestComponent) = Unit +} + +internal class WorldTest { + @Test + fun `create empty world for 32 entities`() { + val w = World { entityCapacity = 32 } + + assertAll( + { assertEquals(0, w.numEntities) }, + { assertEquals(32, w.capacity) } + ) + } + + @Test + fun `create empty world with 1 no-args IntervalSystem`() { + val w = World { system(::WorldTestIntervalSystem) } + + assertNotNull(w.system()) + } + + @Test + fun `create empty world with 1 injectable args IteratingSystem`() { + val w = World { + system(::WorldTestIteratingSystem) + component(::WorldTestComponent) + + inject("42") + } + + assertAll( + { assertNotNull(w.system()) }, + { assertEquals("42", w.system().testInject) } + ) + } + + @Test + fun `create empty world with 2 named injectables system`() { + val expectedName = "myName" + val expectedLevel = "myLevel" + val w = World { + system(::WorldTestNamedDependencySystem) + component(::WorldTestComponent) + + inject("name", expectedName) + inject("level", "myLevel") + } + + assertAll( + { assertNotNull(w.system()) }, + { assertEquals(expectedName, w.system().name) }, + { assertEquals(expectedLevel, w.system().level) } + ) + } + + @Test + fun `cannot add the same system twice`() { + assertThrows { + World { + system(::WorldTestIntervalSystem) + system(::WorldTestIntervalSystem) + } + } + } + + @Test + fun `cannot access a system that was not added`() { + val w = World {} + + assertThrows { w.system() } + } + + @Test + fun `cannot create a system when injectables are missing`() { + assertThrows { + World { system(::WorldTestIteratingSystem) } + } + } + + @Test + fun `cannot inject the same type twice`() { + assertThrows { + World { + inject("42") + inject("42") + } + } + } + + @Test + fun `create new entity`() { + val w = World { + system(::WorldTestIteratingSystem) + component(::WorldTestComponent) + inject("42") + } + + val e = w.entity { + add { x = 5f } + } + + assertAll( + { assertEquals(1, w.numEntities) }, + { assertEquals(0, e.id) }, + { assertEquals(5f, w.system().mapper[e].x) } + ) + } + + @Test + fun `remove existing entity`() { + val w = World {} + val e = w.entity() + + w.remove(e) + + assertEquals(0, w.numEntities) + } + + @Test + fun `update world with deltaTime of 1`() { + val w = World { + system(::WorldTestIntervalSystem) + system(::WorldTestIteratingSystem) + component(::WorldTestComponent) + + inject("42") + } + w.system().enabled = false + + w.update(1f) + + assertAll( + { assertEquals(1f, w.deltaTime) }, + { assertEquals(1, w.system().numCalls) }, + { assertEquals(0, w.system().numCalls) } + ) + } + + @Test + fun `remove all entities`() { + val w = World {} + w.entity() + w.entity() + + w.removeAll() + + assertEquals(0, w.numEntities) + } + + @Test + fun `dispose world`() { + val w = World { + system(::WorldTestIntervalSystem) + } + w.entity() + w.entity() + + w.dispose() + + assertAll( + { assertTrue(w.system().disposed) }, + { assertEquals(0, w.numEntities) } + ) + } + + @Test + fun `create world with ComponentListener`() { + val w = World { + component(::WorldTestComponent, ::WorldTestComponentListener) + } + + assertEquals(1, w.componentService.mapper().listeners.size) + } + + @Test + fun `cannot add same Component twice`() { + assertThrows { + World { + component(::WorldTestComponent) + component(::WorldTestComponent) + } + } + } + + @Test + fun `get mapper`() { + val w = World { + component(::WorldTestComponent) + } + + val mapper = w.mapper() + + assertEquals(0, mapper.id) + } + + @Test + fun `throw exception when there are unused injectables`() { + assertThrows { + World { + inject("42") + } + } + } + + @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) + } +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt new file mode 100644 index 000000000..e60d18a2f --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt @@ -0,0 +1,280 @@ +package com.github.quillraven.fleks.collection + +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class BagTest { + @Nested + inner class GenericBagTest { + @Test + fun `create empty bag of String of size 32`() { + val bag = bag(32) + + assertAll( + { assertEquals(32, bag.capacity) }, + { assertEquals(0, bag.size) } + ) + } + + @Test + fun `add String to bag`() { + val bag = bag() + + bag.add("42") + + assertAll( + { assertEquals(1, bag.size) }, + { assertTrue("42" in bag) } + ) + } + + @Test + fun `remove existing String from bag`() { + val bag = bag() + bag.add("42") + + val expected = bag.removeValue("42") + + assertAll( + { assertEquals(0, bag.size) }, + { assertFalse { "42" in bag } }, + { assertTrue(expected) } + ) + } + + @Test + fun `remove non-existing String from bag`() { + val bag = bag() + + val expected = bag.removeValue("42") + + assertFalse(expected) + } + + @Test + fun `set String value at index with sufficient capacity`() { + val bag = bag() + + bag[3] = "42" + + assertAll( + { assertEquals(4, bag.size) }, + { assertEquals("42", bag[3]) } + ) + } + + @Test + fun `set String value at index with insufficient capacity`() { + val bag = bag(2) + + bag[2] = "42" + + assertAll( + { assertEquals(3, bag.size) }, + { assertEquals("42", bag[2]) }, + { assertEquals(3, bag.capacity) } + ) + } + + @Test + fun `add String to bag with insufficient capacity`() { + val bag = bag(0) + + bag.add("42") + + assertAll( + { assertEquals(1, bag.size) }, + { assertEquals("42", bag[0]) }, + { assertEquals(1, bag.capacity) } + ) + } + + @Test + fun `cannot get String value of invalid in bounds index`() { + val bag = bag() + + assertThrows { bag[0] } + } + + @Test + fun `cannot get String value of invalid out of bounds index`() { + val bag = bag(2) + + assertThrows { bag[2] } + } + + @Test + fun `execute action for each value of a String bag`() { + val bag = bag(4) + bag[1] = "42" + bag[2] = "43" + var numCalls = 0 + val valuesCalled = mutableListOf() + + bag.forEach { + ++numCalls + valuesCalled.add(it) + } + + assertAll( + { assertEquals(2, numCalls) }, + { assertEquals(listOf("42", "43"), valuesCalled) } + ) + } + } + + @Nested + inner class IntBagTest { + @Test + fun `create empty bag of size 32`() { + val bag = IntBag(32) + + assertAll( + { assertEquals(32, bag.capacity) }, + { assertEquals(0, bag.size) } + ) + } + + @Test + fun `add value to bag`() { + val bag = IntBag() + + bag.add(42) + + assertAll( + { assertTrue(bag.isNotEmpty) }, + { assertEquals(1, bag.size) }, + { assertTrue(42 in bag) } + ) + } + + @Test + fun `clear all values from bag`() { + val bag = IntBag() + bag.add(42) + bag.add(43) + + bag.clear() + + assertAll( + { assertEquals(0, bag.size) }, + { assertFalse { 42 in bag } }, + { assertFalse { 43 in bag } } + ) + } + + @Test + fun `add value unsafe with sufficient capacity`() { + val bag = IntBag(1) + + bag.unsafeAdd(42) + + assertTrue(42 in bag) + } + + @Test + fun `add value unsafe with insufficient capacity`() { + val bag = IntBag(0) + + assertThrows { bag.unsafeAdd(42) } + } + + @Test + fun `add value to bag with insufficient capacity`() { + val bag = IntBag(0) + + bag.add(42) + + assertAll( + { assertEquals(1, bag.size) }, + { assertEquals(42, bag[0]) }, + { assertEquals(1, bag.capacity) } + ) + } + + @Test + fun `cannot get value of out of bounds index`() { + val bag = IntBag(2) + + assertThrows { bag[2] } + } + + @Test + fun `do not resize when bag has sufficient capacity`() { + val bag = IntBag(8) + + bag.ensureCapacity(7) + + assertEquals(8, bag.capacity) + } + + @Test + fun `resize when bag has insufficient capacity`() { + val bag = IntBag(8) + + bag.ensureCapacity(9) + + assertEquals(10, bag.capacity) + } + + @Test + fun `execute action for each value of the bag`() { + val bag = IntBag(4) + bag.add(42) + bag.add(43) + var numCalls = 0 + val valuesCalled = mutableListOf() + + bag.forEach { + ++numCalls + valuesCalled.add(it) + } + + assertAll( + { assertEquals(2, numCalls) }, + { assertEquals(listOf(42, 43), valuesCalled) } + ) + } + + @Test + fun `sort values by normal Int comparison with size less than 7`() { + val bag = IntBag() + repeat(6) { bag.add(6 - it) } + + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + + repeat(6) { + assertEquals(it + 1, bag[it]) + } + } + + @Test + fun `sort values by normal Int comparison with size less than 50 but greater 7`() { + val bag = IntBag() + repeat(8) { bag.add(8 - it) } + + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + + repeat(8) { + assertEquals(it + 1, bag[it]) + } + } + + @Test + fun `sort values by normal Int comparison with size greater 50`() { + val bag = IntBag() + repeat(51) { bag.add(51 - it) } + + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + + repeat(51) { + assertEquals(it + 1, bag[it]) + } + } + } +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt new file mode 100644 index 000000000..e9525c86c --- /dev/null +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt @@ -0,0 +1,165 @@ +package com.github.quillraven.fleks.collection + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class BitArrayTest { + @Test + fun `create empty BitArray`() { + val bits = BitArray(0) + + assertAll( + { assertEquals(0, bits.length()) }, + { assertEquals(0, bits.capacity) } + ) + } + + @Test + fun `set bit at index 3 with sufficient capacity`() { + val bits = BitArray(3) + + bits.set(2) + + assertAll( + { assertEquals(3, bits.length()) }, + { assertEquals(64, bits.capacity) }, + { assertTrue { bits[2] } } + ) + } + + @Test + fun `set bit at index 3 with insufficient capacity`() { + val bits = BitArray(0) + + bits.set(2) + + assertAll( + { assertEquals(3, bits.length()) }, + { assertEquals(64, bits.capacity) }, + { assertTrue { bits[2] } } + ) + } + + @Test + fun `get bit of out of bounds index`() { + val bits = BitArray(0) + + assertFalse(bits[64]) + } + + @Test + fun `clear all set bits`() { + val bits = BitArray() + bits.set(2) + bits.set(4) + + bits.clearAll() + + assertEquals(0, bits.length()) + } + + @Test + fun `clear specific bit`() { + val bits = BitArray() + bits.set(2) + + bits.clear(2) + + assertEquals(0, bits.length()) + } + + @Test + fun `two BitArrays intersect when they have at least one bit set at the same index`() { + val bitsA = BitArray(256) + val bitsB = BitArray(1) + bitsA.set(2) + bitsA.set(4) + bitsA.set(6) + bitsB.set(4) + + val actualA = bitsA.intersects(bitsB) + val actualB = bitsB.intersects(bitsA) + + assertAll( + { assertTrue(actualA) }, + { assertTrue(actualB) } + ) + } + + @Test + fun `two BitArrays do not intersect when they do not have at least one bit set at the same index`() { + val bitsA = BitArray(256) + val bitsB = BitArray(1) + bitsA.set(2) + bitsA.set(4) + bitsA.set(6) + bitsB.set(3) + + val actualA = bitsA.intersects(bitsB) + val actualB = bitsB.intersects(bitsA) + + assertAll( + { assertFalse(actualA) }, + { assertFalse(actualB) } + ) + } + + @Test + fun `BitArray contains BitArray if the same bits are set`() { + val bitsA = BitArray(256) + val bitsB = BitArray(1) + bitsA.set(2) + bitsA.set(4) + bitsB.set(2) + bitsB.set(4) + + val actualA = bitsA.contains(bitsB) + val actualB = bitsB.contains(bitsA) + + assertAll( + { assertTrue(actualA) }, + { assertTrue(actualB) } + ) + } + + @Test + fun `BitArray does not contain BitArray if different bits are set`() { + val bitsA = BitArray(256) + val bitsB = BitArray(1) + bitsA.set(2) + bitsA.set(4) + bitsB.set(2) + bitsB.set(3) + + val actualA = bitsA.contains(bitsB) + val actualB = bitsB.contains(bitsA) + + assertAll( + { assertFalse(actualA) }, + { assertFalse(actualB) } + ) + } + + @Test + fun `run action for each set bit`() { + val bits = BitArray(128) + bits.set(3) + bits.set(5) + bits.set(117) + var numCalls = 0 + val bitsCalled = mutableListOf() + + bits.forEachSetBit { + ++numCalls + bitsCalled.add(it) + } + + assertAll( + { assertEquals(3, numCalls) }, + { assertEquals(listOf(117, 5, 3), bitsCalled) } + ) + } +} diff --git a/samples/fleks-ecs/build.gradle b/samples/fleks-ecs/build.gradle index 0653ded39..85c89ed30 100644 --- a/samples/fleks-ecs/build.gradle +++ b/samples/fleks-ecs/build.gradle @@ -1,4 +1,4 @@ dependencies { - add("commonMainApi", project(":korge")) + add("commonMainApi", project(":korge")) + add("commonMainApi", project(":korge-fleks")) } - diff --git a/samples/fleks-ecs/src/commonMain/kotlin/assets/Assets.kt b/samples/fleks-ecs/src/commonMain/kotlin/assets/Assets.kt new file mode 100644 index 000000000..0b8f45f74 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/assets/Assets.kt @@ -0,0 +1,43 @@ +package assets + +import com.soywiz.klock.Stopwatch +import com.soywiz.korim.atlas.MutableAtlasUnit +import com.soywiz.korim.format.* +import com.soywiz.korio.file.std.resourcesVfs + +class Assets { + + private val atlas = MutableAtlasUnit(512, 2048, border = 1) + lateinit var images: Map + + fun getImage(name: String, slice: String = "") : ImageData { + return if (images[name] != null) { + if (slice.isEmpty()) { + images[name]!!.default + } else { + if (images[name]!![slice] != null) { + images[name]!![slice]!! + } else { + throw RuntimeException("Slice '$slice' of image '$name' not found in asset images!") + } + } + } else { + throw RuntimeException("Image '$name' not found in asset images!") + } + } + + suspend fun load(config: Config) { + val sw = Stopwatch().start() + println("start resources loading...") + images = config.images.associate { it.first to resourcesVfs[it.second].readImageDataContainer(ASE, atlas = atlas) } + println("loaded resources in ${sw.elapsed}") + } + + /** + * Data class which contains the config for loading assets. + */ + data class Config( + var reloading: Boolean = false, + val images: List> = listOf() + ) +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt index cf5918944..37b755039 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Destruct.kt @@ -8,6 +8,10 @@ package components */ data class Destruct( // Setting this to true triggers the DestructSystem to execute destruction of the entity + // TODO Instead of triggering the destruction here with a property the destruction can also be triggered by adding this + // component to the entity which shall be destroyed. That means as long as an entity does not contain this "Destruct" component + // it will live. Once this component is added to an entity the destruction of the entity will start and the + // entity will be destroyed finally. var triggerDestruction: Boolean = false, // details about what explosion animation should be spawned, etc. var spawnExplosion: Boolean = false, diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt index ea508292e..a27653415 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Impulse.kt @@ -1,5 +1,8 @@ package components +/** + * This component is used to add an entity the behaviour to "bounce" at collision with the ground. + */ data class Impulse( var xForce: Double = 0.0, // not used currently var yForce: Double = 0.0 diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt index 246e76820..ade7162ba 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Position.kt @@ -1,8 +1,12 @@ package components +/** + * This component is used to add position and acceleration to an entity. The data from this + * component will be processed by the MoveSystem of the Fleks ECS. + */ data class Position( - var x: Double = 0.0, - var y: Double = 0.0, + var x: Double = 100.0, + var y: Double = 100.0, var xAcceleration: Double = 0.0, var yAcceleration: Double = 0.0, ) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt index 85b1daa26..051fed5f0 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Rigidbody.kt @@ -1,11 +1,11 @@ package components /** - * This is a very basic definition of a rigidbody which does not take rotation into account. + * This is a very basic definition of a rigid body which does not take rotation into account. */ data class Rigidbody( var mass: Double = 0.0, - var velocityX: Double = 0.0, + var velocityX: Double = 0.0, // This and below are not yet used var velocityY: Double = 0.0, var damping: Double = 0.0, // e.g. air resistance of the object when falling var friction: Double = 0.0, // e.g. friction of the object when it moves over surfaces diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt index 2253c0a65..b72fdae7b 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Spawner.kt @@ -1,8 +1,11 @@ package components +/** + * This component makes an entity a spawner. That means the entity will spawn new entities as configured below. + */ data class Spawner( // config - var numberOfObjects: Int = 1, + var numberOfObjects: Int = 1, // The spawner will generate this number of object when triggered (by interval) var interval: Int = 0, // 0 - disabled, 1 - every frame, 2 - every second frame, 3 - every third frame,... var timeVariation: Int = 0, // 0 - no variation, 1 - one frame variation, 2 - two frames variation, ... // Spawner details for spawned objects (spawned objects do also spawn objects itself) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt b/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt index 3d25d5981..29c2ae848 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/components/Sprite.kt @@ -4,8 +4,11 @@ import com.soywiz.korge.view.Image import com.soywiz.korge.view.animation.ImageAnimationView import com.soywiz.korim.bitmap.Bitmaps +/** + * The sprite component adds visible details to an entity. By adding sprite to an entity the entity will be + * visible on the screen. + */ data class Sprite( - var lifeCycle: LifeCycle = LifeCycle.INACTIVE, var imageData: String = "", var animation: String = "", var isPlaying: Boolean = false, @@ -13,8 +16,4 @@ data class Sprite( var loop: Boolean = false, // internal data var imageAnimView: ImageAnimationView = ImageAnimationView { Image(Bitmaps.transparent) }.apply { smoothing = false } -) { - enum class LifeCycle { - INACTIVE, INIT, ACTIVE, DESTROY - } -} +) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/entities/Explosions.kt b/samples/fleks-ecs/src/commonMain/kotlin/entities/Explosions.kt new file mode 100644 index 000000000..a13da84fe --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/entities/Explosions.kt @@ -0,0 +1,33 @@ +package entities + +import com.github.quillraven.fleks.World +import components.* +import utils.random + +fun World.createExplosionArtefact(position: Position, destruct: Destruct) { + entity { + add { // Position of explosion object + // set initial position of explosion object to collision position + x = position.x + y = position.y - (destruct.explosionParticleRange * 0.5) + if (destruct.explosionParticleRange != 0.0) { + x += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() + y += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() + } + // make sure that all spawned objects are above 200 - this is hardcoded for now since we only have some basic collision detection at y > 200 + // otherwise they will be destroyed immediately and false appear at position 0x0 + if (y > 200.0) { y = 199.0 } + xAcceleration = position.xAcceleration + random(destruct.explosionParticleAcceleration) + yAcceleration = -position.yAcceleration + random(destruct.explosionParticleAcceleration) + } + add { + imageData = "meteorite" // "" - Disable sprite graphic for spawned object + animation = "FireTrail" // "FireTrail" - "TestNum" + isPlaying = true + } + add { + mass = 2.0 + } + add {} + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/entities/SpawnerEntities.kt b/samples/fleks-ecs/src/commonMain/kotlin/entities/SpawnerEntities.kt new file mode 100644 index 000000000..368b51715 --- /dev/null +++ b/samples/fleks-ecs/src/commonMain/kotlin/entities/SpawnerEntities.kt @@ -0,0 +1,112 @@ +package entities + +import com.github.quillraven.fleks.Entity +import com.github.quillraven.fleks.World +import components.Destruct +import components.Position +import components.Spawner +import components.Sprite +import utils.random + +/** + * This function creates a spawner entity which sits on top of the screen and + * spawns the meteorite objects. The config for it contains: + * - a "Position" component which set the position of the spawner area 10 pixels + * above the visible area. + * - a "Spawner" component which tells the system that the spawned + * meteorite objects itself are spawner objects. These are spawning the fire trails while + * moving downwards. + */ +fun World.createMeteoriteSpawner() : Entity { + return entity { + add { // Position of spawner + x = 100.0 + y = -10.0 // 10 pixel above the visible area + } + add { // Config for spawned objects + numberOfObjects = 1 // The spawner will generate one object per second + interval = 60 // 60 frames mean once per second + timeVariation = 30 // bring a bit of variation in the interval, so the respawning will happen every 30 to 90 frames (0.5 to 1.5 seconds) + // Spawner details for spawned objects (spawned objects do also spawn objects itself) + spawnerNumberOfObjects = 5 // Enable spawning feature for spawned object + spawnerInterval = 1 + spawnerPositionVariationX = 5.0 + spawnerPositionVariationY = 5.0 + spawnerPositionAccelerationX = -30.0 + spawnerPositionAccelerationY = -100.0 + spawnerPositionAccelerationVariation = 15.0 + spawnerSpriteImageData = "meteorite" // "" - Disable sprite graphic for spawned object + spawnerSpriteAnimation = "FireTrail" // "FireTrail" - "TestNum" + spawnerSpriteIsPlaying = true + // Set position details for spawned objects + positionVariationX = 100.0 + positionVariationY = 0.0 + positionAccelerationX = 90.0 + positionAccelerationY = 200.0 + positionAccelerationVariation = 10.0 + // Destruct info for spawned objects + destruct = true + } + } +} + +/** + * + */ +fun World.createMeteoriteObject(position: Position, spawner: Spawner) : Entity { + return entity { + add { // Position of spawner + x = position.x + spawner.positionX + if (spawner.positionVariationX != 0.0) x += (-spawner.positionVariationX..spawner.positionVariationX).random() + y = position.y + spawner.positionY + if (spawner.positionVariationY != 0.0) y += (-spawner.positionVariationY..spawner.positionVariationY).random() + xAcceleration = spawner.positionAccelerationX + yAcceleration = spawner.positionAccelerationY + if (spawner.positionAccelerationVariation != 0.0) { + val variation = (-spawner.positionAccelerationVariation..spawner.positionAccelerationVariation).random() + xAcceleration += variation + xAcceleration += variation + } + } + // Add spawner feature + if (spawner.spawnerNumberOfObjects != 0) { + add { + numberOfObjects = spawner.spawnerNumberOfObjects + interval = spawner.spawnerInterval + timeVariation = spawner.spawnerTimeVariation + // Position details for spawned objects + positionX = spawner.spawnerPositionX + positionY = spawner.spawnerPositionY + positionVariationX = spawner.spawnerPositionVariationX + positionVariationY = spawner.spawnerPositionVariationY + positionAccelerationX = spawner.spawnerPositionAccelerationX + positionAccelerationY = spawner.spawnerPositionAccelerationY + positionAccelerationVariation = spawner.spawnerPositionAccelerationVariation + // Sprite animation details for spawned objects + spriteImageData = spawner.spawnerSpriteImageData + spriteAnimation = spawner.spawnerSpriteAnimation + spriteIsPlaying = spawner.spawnerSpriteIsPlaying + spriteForwardDirection = spawner.spawnerSpriteForwardDirection + spriteLoop = spawner.spawnerSpriteLoop + } + } + // Add sprite animations + if (spawner.spriteImageData.isNotEmpty()) { + add { // Config for spawned object + imageData = spawner.spriteImageData + animation = spawner.spriteAnimation + isPlaying = spawner.spriteIsPlaying + forwardDirection = spawner.spriteForwardDirection + loop = spawner.spriteLoop + } + } + // Add destruct details + if (spawner.destruct) { + add { + spawnExplosion = true + explosionParticleRange = 15.0 + explosionParticleAcceleration = 300.0 + } + } + } +} diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 253f08ac1..18943c00c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -4,18 +4,13 @@ import com.soywiz.korge.scene.sceneContainer import com.soywiz.korge.view.Container import com.soywiz.korge.view.container import com.soywiz.korge.view.addUpdater -import com.soywiz.korim.atlas.MutableAtlasUnit import com.soywiz.korim.color.Colors -import com.soywiz.klock.Stopwatch -import com.soywiz.korim.format.ASE -import com.soywiz.korim.format.ImageData -import com.soywiz.korim.format.readImageData -import com.soywiz.korio.file.std.resourcesVfs - import com.github.quillraven.fleks.* +import assets.Assets import systems.* import systems.SpriteSystem.SpriteListener import components.* +import entities.createMeteoriteSpawner const val scaleFactor = 3 @@ -29,37 +24,43 @@ suspend fun main() = Korge(width = 384 * scaleFactor, height = 216 * scaleFactor rootSceneContainer.changeTo() } -var aseImage: ImageData? = null - class ExampleScene : Scene() { - private val atlas = MutableAtlasUnit(1024, 1024) + private val assets = Assets() override suspend fun Container.sceneInit() { - val sw = Stopwatch().start() - aseImage = resourcesVfs["sprites.ase"].readImageData(ASE, atlas = atlas) - println("loaded resources in ${sw.elapsed}") + + // Configure and load the asset objects + val config = Assets.Config( + images = listOf( + Pair("meteorite", "sprites.ase") + ) + ) + assets.load(config) } override suspend fun Container.sceneMain() { container { scale = scaleFactor.toDouble() - // TODO build a views container for handling layers for the ImageAnimationSystem of Fleks ECS + // Here are the container views which contain the generated entity objects with visible component "Sprite" attached to it + // + // TODO Build a more flexible views container system for handling layers for the SpriteSystem of Fleks ECS val layer0 = container() - val layer1 = container() + // val layer1 = container() // Add more layers when needed - This will be on top of layer0 // This is the world object of the entity component system (ECS) // It contains all ECS related system and component configuration val world = World { entityCapacity = 512 - // Register all needed systems + // Register all needed systems of the entity component system + // The order of systems here also define the order in which the systems are called inside Fleks ECS system(::MoveSystem) system(::SpawnerSystem) - system(::SpriteSystem) system(::CollisionSystem) system(::DestructSystem) + system(::SpriteSystem) // Drawing images on screen should be last otherwise the position might be (0, 0) because it was not set before // Register all needed components and its listeners (if needed) component(::Position) @@ -70,47 +71,15 @@ class ExampleScene : Scene() { component(::Impulse) // Register external objects which are used by systems and component listeners - inject("layer0", layer0) -// inject("layer1", layer1) TODO add more layers for explosion objects to be on top + inject(assets) // Assets are used by the SpriteSystem / SpriteListener to get the image data for drawing + inject("layer0", layer0) // Currently we use only one layer to draw all objects to - this is also used in SpriteListener to add the image to the layer container + // inject("layer1", layer1) // Add more layers when needed e.g. for explosion objects to be on top, etc. } - // This is the config for the spawner entity which sits on top of the screen and which - // spawns the meteorite objects. - // - The spawner get a "Position" component which set the position of it 10 pixels - // above the visible area. - // - Secondly it gets a "Spawner" component. That tells the system that the spawned - // meteorite objects itself are spawning objects. These are the visible fire trails. - world.entity { - add { // Position of spawner - x = 100.0 - y = -10.0 - } - add { // Config for spawner object - numberOfObjects = 1 // The spawner will generate one object per second - interval = 60 // every 60 frames which means once per second - timeVariation = 0 - // Spawner details for spawned objects (spawned objects do also spawn objects itself) - spawnerNumberOfObjects = 5 // Enable spawning feature for spawned object - spawnerInterval = 1 - spawnerPositionVariationX = 5.0 - spawnerPositionVariationY = 5.0 - spawnerPositionAccelerationX = -30.0 - spawnerPositionAccelerationY = -100.0 - spawnerPositionAccelerationVariation = 15.0 - spawnerSpriteImageData = "sprite" // "" - Disable sprite graphic for spawned object - spawnerSpriteAnimation = "FireTrail" // "FireTrail" - "TestNum" - spawnerSpriteIsPlaying = true - // Set position details for spawned objects - positionVariationX = 100.0 - positionVariationY = 0.0 - positionAccelerationX = 90.0 - positionAccelerationY = 200.0 - positionAccelerationVariation = 10.0 - // Destruct info for spawned objects - destruct = true - } - } + // Create an entity object which will spawn meteorites on top of the visual screen area + world.createMeteoriteSpawner() + // Run the update of the Fleks ECS - this will periodically call all update functions of the systems (e.g. onTick(), onTickEntity(), etc.) addUpdater { dt -> world.update(dt.seconds.toFloat()) } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt index cdb4379bf..a409d210d 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/CollisionSystem.kt @@ -19,13 +19,13 @@ class CollisionSystem : IteratingSystem( } override fun onTickEntity(entity: Entity) { -// println("[Entity: ${entity.id}] MoveSystem onTickEntity") val pos = positions[entity] // To make collision detection easy we check here just the Y position if it is below 200 which means // that the object is colliding - In real games here is a more sophisticated collision check necessary ;-) if (pos.y > 200.0) { - // Check if entity has a destruct or impulse component + pos.y = 200.0 + // Check if entity has a Destruct or Impulse component if (destructs.contains(entity)) { // Delegate "destruction" of the entity to the DestructSystem - it will destroy the entity after some other task are done destructs[entity].triggerDestruction = true @@ -33,7 +33,6 @@ class CollisionSystem : IteratingSystem( // Do not destruct entity but let it bounce on the surface pos.xAcceleration = pos.xAcceleration * 0.7 pos.yAcceleration = -pos.yAcceleration * 0.9 - pos.y = 199.0 } else { // Entity gets destroyed immediately world.remove(entity) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt index f3b131d7c..3bb6a2631 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/DestructSystem.kt @@ -4,10 +4,10 @@ import com.github.quillraven.fleks.Entity import com.github.quillraven.fleks.Inject import com.github.quillraven.fleks.IteratingSystem import components.* -import utils.random +import entities.createExplosionArtefact /** - * This system controls the "destruction" of an game object / entity. + * This system controls the "destruction" of an entity (game object). * */ class DestructSystem : IteratingSystem( @@ -20,34 +20,10 @@ class DestructSystem : IteratingSystem( override fun onTickEntity(entity: Entity) { val destruct = destructs[entity] if (destruct.triggerDestruction) { - val pos = positions[entity] - // The spawning of exposion objects is hardcoded here to 40 objects - that should be put into some component later + val position = positions[entity] + // The spawning of explosion objects is hardcoded here to 40 objects - TODO that should be put into some component config later for (i in 0 until 40) { - world.entity { - add { // Position of explosion object - // set initial position of explosion object to collision position - x = pos.x - y = pos.y - (destruct.explosionParticleRange * 0.5) - if (destruct.explosionParticleRange != 0.0) { - x += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() - y += (-destruct.explosionParticleRange..destruct.explosionParticleRange).random() - } - // make sure that all spawned objects are above 200 - this is hardcoded for now since we only have some basic collision detection at y > 200 - // otherwise they will be destroyed immediately and false appear at position 0x0 - if (y > 200.0) { y = 199.0 } - xAcceleration = pos.xAcceleration + random(destruct.explosionParticleAcceleration) - yAcceleration = -pos.yAcceleration + random(destruct.explosionParticleAcceleration) - } - add { - imageData = "sprite" // "" - Disable sprite graphic for spawned object - animation = "FireTrail" // "FireTrail" - "TestNum" - isPlaying = true - } - add { - mass = 2.0 - } - add {} - } + world.createExplosionArtefact(position, destruct) } // now destroy entity world.remove(entity) diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt index 5dd62bbee..a30c81ccc 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/MoveSystem.kt @@ -11,25 +11,22 @@ class MoveSystem : IteratingSystem( allOfComponents = arrayOf(Position::class), // Position component absolutely needed for movement of entity objects anyOfComponents = arrayOf(Position::class, Rigidbody::class), // Rigidbody not necessarily needed for movement interval = EachFrame -// interval = Fixed(500f) // for testing every 500 millisecond ) { private val positions = Inject.componentMapper() private val rigidbodies = Inject.componentMapper() - override fun onInit() { - } + override fun onInit() {} override fun onTickEntity(entity: Entity) { -// println("[Entity: ${entity.id}] MoveSystem onTickEntity") val pos = positions[entity] if (rigidbodies.contains(entity)) { // Entity has a rigidbody - that means the movement will be calculated depending on it val rigidbody = rigidbodies[entity] -// pos.xAcceleration * deltaTime - // TODO implement movement with rigidbody + // Currently we just add gravity to the entity pos.yAcceleration += rigidbody.mass * 9.81 + // TODO implement more sophisticated movement with rigidbody taking damping and friction into account } pos.x += pos.xAcceleration * deltaTime diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt index 830254818..b5385a113 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpawnerSystem.kt @@ -2,25 +2,20 @@ package systems import com.github.quillraven.fleks.* import components.* -import utils.random +import entities.createMeteoriteObject class SpawnerSystem : IteratingSystem( allOfComponents = arrayOf(Spawner::class), interval = EachFrame -// interval = Fixed(500f) // for testing every 500 millisecond ) { private val positions = Inject.componentMapper() private val spawners = Inject.componentMapper() - override fun onInit() { - } - override fun onTickEntity(entity: Entity) { val spawner = spawners[entity] if (spawner.interval > 0) { if (spawner.nextSpawnIn <= 0) { -// println("[Entity: ${entity.id}] SpawnerSystem onTickEntity - create new entity") spawn(entity) spawner.nextSpawnIn = spawner.interval if (spawner.timeVariation != 0) spawner.nextSpawnIn += (-spawner.timeVariation..spawner.timeVariation).random() @@ -34,61 +29,7 @@ class SpawnerSystem : IteratingSystem( val spawnerPosition = positions[entity] val spawner = spawners[entity] for (i in 0 until spawner.numberOfObjects) { - world.entity { - add { // Position of spawner - x = spawnerPosition.x + spawner.positionX - if (spawner.positionVariationX != 0.0) x += (-spawner.positionVariationX..spawner.positionVariationX).random() - y = spawnerPosition.y + spawner.positionY - if (spawner.positionVariationY != 0.0) y += (-spawner.positionVariationY..spawner.positionVariationY).random() - xAcceleration = spawner.positionAccelerationX - yAcceleration = spawner.positionAccelerationY - if (spawner.positionAccelerationVariation != 0.0) { - val variation = (-spawner.positionAccelerationVariation..spawner.positionAccelerationVariation).random() - xAcceleration += variation - xAcceleration += variation - } - } - // Add spawner feature - if (spawner.spawnerNumberOfObjects != 0) { - add { - numberOfObjects = spawner.spawnerNumberOfObjects - interval = spawner.spawnerInterval - timeVariation = spawner.spawnerTimeVariation - // Position details for spawned objects - positionX = spawner.spawnerPositionX - positionY = spawner.spawnerPositionY - positionVariationX = spawner.spawnerPositionVariationX - positionVariationY = spawner.spawnerPositionVariationY - positionAccelerationX = spawner.spawnerPositionAccelerationX - positionAccelerationY = spawner.spawnerPositionAccelerationY - positionAccelerationVariation = spawner.spawnerPositionAccelerationVariation - // Sprite animation details for spawned objects - spriteImageData = spawner.spawnerSpriteImageData - spriteAnimation = spawner.spawnerSpriteAnimation - spriteIsPlaying = spawner.spawnerSpriteIsPlaying - spriteForwardDirection = spawner.spawnerSpriteForwardDirection - spriteLoop = spawner.spawnerSpriteLoop - } - } - // Add sprite animations - if (spawner.spriteImageData.isNotEmpty()) { - add { // Config for spawned object - imageData = spawner.spriteImageData - animation = spawner.spriteAnimation - isPlaying = spawner.spriteIsPlaying - forwardDirection = spawner.spriteForwardDirection - loop = spawner.spriteLoop - } - } - // Add destruct details - if (spawner.destruct) { - add { - spawnExplosion = true - explosionParticleRange = 15.0 - explosionParticleAcceleration = 300.0 - } - } - } + world.createMeteoriteObject(spawnerPosition, spawner) } } } diff --git a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt index fc16f5482..66f98fbe2 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/systems/SpriteSystem.kt @@ -6,67 +6,50 @@ import com.github.quillraven.fleks.* import com.soywiz.korge.view.Container import com.soywiz.korge.view.addTo import com.soywiz.korim.format.ImageAnimation - import components.* import components.Sprite -import components.Sprite.LifeCycle -import aseImage +import assets.Assets /** - * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the configuration from + * This System takes care of displaying sprites (image-animation objects) on the screen. It takes the image configuration from * [Sprite] component to setup graphics from Assets and create an ImageAnimationView object for displaying in the Container. * */ class SpriteSystem : IteratingSystem( allOfComponents = arrayOf(Sprite::class, Position::class), interval = EachFrame -// interval = Fixed(500f) // for testing every 500 millisecond ) { - private val positions: ComponentMapper = Inject.componentMapper() - private val sprites: ComponentMapper = Inject.componentMapper() + private val positions = Inject.componentMapper() + private val sprites = Inject.componentMapper() override fun onInit() { } override fun onTickEntity(entity: Entity) { -// println("[Entity: ${entity.id}] SpriteSystem onTickEntity") val sprite = sprites[entity] val pos = positions[entity] - when (sprite.lifeCycle) { - LifeCycle.INIT -> { - sprite.lifeCycle = LifeCycle.ACTIVE - } - LifeCycle.ACTIVE -> { - // sync view position - sprite.imageAnimView.x = pos.x - sprite.imageAnimView.y = pos.y - } - LifeCycle.DESTROY -> { - // Object is going to be recycled - world.remove(entity) - } - else -> {} - } + // sync view position + sprite.imageAnimView.x = pos.x + sprite.imageAnimView.y = pos.y } class SpriteListener : ComponentListener { private val world = Inject.dependency() private val layerContainer = Inject.dependency("layer0") + private val assets = Inject.dependency() override fun onComponentAdded(entity: Entity, component: Sprite) { // Set animation object - component.imageAnimView.animation = - // TODO get this from Assets object with "imageData" string - aseImage?.animationsByName?.getOrElse(component.animation) { aseImage?.defaultAnimation } -// component.imageAnimView.onDestroyLayer = { image -> imageBitmapTransparentPool.free(image) } - component.imageAnimView.onPlayFinished = { component.lifeCycle = LifeCycle.DESTROY } -// component.imageAnimView.onPlayFinished = { -// component.imageAnimView.removeFromParent() -// world.remove(entity) -// } + val asset = assets.getImage(component.imageData) + component.imageAnimView.animation = asset.animationsByName.getOrElse(component.animation) { asset.defaultAnimation } + component.imageAnimView.onPlayFinished = { + // when animation finished playing trigger destruction of entity + // TODO handle destruction with "Destruct" component + world.remove(entity) + } component.imageAnimView.addTo(layerContainer) // Set play status component.imageAnimView.direction = when { @@ -76,16 +59,9 @@ class SpriteSystem : IteratingSystem( else -> ImageAnimation.Direction.FORWARD } if (component.isPlaying) { component.imageAnimView.play() } - component.lifeCycle = LifeCycle.ACTIVE - -// println("Component $component") -// println(" added to Entity '${entity.id}'!") } override fun onComponentRemoved(entity: Entity, component: Sprite) { -// println("Component $component") -// println(" removed from Entity '${entity.id}'!") - component.imageAnimView.removeFromParent() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7c8288781..d50f73ef2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include(":korau") include(":korgw") include(":korvi") include(":korge") +include(":korge-fleks") if (System.getenv("DISABLED_EXTRA_KORGE_LIBS") != "true") { include(":luak") From fd8e16917c1b098fa25a657b7e23422d6e3e3726 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Thu, 10 Mar 2022 18:48:37 +0100 Subject: [PATCH 19/27] Convert unit tests of korge-fleks into junit4 style --- .../github/quillraven/fleks/ComponentTest.kt | 106 ++--- .../com/github/quillraven/fleks/EntityTest.kt | 135 +++--- .../com/github/quillraven/fleks/FamilyTest.kt | 103 ++--- .../com/github/quillraven/fleks/SystemTest.kt | 111 ++--- .../com/github/quillraven/fleks/WorldTest.kt | 98 ++--- .../quillraven/fleks/collection/BagTest.kt | 389 ++++++++---------- .../fleks/collection/BitArrayTest.kt | 80 ++-- 7 files changed, 431 insertions(+), 591 deletions(-) diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt index b1bce0047..c9e59e83b 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt @@ -1,12 +1,6 @@ package com.github.quillraven.fleks -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertSame +import kotlin.test.* private data class ComponentTestComponent(var x: Int = 0) @@ -49,35 +43,31 @@ internal class ComponentTest { } @Test - fun `add entity to mapper with sufficient capacity`() { + fun addEntityToMapperWithSufficientCapacity() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(0) val cmp = mapper.addInternal(entity) { x = 5 } - assertAll( - { assertTrue(entity in mapper) }, - { assertEquals(5, cmp.x) } - ) + assertTrue(entity in mapper) + assertEquals(5, cmp.x) } @Test - fun `add entity to mapper with insufficient capacity`() { + fun addEntityToMapperWithInsufficientCapacity() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(10_000) val cmp = mapper.addInternal(entity) - assertAll( - { assertTrue(entity in mapper) }, - { assertEquals(0, cmp.x) } - ) + assertTrue(entity in mapper) + assertEquals(0, cmp.x) } @Test - fun `add already existing entity to mapper`() { + fun addAlreadyExistingEntityToMapper() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(10_000) @@ -85,25 +75,21 @@ internal class ComponentTest { val actual = mapper.addInternal(entity) { x = 2 } - assertAll( - { assertSame(expected, actual) }, - { assertEquals(2, actual.x) } - ) + assertSame(expected, actual) + assertEquals(2, actual.x) } @Test - fun `returns false when entity is not part of mapper`() { + fun returnsFalseWhenEntityIsNotPartOfMapper() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() - assertAll( - { assertFalse(Entity(0) in mapper) }, - { assertFalse(Entity(10_000) in mapper) } - ) + assertFalse(Entity(0) in mapper) + assertFalse(Entity(10_000) in mapper) } @Test - fun `remove existing entity from mapper`() { + fun removeExistingEntityFromMapper() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(0) @@ -115,16 +101,16 @@ internal class ComponentTest { } @Test - fun `cannot remove non-existing entity from mapper with insufficient capacity`() { + fun cannotRemoveNonExistingEntityFromMapperWithInsufficientCapacity() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(10_000) - assertThrows { mapper.removeInternal(entity) } + assertFailsWith { mapper.removeInternal(entity) } } @Test - fun `get component of existing entity`() { + fun getComponentOfExistingEntity() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(0) @@ -136,16 +122,16 @@ internal class ComponentTest { } @Test - fun `cannot get component of non-existing entity`() { + fun cannotGetComponentOfNonExistingEntity() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(0) - assertThrows { mapper[entity] } + assertFailsWith { mapper[entity] } } @Test - fun `create new mapper`() { + fun createNewMapper() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() @@ -154,7 +140,7 @@ internal class ComponentTest { } @Test - fun `do not create the same mapper twice`() { + fun doNotCreateTheSameMapperTwice() { val cmpService = ComponentService(componentFactory) val expected = cmpService.mapper() @@ -164,7 +150,7 @@ internal class ComponentTest { } @Test - fun `get mapper by component id`() { + fun getMapperByComponentId() { val cmpService = ComponentService(componentFactory) val expected = cmpService.mapper() @@ -174,7 +160,7 @@ internal class ComponentTest { } @Test - fun `add ComponentListener`() { + fun addComponentListener() { val cmpService = ComponentService(componentFactory) val listener = ComponentTestComponentListener() val mapper = cmpService.mapper() @@ -185,7 +171,7 @@ internal class ComponentTest { } @Test - fun `remove ComponentListener`() { + fun removeComponentListener() { val cmpService = ComponentService(componentFactory) val listener = ComponentTestComponentListener() val mapper = cmpService.mapper() @@ -197,7 +183,7 @@ internal class ComponentTest { } @Test - fun `add component with ComponentListener`() { + fun addComponentWithComponentListener() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val listener = ComponentTestComponentListener() @@ -206,16 +192,14 @@ internal class ComponentTest { val expectedCmp = mapper.addInternal(expectedEntity) - assertAll( - { assertEquals(1, listener.numAddCalls) }, - { assertEquals(0, listener.numRemoveCalls) }, - { assertEquals(expectedEntity, listener.entityCalled) }, - { assertEquals(expectedCmp, listener.cmpCalled) } - ) + assertEquals(1, listener.numAddCalls) + assertEquals(0, listener.numRemoveCalls) + assertEquals(expectedEntity, listener.entityCalled) + assertEquals(expectedCmp, listener.cmpCalled) } @Test - fun `add component with ComponentListener when component already present`() { + fun addComponentWithComponentListenerWhenComponentAlreadyPresent() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val expectedEntity = Entity(1) @@ -225,31 +209,27 @@ internal class ComponentTest { val expectedCmp = mapper.addInternal(expectedEntity) - assertAll( - { assertEquals(1, listener.numAddCalls) }, - { assertEquals(1, listener.numRemoveCalls) }, - { assertEquals(expectedEntity, listener.entityCalled) }, - { assertEquals(expectedCmp, listener.cmpCalled) }, - { assertEquals("add", listener.lastCall) } - ) + assertEquals(1, listener.numAddCalls) + assertEquals(1, listener.numRemoveCalls) + assertEquals(expectedEntity, listener.entityCalled) + assertEquals(expectedCmp, listener.cmpCalled) + assertEquals("add", listener.lastCall) } @Test - fun `add component if it does not exist yet`() { + fun addComponentIfItDoesNotExistYet() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(1) val cmp = mapper.addOrUpdateInternal(entity) { x++ } - assertAll( - { assertTrue(entity in mapper) }, - { assertEquals(1, cmp.x) } - ) + assertTrue(entity in mapper) + assertEquals(1, cmp.x) } @Test - fun `update component if it already exists`() { + fun updateComponentIfItAlreadyExists() { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(1) @@ -257,10 +237,8 @@ internal class ComponentTest { val actualCmp = mapper.addOrUpdateInternal(entity) { x++ } - assertAll( - { assertTrue(entity in mapper) }, - { assertEquals(expectedCmp, actualCmp) }, - { assertEquals(2, actualCmp.x) } - ) + assertTrue(entity in mapper) + assertEquals(expectedCmp, actualCmp) + assertEquals(2, actualCmp.x) } } diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt index 666943ea7..2f695066a 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/EntityTest.kt @@ -1,8 +1,7 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test +import kotlin.test.* private class EntityTestListener : EntityListener { var numCalls = 0 @@ -35,46 +34,40 @@ internal class EntityTest { } @Test - fun `create empty service for 32 entities`() { + fun createEmptyServiceFor32Entities() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) - assertAll( - { assertEquals(0, entityService.numEntities) }, - { assertEquals(32, entityService.capacity) } - ) + assertEquals(0, entityService.numEntities) + assertEquals(32, entityService.capacity) } @Test - fun `create entity without configuration and sufficient capacity`() { + fun createEntityWithoutConfigurationAndSufficientCapacity() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val entity = entityService.create {} - assertAll( - { assertEquals(0, entity.id) }, - { assertEquals(1, entityService.numEntities) } - ) + assertEquals(0, entity.id) + assertEquals(1, entityService.numEntities) } @Test - fun `create entity without configuration and insufficient capacity`() { + fun createEntityWithoutConfigurationAndInsufficientCapacity() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(0, cmpService) val entity = entityService.create {} - assertAll( - { assertEquals(0, entity.id) }, - { assertEquals(1, entityService.numEntities) }, - { assertEquals(1, entityService.capacity) }, - ) + assertEquals(0, entity.id) + assertEquals(1, entityService.numEntities) + assertEquals(1, entityService.capacity) } @Test - fun `create entity with configuration and custom listener`() { + fun createEntityWithConfigurationAndCustomListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -87,17 +80,15 @@ internal class EntityTest { } val mapper = cmpService.mapper() - assertAll( - { assertEquals(1, listener.numCalls) }, - { assertEquals(expectedEntity, listener.entityReceived) }, - { assertTrue(listener.cmpMaskReceived[0]) }, - { assertEquals(0f, mapper[listener.entityReceived].x) }, - { assertEquals(expectedEntity, processedEntity) } - ) + assertEquals(1, listener.numCalls) + assertEquals(expectedEntity, listener.entityReceived) + assertTrue(listener.cmpMaskReceived[0]) + assertEquals(0f, mapper[listener.entityReceived].x) + assertEquals(expectedEntity, processedEntity) } @Test - fun `remove component from entity with custom listener`() { + fun removeComponentFromEntityWithCustomListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -107,16 +98,14 @@ internal class EntityTest { entityService.configureEntity(expectedEntity) { mapper.remove(expectedEntity) } - assertAll( - { assertEquals(1, listener.numCalls) }, - { assertEquals(expectedEntity, listener.entityReceived) }, - { assertFalse(listener.cmpMaskReceived[0]) }, - { assertFalse(expectedEntity in mapper) } - ) + assertEquals(1, listener.numCalls) + assertEquals(expectedEntity, listener.entityReceived) + assertFalse(listener.cmpMaskReceived[0]) + assertFalse(expectedEntity in mapper) } @Test - fun `add component to entity with custom listener`() { + fun addComponentToEntityWithCustomListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -126,16 +115,14 @@ internal class EntityTest { entityService.configureEntity(expectedEntity) { mapper.add(expectedEntity) } - assertAll( - { assertEquals(1, listener.numCalls) }, - { assertEquals(expectedEntity, listener.entityReceived) }, - { assertTrue(listener.cmpMaskReceived[0]) }, - { assertTrue(expectedEntity in mapper) } - ) + assertEquals(1, listener.numCalls) + assertEquals(expectedEntity, listener.entityReceived) + assertTrue(listener.cmpMaskReceived[0]) + assertTrue(expectedEntity in mapper) } @Test - fun `update component of entity if it already exists with custom listener`() { + fun updateComponentOfEntityIfItAlreadyExistsWithCustomListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -148,15 +135,13 @@ internal class EntityTest { mapper.addOrUpdate(expectedEntity) { x++ } } - assertAll( - { assertTrue(expectedEntity in mapper) }, - { assertEquals(2f, mapper[expectedEntity].x) }, - { assertEquals(1, listener.numCalls) } - ) + assertTrue(expectedEntity in mapper) + assertEquals(2f, mapper[expectedEntity].x) + assertEquals(1, listener.numCalls) } @Test - fun `remove entity with a component immediately with custom listener`() { + fun removeEntityWithAComponentImmediatelyWithCustomListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -166,30 +151,26 @@ internal class EntityTest { entityService.remove(expectedEntity) - assertAll( - { assertEquals(1, listener.numCalls) }, - { assertEquals(expectedEntity, listener.entityReceived) }, - { assertFalse(listener.cmpMaskReceived[0]) }, - { assertFalse(expectedEntity in mapper) } - ) + assertEquals(1, listener.numCalls) + assertEquals(expectedEntity, listener.entityReceived) + assertFalse(listener.cmpMaskReceived[0]) + assertFalse(expectedEntity in mapper) } @Test - fun `remove all entities`() { + fun removeAllEntities() { val entityService = EntityService(32, ComponentService(componentFactory)) entityService.create {} entityService.create {} entityService.removeAll() - assertAll( - { assertEquals(2, entityService.recycledEntities.size) }, - { assertEquals(0, entityService.numEntities) } - ) + assertEquals(2, entityService.recycledEntities.size) + assertEquals(0, entityService.numEntities) } @Test - fun `remove all entities with already recycled entities`() { + fun removeAllEntitiesWithAlreadyRecycledEntities() { val entityService = EntityService(32, ComponentService(componentFactory)) val recycled = entityService.create {} entityService.create {} @@ -197,14 +178,12 @@ internal class EntityTest { entityService.removeAll() - assertAll( - { assertEquals(2, entityService.recycledEntities.size) }, - { assertEquals(0, entityService.numEntities) } - ) + assertEquals(2, entityService.recycledEntities.size) + assertEquals(0, entityService.numEntities) } @Test - fun `remove all entities when removal is delayed`() { + fun removeAllEntitiesWhenRemovalIsDelayed() { val entityService = EntityService(32, ComponentService(componentFactory)) entityService.create {} entityService.create {} @@ -214,14 +193,12 @@ internal class EntityTest { entityService.removeAll() - assertAll( - { assertEquals(0, listener.numCalls) }, - { assertTrue(entityService.delayRemoval) } - ) + assertEquals(0, listener.numCalls) + assertTrue(entityService.delayRemoval) } @Test - fun `create recycled entity`() { + fun createRecycledEntity() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val expectedEntity = entityService.create { } @@ -233,7 +210,7 @@ internal class EntityTest { } @Test - fun `delay entity removal`() { + fun delayEntityRemoval() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val entity = entityService.create { } @@ -247,7 +224,7 @@ internal class EntityTest { } @Test - fun `remove delayed entity`() { + fun removeDelayedEntity() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val entity = entityService.create { } @@ -260,14 +237,12 @@ internal class EntityTest { entityService.cleanupDelays() entityService.cleanupDelays() - assertAll( - { assertFalse(entityService.delayRemoval) }, - { assertEquals(1, listener.numCalls) } - ) + assertFalse(entityService.delayRemoval) + assertEquals(1, listener.numCalls) } @Test - fun `remove existing listener`() { + fun removeExistingListener() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val listener = EntityTestListener() @@ -279,7 +254,7 @@ internal class EntityTest { } @Test - fun `remove entity twice`() { + fun removeEntityTwice() { val cmpService = ComponentService(componentFactory) val entityService = EntityService(32, cmpService) val entity = entityService.create { } @@ -289,9 +264,7 @@ internal class EntityTest { entityService.remove(entity) entityService.remove(entity) - assertAll( - { assertEquals(1, entityService.recycledEntities.size) }, - { assertEquals(1, listener.numCalls) } - ) + assertEquals(1, entityService.recycledEntities.size) + assertEquals(1, listener.numCalls) } } diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt index 310254264..980c4a6ac 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyTest.kt @@ -2,13 +2,7 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.compareEntity -import org.junit.jupiter.api.DynamicTest -import org.junit.jupiter.api.DynamicTest.dynamicTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestFactory -import org.junit.jupiter.api.assertAll -import kotlin.test.assertEquals -import kotlin.test.assertFalse +import kotlin.test.* internal class FamilyTest { private fun createCmpBitmask(cmpIdx: Int): BitArray? { @@ -19,10 +13,9 @@ internal class FamilyTest { } } - - @TestFactory - fun `test contains`(): Collection { - return listOf( + @Test + fun testFamily() { + listOf( arrayOf( "empty family contains entity without components", BitArray(), // entity component mask @@ -72,35 +65,31 @@ internal class FamilyTest { true // expected ), ).map { - dynamicTest("test ${it[0]}") { - val eCmpMask = it[1] as BitArray - val fAllOf = createCmpBitmask(it[2] as Int) - val fNoneOf = createCmpBitmask(it[3] as Int) - val fAnyOf = createCmpBitmask(it[4] as Int) - val family = Family(fAllOf, fNoneOf, fAnyOf) - val expected = it[5] as Boolean - - assertEquals(expected, eCmpMask in family) - } + val eCmpMask = it[1] as BitArray + val fAllOf = createCmpBitmask(it[2] as Int) + val fNoneOf = createCmpBitmask(it[3] as Int) + val fAnyOf = createCmpBitmask(it[4] as Int) + val family = Family(fAllOf, fNoneOf, fAnyOf) + val expected = it[5] as Boolean + + assertEquals(expected, eCmpMask in family, "FAILED: testFamily: " + it[0] + " - ") } } @Test - fun `update active entities`() { + fun updateActiveEntities() { val family = Family() family.onEntityCfgChanged(Entity(0), BitArray()) family.updateActiveEntities() - assertAll( - { assertFalse { family.isDirty } }, - { assertEquals(1, family.entitiesBag.size) }, - { assertEquals(0, family.entitiesBag[0]) } - ) + assertFalse { family.isDirty } + assertEquals(1, family.entitiesBag.size) + assertEquals(0, family.entitiesBag[0]) } @Test - fun `call action for each entity`() { + fun callActionForEachEntity() { val family = Family() family.onEntityCfgChanged(Entity(0), BitArray()) family.updateActiveEntities() @@ -112,14 +101,12 @@ internal class FamilyTest { processedEntity = it.id } - assertAll( - { assertEquals(0, processedEntity) }, - { assertEquals(1, numExecutions) } - ) + assertEquals(0, processedEntity) + assertEquals(1, numExecutions) } @Test - fun `sort entities`() { + fun sortEntities() { val family = Family() family.onEntityCfgChanged(Entity(0), BitArray()) family.onEntityCfgChanged(Entity(2), BitArray()) @@ -129,16 +116,14 @@ internal class FamilyTest { // sort descending by entity id family.sort(compareEntity { e1, e2 -> e2.id.compareTo(e1.id) }) - assertAll( - { assertEquals(2, family.entitiesBag[0]) }, - { assertEquals(1, family.entitiesBag[1]) }, - { assertEquals(0, family.entitiesBag[2]) }, - ) + assertEquals(2, family.entitiesBag[0]) + assertEquals(1, family.entitiesBag[1]) + assertEquals(0, family.entitiesBag[2]) } - @TestFactory - fun `test onEntityCfgChange`(): Collection { - return listOf( + @Test + fun testOnEntityCfgChange() { + listOf( // first = add entity to family before calling onChange // second = make entity part of family Pair(false, false), @@ -146,25 +131,23 @@ internal class FamilyTest { Pair(true, false), Pair(true, true), ).map { - dynamicTest("addEntityBefore=${it.first}, addEntity=${it.second}") { - val family = Family(BitArray().apply { set(1) }, null, null) - val addEntityBeforeCall = it.first - val addEntityToFamily = it.second - val entity = Entity(1) - if (addEntityBeforeCall) { - family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) - family.updateActiveEntities() - } - - if (addEntityToFamily) { - family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) - - assertEquals(!addEntityBeforeCall, family.isDirty) - } else { - family.onEntityCfgChanged(entity, BitArray()) - - assertEquals(addEntityBeforeCall, family.isDirty) - } + val family = Family(BitArray().apply { set(1) }, null, null) + val addEntityBeforeCall = it.first + val addEntityToFamily = it.second + val entity = Entity(1) + if (addEntityBeforeCall) { + family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) + family.updateActiveEntities() + } + + if (addEntityToFamily) { + family.onEntityCfgChanged(entity, BitArray().apply { set(1) }) + + assertEquals(!addEntityBeforeCall, family.isDirty, "FAILED: testOnEntityCfgChanged: addEntityBefore=${it.first}, addEntity=${it.second} - ") + } else { + family.onEntityCfgChanged(entity, BitArray()) + + assertEquals(addEntityBeforeCall, family.isDirty, "FAILED: testOnEntityCfgChanged: addEntityBefore=${it.first}, addEntity=${it.second} - ") } } } diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt index 02dcc1513..780cefc74 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/SystemTest.kt @@ -1,15 +1,8 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.EntityComparator -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows -import java.lang.reflect.InvocationTargetException import kotlin.reflect.KClass -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame +import kotlin.test.* private class SystemTestIntervalSystemEachFrame : IntervalSystem( interval = EachFrame @@ -186,7 +179,7 @@ internal class SystemTest { ) @Test - fun `system with interval EachFrame gets called every time`() { + fun systemWithIntervalEachFrameGetsCalledEveryTime() { val system = SystemTestIntervalSystemEachFrame() system.onUpdate() @@ -196,7 +189,7 @@ internal class SystemTest { } @Test - fun `system with interval EachFrame returns world's delta time`() { + fun systemWithIntervalEachFrameReturnsWorldsDeltaTime() { val system = SystemTestIntervalSystemEachFrame().apply { this.world = World {} } system.world.update(42f) @@ -204,42 +197,38 @@ internal class SystemTest { } @Test - fun `system with fixed interval of 0,25f gets called four times when delta time is 1,1f`() { + fun systemWithFixedIntervalOf0_25fGetsCalledFourTimesWhenDeltaTimeIs1_1f() { val system = SystemTestIntervalSystemFixed().apply { this.world = World {} } system.world.update(1.1f) system.onUpdate() - assertAll( - { assertEquals(4, system.numCalls) }, - { assertEquals(0.1f / 0.25f, system.lastAlpha, 0.0001f) } - ) + assertEquals(4, system.numCalls) + assertEquals(0.1f / 0.25f, system.lastAlpha, 0.0001f) } @Test - fun `system with fixed interval returns step rate as delta time`() { + fun systemWithFixedIntervalReturnsStepRateAsDeltaTime() { val system = SystemTestIntervalSystemFixed() assertEquals(0.25f, system.deltaTime, 0.0001f) } @Test - fun `create IntervalSystem with no-args`() { + fun createIntervalSystem() { val expectedWorld = World { component(::SystemTestComponent) } val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame), world = expectedWorld) - assertAll( - { assertEquals(1, service.systems.size) }, - { assertNotNull(service.system()) }, - { assertSame(expectedWorld, service.system().world) } - ) + assertEquals(1, service.systems.size) + assertNotNull(service.system()) + assertSame(expectedWorld, service.system().world) } @Test - fun `create IteratingSystem with a ComponentMapper arg`() { + fun createIteratingSystemWithComponentMapper() { val expectedWorld = World { component(::SystemTestComponent) } @@ -247,15 +236,13 @@ internal class SystemTest { val service = systemService(mutableMapOf(SystemTestIteratingSystemMapper::class to ::SystemTestIteratingSystemMapper), world = expectedWorld) val actualSystem = service.system() - assertAll( - { assertEquals(1, service.systems.size) }, - { assertSame(expectedWorld, actualSystem.world) }, - { assertEquals(SystemTestComponent::class.simpleName, "SystemTestComponent") } - ) + assertEquals(1, service.systems.size) + assertSame(expectedWorld, actualSystem.world) + assertEquals(SystemTestComponent::class.simpleName, "SystemTestComponent") } @Test - fun `create IteratingSystem with an injectable arg`() { + fun createIteratingSystemWithAnInjectable() { val expectedWorld = World { component(::SystemTestComponent) } @@ -267,15 +254,13 @@ internal class SystemTest { ) val actualSystem = service.system() - assertAll( - { assertEquals(1, service.systems.size) }, - { assertSame(expectedWorld, actualSystem.world) }, - { assertEquals("42", actualSystem.injectable) }, - ) + assertEquals(1, service.systems.size) + assertSame(expectedWorld, actualSystem.world) + assertEquals("42", actualSystem.injectable) } @Test - fun `create IteratingSystem with qualified args`() { + fun createIteratingSystemWithQualifiedNames() { val expectedWorld = World { component(::SystemTestComponent) } @@ -287,21 +272,19 @@ internal class SystemTest { ) val actualSystem = service.system() - assertAll( - { assertEquals(1, service.systems.size) }, - { assertSame(expectedWorld, actualSystem.world) }, - { assertEquals("42", actualSystem.injectable) }, - { assertEquals("43", actualSystem.injectable2) }, - ) + assertEquals(1, service.systems.size) + assertSame(expectedWorld, actualSystem.world) + assertEquals("42", actualSystem.injectable) + assertEquals("43", actualSystem.injectable2) } @Test - fun `cannot create IteratingSystem with missing injectables`() { - assertThrows { systemService(mutableMapOf(SystemTestIteratingSystemInjectable::class to ::SystemTestIteratingSystemInjectable)) } + fun cannotCreateIteratingSystemWithMissingInjectables() { + assertFailsWith { systemService(mutableMapOf(SystemTestIteratingSystemInjectable::class to ::SystemTestIteratingSystemInjectable)) } } @Test - fun `IteratingSystem calls onTick and onAlpha for each entity of the system`() { + fun iteratingSystemCallsOnTickAndOnAlphaForEachEntityOfTheSystem() { val world = World { component(::SystemTestComponent) } @@ -313,15 +296,13 @@ internal class SystemTest { service.update() val system = service.system() - assertAll( - { assertEquals(2, system.numEntityCalls) }, - { assertEquals(2, system.numAlphaCalls) }, - { assertEquals(0.05f / 0.25f, system.lastAlpha, 0.0001f) } - ) + assertEquals(2, system.numEntityCalls) + assertEquals(2, system.numAlphaCalls) + assertEquals(0.05f / 0.25f, system.lastAlpha, 0.0001f) } @Test - fun `configure entity during iteration`() { + fun configureEntityDuringIteration() { val world = World { component(::SystemTestComponent) } @@ -337,7 +318,7 @@ internal class SystemTest { } @Test - fun `sort entities automatically`() { + fun sortEntitiesAutomatically() { val world = World { component(::SystemTestComponent) } @@ -352,7 +333,7 @@ internal class SystemTest { } @Test - fun `sort entities programmatically`() { + fun sortEntitiesProgrammatically() { val world = World { component(::SystemTestComponent) } @@ -365,26 +346,22 @@ internal class SystemTest { system.doSort = true service.update() - assertAll( - { assertEquals(expectedEntity, system.lastEntityProcess) }, - { assertFalse(system.doSort) } - ) + assertEquals(expectedEntity, system.lastEntityProcess) + assertFalse(system.doSort) } @Test - fun `cannot get a non-existing system`() { + fun cannotGetNonExistingSystem() { val world = World { component(::SystemTestComponent) } val service = systemService(mutableMapOf(SystemTestIteratingSystemSortAutomatic::class to ::SystemTestIteratingSystemSortAutomatic), world = world) - assertThrows { - service.system() - } + assertFailsWith { service.system() } } @Test - fun `update only calls enabled systems`() { + fun updateOnlyCallsEnabledSystems() { val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame)) val system = service.system() system.enabled = false @@ -395,7 +372,7 @@ internal class SystemTest { } @Test - fun `removing an entity during update is delayed`() { + fun removingAnEntityDuringUpdateIsDelayed() { val world = World { component(::SystemTestComponent) } @@ -415,7 +392,7 @@ internal class SystemTest { } @Test - fun `removing an entity during alpha is delayed`() { + fun removingAnEntityDuringAlphaIsDelayed() { val world = World { component(::SystemTestComponent) } @@ -437,7 +414,7 @@ internal class SystemTest { } @Test - fun `dispose service`() { + fun disposeService() { val service = systemService(mutableMapOf(SystemTestIntervalSystemEachFrame::class to ::SystemTestIntervalSystemEachFrame)) service.dispose() @@ -446,12 +423,12 @@ internal class SystemTest { } @Test - fun `init block of a system constructor has no access to the world`() { - assertThrows { systemService(mutableMapOf(SystemTestInitBlock::class to ::SystemTestInitBlock)) } + fun initBlockOfSystemConstructorHasNoAccessToTheWorld() { + assertFailsWith { systemService(mutableMapOf(SystemTestInitBlock::class to ::SystemTestInitBlock)) } } @Test - fun `onInit block is called for any newly created system`() { + fun onInitBlockIsCalledForAnyNewlyCreatedSystem() { val expected = 0f val service = systemService(mutableMapOf(SystemTestOnInitBlock::class to ::SystemTestOnInitBlock)) diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt index e69054866..f7af9b8db 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/WorldTest.kt @@ -1,12 +1,6 @@ package com.github.quillraven.fleks -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 +import kotlin.test.* private data class WorldTestComponent(var x: Float = 0f) @@ -55,24 +49,22 @@ private class WorldTestComponentListener : ComponentListener internal class WorldTest { @Test - fun `create empty world for 32 entities`() { + fun createEmptyWorldTor32Rntities() { val w = World { entityCapacity = 32 } - assertAll( - { assertEquals(0, w.numEntities) }, - { assertEquals(32, w.capacity) } - ) + assertEquals(0, w.numEntities) + assertEquals(32, w.capacity) } @Test - fun `create empty world with 1 no-args IntervalSystem`() { + fun createEmptyWorldWithIntervalSystem() { val w = World { system(::WorldTestIntervalSystem) } assertNotNull(w.system()) } @Test - fun `create empty world with 1 injectable args IteratingSystem`() { + fun createEmptyWorldWithOneInjectableIteratingSystem() { val w = World { system(::WorldTestIteratingSystem) component(::WorldTestComponent) @@ -80,14 +72,12 @@ internal class WorldTest { inject("42") } - assertAll( - { assertNotNull(w.system()) }, - { assertEquals("42", w.system().testInject) } - ) + assertNotNull(w.system()) + assertEquals("42", w.system().testInject) } @Test - fun `create empty world with 2 named injectables system`() { + fun createEmptyWorldWith2NamedInjectablesSystem() { val expectedName = "myName" val expectedLevel = "myLevel" val w = World { @@ -98,16 +88,14 @@ internal class WorldTest { inject("level", "myLevel") } - assertAll( - { assertNotNull(w.system()) }, - { assertEquals(expectedName, w.system().name) }, - { assertEquals(expectedLevel, w.system().level) } - ) + assertNotNull(w.system()) + assertEquals(expectedName, w.system().name) + assertEquals(expectedLevel, w.system().level) } @Test - fun `cannot add the same system twice`() { - assertThrows { + fun cannotAddTheSameSystemTwice() { + assertFailsWith { World { system(::WorldTestIntervalSystem) system(::WorldTestIntervalSystem) @@ -116,22 +104,22 @@ internal class WorldTest { } @Test - fun `cannot access a system that was not added`() { + fun cannotAccessSystemThatWasNotAdded() { val w = World {} - assertThrows { w.system() } + assertFailsWith { w.system() } } @Test - fun `cannot create a system when injectables are missing`() { - assertThrows { + fun cannotCreateSystemWhenInjectablesAreMissing() { + assertFailsWith { World { system(::WorldTestIteratingSystem) } } } @Test - fun `cannot inject the same type twice`() { - assertThrows { + fun cannotInjectTheSameTypeTwice() { + assertFailsWith { World { inject("42") inject("42") @@ -140,7 +128,7 @@ internal class WorldTest { } @Test - fun `create new entity`() { + fun createNewEntity() { val w = World { system(::WorldTestIteratingSystem) component(::WorldTestComponent) @@ -151,15 +139,13 @@ internal class WorldTest { add { x = 5f } } - assertAll( - { assertEquals(1, w.numEntities) }, - { assertEquals(0, e.id) }, - { assertEquals(5f, w.system().mapper[e].x) } - ) + assertEquals(1, w.numEntities) + assertEquals(0, e.id) + assertEquals(5f, w.system().mapper[e].x) } @Test - fun `remove existing entity`() { + fun removeExistingEntity() { val w = World {} val e = w.entity() @@ -169,7 +155,7 @@ internal class WorldTest { } @Test - fun `update world with deltaTime of 1`() { + fun updateWorldWithDeltaTimeOf1() { val w = World { system(::WorldTestIntervalSystem) system(::WorldTestIteratingSystem) @@ -181,15 +167,13 @@ internal class WorldTest { w.update(1f) - assertAll( - { assertEquals(1f, w.deltaTime) }, - { assertEquals(1, w.system().numCalls) }, - { assertEquals(0, w.system().numCalls) } - ) + assertEquals(1f, w.deltaTime) + assertEquals(1, w.system().numCalls) + assertEquals(0, w.system().numCalls) } @Test - fun `remove all entities`() { + fun removeAllEntities() { val w = World {} w.entity() w.entity() @@ -200,7 +184,7 @@ internal class WorldTest { } @Test - fun `dispose world`() { + fun disposeWorld() { val w = World { system(::WorldTestIntervalSystem) } @@ -209,14 +193,12 @@ internal class WorldTest { w.dispose() - assertAll( - { assertTrue(w.system().disposed) }, - { assertEquals(0, w.numEntities) } - ) + assertTrue(w.system().disposed) + assertEquals(0, w.numEntities) } @Test - fun `create world with ComponentListener`() { + fun createWorldWithComponentListener() { val w = World { component(::WorldTestComponent, ::WorldTestComponentListener) } @@ -225,8 +207,8 @@ internal class WorldTest { } @Test - fun `cannot add same Component twice`() { - assertThrows { + fun cannotAddSameComponentTtwice() { + assertFailsWith { World { component(::WorldTestComponent) component(::WorldTestComponent) @@ -235,7 +217,7 @@ internal class WorldTest { } @Test - fun `get mapper`() { + fun getMapper() { val w = World { component(::WorldTestComponent) } @@ -246,8 +228,8 @@ internal class WorldTest { } @Test - fun `throw exception when there are unused injectables`() { - assertThrows { + fun throwExceptionWhenThereAreUnusedInjectables() { + assertFailsWith { World { inject("42") } @@ -255,7 +237,7 @@ internal class WorldTest { } @Test - fun `iterate over all active entities`() { + fun iterateOverAllActiveEntities() { val w = World {} val e1 = w.entity() val e2 = w.entity() diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt index e60d18a2f..4638b11e7 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt @@ -1,280 +1,247 @@ package com.github.quillraven.fleks.collection -import org.junit.jupiter.api.Assertions.assertAll -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* internal class BagTest { - @Nested - inner class GenericBagTest { - @Test - fun `create empty bag of String of size 32`() { - val bag = bag(32) - - assertAll( - { assertEquals(32, bag.capacity) }, - { assertEquals(0, bag.size) } - ) - } - @Test - fun `add String to bag`() { - val bag = bag() + // GenericBag Test + @Test + fun createEmptyBagOfStringOfSize32() { + val bag = bag(32) - bag.add("42") + assertEquals(32, bag.capacity) + assertEquals(0, bag.size) + } - assertAll( - { assertEquals(1, bag.size) }, - { assertTrue("42" in bag) } - ) - } + @Test + fun addStringToBag() { + val bag = bag() - @Test - fun `remove existing String from bag`() { - val bag = bag() - bag.add("42") + bag.add("42") - val expected = bag.removeValue("42") + assertEquals(1, bag.size) + assertTrue("42" in bag) + } - assertAll( - { assertEquals(0, bag.size) }, - { assertFalse { "42" in bag } }, - { assertTrue(expected) } - ) - } + @Test + fun removeExistingStringFromBag() { + val bag = bag() + bag.add("42") - @Test - fun `remove non-existing String from bag`() { - val bag = bag() + val expected = bag.removeValue("42") - val expected = bag.removeValue("42") + assertEquals(0, bag.size) + assertFalse { "42" in bag } + assertTrue(expected) + } - assertFalse(expected) - } + @Test + fun removeNonExistingStringFromBag() { + val bag = bag() - @Test - fun `set String value at index with sufficient capacity`() { - val bag = bag() + val expected = bag.removeValue("42") - bag[3] = "42" + assertFalse(expected) + } - assertAll( - { assertEquals(4, bag.size) }, - { assertEquals("42", bag[3]) } - ) - } + @Test + fun setStringValueAtIndexWithSufficientCapacity() { + val bag = bag() - @Test - fun `set String value at index with insufficient capacity`() { - val bag = bag(2) + bag[3] = "42" - bag[2] = "42" + assertEquals(4, bag.size) + assertEquals("42", bag[3]) + } - assertAll( - { assertEquals(3, bag.size) }, - { assertEquals("42", bag[2]) }, - { assertEquals(3, bag.capacity) } - ) - } + @Test + fun setStringValueAtIndexWithInsufficientCapacity() { + val bag = bag(2) - @Test - fun `add String to bag with insufficient capacity`() { - val bag = bag(0) + bag[2] = "42" - bag.add("42") + assertEquals(3, bag.size) + assertEquals("42", bag[2]) + assertEquals(3, bag.capacity) + } - assertAll( - { assertEquals(1, bag.size) }, - { assertEquals("42", bag[0]) }, - { assertEquals(1, bag.capacity) } - ) - } + @Test + fun addStringToBagWithInsufficientCapacity() { + val bag = bag(0) - @Test - fun `cannot get String value of invalid in bounds index`() { - val bag = bag() + bag.add("42") - assertThrows { bag[0] } - } + assertEquals(1, bag.size) + assertEquals("42", bag[0]) + assertEquals(1, bag.capacity) + } - @Test - fun `cannot get String value of invalid out of bounds index`() { - val bag = bag(2) + @Test + fun cannotGetStringValueOfInvalidInBoundsIndex() { + val bag = bag() - assertThrows { bag[2] } - } + assertFailsWith { bag[0] } + } - @Test - fun `execute action for each value of a String bag`() { - val bag = bag(4) - bag[1] = "42" - bag[2] = "43" - var numCalls = 0 - val valuesCalled = mutableListOf() - - bag.forEach { - ++numCalls - valuesCalled.add(it) - } - - assertAll( - { assertEquals(2, numCalls) }, - { assertEquals(listOf("42", "43"), valuesCalled) } - ) - } + @Test + fun cannotGetStringValueOfInvalidOutOfBoundsIndex() { + val bag = bag(2) + + assertFailsWith { bag[2] } } - @Nested - inner class IntBagTest { - @Test - fun `create empty bag of size 32`() { - val bag = IntBag(32) + @Test + fun executeActionForEachValueOfStringBag() { + val bag = bag(4) + bag[1] = "42" + bag[2] = "43" + var numCalls = 0 + val valuesCalled = mutableListOf() - assertAll( - { assertEquals(32, bag.capacity) }, - { assertEquals(0, bag.size) } - ) + bag.forEach { + ++numCalls + valuesCalled.add(it) } - @Test - fun `add value to bag`() { - val bag = IntBag() + assertEquals(2, numCalls) + assertEquals(listOf("42", "43"), valuesCalled) + } - bag.add(42) + // IntBag Test + @Test + fun createEmptyBagOfSize32() { + val bag = IntBag(32) - assertAll( - { assertTrue(bag.isNotEmpty) }, - { assertEquals(1, bag.size) }, - { assertTrue(42 in bag) } - ) - } + assertEquals(32, bag.capacity) + assertEquals(0, bag.size) + } - @Test - fun `clear all values from bag`() { - val bag = IntBag() - bag.add(42) - bag.add(43) + @Test + fun addValueToBag() { + val bag = IntBag() - bag.clear() + bag.add(42) - assertAll( - { assertEquals(0, bag.size) }, - { assertFalse { 42 in bag } }, - { assertFalse { 43 in bag } } - ) - } + assertTrue(bag.isNotEmpty) + assertEquals(1, bag.size) + assertTrue(42 in bag) + } - @Test - fun `add value unsafe with sufficient capacity`() { - val bag = IntBag(1) + @Test + fun clearAllValuesFromBag() { + val bag = IntBag() + bag.add(42) + bag.add(43) - bag.unsafeAdd(42) + bag.clear() - assertTrue(42 in bag) - } + assertEquals(0, bag.size) + assertFalse { 42 in bag } + assertFalse { 43 in bag } + } - @Test - fun `add value unsafe with insufficient capacity`() { - val bag = IntBag(0) + @Test + fun addValueUnsafeWithSufficientCapacity() { + val bag = IntBag(1) - assertThrows { bag.unsafeAdd(42) } - } + bag.unsafeAdd(42) - @Test - fun `add value to bag with insufficient capacity`() { - val bag = IntBag(0) + assertTrue(42 in bag) + } - bag.add(42) + @Test + fun addValueUnsafeWithInsufficientCapacity() { + val bag = IntBag(0) - assertAll( - { assertEquals(1, bag.size) }, - { assertEquals(42, bag[0]) }, - { assertEquals(1, bag.capacity) } - ) - } + assertFailsWith { bag.unsafeAdd(42) } + } - @Test - fun `cannot get value of out of bounds index`() { - val bag = IntBag(2) + @Test + fun addValueToBagWithInsufficientCapacity() { + val bag = IntBag(0) - assertThrows { bag[2] } - } + bag.add(42) - @Test - fun `do not resize when bag has sufficient capacity`() { - val bag = IntBag(8) + assertEquals(1, bag.size) + assertEquals(42, bag[0]) + assertEquals(1, bag.capacity) + } - bag.ensureCapacity(7) + @Test + fun cannotGetValueOfOutOfBoundsIndex() { + val bag = IntBag(2) - assertEquals(8, bag.capacity) - } + assertFailsWith { bag[2] } + } - @Test - fun `resize when bag has insufficient capacity`() { - val bag = IntBag(8) + @Test + fun doNotResizeWhenBagHasSufficientCapacity() { + val bag = IntBag(8) - bag.ensureCapacity(9) + bag.ensureCapacity(7) - assertEquals(10, bag.capacity) - } + assertEquals(8, bag.capacity) + } + + @Test + fun resizeWhenBagHasInsufficientCapacity() { + val bag = IntBag(8) + + bag.ensureCapacity(9) + + assertEquals(10, bag.capacity) + } - @Test - fun `execute action for each value of the bag`() { - val bag = IntBag(4) - bag.add(42) - bag.add(43) - var numCalls = 0 - val valuesCalled = mutableListOf() - - bag.forEach { - ++numCalls - valuesCalled.add(it) - } - - assertAll( - { assertEquals(2, numCalls) }, - { assertEquals(listOf(42, 43), valuesCalled) } - ) + @Test + fun executeActionForEachValueOfTheBag() { + val bag = IntBag(4) + bag.add(42) + bag.add(43) + var numCalls = 0 + val valuesCalled = mutableListOf() + + bag.forEach { + ++numCalls + valuesCalled.add(it) } - @Test - fun `sort values by normal Int comparison with size less than 7`() { - val bag = IntBag() - repeat(6) { bag.add(6 - it) } + assertEquals(2, numCalls) + assertEquals(listOf(42, 43), valuesCalled) + } + + @Test + fun sortValuesByNormalIntComparisonWithSizeLessThan7() { + val bag = IntBag() + repeat(6) { bag.add(6 - it) } - bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) - repeat(6) { - assertEquals(it + 1, bag[it]) - } + repeat(6) { + assertEquals(it + 1, bag[it]) } + } - @Test - fun `sort values by normal Int comparison with size less than 50 but greater 7`() { - val bag = IntBag() - repeat(8) { bag.add(8 - it) } + @Test + fun sortValuesByNormalIntComparisonWithSizeLessThan50ButGreater7() { + val bag = IntBag() + repeat(8) { bag.add(8 - it) } - bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) - repeat(8) { - assertEquals(it + 1, bag[it]) - } + repeat(8) { + assertEquals(it + 1, bag[it]) } + } - @Test - fun `sort values by normal Int comparison with size greater 50`() { - val bag = IntBag() - repeat(51) { bag.add(51 - it) } + @Test + fun sortValuesByNormalIntComparisonWithSizeGreater50() { + val bag = IntBag() + repeat(51) { bag.add(51 - it) } - bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) + bag.sort(compareEntity { e1, e2 -> e1.id.compareTo(e2.id) }) - repeat(51) { - assertEquals(it + 1, bag[it]) - } + repeat(51) { + assertEquals(it + 1, bag[it]) } } } diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt index e9525c86c..3f2f7200d 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt @@ -1,57 +1,47 @@ package com.github.quillraven.fleks.collection -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertAll -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* internal class BitArrayTest { @Test - fun `create empty BitArray`() { + fun createEmptyBitArray() { val bits = BitArray(0) - assertAll( - { assertEquals(0, bits.length()) }, - { assertEquals(0, bits.capacity) } - ) + assertEquals(0, bits.length()) + assertEquals(0, bits.capacity) } @Test - fun `set bit at index 3 with sufficient capacity`() { + fun setBitAtIndex3WithSufficientCapacity() { val bits = BitArray(3) bits.set(2) - assertAll( - { assertEquals(3, bits.length()) }, - { assertEquals(64, bits.capacity) }, - { assertTrue { bits[2] } } - ) + assertEquals(3, bits.length()) + assertEquals(64, bits.capacity) + assertTrue { bits[2] } } @Test - fun `set bit at index 3 with insufficient capacity`() { + fun setBitAtIndex3WithInsufficientCapacity() { val bits = BitArray(0) bits.set(2) - assertAll( - { assertEquals(3, bits.length()) }, - { assertEquals(64, bits.capacity) }, - { assertTrue { bits[2] } } - ) + assertEquals(3, bits.length()) + assertEquals(64, bits.capacity) + assertTrue { bits[2] } } @Test - fun `get bit of out of bounds index`() { + fun getBitOfOutOfBoundsIndex() { val bits = BitArray(0) assertFalse(bits[64]) } @Test - fun `clear all set bits`() { + fun clearAllSetBits() { val bits = BitArray() bits.set(2) bits.set(4) @@ -62,7 +52,7 @@ internal class BitArrayTest { } @Test - fun `clear specific bit`() { + fun clearSpecificBit() { val bits = BitArray() bits.set(2) @@ -72,7 +62,7 @@ internal class BitArrayTest { } @Test - fun `two BitArrays intersect when they have at least one bit set at the same index`() { + fun twoBitArraysIntersectWhenTheyHaveAtLeastOneBitSetAtTheSameIndex() { val bitsA = BitArray(256) val bitsB = BitArray(1) bitsA.set(2) @@ -83,14 +73,12 @@ internal class BitArrayTest { val actualA = bitsA.intersects(bitsB) val actualB = bitsB.intersects(bitsA) - assertAll( - { assertTrue(actualA) }, - { assertTrue(actualB) } - ) + assertTrue(actualA) + assertTrue(actualB) } @Test - fun `two BitArrays do not intersect when they do not have at least one bit set at the same index`() { + fun twoBitArraysDoNotIntersectWhenTheyDoNotHaveAtLeastOneBitSetAtTheSameIndex() { val bitsA = BitArray(256) val bitsB = BitArray(1) bitsA.set(2) @@ -101,14 +89,12 @@ internal class BitArrayTest { val actualA = bitsA.intersects(bitsB) val actualB = bitsB.intersects(bitsA) - assertAll( - { assertFalse(actualA) }, - { assertFalse(actualB) } - ) + assertFalse(actualA) + assertFalse(actualB) } @Test - fun `BitArray contains BitArray if the same bits are set`() { + fun bitArrayContainsBitArrayIfTheSameBitsAreSet() { val bitsA = BitArray(256) val bitsB = BitArray(1) bitsA.set(2) @@ -119,14 +105,12 @@ internal class BitArrayTest { val actualA = bitsA.contains(bitsB) val actualB = bitsB.contains(bitsA) - assertAll( - { assertTrue(actualA) }, - { assertTrue(actualB) } - ) + assertTrue(actualA) + assertTrue(actualB) } @Test - fun `BitArray does not contain BitArray if different bits are set`() { + fun bitArrayDoesNotContainBitArrayIfDifferentBitsAreSet() { val bitsA = BitArray(256) val bitsB = BitArray(1) bitsA.set(2) @@ -137,14 +121,12 @@ internal class BitArrayTest { val actualA = bitsA.contains(bitsB) val actualB = bitsB.contains(bitsA) - assertAll( - { assertFalse(actualA) }, - { assertFalse(actualB) } - ) + assertFalse(actualA) + assertFalse(actualB) } @Test - fun `run action for each set bit`() { + fun runActionForEachSetBit() { val bits = BitArray(128) bits.set(3) bits.set(5) @@ -157,9 +139,7 @@ internal class BitArrayTest { bitsCalled.add(it) } - assertAll( - { assertEquals(3, numCalls) }, - { assertEquals(listOf(117, 5, 3), bitsCalled) } - ) + assertEquals(3, numCalls) + assertEquals(listOf(117, 5, 3), bitsCalled) } } From 60fe985db5136cdd52b20a749fde22fb6678e30d Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 11 Mar 2022 15:05:52 +0100 Subject: [PATCH 20/27] Fix exceptions in unit tests --- korge-fleks/build.gradle.kts | 6 +++--- .../kotlin/com/github/quillraven/fleks/ComponentTest.kt | 3 ++- .../com/github/quillraven/fleks/collection/BagTest.kt | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/korge-fleks/build.gradle.kts b/korge-fleks/build.gradle.kts index f037e91aa..3dc0f250b 100644 --- a/korge-fleks/build.gradle.kts +++ b/korge-fleks/build.gradle.kts @@ -1,5 +1,5 @@ description = "Multiplatform Game Engine written in Kotlin" -//dependencies { -// add("commonMainApi", project(":korge")) -//} +dependencies { + add("commonMainApi", project(":korio")) +} diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt index c9e59e83b..09b18e7f4 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/ComponentTest.kt @@ -1,5 +1,6 @@ package com.github.quillraven.fleks +import com.soywiz.korio.async.* import kotlin.test.* private data class ComponentTestComponent(var x: Int = 0) @@ -101,7 +102,7 @@ internal class ComponentTest { } @Test - fun cannotRemoveNonExistingEntityFromMapperWithInsufficientCapacity() { + fun cannotRemoveNonExistingEntityFromMapperWithInsufficientCapacity() = suspendTestNoJs { val cmpService = ComponentService(componentFactory) val mapper = cmpService.mapper() val entity = Entity(10_000) diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt index 4638b11e7..41669b8dc 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BagTest.kt @@ -1,5 +1,6 @@ package com.github.quillraven.fleks.collection +import com.soywiz.korio.async.* import kotlin.test.* internal class BagTest { @@ -84,7 +85,7 @@ internal class BagTest { } @Test - fun cannotGetStringValueOfInvalidOutOfBoundsIndex() { + fun cannotGetStringValueOfInvalidOutOfBoundsIndex() = suspendTestNoJs { val bag = bag(2) assertFailsWith { bag[2] } @@ -150,7 +151,7 @@ internal class BagTest { } @Test - fun addValueUnsafeWithInsufficientCapacity() { + fun addValueUnsafeWithInsufficientCapacity() = suspendTestNoJs { val bag = IntBag(0) assertFailsWith { bag.unsafeAdd(42) } @@ -168,7 +169,7 @@ internal class BagTest { } @Test - fun cannotGetValueOfOutOfBoundsIndex() { + fun cannotGetValueOfOutOfBoundsIndex() = suspendTestNoJs { val bag = IntBag(2) assertFailsWith { bag[2] } From f84c7ea0a00eb30c7120cd2d411a1991f02281a2 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Fri, 11 Mar 2022 17:46:40 +0100 Subject: [PATCH 21/27] Add missing android manifest file --- korge-fleks/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 korge-fleks/src/main/AndroidManifest.xml diff --git a/korge-fleks/src/main/AndroidManifest.xml b/korge-fleks/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4055588f0 --- /dev/null +++ b/korge-fleks/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + From e2d147474dcef50f8742e4f30503752a73379a4d Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Sun, 13 Mar 2022 18:36:55 +0100 Subject: [PATCH 22/27] Fix excepton with injection on Kotlin native --- .../src/commonMain/kotlin/com/github/quillraven/fleks/system.kt | 2 ++ samples/fleks-ecs/src/commonMain/kotlin/main.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt index 5a2fcae91..cd2739b48 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/system.kt @@ -2,6 +2,7 @@ package com.github.quillraven.fleks import com.github.quillraven.fleks.collection.BitArray import com.github.quillraven.fleks.collection.EntityComparator +import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass /** @@ -368,6 +369,7 @@ class SystemService( * for the given type in its internal map. * @throws [FleksInjectableTypeHasNoName] if the dependency type has no T::class.simpleName. */ +@ThreadLocal object Inject { @PublishedApi internal lateinit var injectObjects: Map diff --git a/samples/fleks-ecs/src/commonMain/kotlin/main.kt b/samples/fleks-ecs/src/commonMain/kotlin/main.kt index 18943c00c..872414d3c 100644 --- a/samples/fleks-ecs/src/commonMain/kotlin/main.kt +++ b/samples/fleks-ecs/src/commonMain/kotlin/main.kt @@ -72,7 +72,7 @@ class ExampleScene : Scene() { // Register external objects which are used by systems and component listeners inject(assets) // Assets are used by the SpriteSystem / SpriteListener to get the image data for drawing - inject("layer0", layer0) // Currently we use only one layer to draw all objects to - this is also used in SpriteListener to add the image to the layer container + inject("layer0", layer0) // Currently, we use only one layer to draw all objects to - this is also used in SpriteListener to add the image to the layer container // inject("layer1", layer1) // Add more layers when needed e.g. for explosion objects to be on top, etc. } From 19859d912a4e79d8c7ed7d4747ae91ea9fc88963 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 14 Mar 2022 11:32:39 +0100 Subject: [PATCH 23/27] Fix BitArray equal function and add test cases The equal function of BitArray was unsafe regarding comparing objects of different type. Added unit tests to avoid regression. --- .../quillraven/fleks/collection/bitArray.kt | 6 ++- .../com/github/quillraven/fleks/exception.kt | 3 ++ .../fleks/collection/BitArrayTest.kt | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index 13303d4cd..cabc1c1c3 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -1,5 +1,6 @@ package com.github.quillraven.fleks.collection +import com.github.quillraven.fleks.FleksBitArrayTypeException import kotlin.math.min /** @@ -140,10 +141,11 @@ class BitArray( } override fun equals(other: Any?): Boolean { - if (other == null) return false if (this === other) return true + if (other !is BitArray) throw FleksBitArrayTypeException( + if (other != null) other::class.toString() else "null" + ) - other as BitArray val otherBits = other.bits val commonWords: Int = min(bits.size, otherBits.size) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 268ae037a..5387106e5 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -37,3 +37,6 @@ class FleksNoSuchEntityComponentException(entity: Entity, component: String) : class FleksUnusedInjectablesException(unused: List>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") + +class FleksBitArrayTypeException(type: String) : + FleksException("Cannot compare two BitArrays if the other is of type '$type'.") diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt index 3f2f7200d..a3bb1acdf 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt @@ -1,5 +1,6 @@ package com.github.quillraven.fleks.collection +import com.github.quillraven.fleks.FleksBitArrayTypeException import kotlin.test.* internal class BitArrayTest { @@ -142,4 +143,40 @@ internal class BitArrayTest { assertEquals(3, numCalls) assertEquals(listOf(117, 5, 3), bitsCalled) } + + @Test + fun bitArrayEqualsToIncompatibleTypeObject() { + val bits = BitArray(42) + bits.set(3) + val otherBits = 42 + + assertFailsWith { val result = bits.equals(otherBits) } + assertFailsWith { val result = bits.equals(null) } + } + + @Test + fun bitArrayEqualsToSameObject() { + val bits = BitArray(42) + bits.set(3) + + assertEquals(true, bits == bits) + } + + @Test + fun bitArrayEqualsToOtherBitArray() { + val bits = BitArray(42) + bits.set(4) + val otherBits = BitArray(44) + otherBits.set(4) + val bits42 = BitArray(42) + bits42.set(4) + + assertEquals(true, bits == otherBits, "bitArray equals to another bitArray with different size") + assertEquals(true, bits == bits42, "bitArray equals to another bitArray with equal size") + + otherBits.set(7) + bits42.set(7) + assertEquals(false, bits == otherBits, "bitArray equals not to another bitArray with different size") + assertEquals(false, bits == bits42, "bitArray equals not to another bitArray with equal size") + } } From 353b9017ab75904a3f6e9fef2939f8a1e7167647 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 14 Mar 2022 13:46:08 +0100 Subject: [PATCH 24/27] Add support of fleks into korge-gradle-plugin --- .../src/main/kotlin/com/soywiz/korge/gradle/KorgeExtension.kt | 4 ++++ settings.gradle.kts | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/korge-gradle-plugin/src/main/kotlin/com/soywiz/korge/gradle/KorgeExtension.kt b/korge-gradle-plugin/src/main/kotlin/com/soywiz/korge/gradle/KorgeExtension.kt index 1f8a40620..37cc7754b 100644 --- a/korge-gradle-plugin/src/main/kotlin/com/soywiz/korge/gradle/KorgeExtension.kt +++ b/korge-gradle-plugin/src/main/kotlin/com/soywiz/korge/gradle/KorgeExtension.kt @@ -421,6 +421,10 @@ class KorgeExtension(val project: Project) { dependencyMulti("com.soywiz.korlibs.korge2:korge-spine:${BuildVersions.KORGE}", registerPlugin = false) } + fun supportFleks() { + dependencyMulti("com.soywiz.korlibs.korge2:korge-fleks:${BuildVersions.KORGE}", registerPlugin = false) + } + fun supportBox2d() { // https://awesome.korge.org/ //bundle("https://github.com/korlibs/korge-bundles.git::korge-box2d::7439e5c7de7442f2cd33a1944846d44aea31af0a##9fd9d54abd8abc4736fd3439f0904141d9b6a26e9e2f1e1f8e2ed10c51f490fd") diff --git a/settings.gradle.kts b/settings.gradle.kts index d50f73ef2..09ea080ef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,7 +45,6 @@ include(":korau") include(":korgw") include(":korvi") include(":korge") -include(":korge-fleks") if (System.getenv("DISABLED_EXTRA_KORGE_LIBS") != "true") { include(":luak") @@ -55,12 +54,12 @@ if (System.getenv("DISABLED_EXTRA_KORGE_LIBS") != "true") { include(":korge-swf") include(":korge-box2d") include(":korge-gradle-plugin") + include(":korge-fleks") } //include(":tensork") //include(":samples:parallax-scrolling-aseprite") //include(":samples:tiled-background") -//include(":samples:ase-animations") include(":samples:fleks-ecs") if (!inCI) { From a099ca9ed4b5625dcfffc3d2ef4357ab5fa4af3d Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 14 Mar 2022 16:37:31 +0100 Subject: [PATCH 25/27] Fix bitArray equals to not throw exception --- .../kotlin/com/github/quillraven/fleks/collection/bitArray.kt | 4 +--- .../com/github/quillraven/fleks/collection/BitArrayTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index cabc1c1c3..5c1af2de8 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -142,9 +142,7 @@ class BitArray( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is BitArray) throw FleksBitArrayTypeException( - if (other != null) other::class.toString() else "null" - ) + if (other !is BitArray) return false val otherBits = other.bits diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt index a3bb1acdf..2c035d47a 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt @@ -150,8 +150,8 @@ internal class BitArrayTest { bits.set(3) val otherBits = 42 - assertFailsWith { val result = bits.equals(otherBits) } - assertFailsWith { val result = bits.equals(null) } + assertEquals(false, bits.equals(otherBits)) + assertEquals(false, bits.equals(null)) } @Test From f6c134966394cf1f8f39d0716b75b29e7f719094 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 14 Mar 2022 16:39:01 +0100 Subject: [PATCH 26/27] Remove unused exception in Fleks --- .../commonMain/kotlin/com/github/quillraven/fleks/exception.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt index 5387106e5..268ae037a 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/exception.kt @@ -37,6 +37,3 @@ class FleksNoSuchEntityComponentException(entity: Entity, component: String) : class FleksUnusedInjectablesException(unused: List>) : FleksException("There are unused injectables of following types: ${unused.map { it.simpleName }}") - -class FleksBitArrayTypeException(type: String) : - FleksException("Cannot compare two BitArrays if the other is of type '$type'.") From 1aaa952aa025033d092f6a566a2dda2a5b53fb21 Mon Sep 17 00:00:00 2001 From: Marko Koschak Date: Mon, 14 Mar 2022 17:39:56 +0100 Subject: [PATCH 27/27] Remove exception imports to fix build --- .../kotlin/com/github/quillraven/fleks/collection/bitArray.kt | 1 - .../com/github/quillraven/fleks/collection/BitArrayTest.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt index 5c1af2de8..4285f0e31 100644 --- a/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt +++ b/korge-fleks/src/commonMain/kotlin/com/github/quillraven/fleks/collection/bitArray.kt @@ -1,6 +1,5 @@ package com.github.quillraven.fleks.collection -import com.github.quillraven.fleks.FleksBitArrayTypeException import kotlin.math.min /** diff --git a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt index 2c035d47a..6c542caed 100644 --- a/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt +++ b/korge-fleks/src/commonTest/kotlin/com/github/quillraven/fleks/collection/BitArrayTest.kt @@ -1,6 +1,5 @@ package com.github.quillraven.fleks.collection -import com.github.quillraven.fleks.FleksBitArrayTypeException import kotlin.test.* internal class BitArrayTest {