-
Notifications
You must be signed in to change notification settings - Fork 20
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
Ability to add systems to the world after world creation #145
Conversation
Thank you! I will try to have a look by tomorrow and give you feedback :) |
Had a quick check on my phone. In general looks good to me but need to have a more detailed look. Two things that I can already tell:
|
…l create unexpected events.
Updated with review feedback 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if you need help with the setUpAggregatedFamilyHooks
part. Other than that looks great with just some minor change requests.
Again, thank you for your time and support!
@@ -175,7 +175,7 @@ class WorldConfiguration(@PublishedApi internal val world: World) { | |||
systemCfg?.invoke(it) | |||
// assign world systems afterward to resize the systems array only once to the correct size | |||
// instead of resizing every time a system gets added to the configuration | |||
world.systems = it.systems.toTypedArray() | |||
world.systems = it.systems |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't use an array anymore we can directly add the system to world.systems
. I think a separate world configuration system list is no longer necessary. Also, the comment above is no longer valid and can be removed.
With that change you should be able to avoid _systems
and systems
in the world and also replace var with val.
* | ||
* @throws FleksSystemAlreadyAddedException if the system was already added before. | ||
*/ | ||
fun add(system: IntervalSystem, index: Int? = null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would switch the argument because for normal list operations where you add an element at a specific index, the index parameter is first like in:
val numbers = mutableListOf("one", "five", "six")
numbers.add(1, "two")
To have a separate method without the index that simply adds at the end, I would go for a second method. To reduce code it could be something like this:
fun add(index: Int, system: IntervalSystem) {
// do main logic here
}
fun add(system: IntervalSystem) = add(systems.size -1, system)
systems.reversed().forEach { it.onDispose() } | ||
} | ||
|
||
internal fun setUpAggregatedFamilyHooks(systems: List<IntervalSystem>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this functionality is not properly working anymore with the new change. It was a very special feature requested by some other user. To explain it:
- In Fleks you can have family hooks to react when an entity gets added/removed from a family
- To make life easier we added a
FamilyOnAdd
andFamilyOnRemove
interface that can be added to anIteratingsystem
and then Fleks will automatically register those hooks to the family - Since there is just one hook on a family we then simply combined all system hook functions into a single one and register that special function as a hook to the family
- One additional point that makes it a little complex is that you can also register your own family hook in the world configuration already (that's the
ownHook
in the code below. The final hook is then the family original hook + all systems' hooks
Before, this was just necessary once because systems could not be modified after world creation. Now, imo we need to do that all the time when a system gets added or removed.
However, it is not necessary to iterate over all systems all the time and create the hooks again. My idea is:
- When adding/removing a system check if it implements the
FamilyOnAdd
orFamilyOnRemove
interface - If it does then call this method or a new one which does the following:
- get all systems that have the same family in their current order
- update the family hook with the already existing logic
- This will add/remove the newly added/removed system's hooks
However, there is one issue with the ownHook
of the family. I think we don't have that anymore and it is currently impossible to retrieve it from somewhere. Needs to be checked but I am pretty sure that that's the case, unfortunately :(
For that I see two solutions:
- store the own hook of the family, that gets created in the world configuration in a separate place somewhere where we can then use it for the logic above (e.g. directly in the family class)
- The downside is that we introduce an additional field per family which increases memory consumption but I guess it is not huge
- This could maybe be solved more efficiently if we e.g. use a
ownHookCache
in the world directly. When we retrieve a family`s ownHook for the first time we could read this cache. If there is an entry then use it and if there is no entry then add it. That way we don't add an additional field to every family.
- instead of just having one hook on a family just make it a list or an array. That way we don't need to override the hook at all and just add/remove system hooks.
- This is however inconsistent with the other hooks in Fleks
@@ -326,8 +278,55 @@ class World internal constructor( | |||
/** | |||
* Returns the world's systems. | |||
*/ | |||
var systems = emptyArray<IntervalSystem>() | |||
internal set | |||
private var _systems = mutableListOf<IntervalSystem>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot remember anymore but I think mutableListOf creates a LinkedHashList or something. I don't know if the iteration over systems is then with an iterator or not.
We need to check that because otherwise we create a lot of iterator instances and trigger the garbage collector.
If possible, we should maybe just use an ArrayList here. I think that has the least overhead and iteration should be a simple one without iterator.
haha unlucky timing, I just did the detailed review in parallel. Will have another look this afternoon :) |
Resolved the own hook conundrum using your first approach. Let me know if you like how it looks. |
Looks better now but the hook stuff is still not correct. It is a little bit confusing because it is a single hook from a family point of view but actually multiple things can be called. Here is a scenario that describes it better:
The special
For removal it is the same but in reversed order. If you now remove system A of the world then the hook of the family remains but the new execution will be:
I understand if this is a little complex for someone who does not know about that special use-case and all the details ;) I think this is also not covered by any of the test cases because otherwise you would already have failing tests. I will have some time on the weekend and can help you with that. But of course, if you feel motivated to do it on your own then also feel free to do so :) Everything else looks fine from my side now and can be merged. We are just missing the proper logic for this special |
had a little bit of time this morning. In theory the code should work but needs to be verified in a proper test case. Please have a look as well:
The two lazy caches. I hope mutableMapOf supports null values: /**
* Map of add [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyAddHooks = mutableMapOf<Family, FamilyHook?>()
/**
* Map of remove [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyRemoveHooks = mutableMapOf<Family, FamilyHook?>() Updated system add: /**
* Adds a new system to the world.
*
* @param index The position at which the system should be inserted in the list of systems. If null, the system is added at the end of the list.
* This parameter is optional and defaults to null.
* @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
fun add(index: Int, system: IntervalSystem) {
if (systems.any { it::class == system::class }) {
throw FleksSystemAlreadyAddedException(system::class)
}
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
mutableSystems.add(index, system)
} Updated system remove: /**
* Removes the specified system from the world.
*
* @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
* @return True if the system was successfully removed, false otherwise.
*/
fun remove(system: IntervalSystem) {
mutableSystems.remove(system)
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
} The updated init aggregated hooks and update aggregated hooks: /**
* Extend [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove].
*/
internal fun initAggregatedFamilyHooks() {
// validate systems against illegal interfaces
systems.forEach { system ->
// FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem
if (system !is IteratingSystem) {
if (system is FamilyOnAdd) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnAdd::class)
}
if (system is FamilyOnRemove) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnRemove::class)
}
}
}
// register family hooks for IteratingSystem.FamilyOnAdd containing systems
systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnAdd) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
family.addHook = if (ownHook != null) { entity ->
ownHook(this, entity)
systemList.forEach { it.onAddEntity(entity) }
} else { entity ->
systemList.forEach { it.onAddEntity(entity) }
}
}
// register family hooks for IteratingSystem.FamilyOnRemove containing systems
systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnRemove) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook as FamilyHook }
family.removeHook = if (ownHook != null) { entity ->
systemList.forEachReverse { it.onRemoveEntity(entity) }
ownHook(this, entity)
} else { entity ->
systemList.forEachReverse { it.onRemoveEntity(entity) }
}
}
}
/**
* Update [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove]
* and iterate over the given [family].
*/
private fun updateAggregatedFamilyHooks(family: Family) {
// system validation like in initAggregatedFamilyHooks is not necessary
// because it is already validated before (in initAggregatedFamilyHooks and in add/remove system)
// update family add hook by adding systems' onAddEntity calls after its original world cfg hook
val ownAddHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
val addSystems = systems.filter { it is IteratingSystem && it is FamilyOnAdd && it.family == family }
family.addHook = if (ownAddHook != null) { entity ->
ownAddHook(this, entity)
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
} else { entity ->
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
}
// update family remove hook by adding systems' onRemoveEntity calls before its original world cfg hook
val ownRemoveHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook }
val removeSystems = systems.filter { it is IteratingSystem && it is FamilyOnRemove && it.family == family }
family.removeHook = if (ownRemoveHook != null) { entity ->
removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
ownRemoveHook(this, entity)
} else { entity ->
removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
}
} Complete family.kt filepackage com.github.quillraven.fleks
import com.github.quillraven.fleks.collection.*
/**
* Type alias for an optional hook function for a [Family].
* Such a function runs within a [World] and takes the [Entity] as an argument.
*/
typealias FamilyHook = World.(Entity) -> Unit
/**
* A class to define the configuration of a [Family]. A [family][Family] contains of three parts:
*
* - **allOf**: an [entity][Entity] must have all specified [components][Component] to be part of the [family][Family].
* - **noneOf**: an [entity][Entity] must not have any of the specified [components][Component] to be part of the [family][Family].
* - **anyOf**: an [entity][Entity] must have at least one of the specified [components][Component] to be part of the [family][Family].
*
* It is not mandatory to specify all three parts but **at least one** part must be provided.
*/
data class FamilyDefinition(
internal var allOf: BitArray? = null,
internal var noneOf: BitArray? = null,
internal var anyOf: BitArray? = null,
) {
/**
* Any [entity][Entity] must have all given [types] to be part of the [family][Family].
*/
fun all(vararg types: UniqueId<*>): FamilyDefinition {
allOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
return this
}
/**
* Any [entity][Entity] must not have any of the given [types] to be part of the [family][Family].
*/
fun none(vararg types: UniqueId<*>): FamilyDefinition {
noneOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
return this
}
/**
* Any [entity][Entity] must have at least one of the given [types] to be part of the [family][Family].
*/
fun any(vararg types: UniqueId<*>): FamilyDefinition {
anyOf = BitArray(types.size).also { bits ->
types.forEach { bits.set(it.id) }
}
return this
}
/**
* Returns true if and only if [allOf], [noneOf] and [anyOf] are either null or empty.
*/
internal fun isEmpty(): Boolean {
return allOf.isNullOrEmpty() && noneOf.isNullOrEmpty() && anyOf.isNullOrEmpty()
}
}
/**
* A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components.
* A configuration is defined via the a [FamilyDefinition].
* Each [component][Component] is assigned to a unique index via its [ComponentType].
* That index is set in the [allOf], [noneOf] or [anyOf][] [BitArray].
*
* A family gets notified when an [entity][Entity] is added, updated or removed of the [world][World].
*
* Every [IteratingSystem] is linked to exactly one family but a family can also exist outside of systems.
* It gets created via the [World.family] function.
*/
data class Family(
internal val allOf: BitArray? = null,
internal val noneOf: BitArray? = null,
internal val anyOf: BitArray? = null,
private val world: World,
@PublishedApi
internal val entityService: EntityService = world.entityService,
) : EntityComponentContext(world.componentService) {
/**
* An optional [FamilyHook] that gets called whenever an [entity][Entity] enters the family.
*/
internal var addHook: FamilyHook? = null
/**
* An optional [FamilyHook] that gets called whenever an [entity][Entity] leaves the family.
*/
internal var removeHook: FamilyHook? = null
/**
* Returns the [entities][Entity] that belong to this family.
*/
private val activeEntities = bag<Entity>(world.capacity)
private var countEntities = 0
/**
* Returns true if an iteration of this family is currently in process.
*/
@PublishedApi
internal var isIterating = false
// This bag is added for better iteration performance.
@PublishedApi
internal val mutableEntities = MutableEntityBag()
get() {
if (isDirty && !isIterating) {
// no iteration in process -> update entities if necessary
isDirty = false
field.clearEnsuringCapacity(activeEntities.size)
activeEntities.forEach { field += it }
}
return field
}
/**
* Returns the [entities][Entity] that belong to this family.
* Be aware that the underlying [EntityBag] collection is not always up to date.
* The collection is not updated while a family iteration is in progress. It
* gets automatically updated whenever it is accessed and no iteration is currently
* in progress.
*/
val entities: EntityBag
get() = mutableEntities
/**
* Returns the number of [entities][Entity] that belong to this family.
*/
val numEntities: Int
get() = countEntities
/**
* Returns true if and only if this [Family] does not contain any entity.
*/
val isEmpty: Boolean
get() = countEntities == 0
/**
* Returns true if and only if this [Family] contains at least one entity.
*/
val isNotEmpty: Boolean
get() = countEntities > 0
/**
* Flag to indicate if there are changes in the [activeEntities].
* If it is true then the [mutableEntities] will get updated the next time it is accessed.
*/
private var isDirty = false
/**
* Returns true if the specified [compMask] matches the family's component configuration.
*
* @param compMask the component configuration of an [entity][Entity].
*/
internal operator fun contains(compMask: BitArray): Boolean {
return (allOf == null || compMask.contains(allOf))
&& (noneOf == null || !compMask.intersects(noneOf))
&& (anyOf == null || compMask.intersects(anyOf))
}
/**
* Returns true if and only if the given [entity] is part of the family.
*/
operator fun contains(entity: Entity): Boolean = activeEntities.hasValueAtIndex(entity.id)
/**
* Updates this family if needed and runs the given [action] for all [entities][Entity].
*
* **Important note**: There is a potential risk when iterating over entities and one of those entities
* gets removed. Removing the entity immediately and cleaning up its components could
* cause problems because if you access a component which is mandatory for the family, you will get
* a FleksNoSuchComponentException. To avoid that you could check if an entity really has the component
* before accessing it but that is redundant in context of a family.
*
* To avoid these kinds of issues, entity removals are delayed until the end of the iteration. This also means
* that a removed entity of this family will still be part of the [action] for the current iteration.
*/
inline fun forEach(crossinline action: Family.(Entity) -> Unit) {
// Access entities before 'forEach' call to properly update them.
// Check mutableEntities getter for more details.
val entitiesForIteration = mutableEntities
if (!entityService.delayRemoval) {
entityService.delayRemoval = true
isIterating = true
entitiesForIteration.forEach { action(it) }
isIterating = false
entityService.cleanupDelays()
} else {
val origIterating = isIterating
isIterating = true
entitiesForIteration.forEach { this.action(it) }
isIterating = origIterating
}
}
/**
* Updates this family if needed and returns its first [Entity].
* @throws [NoSuchElementException] if the family has no entities.
*/
fun first(): Entity = mutableEntities.first()
/**
* Updates this family if needed and returns its first [Entity] or null if the family has no entities.
*/
fun firstOrNull(): Entity? = mutableEntities.firstOrNull()
/**
* Sorts the [entities][Entity] of this family by the given [comparator].
*/
fun sort(comparator: EntityComparator) = mutableEntities.sort(comparator)
/**
* Adds the [entity] to the family and sets the [isDirty] flag if and only
* if the entity's [compMask] is matching the family configuration.
*/
@PublishedApi
internal fun onEntityAdded(entity: Entity, compMask: BitArray) {
if (compMask in this) {
isDirty = true
if (activeEntities.hasNoValueAtIndex(entity.id)) countEntities++
activeEntities[entity.id] = entity
addHook?.invoke(world, entity)
}
}
/**
* Checks if the [entity] is part of the family by analyzing the entity's components.
* The [compMask] is a [BitArray] that indicates which components the [entity] currently has.
*
* The [entity] gets either added to the [activeEntities] or removed and [isDirty] is set when needed.
*/
@PublishedApi
internal fun onEntityCfgChanged(entity: Entity, compMask: BitArray) {
val entityInFamily = compMask in this
val currentEntity = activeEntities.getOrNull(entity.id)
if (entityInFamily && currentEntity == null) {
// new entity gets added
isDirty = true
countEntities++
activeEntities[entity.id] = entity
addHook?.invoke(world, entity)
} else if (!entityInFamily && currentEntity != null) {
// existing entity gets removed
isDirty = true
countEntities--
activeEntities.removeAt(entity.id)
removeHook?.invoke(world, entity)
}
}
/**
* Removes the [entity] of the family and sets the [isDirty] flag if and only
* if the [entity] is already in the family.
*/
internal fun onEntityRemoved(entity: Entity) {
if (activeEntities.hasValueAtIndex(entity.id)) {
// existing entity gets removed
isDirty = true
activeEntities.removeAt(entity.id)
countEntities--
removeHook?.invoke(world, entity)
}
}
override fun toString(): String {
return "Family(allOf=$allOf, noneOf=$noneOf, anyOf=$anyOf, numEntities=$numEntities)"
}
} Complete world.kt filepackage com.github.quillraven.fleks
import com.github.quillraven.fleks.World.Companion.CURRENT_WORLD
import com.github.quillraven.fleks.collection.EntityBag
import com.github.quillraven.fleks.collection.MutableEntityBag
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlin.native.concurrent.ThreadLocal
import kotlin.reflect.KClass
/**
* DSL marker for the [WorldConfiguration].
*/
@DslMarker
annotation class WorldCfgMarker
/**
* Wrapper class for injectables of the [WorldConfiguration].
* It is used to identify unused injectables after [world][World] creation.
*/
data class Injectable(val injObj: Any, var used: Boolean = false)
/**
* A DSL class to configure [Injectable] of a [WorldConfiguration].
*/
@WorldCfgMarker
class InjectableConfiguration(private val world: World) {
/**
* Adds the specified [dependency] under the given [name] which
* can then be injected via [World.inject].
*
* @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before.
*/
fun <T : Any> add(name: String, dependency: T) {
if (name in world.injectables) {
throw FleksInjectableAlreadyAddedException(name)
}
world.injectables[name] = Injectable(dependency)
}
/**
* Adds the specified [dependency] via its [simpleName][KClass.simpleName],
* or via its [toString][KClass.toString] if it has no name.
* It can then be injected via [World.inject].
*
* @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before.
*/
inline fun <reified T : Any> add(dependency: T) = add(T::class.simpleName ?: T::class.toString(), dependency)
}
/**
* A DSL class to configure [IntervalSystem] of a [WorldConfiguration].
*/
@WorldCfgMarker
class SystemConfiguration(
private val systems: MutableList<IntervalSystem>
) {
/**
* Adds the [system] to the [world][World].
* The order in which systems are added is the order in which they will be executed when calling [World.update].
*
* @throws [FleksSystemAlreadyAddedException] if the system was already added before.
*/
fun add(system: IntervalSystem) {
if (systems.any { it::class == system::class }) {
throw FleksSystemAlreadyAddedException(system::class)
}
systems += system
}
}
/**
* A DSL class to configure [FamilyHook] for specific [families][Family].
*/
@WorldCfgMarker
class FamilyConfiguration(
@PublishedApi
internal val world: World,
) {
/**
* Sets the add [hook][Family.addHook] for the given [family].
* This hook gets called whenever an [entity][Entity] enters the [family].
*/
fun onAdd(
family: Family,
hook: FamilyHook
) {
if (family.addHook != null) {
throw FleksHookAlreadyAddedException("addHook", "Family $family")
}
family.addHook = hook
}
/**
* Sets the remove [hook][Family.removeHook] for the given [family].
* This hook gets called whenever an [entity][Entity] leaves the [family].
*/
fun onRemove(
family: Family,
hook: FamilyHook
) {
if (family.removeHook != null) {
throw FleksHookAlreadyAddedException("removeHook", "Family $family")
}
family.removeHook = hook
}
}
/**
* A configuration for an entity [world][World] to define the systems, dependencies to be injected,
* [component][Component]- and [family][Family] hooks.
*
* @param world the [World] to be configured.
*/
@WorldCfgMarker
class WorldConfiguration(@PublishedApi internal val world: World) {
private var injectableCfg: (InjectableConfiguration.() -> Unit)? = null
private var familyCfg: (FamilyConfiguration.() -> Unit)? = null
private var systemCfg: (SystemConfiguration.() -> Unit)? = null
fun injectables(cfg: InjectableConfiguration.() -> Unit) {
injectableCfg = cfg
}
fun families(cfg: FamilyConfiguration.() -> Unit) {
familyCfg = cfg
}
fun systems(cfg: SystemConfiguration.() -> Unit) {
systemCfg = cfg
}
/**
* Sets the add entity [hook][EntityService.addHook].
* This hook gets called whenever an [entity][Entity] gets created and
* after its [components][Component] are assigned and [families][Family] are updated.
*/
fun onAddEntity(hook: EntityHook) {
world.setEntityAddHook(hook)
}
/**
* Sets the remove entity [hook][EntityService.removeHook].
* This hook gets called whenever an [entity][Entity] gets removed and
* before its [components][Component] are removed and [families][Family] are updated.
*/
fun onRemoveEntity(hook: EntityHook) {
world.setEntityRemoveHook(hook)
}
/**
* Sets the [EntityProvider] for the [EntityService] by calling the [factory] function
* within the context of a [World]. Per default the [DefaultEntityProvider] is used.
*/
fun entityProvider(factory: World.() -> EntityProvider) {
world.entityService.entityProvider = world.run(factory)
}
/**
* Configures the world in following sequence:
* - injectables
* - family
* - system
*
* The order is important to correctly trigger [FamilyHook]s and [EntityHook]s.
*/
fun configure() {
injectableCfg?.invoke(InjectableConfiguration(world))
familyCfg?.invoke(FamilyConfiguration(world))
SystemConfiguration(world.mutableSystems).also {
systemCfg?.invoke(it)
}
if (world.numEntities > 0) {
throw FleksWorldModificationDuringConfigurationException()
}
world.initAggregatedFamilyHooks()
world.systems.forEach { it.onInit() }
}
}
/**
* Creates a new [world][World] with the given [cfg][WorldConfiguration].
*
* @param entityCapacity initial maximum entity capacity.
* Will be used internally when a [world][World] is created to set the initial
* size of some collections and to avoid slow resizing calls.
*
* @param cfg the [configuration][WorldConfiguration] of the world containing the [systems][IntervalSystem],
* [injectables][Injectable] and [FamilyHook]s.
*/
fun configureWorld(entityCapacity: Int = 512, cfg: WorldConfiguration.() -> Unit): World {
val newWorld = World(entityCapacity)
CURRENT_WORLD = newWorld
try {
WorldConfiguration(newWorld).apply(cfg).configure()
} finally {
CURRENT_WORLD = null
}
return newWorld
}
/**
* Snapshot for an [entity][Entity] that contains its [components][Component] and [tags][EntityTag].
*/
@Serializable
data class Snapshot(
val components: List<Component<out @Contextual Any>>,
val tags: List<UniqueId<out @Contextual Any>>,
)
/**
* Utility function to manually create a [Snapshot].
*/
@Suppress("UNCHECKED_CAST")
fun wildcardSnapshotOf(components: List<Component<*>>, tags: List<UniqueId<*>>): Snapshot {
return Snapshot(components as List<Component<out Any>>, tags as List<UniqueId<out Any>>)
}
/**
* A world to handle [entities][Entity] and [systems][IntervalSystem].
*
* @param entityCapacity the initial maximum capacity of entities.
*/
class World internal constructor(
entityCapacity: Int,
) : EntityComponentContext(ComponentService()) {
@PublishedApi
internal val injectables = mutableMapOf<String, Injectable>()
/**
* Returns the time that is passed to [update][World.update].
* It represents the time in seconds between two frames.
*/
var deltaTime = 0f
private set
@PublishedApi
internal val entityService = EntityService(this, entityCapacity)
/**
* List of all [families][Family] of the world that are created either via
* an [IteratingSystem] or via the world's [family] function to
* avoid creating duplicates.
*/
@PublishedApi
internal var allFamilies = emptyArray<Family>()
/**
* Returns the amount of active entities.
*/
val numEntities: Int
get() = entityService.numEntities
/**
* Returns the maximum capacity of active entities.
*/
val capacity: Int
get() = entityService.capacity
/**
* Returns the world's systems.
*/
internal val mutableSystems = arrayListOf<IntervalSystem>()
val systems: List<IntervalSystem>
get() = mutableSystems
/**
* Map of add [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyAddHooks = mutableMapOf<Family, FamilyHook?>()
/**
* Map of remove [FamilyHook] out of the [WorldConfiguration].
* Only used if there are also aggregated system hooks for the family to remember
* its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
*/
private val worldCfgFamilyRemoveHooks = mutableMapOf<Family, FamilyHook?>()
/**
* Adds a new system to the world.
*
* @param index The position at which the system should be inserted in the list of systems. If null, the system is added at the end of the list.
* This parameter is optional and defaults to null.
* @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
fun add(index: Int, system: IntervalSystem) {
if (systems.any { it::class == system::class }) {
throw FleksSystemAlreadyAddedException(system::class)
}
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
mutableSystems.add(index, system)
}
/**
* Adds a new system to the world.
*
* @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
*/
fun add(system: IntervalSystem) = add(systems.size, system)
/**
* Removes the specified system from the world.
*
* @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
* @return True if the system was successfully removed, false otherwise.
*/
fun remove(system: IntervalSystem) {
mutableSystems.remove(system)
if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
updateAggregatedFamilyHooks(system.family)
}
}
/**
* Adds a new system to the world using the '+=' operator.
*
* @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
operator fun plusAssign(system: IntervalSystem) = add(system)
/**
* Removes the specified system from the world using the '-=' operator.
*
* @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
*/
operator fun minusAssign(system: IntervalSystem) {
remove(system)
}
/**
* Cache of used [EntityTag] instances. Needed for snapshot functionality.
*/
@PublishedApi
internal val tagCache = mutableMapOf<Int, UniqueId<*>>()
init {
/**
* Maybe because of design flaws, the world reference of the ComponentService must be
* set in the world's constructor because the parent class (=EntityComponentContext) already
* requires a ComponentService, and it is not possible to pass "this" reference directly.
*
* That's why it is happening here to set it as soon as possible.
*/
componentService.world = this
}
/**
* Returns an already registered injectable of the given [name] and marks it as used.
*
* @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
*/
inline fun <reified T> inject(name: String = T::class.simpleName ?: T::class.toString()): T {
val injectable = injectables[name] ?: throw FleksNoSuchInjectableException(name)
injectable.used = true
return injectable.injObj as T
}
/**
* Returns a new map of unused [injectables][Injectable]. An injectable gets set to 'used'
* when it gets injected at least once via a call to [inject].
*/
fun unusedInjectables(): Map<String, Any> =
injectables.filterValues { !it.used }.mapValues { it.value.injObj }
/**
* Returns a new [EntityBag] instance containing all [entities][Entity] of the world.
*
* Do not call this operation each frame, as it can be expensive depending on the amount
* of entities in your world.
*
* For frequent entity operations on specific entities, use [families][Family].
*/
fun asEntityBag(): EntityBag {
val result = MutableEntityBag(numEntities)
entityService.forEach {
result += it
}
return result
}
/**
* Adds a new [entity][Entity] to the world using the given [configuration][EntityCreateContext].
*
* **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 {
* // modifying the current entity is allowed ✅
* it += Position()
* // don't modify other entities ❌
* someOtherEntity += Position()
* }
* ```
*/
inline fun entity(configuration: EntityCreateContext.(Entity) -> Unit = {}): Entity {
return entityService.create(configuration)
}
/**
* Returns true if and only if the [entity] is not removed and is part of the [World].
*/
operator fun contains(entity: Entity) = entityService.contains(entity)
/**
* Removes the given [entity] from the world. The [entity] will be recycled and reused for
* future calls to [World.entity].
*/
operator fun minusAssign(entity: Entity) {
entityService -= entity
}
/**
* Removes all [entities][Entity] from the world. The entities will be recycled and reused for
* future calls to [World.entity].
* If [clearRecycled] is true then the recycled entities are cleared and the ids for newly
* created entities start at 0 again.
*/
fun removeAll(clearRecycled: Boolean = false) {
entityService.removeAll(clearRecycled)
}
/**
* Performs the given [action] on each active [entity][Entity].
*/
fun forEach(action: World.(Entity) -> Unit) {
entityService.forEach(action)
}
/**
* Returns the specified [system][IntervalSystem].
*
* @throws [FleksNoSuchSystemException] if there is no such system.
*/
inline fun <reified T : IntervalSystem> system(): T {
systems.forEach { system ->
if (system is T) {
return system
}
}
throw FleksNoSuchSystemException(T::class)
}
/**
* Sets the [hook] as an [EntityService.addHook].
*
* @throws FleksHookAlreadyAddedException if the [EntityService] already has an add hook set.
*/
@PublishedApi
internal fun setEntityAddHook(hook: EntityHook) {
if (entityService.addHook != null) {
throw FleksHookAlreadyAddedException("addHook", "Entity")
}
entityService.addHook = hook
}
/**
* Sets the [hook] as an [EntityService.removeHook].
*
* @throws FleksHookAlreadyAddedException if the [EntityService] already has a remove hook set.
*/
@PublishedApi
internal fun setEntityRemoveHook(hook: EntityHook) {
if (entityService.removeHook != null) {
throw FleksHookAlreadyAddedException("removeHook", "Entity")
}
entityService.removeHook = hook
}
/**
* Creates a new [Family] for the given [cfg][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
*/
fun family(cfg: FamilyDefinition.() -> Unit): Family = family(FamilyDefinition().apply(cfg))
/**
* Creates a new [Family] for the given [definition][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
*/
@PublishedApi
internal fun family(definition: FamilyDefinition): Family {
if (definition.isEmpty()) {
throw FleksFamilyException(definition)
}
val (defAll, defNone, defAny) = definition
var family = allFamilies.find { it.allOf == defAll && it.noneOf == defNone && it.anyOf == defAny }
if (family == null) {
family = Family(defAll, defNone, defAny, this)
allFamilies += family
// initialize a newly created family by notifying it for any already existing entity
// world.allFamilies.forEach { it.onEntityCfgChanged(entity, compMask) }
entityService.forEach { family.onEntityCfgChanged(it, entityService.compMasks[it.id]) }
}
return family
}
/**
* Returns a map that contains all [entities][Entity] and their components of this world.
* The keys of the map are the entities.
* The values are a list of components that a specific entity has. If the entity
* does not have any components then the value is an empty list.
*/
fun snapshot(): Map<Entity, Snapshot> {
val result = mutableMapOf<Entity, Snapshot>()
entityService.forEach { result[it] = snapshotOf(it) }
return result
}
/**
* Returns a list that contains all components of the given [entity] of this world.
* If the entity does not have any components then an empty list is returned.
*/
@Suppress("UNCHECKED_CAST")
fun snapshotOf(entity: Entity): Snapshot {
val comps = mutableListOf<Component<*>>()
val tags = mutableListOf<UniqueId<*>>()
if (entity in entityService) {
entityService.compMasks[entity.id].forEachSetBit { cmpId ->
val holder = componentService.holderByIndexOrNull(cmpId)
if (holder == null) {
// tag instead of component
tags += tagCache[cmpId] ?: throw FleksSnapshotException("Tag with id $cmpId was never assigned")
} else {
comps += holder[entity]
}
}
}
return Snapshot(comps as List<Component<out Any>>, tags as List<UniqueId<out Any>>)
}
/**
* 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 [FamilyHook].
*
* @throws FleksSnapshotException if a family iteration is currently in process.
*/
fun loadSnapshot(snapshot: Map<Entity, Snapshot>) {
if (entityService.delayRemoval) {
throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
}
// remove any existing entity and clean up recycled ids
removeAll(true)
if (snapshot.isEmpty()) {
// snapshot is empty -> nothing to load
return
}
val versionLookup = snapshot.keys.associateBy { it.id }
// Set next entity id to the maximum provided id + 1.
// All ids before that will be either created or added to the recycled
// ids to guarantee that the provided snapshot entity ids match the newly created ones.
with(entityService) {
val maxId = snapshot.keys.maxOf { it.id }
repeat(maxId + 1) {
val entity = Entity(it, version = (versionLookup[it]?.version ?: 0u) - 1u)
this.recycle(entity)
val entitySnapshot = snapshot[versionLookup[it]]
if (entitySnapshot != null) {
// snapshot for entity is provided -> create it
// note that the id for the entity will be the recycled id from above
this.configure(this.create { }, entitySnapshot)
}
}
}
}
/**
* Loads the given [entity] and its [snapshot][Snapshot].
* If the entity does not exist yet, it will be created.
* If the entity already exists it will be updated with the given components.
*
* @throws FleksSnapshotException if a family iteration is currently in process.
*/
fun loadSnapshotOf(entity: Entity, snapshot: Snapshot) {
if (entityService.delayRemoval) {
throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
}
if (entity !in entityService) {
// entity not part of service yet -> create it
entityService.create(entity.id) { }
}
// load components for entity
entityService.configure(entity, snapshot)
}
/**
* Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world
* using the given [deltaTime].
*/
fun update(deltaTime: Float) {
this.deltaTime = deltaTime
systems.forEach { system ->
if (system.enabled) {
system.onUpdate()
}
}
}
/**
* Removes all [entities][Entity] of the world and calls the
* [onDispose][IntervalSystem.onDispose] function of each system.
*/
fun dispose() {
entityService.removeAll()
systems.reversed().forEach { it.onDispose() }
}
/**
* Extend [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove].
*/
internal fun initAggregatedFamilyHooks() {
// validate systems against illegal interfaces
systems.forEach { system ->
// FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem
if (system !is IteratingSystem) {
if (system is FamilyOnAdd) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnAdd::class)
}
if (system is FamilyOnRemove) {
throw FleksWrongSystemInterfaceException(system::class, FamilyOnRemove::class)
}
}
}
// register family hooks for IteratingSystem.FamilyOnAdd containing systems
systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnAdd) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
family.addHook = if (ownHook != null) { entity ->
ownHook(this, entity)
systemList.forEach { it.onAddEntity(entity) }
} else { entity ->
systemList.forEach { it.onAddEntity(entity) }
}
}
// register family hooks for IteratingSystem.FamilyOnRemove containing systems
systems
.mapNotNull { if (it is IteratingSystem && it is FamilyOnRemove) it else null }
.groupBy { it.family }
.forEach { entry ->
val (family, systemList) = entry
val ownHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook as FamilyHook }
family.removeHook = if (ownHook != null) { entity ->
systemList.forEachReverse { it.onRemoveEntity(entity) }
ownHook(this, entity)
} else { entity ->
systemList.forEachReverse { it.onRemoveEntity(entity) }
}
}
}
/**
* Update [Family.addHook] and [Family.removeHook] for all
* [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove]
* and iterate over the given [family].
*/
private fun updateAggregatedFamilyHooks(family: Family) {
// system validation like in initAggregatedFamilyHooks is not necessary
// because it is already validated before (in initAggregatedFamilyHooks and in add/remove system)
// update family add hook by adding systems' onAddEntity calls after its original world cfg hook
val ownAddHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
val addSystems = systems.filter { it is IteratingSystem && it is FamilyOnAdd && it.family == family }
family.addHook = if (ownAddHook != null) { entity ->
ownAddHook(this, entity)
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
} else { entity ->
addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
}
// update family remove hook by adding systems' onRemoveEntity calls before its original world cfg hook
val ownRemoveHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook }
val removeSystems = systems.filter { it is IteratingSystem && it is FamilyOnRemove && it.family == family }
family.removeHook = if (ownRemoveHook != null) { entity ->
removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
ownRemoveHook(this, entity)
} else { entity ->
removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
}
}
@ThreadLocal
companion object {
@PublishedApi
internal var CURRENT_WORLD: World? = null
/**
* Returns an already registered injectable of the given [name] and marks it as used.
*
* @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
* @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
*/
inline fun <reified T> inject(name: String = T::class.simpleName ?: T::class.toString()): T =
CURRENT_WORLD?.inject(name) ?: throw FleksWrongConfigurationUsageException()
/**
* Creates a new [Family] for the given [cfg][FamilyDefinition].
*
* This function internally either creates or reuses an already existing [family][Family].
* In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
* that matches its configuration.
* Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
*
* As a best practice families should be created as early as possible, ideally during world creation.
* Also, store the result of this function instead of calling this function multiple times with the same arguments.
*
* @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
* @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
*/
fun family(cfg: FamilyDefinition.() -> Unit): Family =
CURRENT_WORLD?.family(cfg) ?: throw FleksWrongConfigurationUsageException()
}
}
private inline fun <T> List<T>.forEachReverse(action: (T) -> Unit) {
val lastIndex = this.lastIndex
for (i in lastIndex downTo 0) {
action(this[i])
}
} |
Great, I was just about to start looking at this and was writing some test cases 👍 |
looks good now - thanks a lot for adding the new test cases to also verify this special hook behavior <3 Btw, is it okay for you if this feature is currently only available in SNAPSHOT version? Or do you need a stable release for it? |
I'm currently running off a local jar, so I'm not fussed 😎 |
It's warning me about unmerged commits, so maybe I can't close this yet. |
Yeah, I did not merge it yet, just approved it :) Will merge it tomorrow. One more thing that I noticed now: it seems like systems.forEach is now using an iterator in the generated Bytecode which is not ideal because that floods the garbage collector with useless objects in our scenario. Can you please update that to use a for loop with indices inside |
…pdating to use a for loop with indices.
@RefuX: should be ready in SNAPSHOT version in a few minutes. Thanks again for your contribution, especially for adding the test cases to cover the special hook behavior! |
Great, thanks so much, using the SNAPSHOT 👍 |
I changed world.system to return a List instead on an Array, so while it didn't impact any tests, I guess its possible some users are expecting an Array?
I ran the benchmarks before and after changes and results are basically identical.
I added a constructor to IntervalSystem (to match old signature), as it was breaking some js/wasm tests otherwise.
Please review and give feedback.