Skip to content

Commit

Permalink
add entity tag support (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
Quillraven authored Oct 1, 2023
1 parent 01fb0d6 commit c677f2e
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Bag<T>(
/**
* Returns the element at position [index] or null if there is no element or if [index] is out of bounds
*/
inline fun getOrNull(index: Int): T? = values.getOrNull(index)
fun getOrNull(index: Int): T? = values.getOrNull(index)

fun hasNoValueAtIndex(index: Int): Boolean {
return index >= size || index < 0 || values[index] == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ class BitArray(
}
}

inline fun clearAndForEachSetBit(action: (Int) -> Unit) {
for (word in bits.size - 1 downTo 0) {
var bitsAtWord = bits[word]
if (bitsAtWord != 0L) {
val w = word shl 6
while (bitsAtWord != 0L) {
bits[word] = 0L // clear
// gets the distance from the start of the word to the highest (leftmost) bit
val bit = 63 - bitsAtWord.countLeadingZeroBits()
action(w + bit)
bitsAtWord = (bitsAtWord xor (1L shl bit)) // removes highest bit
}
}
}
}

override fun hashCode(): Int {
if (bits.isEmpty()) {
return 0
Expand Down
56 changes: 45 additions & 11 deletions src/commonMain/kotlin/com/github/quillraven/fleks/component.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,61 @@ import kotlin.math.max
import kotlin.native.concurrent.ThreadLocal

/**
* A class that assigns a unique [id] per type of [Component] starting from 0.
* An interface that specifies a unique [id].
* This [id] is used internally by Fleks as an index for some arrays.
* Every [Component] class must have at least one [ComponentType].
*/
abstract class ComponentType<T> {
val id: Int = nextId++
// 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<T> {
val id: Int

@ThreadLocal
companion object {
private var nextId = 0
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].
*/
abstract class ComponentType<T> : UniqueId<T> {
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 <reified T> componentTypeOf(): ComponentType<T> = object : ComponentType<T>() {}

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

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

/**
* 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.
Expand Down Expand Up @@ -194,14 +230,12 @@ class ComponentService {
}

/**
* Returns the [ComponentsHolder] of the given [index] inside the [holdersBag]. The index
* is linked to the id of a [ComponentType].
* 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].
*
* @throws [IndexOutOfBoundsException] if the [index] exceeds the bag's capacity.
*/
internal fun holderByIndex(index: Int): ComponentsHolder<*> {
return holdersBag[index]
internal fun holderByIndexOrNull(index: Int): ComponentsHolder<*>? {
return holdersBag.getOrNull(index)
}
}
76 changes: 53 additions & 23 deletions src/commonMain/kotlin/com/github/quillraven/fleks/entity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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
Expand Down Expand Up @@ -47,22 +48,22 @@ abstract class EntityComponentContext(
componentService.holder(type).getOrNull(this)

/**
* Returns true if and only if the [entity][Entity] has a [component][Component] of the given [type].
* Returns true if and only if the [entity][Entity] has a [component][Component] or [tag][EntityTag] of the given [type].
*/
inline operator fun <reified T : Component<*>> Entity.contains(type: ComponentType<T>): Boolean =
this in componentService.holder(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] of the given [type].
* Returns true if and only if the [entity][Entity] has a [component][Component] or [tag][EntityTag] of the given [type].
*/
inline infix fun <reified T : Component<*>> Entity.has(type: ComponentType<T>): Boolean =
this in componentService.holder(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] of the given [type].
* Returns true if and only if the [entity][Entity] doesn't have a [component][Component] or [tag][EntityTag] of the given [type].
*/
inline infix fun <reified T : Component<*>> Entity.hasNo(type: ComponentType<T>): Boolean =
this !in componentService.holder(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].
Expand Down Expand Up @@ -136,6 +137,27 @@ open class EntityCreateContext(
holder.setWildcard(this, cmp)
}
}

/**
* Sets the [tag][EntityTag] to the [entity][Entity].
*/
operator fun Entity.plusAssign(tag: UniqueId<*>) {
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<UniqueId<*>>) {
tags.forEach { this += it }
}

}

/**
Expand All @@ -146,6 +168,7 @@ class EntityUpdateContext(
compService: ComponentService,
compMasks: Bag<BitArray>,
) : EntityCreateContext(compService, compMasks) {

/**
* Removes a [component][Component] of the given [type] from the [entity][Entity].
*
Expand All @@ -165,10 +188,7 @@ class EntityUpdateContext(
* 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 <reified T : Component<T>> Entity.getOrAdd(
type: ComponentType<T>,
add: () -> T,
): T {
inline fun <reified T : Component<T>> Entity.getOrAdd(type: ComponentType<T>, add: () -> T): T {
val holder: ComponentsHolder<T> = componentService.holder(type)
val existingCmp = holder.getOrNull(this)
if (existingCmp != null) {
Expand All @@ -180,6 +200,11 @@ class EntityUpdateContext(
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)
}

/**
Expand Down Expand Up @@ -458,29 +483,35 @@ class EntityService(
}

/**
* Updates an [entity] with the given [components].
* 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, components: List<Component<*>>) {
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.forEachSetBit { cmpId ->
if (components.any { it.type().id == cmpId }) return@forEachSetBit
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.holderByIndex(cmpId) -= entity
compService.holderByIndexOrNull(cmpId)?.minusAssign(entity)
}
compMask.clearAll()

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

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

// notify families
Expand Down Expand Up @@ -518,10 +549,9 @@ class EntityService(
removeHook?.invoke(world, entity)

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

// update families
world.allFamilies.forEach { it.onEntityRemoved(entity) }
Expand Down
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/com/github/quillraven/fleks/family.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ data class FamilyDefinition(
/**
* Any [entity][Entity] must have all given [types] to be part of the [family][Family].
*/
fun all(vararg types: ComponentType<*>): FamilyDefinition {
fun all(vararg types: UniqueId<*>): FamilyDefinition {
allOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
Expand All @@ -36,7 +36,7 @@ data class FamilyDefinition(
/**
* Any [entity][Entity] must not have any of the given [types] to be part of the [family][Family].
*/
fun none(vararg types: ComponentType<*>): FamilyDefinition {
fun none(vararg types: UniqueId<*>): FamilyDefinition {
noneOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
Expand All @@ -46,7 +46,7 @@ data class FamilyDefinition(
/**
* Any [entity][Entity] must have at least one of the given [types] to be part of the [family][Family].
*/
fun any(vararg types: ComponentType<*>): FamilyDefinition {
fun any(vararg types: UniqueId<*>): FamilyDefinition {
anyOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
Expand Down
Loading

0 comments on commit c677f2e

Please sign in to comment.