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

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

There is a newer version: 2.10
Show newest version
package com.github.quillraven.fleks

import com.github.quillraven.fleks.collection.Bag
import com.github.quillraven.fleks.collection.bag
import kotlinx.serialization.Serializable
import kotlin.math.max
import kotlin.native.concurrent.ThreadLocal

/**
 * An interface that specifies a unique [id].
 * This [id] is used internally by Fleks as an index for some arrays.
 */
// interface was necessary for (#118) to support enum classes as entity tags
// because enum classes can only inherit from an interface and not from an abstract class.
interface UniqueId {
    val id: Int

    @ThreadLocal
    companion object {
        internal var nextId = 0
    }
}

/**
 * An abstract class that assigns a unique [id] per type of [Component] starting from 0.
 * Every [Component] class must have at least one [ComponentType] which serves
 * as a [UniqueId].
 */
@Serializable
abstract class ComponentType : UniqueId {
    override val id: Int = UniqueId.nextId++
}

/**
 * Function to create an object for a [ComponentType] of type T.
 * This is a convenience function for [components][Component] that have more than one [ComponentType].
 */
inline fun  componentTypeOf(): ComponentType = object : ComponentType() {}

/**
 * Type alias for a special type of [ComponentType] that is used to tag [entities][Entity].
 * A tag is a special form of a [Component] that does not have any data. It is stored
 * more efficiently when compared to an empty [Component] and should therefore be preferred
 * in those cases.
 */
typealias EntityTag = ComponentType

/**
 * Type alias for a special type of [UniqueId]. It can be used to make values of an enum
 * class an [EntityTag].
 *
 * ```
 * enum class MyTags : EntityTags by entityTagOf() {
 *     TAG_A, TAG_B
 * }
 * ```
 */
typealias EntityTags = UniqueId

/**
 * Function to create an object for an [EntityTag].
 * It can be used to make values of an enum class an [EntityTag]. Refer to [EntityTags].
 */
fun entityTagOf(): EntityTag = object : EntityTag() {}

/**
 * An interface that must be implemented by any component that is used for Fleks.
 * A component must have at least one [ComponentType] that is provided via the [type] function.
 *
 * One convenient approach is to use the unnamed companion object of a Kotlin class as a [ComponentType].
 * Sample code for a component that stores the position of an entity:
 *
 *     data class Position(
 *         var x: Float,
 *         var y: Float,
 *     ) : Component {
 *         override fun type(): ComponentType = Position
 *
 *         companion object : ComponentType()
 *     }
 */
interface Component {
    /**
     * Returns the [ComponentType] of a [Component].
     */
    fun type(): ComponentType

    /**
     * Lifecycle method that gets called whenever a [component][Component] gets set for an [entity][Entity].
     */
    fun World.onAdd(entity: Entity) = Unit

    /**
     * Lifecycle method that gets called whenever a [component][Component] gets removed from an [entity][Entity].
     */
    fun World.onRemove(entity: Entity) = Unit
}

/**
 * A class that is responsible to store components of a specific type for all [entities][Entity] in a [world][World].
 * The index of the [components] array is linked to the id of an [entity][Entity]. If an [entity][Entity] has
 * a component of this specific type then the value at index 'entity.id' is not null.
 *
 * Refer to [ComponentService] for more details.
 */
class ComponentsHolder>(
    private val world: World,
    private val type: ComponentType<*>,
    private var components: Array,
) {
    /**
     * Sets the [component] for the given [entity]. This function is only
     * used by [World.loadSnapshot] where we don't have the correct type information
     * during runtime, and therefore we can only provide 'Any' as a type and need to cast it internally.
     */
    @Suppress("UNCHECKED_CAST")
    @PublishedApi
    internal fun setWildcard(entity: Entity, component: Any) = set(entity, component as T)

    /**
     * Sets the [component] for the given [entity].
     * If the [entity] already had a component, the [onRemove][Component.onRemove] lifecycle method
     * will be called.
     * After the [component] is assigned to the [entity], the [onAdd][Component.onAdd] lifecycle method
     * will be called.
     */
    operator fun set(entity: Entity, component: T) {
        if (entity.id >= components.size) {
            // not enough space to store the new component -> resize array
            components = components.copyOf(max(components.size * 2, entity.id + 1))
        }

        // check if the remove lifecycle method of the previous component needs to be called
        components[entity.id]?.run {
            // assign current component to null in order for 'contains' calls inside the lifecycle
            // method to correctly return false
            components[entity.id] = null
            world.onRemove(entity)
        }

        // set component and call lifecycle method
        components[entity.id] = component
        component.run { world.onAdd(entity) }
    }

    /**
     * Removes a component of the specific type from the given [entity].
     * If the entity has such a component, its [onRemove][Component.onRemove] lifecycle method will
     * be called.
     *
     * @throws [IndexOutOfBoundsException] if the id of the [entity] exceeds the components' capacity.
     */
    operator fun minusAssign(entity: Entity) {
        if (entity.id < 0 || entity.id >= components.size) throw IndexOutOfBoundsException("$entity.id is not valid for components of size ${components.size}")

        val existingCmp = components[entity.id]
        // assign null before running the lifecycle method in order for 'contains' calls to correctly return false
        components[entity.id] = null
        existingCmp?.run { world.onRemove(entity) }
    }

    /**
     * Returns a component of the specific type of the given [entity].
     *
     * @throws [FleksNoSuchEntityComponentException] if the [entity] does not have such a component.
     */
    operator fun get(entity: Entity): T {
        return components[entity.id] ?: throw FleksNoSuchEntityComponentException(entity, componentName())
    }

    /**
     * Returns a component of the specific type of the given [entity] or null if the entity does not have such a component.
     */
    fun getOrNull(entity: Entity): T? =
        components.getOrNull(entity.id)

    /**
     * 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

    /**
     * Returns the simplified component name of a [ComponentType].
     * The default toString() format is 'package.Component$Companion'.
     * This method returns 'Component' without package and companion.
     */
    private fun componentName(): String = type::class.toString().substringAfterLast(".").substringBefore("$")

    override fun toString(): String {
        return "ComponentsHolder(type=${componentName()}, id=${type.id})"
    }
}

/**
 * A service class that is responsible for managing [ComponentsHolder] instances.
 * It creates a [ComponentsHolder] for every unique [ComponentType].
 */
class ComponentService {
    @PublishedApi
    internal lateinit var world: World

    /**
     * Returns [Bag] of [ComponentsHolder].
     */
    @PublishedApi
    internal val holdersBag = bag>()

    /**
     * Returns a [ComponentsHolder] for the given [componentType]. This function is only
     * used by [World.loadSnapshot] where we don't have the correct type information
     * during runtime, and therefore we can only provide '*' as a type and need to cast it internally.
     */
    fun wildcardHolder(componentType: ComponentType<*>): ComponentsHolder<*> {
        if (holdersBag.hasNoValueAtIndex(componentType.id)) {
            holdersBag[componentType.id] =
                ComponentsHolder(world, componentType, Array?>(world.capacity) { null })
        }
        return holdersBag[componentType.id]
    }

    /**
     * Returns a [ComponentsHolder] for the given [componentType]. If no such holder exists yet, then it
     * will be created and added to the [holdersBag].
     */
    @Suppress("UNCHECKED_CAST")
    inline fun > holder(componentType: ComponentType): ComponentsHolder {
        if (holdersBag.hasNoValueAtIndex(componentType.id)) {
            holdersBag[componentType.id] = ComponentsHolder(world, componentType, Array(world.capacity) { null })
        }
        return holdersBag[componentType.id] as ComponentsHolder
    }

    /**
     * Returns the [ComponentsHolder] of the given [index] inside the [holdersBag] or null.
     * The index is linked to the id of a [ComponentType].
     * This function is only used internally at safe areas to speed up certain processes like
     * removing an [entity][Entity] or creating a snapshot via [World.snapshot].
     */
    internal fun holderByIndexOrNull(index: Int): ComponentsHolder<*>? {
        return holdersBag.getOrNull(index)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy