Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component lifecycle methods #104

Merged
merged 4 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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