All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.github.quillraven.fleks.entity.kt Maven / Gradle / Ivy

The newest version!
package com.github.quillraven.fleks

import com.github.quillraven.fleks.collection.*
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmName

/**
 * An entity of a [world][World]. It represents a unique identifier that is the combination
 * of an index (=[id]) and a [version].
 *
 * It is possible to have two entities with the same [id] but different [version] but only
 * one of these entities is part of the [world][World] at any given time.
 */
@Serializable
data class Entity(val id: Int, val version: UInt) {
    companion object {
        val NONE = Entity(-1, 0u)
    }
}

/**
 * Type alias for an optional hook function for an [EntityService].
 * Such a function runs within a [World] and takes the [Entity] as an argument.
 */
typealias EntityHook = World.(Entity) -> Unit

/**
 * A class for basic [Entity] extension functions within a [Family],
 * [IntervalSystem], [World] or [compareEntity].
 */
abstract class EntityComponentContext(
    @PublishedApi
    internal val componentService: ComponentService
) {
    /**
     * Returns a [component][Component] of the given [type] for the [entity][Entity].
     *
     * @throws [FleksNoSuchEntityComponentException] if the [entity][Entity] does not have such a component.
     */
    inline operator fun > Entity.get(type: ComponentType): T =
        componentService.holder(type)[this]

    /**
     * Returns a [component][Component] of the given [type] for the [entity][Entity]
     * or null if the [entity][Entity] does not have such a [component][Component].
     */
    inline fun > Entity.getOrNull(type: ComponentType): T? =
        componentService.holder(type).getOrNull(this)

    /**
     * Returns true if and only if the [entity][Entity] has a [component][Component] or [tag][EntityTag] of the given [type].
     */
    operator fun Entity.contains(type: UniqueId<*>): Boolean =
        componentService.world.entityService.compMasks.getOrNull(this.id)?.get(type.id) ?: false

    /**
     * Returns true if and only if the [entity][Entity] has a [component][Component] or [tag][EntityTag] of the given [type].
     */
    infix fun Entity.has(type: UniqueId<*>): Boolean =
        componentService.world.entityService.compMasks.getOrNull(this.id)?.get(type.id) ?: false

    /**
     * Returns true if and only if the [entity][Entity] doesn't have a [component][Component] or [tag][EntityTag] of the given [type].
     */
    infix fun Entity.hasNo(type: UniqueId<*>): Boolean =
        componentService.world.entityService.compMasks.getOrNull(this.id)?.get(type.id)?.not() ?: true

    /**
     * Updates the [entity][Entity] using the given [configuration] to add and remove [components][Component].
     *
     * **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.configure {
     *     // modifying the current entity is allowed ✅
     *     it += Position()
     *     // don't modify other entities ❌
     *     someOtherEntity += Position()
     * }
     * ```
     */
    inline fun Entity.configure(configuration: EntityUpdateContext.(Entity) -> Unit) =
        componentService.world.entityService.configure(this, configuration)

    /**
     * Removes the [entity][Entity] from the world. The [entity][Entity] will be recycled and reused for
     * future calls to [World.entity].
     */
    fun Entity.remove() = componentService.world.minusAssign(this)

    /**
     * Returns true, if and only if an [entity][Entity] will be removed at the end of the current [IteratingSystem].
     * This is the case, if it gets [removed][remove] during the system's iteration.
     */
    fun Entity.isMarkedForRemoval() = this in componentService.world.entityService.delayedEntities
}

/**
 * A class that extends the extension functionality of an [EntityComponentContext] by also providing
 * the possibility to create [components][Component].
 */
open class EntityCreateContext(
    compService: ComponentService,
    @PublishedApi
    internal val compMasks: Bag,
) : EntityComponentContext(compService) {

    /**
     * Adds the [component] to the [entity][Entity].
     *
     * The [onAdd][Component.onAdd] lifecycle method
     * gets called after the [component] is assigned to the [entity][Entity].
     *
     * If the [entity][Entity] already had such a [component] then the [onRemove][Component.onRemove]
     * lifecycle method gets called on the previously assigned component before the [onAdd][Component.onAdd]
     * lifecycle method is called on the new component.
     */
    inline operator fun > Entity.plusAssign(component: T) {
        val compType: ComponentType = component.type()
        compMasks[this.id].set(compType.id)
        val holder: ComponentsHolder = componentService.holder(compType)
        holder[this] = component
    }

    /**
     * Adds the [components] to the [entity][Entity]. This function should only be used
     * in exceptional cases.
     * It is preferred to use the [plusAssign] function whenever possible to have type-safety.
     *
     * The [onAdd][Component.onAdd] lifecycle method
     * gets called after each component is assigned to the [entity][Entity].
     *
     * If the [entity][Entity] already has such a component then the [onRemove][Component.onRemove]
     * lifecycle method gets called on the previously assigned component before the [onAdd][Component.onAdd]
     * lifecycle method is called on the new component.
     */
    operator fun Entity.plusAssign(components: List>) {
        components.forEach { cmp ->
            val compType = cmp.type()
            compMasks[this.id].set(compType.id)
            val holder = componentService.wildcardHolder(compType)
            holder.setWildcard(this, cmp)
        }
    }

    /**
     * Sets the [tag][EntityTag] to the [entity][Entity].
     */
    operator fun Entity.plusAssign(tag: EntityTags) {
        compMasks[this.id].set(tag.id)
        // We need to remember used tags in order to correctly return and load them using
        // the snapshot functionality, because tags are not managed via ComponentHolder and
        // the entity's component mask just knows about the tag's id.
        // However, a snapshot should contain the real object instances related to an entity.
        componentService.world.tagCache[tag.id] = tag
    }

    /**
     * Sets all [tags][EntityTag] on the given [entity][Entity].
     */
    @JvmName("plusAssignTags")
    operator fun Entity.plusAssign(tags: List) {
        tags.forEach { this += it }
    }

}

