commonMain.com.github.quillraven.fleks.world.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Fleks-jvm Show documentation
Show all versions of Fleks-jvm Show documentation
A lightweight entity component system written in Kotlin.
The newest version!
package com.github.quillraven.fleks
import com.github.quillraven.fleks.collection.EntityBag
import com.github.quillraven.fleks.collection.MutableEntityBag
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlin.native.concurrent.ThreadLocal
import kotlin.time.Duration
/**
* Snapshot for an [entity][Entity] that contains its [components][Component] and [tags][EntityTag].
*/
@Serializable
data class Snapshot(
val components: List>,
val tags: List>,
)
/**
* Utility function to manually create a [Snapshot].
*/
@Suppress("UNCHECKED_CAST")
fun wildcardSnapshotOf(components: List>, tags: List>): Snapshot {
return Snapshot(components as List>, tags as List>)
}
/**
* A world to handle [entities][Entity] and [systems][IntervalSystem].
*
* @param entityCapacity the initial maximum capacity of entities.
*/
class World internal constructor(
entityCapacity: Int,
) : EntityComponentContext(ComponentService()) {
@PublishedApi
internal val injectables = mutableMapOf()
/**
* 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 entityService = EntityService(this, entityCapacity)
/**
* List of all [families][Family] of the world that are created either via
* an [IteratingSystem] or via the world's [family] function to
* avoid creating duplicates.
*/
@PublishedApi
internal var allFamilies = emptyArray()
/**
* 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
// internal mutable list of systems
// can be replaced in a later version of Kotlin with "backing field" syntax
internal val mutableSystems = arrayListOf()
/**
* Returns the world's systems.
*/
val systems: List
get() = mutableSystems
/**
* Map of add [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyAddHooks = mutableMapOf()
/**
* Map of remove [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyRemoveHooks = mutableMapOf()
/**
* Cache of used [EntityTag] instances. Needed for snapshot functionality.
*/
@PublishedApi
internal val tagCache = mutableMapOf>()
init {
/**
* Maybe because of design flaws, the world reference of the ComponentService must be
* set in the world's constructor because the parent class (=EntityComponentContext) already
* requires a ComponentService, and it is not possible to pass "this" reference directly.
*
* That's why it is happening here to set it as soon as possible.
*/
componentService.world = this
}
/**
* Returns an already registered injectable of the given [name] and marks it as used.
*
* @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
*/
inline fun inject(name: String = T::class.simpleName ?: T::class.toString()): T {
val injectable = injectables[name] ?: throw FleksNoSuchInjectableException(name)
injectable.used = true
return injectable.injObj as T
}
/**
* Returns a new map of unused [injectables][Injectable]. An injectable gets set to 'used'
* when it gets injected at least once via a call to [inject].
*/
fun unusedInjectables(): Map =
injectables.filterValues { !it.used }.mapValues { it.value.injObj }
/**
* Returns a new [EntityBag] instance containing all [entities][Entity] of the world.
*
* Do not call this operation each frame, as it can be expensive depending on the amount
* of entities in your world.
*
* For frequent entity operations on specific entities, use [families][Family].
*/
fun asEntityBag(): EntityBag {
val result = MutableEntityBag(numEntities)
entityService.forEach {
result += it
}
return result
}
/**
* Adds a new [entity][Entity] to the world using the given [configuration][EntityCreateContext].
*
* **Attention** Make sure that you only modify the entity of the current scope.
* Otherwise, you will get wrong behavior for families. E.g. don't do this:
*
* ```
* entity {
* // modifying the current entity is allowed ✅
* it += Position()
* // don't modify other entities ❌
* someOtherEntity += Position()
* }
* ```
*/
inline fun entity(configuration: EntityCreateContext.(Entity) -> Unit = {}): Entity {
return entityService.create(configuration)
}
/**
* Returns true if and only if the [entity] is not removed and is part of the [World].
*/
operator fun contains(entity: Entity) = entityService.contains(entity)
/**
* Removes the given [entity] from the world. The [entity] will be recycled and reused for
* future calls to [World.entity].
*/
operator fun minusAssign(entity: Entity) {
entityService -= entity
}
/**
* Removes all [entities][Entity] from the world. The entities will be recycled and reused for
* future calls to [World.entity].
* If [clearRecycled] is true then the recycled entities are cleared and the ids for newly
* created entities start at 0 again.
*/
fun removeAll(clearRecycled: Boolean = false) {
entityService.removeAll(clearRecycled)
}
/**
* Performs the given [action] on each active [entity][Entity].
*/
fun forEach(action: World.(Entity) -> Unit) {
entityService.forEach(action)
}
/**
* 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)
}
/**
* Returns true if and only if the given [system][IntervalSystem] is part of the world.
*/
inline fun contains(): Boolean {
return systems.any { it is T }
}
/**
* Returns the specified [system][IntervalSystem] or null if there is no such system.
*/
inline fun systemOrNull(): T? {
return systems.firstOrNull { it is T } as T?
}
/**
* Adds the [system] to the world's [systems] at the given [index].
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
fun add(index: Int, system: IntervalSystem) {
if (systems.any { it::class == system::class }) {
throw FleksSystemAlreadyAddedException(system::class)
}
mutableSystems.add(index, system)
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
system.onInit()
}
/**
* Adds the [system] to the world's [systems].
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
fun add(system: IntervalSystem) = add(systems.size, system)
/**
* Adds the [system] to the world's [systems].
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
operator fun plusAssign(system: IntervalSystem) = add(system)
/**
* Removes the [system] of the world's [systems].
*/
fun remove(system: IntervalSystem) {
mutableSystems.remove(system)
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
system.onDispose()
}
/**
* Removes the [system] of the world's [systems].
*/
operator fun minusAssign(system: IntervalSystem) = remove(system)
/**
* Sets the [hook] as an [EntityService.addHook].
*
* @throws FleksHookAlreadyAddedException if the [EntityService] already has an add hook set.
*/
@PublishedApi
internal fun setEntityAddHook(hook: EntityHook) {
if (entityService.addHook != null) {
throw FleksHookAlreadyAddedException("addHook", "Entity")
}
entityService.addHook = hook
}
/**
* Sets the [hook] as an [EntityService.removeHook].
*
* @throws FleksHookAlreadyAddedException if the [EntityService] already has a remove hook set.
*/
@PublishedApi
internal fun setEntityRemoveHook(hook: EntityHook) {
if (entityService.removeHook != null) {
throw FleksHookAlreadyAddedException("removeHook", "Entity")
}
entityService.removeHook = hook
}
/**
* Creates a new [Family] for the given [cfg][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
*/
fun family(cfg: FamilyDefinition.() -> Unit): Family = family(FamilyDefinition().apply(cfg))
/**
* Creates a new [Family] for the given [definition][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
*/
@PublishedApi
internal fun family(definition: FamilyDefinition): Family {
if (definition.isEmpty()) {
throw FleksFamilyException(definition)
}
val (defAll, defNone, defAny) = definition
var family = allFamilies.find { it.allOf == defAll && it.noneOf == defNone && it.anyOf == defAny }
if (family == null) {
family = Family(defAll, defNone, defAny, this)
allFamilies += family
// initialize a newly created family by notifying it for any already existing entity
// world.allFamilies.forEach { it.onEntityCfgChanged(entity, compMask) }
entityService.forEach { family.onEntityCfgChanged(it, entityService.compMasks[it.id]) }
}
return family
}
/**
* Returns a map that contains all [entities][Entity] and their components of this world.
* The keys of the map are the entities.
* The values are a list of components that a specific entity has. If the entity
* does not have any components then the value is an empty list.
*/
fun snapshot(): Map {
val result = mutableMapOf()
entityService.forEach { result[it] = snapshotOf(it) }
return result
}
/**
* Returns a list that contains all components of the given [entity] of this world.
* If the entity does not have any components then an empty list is returned.
*/
@Suppress("UNCHECKED_CAST")
fun snapshotOf(entity: Entity): Snapshot {
val comps = mutableListOf>()
val tags = mutableListOf>()
if (entity in entityService) {
entityService.compMasks[entity.id].forEachSetBit { cmpId ->
val holder = componentService.holderByIndexOrNull(cmpId)
if (holder == null) {
// tag instead of component
tags += tagCache[cmpId] ?: throw FleksSnapshotException("Tag with id $cmpId was never assigned")
} else {
comps += holder[entity]
}
}
}
return Snapshot(comps as List>, tags as List>)
}
/**
* Loads the given [snapshot] of the world. This will first clear any existing
* entity of the world. After that it will load all provided entities and components.
* This will also execute [FamilyHook].
*
* @throws FleksSnapshotException if a family iteration is currently in process.
*/
fun loadSnapshot(snapshot: Map) {
if (entityService.delayRemoval) {
throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
}
// remove any existing entity and clean up recycled ids
removeAll(true)
if (snapshot.isEmpty()) {
// snapshot is empty -> nothing to load
return
}
val versionLookup = snapshot.keys.associateBy { it.id }
// Set next entity id to the maximum provided id + 1.
// All ids before that will be either created or added to the recycled
// ids to guarantee that the provided snapshot entity ids match the newly created ones.
with(entityService) {
val maxId = snapshot.keys.maxOf { it.id }
repeat(maxId + 1) {
val entity = Entity(it, version = (versionLookup[it]?.version ?: 0u) - 1u)
this.recycle(entity)
val entitySnapshot = snapshot[versionLookup[it]]
if (entitySnapshot != null) {
// snapshot for entity is provided -> create it
// note that the id for the entity will be the recycled id from above
this.configure(this.create { }, entitySnapshot)
}
}
}
}
/**
* Loads the given [entity] and its [snapshot][Snapshot].
* If the entity does not exist yet, it will be created.
* If the entity already exists it will be updated with the given components.
*
* @throws FleksSnapshotException if a family iteration is currently in process.
*/
fun loadSnapshotOf(entity: Entity, snapshot: Snapshot) {
if (entityService.delayRemoval) {
throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
}
if (entity !in entityService) {
// entity not part of service yet -> create it
entityService.create(entity.id) { }
}
// load components for entity
entityService.configure(entity, snapshot)
}
/**
* Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world
* using the given [deltaTime] in seconds.
*/
fun update(deltaTime: Float) {
this.deltaTime = deltaTime
for (i in systems.indices) {
val system = systems[i]
if (system.enabled) {
system.onUpdate()
}
}
}
/**
* Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world
* using the given [duration]. The duration is converted to seconds.
*/
fun update(duration: Duration) {
update(duration.inWholeNanoseconds * 0.000000001f)
}
/**
* Removes all [entities][Entity] of the world and calls the
* [onDispose][IntervalSystem.onDispose] function of each system.
*/
fun dispose() {
entityService.removeAll()
systems.forEachReverse { it.onDispose() }
}
/**
* Extend [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove].
*/
internal fun initAggregatedFamilyHooks() {
// validate systems against illegal interfaces
systems.forEach { system ->
// FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem
if (system !is IteratingSystem) {
if (system is FamilyOnAdd) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnAdd::class)
}
if (system is FamilyOnRemove) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnRemove::class)
}
}
}
// add all the configured family hooks to the cache
allFamilies.forEach {
worldCfgFamilyAddHooks[it] = it.addHook
worldCfgFamilyRemoveHooks[it] = it.removeHook
}
allFamilies.forEach { updateAggregatedFamilyHooks(it) }
}
/**
* Update [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove]
* and iterate over the given [family].
*/
private fun updateAggregatedFamilyHooks(family: Family) {
// system validation like in initAggregatedFamilyHooks is not necessary
// because it is already validated before (in initAggregatedFamilyHooks and in add/remove system)
// update family add hook by adding systems' onAddEntity calls after its original world cfg hook
val addSystems = systems.filter { it is IteratingSystem && it is FamilyOnAdd && it.family == family }
val ownAddHook = worldCfgFamilyAddHooks[family]
family.addHook = if (ownAddHook != null) { entity ->
ownAddHook(this, entity)
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
} else { entity ->
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
}
// update family remove hook by adding systems' onRemoveEntity calls before its original world cfg hook
val removeSystems = systems.filter { it is IteratingSystem && it is FamilyOnRemove && it.family == family }
val ownRemoveHook = worldCfgFamilyRemoveHooks[family]
family.removeHook = if (ownRemoveHook != null) { entity ->
removeSystems.forEachReverse { (it as FamilyOnRemove).onRemoveEntity(entity) }
ownRemoveHook(this, entity)
} else { entity ->
removeSystems.forEachReverse { (it as FamilyOnRemove).onRemoveEntity(entity) }
}
}
@ThreadLocal
companion object {
@PublishedApi
internal var CURRENT_WORLD: World? = null
/**
* Returns an already registered injectable of the given [name] and marks it as used.
*
* @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
* @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
*/
inline fun inject(name: String = T::class.simpleName ?: T::class.toString()): T =
CURRENT_WORLD?.inject(name) ?: throw FleksWrongConfigurationUsageException()
/**
* Creates a new [Family] for the given [cfg][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
* @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
*/
fun family(cfg: FamilyDefinition.() -> Unit): Family =
CURRENT_WORLD?.family(cfg) ?: throw FleksWrongConfigurationUsageException()
}
}
private inline fun List.forEachReverse(action: (T) -> Unit) {
val lastIndex = this.lastIndex
for (i in lastIndex downTo 0) {
action(this[i])
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy