Skip to content

Commit

Permalink
Component lifecycle methods (#104)
Browse files Browse the repository at this point in the history
add lifecycle methods to the `Component` interface
  • Loading branch information
geist-2501 authored Jul 12, 2023
1 parent 2f1d773 commit 01e46dc
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 230 deletions.
59 changes: 29 additions & 30 deletions src/commonMain/kotlin/com/github/quillraven/fleks/component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import kotlin.math.max
import kotlin.native.concurrent.ThreadLocal
import kotlin.reflect.KClass

/**
* Type alias for an optional hook function for a [ComponentsHolder].
* Such a function runs within a [World] and takes the [Entity] and [component][Component] as an argument.
*/
typealias ComponentHook<T> = World.(Entity, T) -> Unit

/**
* A class that assigns a unique [id] per type of [Component] starting from 0.
* This [id] is used internally by Fleks as an index for some arrays.
Expand Down Expand Up @@ -53,6 +47,16 @@ interface Component<T> {
* Returns the [ComponentType] of a [Component].
*/
fun type(): ComponentType<T>

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

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

/**
Expand All @@ -67,18 +71,6 @@ class ComponentsHolder<T : Component<*>>(
private val name: String,
private var components: Array<T?>,
) {
/**
* An optional [ComponentHook] that gets called whenever a [component][Component] gets set for an [entity][Entity].
*/
@PublishedApi
internal var addHook: ComponentHook<T>? = null

/**
* An optional [ComponentHook] that gets called whenever a [component][Component] gets removed from an [entity][Entity].
*/
@PublishedApi
internal var removeHook: ComponentHook<T>? = null

/**
* Sets the [component] for the given [entity]. This function is only
* used by [World.loadSnapshot] where we don't have the correct type information
Expand All @@ -90,42 +82,49 @@ class ComponentsHolder<T : Component<*>>(

/**
* Sets the [component] for the given [entity].
* If a [removeHook] is defined then it gets called if the [entity] already had a component.
* If an [addHook] is defined then it gets called after the [component] is assigned to the [entity].
* If the [entity] already had a component, the [onRemoveComponent][Component.onRemoveComponent] lifecycle method
* will be called.
* After the [component] is assigned to the [entity], the [onAddComponent][Component.onAddComponent] 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 removeHook needs to be called
// check if the remove lifecycle method of the previous component needs to be called
components[entity.id]?.let { existingCmp ->
// assign current component to null in order for 'contains' calls inside the hook
// to correctly return false
// assign current component to null in order for 'contains' calls inside the lifecycle
// method to correctly return false
components[entity.id] = null
removeHook?.invoke(world, entity, existingCmp)
existingCmp.run {
world.onRemoveComponent()
}
}

// set component and call addHook if necessary
// set component and call lifecycle method
components[entity.id] = component
addHook?.invoke(world, entity, component)
component.run {
world.onAddComponent()
}
}

/**
* Removes a component of the specific type from the given [entity].
* If a [removeHook] is defined then it gets called if the [entity] has such a component.
* If the entity has such a component, its [onRemoveComponent][Component.onRemoveComponent] 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 removeHook in order for 'contains' calls to correctly return false
// assign null before running the lifecycle method in order for 'contains' calls to correctly return false
components[entity.id] = null
if (existingCmp != null) {
removeHook?.invoke(world, entity, existingCmp)
existingCmp?.run {
world.onRemoveComponent()
}
}

Expand Down
23 changes: 11 additions & 12 deletions src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ value class Entity(val id: Int)
typealias EntityHook = World.(Entity) -> Unit

/**
* A class for basic [Entity] extension functions within an add/remove hook of a [Component], [Family],
* A class for basic [Entity] extension functions within a [Family],
* [IntervalSystem], [World] or [compareEntity].
*/
abstract class EntityComponentContext(
Expand Down Expand Up @@ -92,12 +92,12 @@ open class EntityCreateContext(
/**
* Adds the [component] to the [entity][Entity].
*
* If a component [addHook][ComponentsHolder.addHook] is defined then it
* The [onAddComponent][Component.onAddComponent] lifecycle method
* gets called after the [component] is assigned to the [entity][Entity].
*
* If a component [removeHook][ComponentsHolder.removeHook] is defined and the [entity][Entity]
* already had such a [component] then it gets called with the previous assigned component before
* the [addHook][ComponentsHolder.addHook] is called.
* If the [entity][Entity] already had such a [component] then the [onRemoveComponent][Component.onRemoveComponent]
* lifecycle method gets called on the previously assigned component before the [onAddComponent][Component.onAddComponent]
* lifecycle method is called on the new component.
*/
inline operator fun <reified T : Component<T>> Entity.plusAssign(component: T) {
val compType: ComponentType<T> = component.type()
Expand All @@ -111,12 +111,12 @@ open class EntityCreateContext(
* in exceptional cases.
* It is preferred to use the [plusAssign] function whenever possible to have type-safety.
*
* If a component [addHook][ComponentsHolder.addHook] is defined then it
* gets called after the component is assigned to the [entity][Entity].
* The [onAddComponent][Component.onAddComponent] lifecycle method
* gets called after each component is assigned to the [entity][Entity].
*
* If a component [removeHook][ComponentsHolder.removeHook] is defined and the [entity][Entity]
* already had such a component then it gets called with the previous assigned component before
* the [addHook][ComponentsHolder.addHook] is called.
* If the [entity][Entity] already has such a component then the [onRemoveComponent][Component.onRemoveComponent]
* lifecycle method gets called on the previously assigned component before the [onAddComponent][Component.onAddComponent]
* lifecycle method is called on the new component.
*/
operator fun Entity.plusAssign(components: List<Component<*>>) {
components.forEach { cmp ->
Expand All @@ -139,8 +139,7 @@ class EntityUpdateContext(
/**
* Removes a [component][Component] of the given [type] from the [entity][Entity].
*
* If a component [removeHook][ComponentsHolder.removeHook] is defined then it gets called
* if the [entity][Entity] has such a component.
* Calls the [onRemoveComponent][Component.onRemoveComponent] 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.
Expand Down
76 changes: 2 additions & 74 deletions src/commonMain/kotlin/com/github/quillraven/fleks/world.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,38 +48,6 @@ class InjectableConfiguration(private val world: World) {
inline fun <reified T : Any> add(dependency: T) = add(T::class.simpleName ?: T::class.toString(), dependency)
}

/**
* A DSL class to configure [ComponentHook] for specific [components][Component].
*/
@WorldCfgMarker
class ComponentConfiguration(
@PublishedApi
internal val world: World,
) {

/**
* Sets the add [hook][ComponentsHolder.addHook] for the given [type].
* This hook gets called whenever a [component][Component] of the given [type] gets added to an [entity][Entity].
*/
inline fun <reified T : Component<*>> onAdd(
type: ComponentType<T>,
noinline hook: ComponentHook<T>
) {
world.setComponentAddHook(type, hook)
}

/**
* Sets the remove [hook][ComponentsHolder.addHook] for the given [type].
* This hook gets called whenever a [component][Component] of the given [type] gets removed from an [entity][Entity].
*/
inline fun <reified T : Component<*>> onRemove(
type: ComponentType<T>,
noinline hook: ComponentHook<T>
) {
world.setComponentRemoveHook(type, hook)
}
}

/**
* A DSL class to configure [IntervalSystem] of a [WorldConfiguration].
*/
Expand Down Expand Up @@ -149,18 +117,13 @@ class FamilyConfiguration(
class WorldConfiguration(@PublishedApi internal val world: World) {

private var injectableCfg: (InjectableConfiguration.() -> Unit)? = null
private var compCfg: (ComponentConfiguration.() -> Unit)? = null
private var familyCfg: (FamilyConfiguration.() -> Unit)? = null
private var systemCfg: (SystemConfiguration.() -> Unit)? = null

fun injectables(cfg: InjectableConfiguration.() -> Unit) {
injectableCfg = cfg
}

fun components(cfg: ComponentConfiguration.() -> Unit) {
compCfg = cfg
}

fun families(cfg: FamilyConfiguration.() -> Unit) {
familyCfg = cfg
}
Expand Down Expand Up @@ -197,7 +160,6 @@ class WorldConfiguration(@PublishedApi internal val world: World) {

fun configure() {
injectableCfg?.invoke(InjectableConfiguration(world))
compCfg?.invoke(ComponentConfiguration(world))
familyCfg?.invoke(FamilyConfiguration(world))
SystemConfiguration().also {
systemCfg?.invoke(it)
Expand All @@ -216,7 +178,7 @@ class WorldConfiguration(@PublishedApi internal val world: World) {
* size of some collections and to avoid slow resizing calls.
*
* @param cfg the [configuration][WorldConfiguration] of the world containing the [systems][IntervalSystem],
* [injectables][Injectable], [ComponentHook]s and [FamilyHook]s.
* [injectables][Injectable] and [FamilyHook]s.
*/
fun configureWorld(entityCapacity: Int = 512, cfg: WorldConfiguration.() -> Unit): World {
val newWorld = World(entityCapacity)
Expand Down Expand Up @@ -382,40 +344,6 @@ class World internal constructor(
throw FleksNoSuchSystemException(T::class)
}

/**
* Sets the [hook] as a [ComponentsHolder.addHook] for the given [type].
*
* @throws FleksHookAlreadyAddedException if the [ComponentsHolder] already has an add hook set.
*/
@PublishedApi
internal inline fun <reified T : Component<*>> setComponentAddHook(
type: ComponentType<T>,
noinline hook: ComponentHook<T>
) {
val holder = componentService.holder(type)
if (holder.addHook != null) {
throw FleksHookAlreadyAddedException("addHook", "Component ${type::class.simpleName}")
}
holder.addHook = hook
}

/**
* Sets the [hook] as a [ComponentsHolder.removeHook] for the given [type].
*
* @throws FleksHookAlreadyAddedException if the [ComponentsHolder] already has a remove hook set.
*/
@PublishedApi
internal inline fun <reified T : Component<*>> setComponentRemoveHook(
type: ComponentType<T>,
noinline hook: ComponentHook<T>
) {
val holder = componentService.holder(type)
if (holder.removeHook != null) {
throw FleksHookAlreadyAddedException("removeHook", "Component ${type::class.simpleName}")
}
holder.removeHook = hook
}

/**
* Sets the [hook] as an [EntityService.addHook].
*
Expand Down Expand Up @@ -529,7 +457,7 @@ class World internal constructor(
/**
* 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 [ComponentHook] or [FamilyHook].
* This will also execute [FamilyHook].
*
* @throws FleksSnapshotException if a family iteration is currently in process.
*/
Expand Down
Loading

0 comments on commit 01e46dc

Please sign in to comment.