/**
 * A class that extends the extension functionality of an [EntityCreateContext] by also providing
 * the possibility to update [components][Component].
 */
class EntityUpdateContext(
    compService: ComponentService,
    compMasks: Bag,
) : EntityCreateContext(compService, compMasks) {

    /**
     * Removes a [component][Component] of the given [type] from the [entity][Entity].
     *
     * Calls the [onRemove][Component.onRemove] lifecycle method on the component being removed.
     *
     * @throws [IndexOutOfBoundsException] if the id of the [entity][Entity] exceeds the internal components' capacity.
     * This can only happen when the [entity][Entity] never had such a component.
     */
    inline operator fun > Entity.minusAssign(type: ComponentType) {
        compMasks[this.id].clear(type.id)
        componentService.holder(type) -= this
    }

    /**
     * Returns a [component][Component] of the given [type] for the [entity][Entity].
     *
     * If the [entity][Entity] does not have such a [component][Component] then [add] is called
     * to assign it to the [entity][Entity] and return it.
     */
    inline fun > Entity.getOrAdd(type: ComponentType, add: () -> T): T {
        val holder: ComponentsHolder = componentService.holder(type)
        val existingCmp = holder.getOrNull(this)
        if (existingCmp != null) {
            return existingCmp
        }

        compMasks[this.id].set(type.id)
        val newCmp = add()
        holder[this] = newCmp
        return newCmp
    }

    /**
     * Removes the [tag][EntityTag] from the [entity][Entity].
     */
    operator fun Entity.minusAssign(tag: UniqueId<*>) = compMasks[this.id].clear(tag.id)
}

/**
 * Interface to describe the functionality needed by an [EntityService]
 * to create and remove [entities][Entity]. The provider can be specified
 * via the [WorldConfiguration]. Per default a [DefaultEntityProvider] will be used.
 */
interface EntityProvider {

    /**
     * Reference to the [World] of the [EntityProvider].
     * It is needed for the [forEach] implementation.
     */
    val world: World

    /**
     * Returns the total amount of active [entities][Entity].
     */
    fun numEntities(): Int

    /**
     * Creates a new [entity][Entity].
     */
    fun create(): Entity

    /**
     * Creates a new [Entity] with a specific [id].
     * Internally, this is only needed by [World.loadSnapshotOf] and
     * if that is not used, then this function can be omitted.
     */
    fun create(id: Int): Entity

    /**
     * Removes an [entity][Entity].
     */
    operator fun minusAssign(entity: Entity)

    /**
     * Returns true if and only if the given [entity] is active and part of the provider.
     */
    operator fun contains(entity: Entity): Boolean

    /**
     * Resets the provider by removing all [entities][Entity].
     */
    fun reset()

    /**
     * Performs the given [action] for all active [entities][Entity].
     */
    fun forEach(action: World.(Entity) -> Unit)
}

/**
 * Default implementation of an [EntityProvider] which uses an [entity][Entity]
 * recycling mechanism to reuse [entities][Entity] that get removed.
 * The first [entity][Entity] starts with [id][Entity.id] zero and [version][Entity.version] zero.
 */
class DefaultEntityProvider(
    override val world: World,
    initialEntityCapacity: Int
) : EntityProvider {

    /**
     * The id that will be given to a newly created [entity][Entity] if there are no [recycledEntities].
     */
    private var nextId = 0

    /**
     * The already removed [entities][Entity] which can be reused whenever a new entity is needed.
     */
    private val recycledEntities = ArrayDeque()

    /**
     * Returns the total amount of active [entities][Entity].
     */
    override fun numEntities(): Int = nextId - recycledEntities.size

    /**
     * Bag of all currently active [entities][Entity].
     */
    private val activeEntities = bag(initialEntityCapacity)

    /**
     * Creates a new [entity][Entity]. If there are [recycledEntities] then they will be preferred
     * over creating new entities.
     */
    override fun create(): Entity {
        return if (recycledEntities.isEmpty()) {
            Entity(nextId++, version = 0u)
        } else {
            val recycled = recycledEntities.removeLast()

            // because of the load snapshot functionality of the world, it is
            // possible that an entity with an ID higher than nextId gets recycled
            // and immediately created afterward. In such a case we need to correct the
            // nextId to avoid creating duplicated entities.
            if (recycled.id >= nextId) {
                nextId = recycled.id + 1
            }

            recycled.copy(version = recycled.version + 1u)
        }.also {
            activeEntities[it.id] = it
        }
    }

    /**
     * Creates a new [Entity] with a specific [id].
     */
    override fun create(id: Int): Entity {
        if (id >= nextId) {
            // entity with given id was never created before -> create all missing entities ...
            repeat(id - nextId + 1) {
                this -= Entity(nextId + it, version = 0u)
            }
            // ... and then create the entity to guarantee that it has the correct ID.
            // The entity is at the end of the recycled list.

            // adjust ID for future entities to be created
            nextId = id + 1
            return create()
        } else {
            // entity with given id was already created before and is part of the recycled entities
            // -> move it to the end to be used by the next create call
            val index = recycledEntities.indexOfFirst { it.id == id }
            val entity = recycledEntities.removeAt(index)
            recycledEntities.addLast(entity)
            return create()
        }
    }

    /**
     * Removes an [entity][Entity].
     */
    override operator fun minusAssign(entity: Entity) {
        recycledEntities.add(entity)
        activeEntities.removeAt(entity.id)
    }

    /**
     * Returns true if and only if the given [entity] is active and part of the provider.
     */
    override fun contains(entity: Entity): Boolean = activeEntities.getOrNull(entity.id)?.version == entity.version

    /**
     * Resets the provider by removing and recycling all [entities][Entity].
     * Also, resets the [nextId] to zero.
     */
    override fun reset() {
        nextId = 0
        recycledEntities.clear()
        activeEntities.clear()
    }

    /**
     * Performs the given [action] for all active [entities][Entity].
     */
    override fun forEach(action: World.(Entity) -> Unit) {
        activeEntities.forEach { world.action(it) }
    }
}

