Skip to content

Commit

Permalink
feat(observers): Entity scoped observers via a relation on entity
Browse files Browse the repository at this point in the history
feat(prefabs): Defining entity observers via config
  • Loading branch information
0ffz committed Apr 28, 2024
1 parent c60b114 commit 42cdd76
Show file tree
Hide file tree
Showing 21 changed files with 238 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface Prefabs {
createParseRelationWithDataListener()
createTrackPrefabsByKeyListener()
createCopyToInstancesSystem()
bindEntityObservers()
}
geary.pipeline.runOnOrAfter(GearyPhase.INIT_ENTITIES) {
loader.loadOrUpdatePrefabs()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mineinabyss.geary.prefabs.configuration.components

import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import com.mineinabyss.geary.serialization.serializers.SerializedComponents
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer

@Serializable(with = EntityObservers.Serializer::class)
class EntityObservers(val observers: List<EventBind>) {
class Serializer : InnerSerializer<List<EventBind>, EntityObservers>(
serialName = "geary:observe",
inner = ListSerializer(EventBind.serializer()),
inverseTransform = { it.observers },
transform = ::EntityObservers
)
}

@Serializable
class EventBind(
val event: SerializableComponentId,
val involving: List<SerializableComponentId> = listOf(),
val emit: SerializedComponents
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.mineinabyss.geary.prefabs.configuration.components

import com.mineinabyss.geary.datatypes.ComponentId
import com.mineinabyss.geary.helpers.componentId
import com.mineinabyss.geary.serialization.serializableComponents
import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with = SerializableComponentId.Serializer::class)
class SerializableComponentId(val id: ComponentId) {
object Serializer : KSerializer<SerializableComponentId> {
override val descriptor = PrimitiveSerialDescriptor("EventComponent", PrimitiveKind.STRING)

private val polymorphicListAsMapSerializer = PolymorphicListAsMapSerializer.ofComponents()

override fun deserialize(decoder: Decoder): SerializableComponentId {
val type = decoder.decodeString()
val namespaces = polymorphicListAsMapSerializer
.getParentConfig(decoder.serializersModule)?.namespaces
?: emptyList()
val typeComponentId = componentId(serializableComponents.serializers.getClassFor(type, namespaces))
return SerializableComponentId(typeComponentId)
}

override fun serialize(encoder: Encoder, value: SerializableComponentId) {
TODO()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mineinabyss.geary.prefabs.configuration.systems

import com.mineinabyss.geary.datatypes.EntityType
import com.mineinabyss.geary.modules.GearyModule
import com.mineinabyss.geary.observers.entity.observe
import com.mineinabyss.geary.observers.events.OnSet
import com.mineinabyss.geary.prefabs.configuration.components.EntityObservers
import com.mineinabyss.geary.systems.builders.observe
import com.mineinabyss.geary.systems.query.query

fun GearyModule.bindEntityObservers() = observe<OnSet>()
.involving(query<EntityObservers>())
.exec { (observers) ->
observers.observers.forEach { observer ->
entity.observe(observer.event.id).involving(EntityType(observer.involving.map { it.id })).exec {
observer.emit.forEach { event -> entity.emit(event) }
}
}
entity.remove<EntityObservers>()
}

Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class SerializableComponentsDSL(
subclass(subclass, serializer)
}

inline fun <reified T : Any> namedComponent(name: String) {
serializers.serialNameToClass[name] = T::class
}

/** Adds a [SerializersModule] to be used for polymorphic serialization within the ECS. */
inline fun module(init: SerializersModuleBuilder.() -> Unit) {
serializers.modules += SerializersModule { init() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mineinabyss.geary.serialization.dsl

import com.mineinabyss.geary.observers.events.*

fun SerializableComponentsDSL.withCommonComponentNames() {
namedComponent<OnAdd>("geary:on_add")
namedComponent<OnSet>("geary:on_set")
namedComponent<OnFirstSet>("geary:on_first_set")
namedComponent<OnRemove>("geary:on_remove")
namedComponent<OnUpdate>("geary:on_update")
namedComponent<OnEntityRemoved>("geary:on_entity_removed")
namedComponent<OnExtend>("geary:on_extend")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.mineinabyss.geary.components

/**
* An entity's archetype with this component may stay even if empty.
* Useful for entities that are created and removed often, ex events.
* Useful for entities that are created and removed often.
*/
sealed class KeepArchetype
sealed class KeepEmptyArchetype
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.mineinabyss.geary.engine

import com.mineinabyss.geary.components.CouldHaveChildren
import com.mineinabyss.geary.components.KeepArchetype
import com.mineinabyss.geary.components.KeepEmptyArchetype
import com.mineinabyss.geary.components.relations.ChildOf
import com.mineinabyss.geary.components.relations.InstanceOf
import com.mineinabyss.geary.datatypes.ComponentId
import com.mineinabyss.geary.helpers.componentId
import com.mineinabyss.geary.observers.Observer
import com.mineinabyss.geary.observers.events.*

class Components {
val any: ComponentId = componentId<Any>()
val suppressRemoveEvent = componentId<SuppressRemoveEvent>()
val couldHaveChildren = componentId<CouldHaveChildren>()
val observer = componentId<Observer>()
val onAdd = componentId<OnAdd>()
val onSet = componentId<OnSet>()
val onFirstSet = componentId<OnFirstSet>()
Expand All @@ -21,5 +23,5 @@ class Components {
val onEntityRemoved = componentId<OnEntityRemoved>()
val childOf = componentId<ChildOf>()
val instanceOf = componentId<InstanceOf>()
val keepArchetype = componentId<KeepArchetype>()
val keepEmptyArchetype = componentId<KeepEmptyArchetype>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ class Archetype internal constructor(
internal val relations: EntityType = type.filter { it.isRelation() }
internal val relationsWithData: EntityType = relations.filter { it.holdsData() }

fun getRelationsByTarget(target: EntityId): List<Relation> {
return relations.filter { Relation.of(it).target.toLong() == target.toLong() }.map { Relation.of(it) }
fun getRelationsByTarget(target: EntityId): EntityType {
return relations.filter { Relation.of(it).target.toLong() == target.toLong() }
}

fun getRelationsByKind(kind: ComponentId): List<Relation> {
return relations.filter { Relation.of(it).kind.toLong() == kind.toLong() }.map { Relation.of(it) }
fun getRelationsByKind(kind: ComponentId): EntityType {
return relations.filter { Relation.of(it).kind.toLong() == kind.toLong() }
}

/** The amount of entities stored in this archetype. */
Expand All @@ -94,6 +94,11 @@ class Archetype internal constructor(
return componentData[compIndex][row]
}

private fun getUnsafe(row: Int, componentId: ComponentId): Component {
val compIndex = indexOf(componentId)
return componentData[compIndex][row]
}

/** @return Whether this archetype has a [componentId] in its type. */
operator fun contains(componentId: ComponentId): Boolean = componentId in type

Expand Down Expand Up @@ -321,13 +326,13 @@ class Archetype internal constructor(
instanceArch = arch; instanceRow = row
}

val noInheritComponents = EntityType(getRelationsByKind(componentId<NoInherit>()).map { it.target })
val noInheritComponents = getRelationsByKind(componentId<NoInherit>()).map { Relation.of(it).target }
type.filter { !it.holdsData() && it !in noInheritComponents }.forEach {
instanceArch.addComponent(instanceRow, it, true) { arch, row -> instanceArch = arch; instanceRow = row }
}
dataHoldingType.forEach {
if (it.withoutRole(HOLDS_DATA) in noInheritComponents) return@forEach
instanceArch.setComponent(instanceRow, it, get(baseRow, it)!!, true) { arch, row ->
instanceArch.setComponent(instanceRow, it, getUnsafe(baseRow, it), true) { arch, row ->
instanceArch = arch; instanceRow = row
}
}
Expand Down Expand Up @@ -382,7 +387,7 @@ class Archetype internal constructor(
if (allowUnregister == FALSE) return
if (ids.size == 0 && type.size != 0 && componentAddEdges.size == 0) {
if (allowUnregister == UNKNOWN) allowUnregister =
if (type.contains(comps.keepArchetype)) FALSE else TRUE
if (type.contains(comps.keepEmptyArchetype)) FALSE else TRUE
if (allowUnregister == FALSE) return
archetypes.queryManager.unregisterArchetype(this)
unregistered = true
Expand Down Expand Up @@ -423,8 +428,8 @@ class Archetype internal constructor(
val specificTarget = target and ENTITY_MASK != comps.any
return when {
specificKind && specificTarget -> listOf(Relation.of(kind, target))
specificTarget -> getRelationsByTarget(target)
specificKind -> getRelationsByKind(kind)
specificTarget -> getRelationsByTarget(target).map { Relation.of(it) }
specificKind -> getRelationsByKind(kind).map { Relation.of(it) }
else -> relations.map { Relation.of(it) }
}.run { //TODO this technically doesnt need to run when specificKind is set
if (kind.hasRole(HOLDS_DATA)) filter { it.hasRole(HOLDS_DATA) } else this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ import com.mineinabyss.geary.datatypes.maps.Family2ObjectArrayMap
import com.mineinabyss.geary.engine.QueryManager
import com.mineinabyss.geary.helpers.contains
import com.mineinabyss.geary.helpers.fastForEach
import com.mineinabyss.geary.observers.Observer
import com.mineinabyss.geary.systems.query.CachedQueryRunner
import com.mineinabyss.geary.systems.query.Query
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized

typealias Observer = Observer

class ArchetypeQueryManager : QueryManager {
private val queries = mutableListOf<CachedQueryRunner<*>>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,58 @@ package com.mineinabyss.geary.observers
import androidx.collection.LongSparseArray
import com.mineinabyss.geary.annotations.optin.UnsafeAccessors
import com.mineinabyss.geary.datatypes.*
import com.mineinabyss.geary.engine.archetypes.Archetype
import com.mineinabyss.geary.helpers.contains
import com.mineinabyss.geary.helpers.fastForEach
import com.mineinabyss.geary.helpers.toGeary
import com.mineinabyss.geary.modules.archetypes
import com.mineinabyss.geary.modules.geary

class ArchetypeEventRunner : EventRunner {
private val observerMap = LongSparseArray<ObserverList>()
private val eventToObserverMap = LongSparseArray<ObserverList>()

override fun addObserver(observer: Observer) {
observer.listenToEvents.forEach { event ->
observerMap.getOrPut(event.toLong()) { ObserverList() }.add(observer)
eventToObserverMap.getOrPut(event.toLong()) { ObserverList() }.add(observer)
}
}


private inline fun matchObservers(
eventType: ComponentId,
involvedComponent: ComponentId,
entity: Entity,
exec: (Observer, Archetype, row: Int) -> Unit
) {
val observerComp = geary.components.observer
val records = archetypes.records
val involved = involvedComponent.withoutRole(HOLDS_DATA)

// Run entity observers
records.runOn(entity) { archetype, _ -> archetype.getRelationsByKind(observerComp) }.forEach { relation ->
val observerList = Relation.of(relation).target.toGeary().get<ObserverList>() ?: return@forEach
observerList.forEach(involved, entity, exec)
}

// Run global observers
eventToObserverMap[eventType.toLong()]?.forEach(involved, entity, exec)
}

override fun callEvent(
eventType: ComponentId,
eventData: Any?,
involvedComponent: ComponentId,
entity: Entity,
) {
val archetypes = archetypes
observerMap[eventType.toLong()]?.forEach(involvedComponent.withoutRole(HOLDS_DATA)) { observer ->
matchObservers(eventType, involvedComponent, entity) { observer, archetype, row ->
// Observer may change the entity record, so we must get each time.
archetypes.records.runOn(entity) { archetype, row ->
if (observer.mustHoldData && eventData == null) return@runOn
if (observer.family.contains(archetype.type)) {
observer.queries.fastForEach { query ->
@OptIn(UnsafeAccessors::class)
query.reset(row, archetype)
}
observer.run(entity, eventData, involvedComponent)
if (observer.mustHoldData && eventData == null) return@matchObservers
if (observer.family.contains(archetype.type)) {
observer.queries.fastForEach { query ->
@OptIn(UnsafeAccessors::class)
query.reset(row, archetype)
}
observer.handle.run(entity, eventData, involvedComponent)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import com.mineinabyss.geary.datatypes.EntityType
import com.mineinabyss.geary.datatypes.family.Family
import com.mineinabyss.geary.systems.query.Query

abstract class Observer(
data class Observer(
val queries: List<Query>,
val family: Family,
val involvedComponents: EntityType,
val listenToEvents: EntityType,
val mustHoldData: Boolean,
) {
abstract fun run(entity: Entity, data: Any?, involvedComponent: ComponentId?)
val handle: ObserverHandle,
)

fun interface ObserverHandle {
fun run(entity: Entity, data: Any?, involvedComponent: ComponentId?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import androidx.collection.LongSparseArray
import androidx.collection.MutableObjectList
import androidx.collection.mutableObjectListOf
import com.mineinabyss.geary.datatypes.ComponentId
import com.mineinabyss.geary.datatypes.GearyEntity
import com.mineinabyss.geary.datatypes.getOrPut
import com.mineinabyss.geary.engine.archetypes.Archetype
import com.mineinabyss.geary.helpers.NO_COMPONENT
import com.mineinabyss.geary.modules.archetypes

class ObserverList {
val involved2Observer = LongSparseArray<MutableObjectList<Observer>>()
Expand All @@ -18,9 +21,14 @@ class ObserverList {
}
}

inline fun forEach(componentId: ComponentId, exec: (Observer) -> Unit) {
involved2Observer[0L]?.forEach(exec)
if (componentId != NO_COMPONENT)
involved2Observer[componentId.toLong()]?.forEach(exec)
inline fun forEach(involvedComp: ComponentId, entity: GearyEntity, exec: (Observer, Archetype, row: Int) -> Unit) {
val records = archetypes.records

involved2Observer[0L]?.forEach {
records.runOn(entity) { archetype, row -> exec(it, archetype, row) }
}
if (involvedComp != NO_COMPONENT) involved2Observer[involvedComp.toLong()]?.forEach {
records.runOn(entity) { archetype, row -> exec(it, archetype, row) }
}
}
}
Loading

0 comments on commit 42cdd76

Please sign in to comment.