/**
 * 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(
    @PublishedApi
    internal val world: World,
    initialEntityCapacity: Int,
    @PublishedApi
    internal var entityProvider: EntityProvider = DefaultEntityProvider(world, initialEntityCapacity),
    private val compService: ComponentService = world.componentService,
) {

    /**
     * Returns the amount of active entities.
     */
    val numEntities: Int
        get() = entityProvider.numEntities()

    /**
     * Returns the maximum capacity of active entities.
     */
    val capacity: Int
        get() = compMasks.capacity

    /**
     * The component configuration per [entity][Entity].
     */
    @PublishedApi
    internal val compMasks = bag(initialEntityCapacity)

    @PublishedApi
    internal val createCtx = EntityCreateContext(compService, compMasks)

    @PublishedApi
    internal var createId = -1

    @PublishedApi
    internal val updateCtx = EntityUpdateContext(compService, compMasks)

    @PublishedApi
    internal var updateId = -1

    /**
     * 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.
     */
    @PublishedApi
    internal var delayRemoval = false

    /**
     * The entities that get removed at the end of an [IteratingSystem] iteration.
     */
    internal val delayedEntities = MutableEntityBag()

    /**
     * An optional [EntityHook] that gets called whenever an [entity][Entity] gets created and
     * after its [components][Component] are assigned and [families][Family] are updated.
     */
    @PublishedApi
    internal var addHook: EntityHook? = null

    /**
     * An optional [EntityHook] that gets called whenever an [entity][Entity] gets removed and
     * before its [components][Component] are removed and [families][Family] are updated.
     */
    @PublishedApi
    internal var removeHook: EntityHook? = null

    /**
     * Creates and returns a new [entity][Entity] and applies the given [configuration].
     * Notifies all [families][World.allFamilies].
     */
    inline fun create(configuration: EntityCreateContext.(Entity) -> Unit): Entity =
        postCreate(entityProvider.create(), configuration)

    /**
     * Creates and returns a new [entity][Entity] with the given [id] and applies the given [configuration].
     * Notifies all [families][World.allFamilies].
     */
    inline fun create(id: Int, configuration: EntityCreateContext.(Entity) -> Unit): Entity =
        postCreate(entityProvider.create(id), configuration)

    /**
     * Applies the given [configuration] to the [entity] and notifies all [families][World.allFamilies].
     * The [addHook] is invoked at the end, if provided.
     */
    @PublishedApi
    internal inline fun postCreate(
        entity: Entity,
        configuration: EntityCreateContext.(Entity) -> Unit
    ): Entity {
        // add components
        if (entity.id >= compMasks.size) {
            compMasks[entity.id] = BitArray(64)
        }

        val prevCreateId = createId
        createId = entity.id
        createCtx.configuration(entity)
        createId = prevCreateId

        // update families
        val compMask = compMasks[entity.id]
        world.allFamilies.forEach { it.onEntityAdded(entity, compMask) }

        // trigger optional add hook
        addHook?.invoke(world, entity)

        return entity
    }

    /**
     * Updates an [entity] with the given [configuration].
     * Notifies all [families][World.allFamilies].
     */
    inline fun configure(entity: Entity, configuration: EntityUpdateContext.(Entity) -> Unit) {
        val skipFamilyNotify = updateId == entity.id || createId == entity.id

        val prevUpdateId = updateId
        updateId = entity.id
        updateCtx.configuration(entity)
        updateId = prevUpdateId

        // notify families
        if (skipFamilyNotify) {
            return
        }
        val compMask = compMasks[entity.id]
        world.allFamilies.forEach { it.onEntityCfgChanged(entity, compMask) }
    }

    /**
     * Updates an [entity] with the given [snapshot][Snapshot].
     * Notifies all [families][World.allFamilies].
     * This function is only used by [World.loadSnapshot] and [World.loadSnapshotOf],
     * and is therefore working with unsafe wildcards ('*').
     */
    internal fun configure(entity: Entity, snapshot: Snapshot) {
        val compMask = compMasks[entity.id]
        val components = snapshot.components

        // remove any existing components that are not part of the new components to set
        compMask.clearAndForEachSetBit { cmpId ->
            if (components.any { it.type().id == cmpId }) return@clearAndForEachSetBit

            // we can use holderByIndex because we can be sure that the holder already exists
            // because otherwise the entity would not even have the component
            compService.holderByIndexOrNull(cmpId)?.minusAssign(entity)
        }

        // set new components
        components.forEach { cmp ->
            compMask.set(cmp.type().id)
            val holder = compService.wildcardHolder(cmp.type())
            holder.setWildcard(entity, cmp)
        }

        // set new tags
        snapshot.tags.forEach {
            compMask.set(it.id)
            world.tagCache[it.id] = it
        }

        // notify families
        world.allFamilies.forEach { it.onEntityCfgChanged(entity, compMask) }
    }

    /**
     * Recycles the given [entity] and resets its component mask with an empty [BitArray].
     * This function is only used by [World.loadSnapshot].
     */
    internal fun recycle(entity: Entity) {
        entityProvider -= entity
        compMasks[entity.id] = BitArray(64)
    }

    /**
     * Removes the given [entity]. If [delayRemoval] is set then the [entity]
     * is not removed immediately and instead will be cleaned up within the [cleanupDelays] function.
     *
     * Notifies all [families][World.allFamilies] when the [entity] gets removed.
     */
    operator fun minusAssign(entity: Entity) {
        if (entity !in entityProvider) {
            // entity is already removed
            return
        }

        if (delayRemoval) {
            delayedEntities += entity
        } else {
            entityProvider -= entity
            val compMask = compMasks[entity.id]

            // trigger optional remove hook
            removeHook?.invoke(world, entity)

            // update families
            world.allFamilies.forEach { it.onEntityRemoved(entity) }

            // remove components
            compMask.clearAndForEachSetBit { compId ->
                compService.holderByIndexOrNull(compId)?.minusAssign(entity)
            }

        }
    }

    /**
     * Removes all [entities][Entity]. If [clearRecycled] is true then the
     * recycled entities are cleared and the ids for newly created entities start at 0 again.
     *
     * Refer to [remove] for more details.
     */
    fun removeAll(clearRecycled: Boolean = false) {
        entityProvider.forEach { this -= it }

        if (clearRecycled) {
            entityProvider.reset()
            compMasks.clear()
        }
    }

    /**
     * Returns true if and only if the [entity] is not removed and is part of the [EntityService].
     */
    operator fun contains(entity: Entity): Boolean = entity in entityProvider

    /**
     * Performs the given [action] on each active [entity][Entity].
     */
    fun forEach(action: World.(Entity) -> Unit) {
        entityProvider.forEach(action)
    }

    /**
     * Clears the [delayRemoval] flag and removes [entities][Entity] which are part of the [delayedEntities].
     */
    fun cleanupDelays() {
        delayRemoval = false
        if (delayedEntities.isNotEmpty()) {
            delayedEntities.forEach { this -= it }
            delayedEntities.clear()
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy